GitHub Actions research and test laboratory.
timeout-minutes
timeout-minutes
to every job.key
and cancel-in-progress
option, it will terminate action when at least 1 pending job is exsits. However you cannot customize how many pending actions are allowed, do not cancel pending job.Relax the limit for GitHub Team Plan
is my strong expectation.
Environment > Deployment protection rules
is not allowed in GitHub team Plan. You cannot use Required reviewers
(Approvabl) and Wait timer
.There are several documents for migration.
Also you may consider migrate from GitHub Actions.
GitHub Actions support handling fork PR.
/pull\/[0-9]+/
. Also allowing public fork to be access secret is not recommended, and there are no easy way to handle it.GitHub Actions support checkout by actions and supports variety of checkout options include sparse checkout.
ssh
or https
protocol, submodule
, shallow-clone
, sparse checkout
and lfs
. actions/checkout
is default shallow-clone
(depth 1).ssh
or https
protocol. It missing submodule
, shallow-clone
, sparse-checkout
and lfs
support. checkout
is default full clone.ssh
or https
protocol, submodule
, shallow-clone
and lfs
. It missing sparse-checkout
support. checkout
is default shallow-clone
(depth 1) for new pipeline created after the September 2022.ssh
or https
protocol, submodule
, shallow-clone
, sparse checkout
and lfs
. GitSCM
is default full clone.All CI has yaml definitions.
name: workflow name
on:
push:
branches: [main]
jobs:
Job_Name:
runs-on: ubuntu-latest
steps:
- run: echo foo
version: 2.1
jobs:
Job_Name:
docker:
- image: circleci/<language>:<version TAG>
steps:
- run: echo foo
workflows:
commit:
jobs:
- Job_Name
trigger:
- main
stages:
- stage: StageName
jobs:
- job: Job_Name
jobs:
- job: Job_Name
pool:
vmImage: "ubuntu-latest"
steps:
- bash: echo "foo"
pipeline {
agent any
triggers {
pollSCM('')
}
stages {
stage('Stage_Name') {
steps {
sh 'echo foo'
}
}
}
}
Every CI offer you to configure runner sizing for SelfHosted Runner, but some CI has limitation for sizing Hosted Runner.
2Core CPU, 7GB RAM and 14GB SSD Disk
.This functionality enables you to stop next job until manually approved.
GitHub Team
pricing.GitHub Actions has Context concept, you can access job specific info via github
.
for example, github.run_id
is A unique number for each run within a repository.
Also you can access default environment variables like GITHUB_RUN_ID
.
GITHUB_RUN_ID
or context github.run_id
CIRCLE_BUILD_NUM
and CIRCLE_WORKFLOW_ID
BuildID
.BUILD_NUMBER
GitHub Actions can use on.<event>.paths-ignore:
and on.<event>.paths:
by default.
GitHub Actions not support exact functionality as CircleCI provide, but you can do via concurrency control. Another option is community actions like rokroskar/workflow-run-cleanup-action, fauguste/auto-cancellation-running-action and yellowmegaman/gh-build-canceller.
whole workflow
, single job
and failed job
.whole workflow
or failed job
again.failed job
only.Job
or Stage
again. But you may find it is unstable.Write script is better than directly write on the step, so that we can reuse same execution from other workflows or jobs.
Reusable workflow
, Composite Actions
and Organization workflow
.YAML anchor
is useul.Define Environment varialbes
in each job step, then reuse it later step is common pattern.
$GITHUB_ENV
via echo "{environment_variable_name}={value}" >> $GITHUB_ENV
(Linux) or "{environment_variable_name}={value}" >> $env:GITHUB_ENV
(Windows) syntax.::set-env
syntax has been deprecated for security reason.$BASH_ENV
via echo "export GIT_SHA1=$CIRCLE_SHA1" >> $BASH_ENV
syntax.echo "##vso[task.setvariable variable=NAME]VALUE"
syntax.Env.
.Define output
in each job step, then reuse it later step is less side-effect than environment variable. Also it can pass value between job via job output
, and it can't achieve with environment variable pattern.
$GITHUB_OUTPUT
via echo "{name}={value}" >> "$GITHUB_OUTPUT"
(Linux) or "{name}=value" >> $env:GITHUB_OUTPUT
(Windows) syntax.echo "##vso[task.setvariable variable=NAME;isoutput=true]VALUE"
syntax.Info GitHub Actions
::set-output
syntax has been deprecated for security reason.
$GITHUB_PATH
via echo "{path}" >> "$GITHUB_PATH"
or echo "{path}" | tee -a "$GITHUB_PATH"
syntax.::add-path
syntax has been deprecated for security reason.$BASH_ENV
wiht name PATH
via echo "export PATH=$GOPATH/bin:$PATH" >> $BASH_ENV
syntax.echo '##vso[task.setvariable variable=path]$(PATH):/dir/to/whatever'
syntax.Env.
.GitHub ACtions offer Secrets for each repository and Organization. Secrets will be masked on the log, and also you can mask desired output in log.
GitHub Actions supports "Organization Secrets", "Repository Secrets" and "Environment Secrets".
Organization > Settings > Secrets
.Repository > Settings > Secrets
.Repository > Environment > Secrets
.If same secrets key is exists, winner is Environment Secrets
> Repository Secrets
> Organization Secrets
.
If you want spread your secrets with personal account, you need set each repository secrets or use google/secrets-sync-action.
GitHub Actions support when HEAD commit contains key word like other ci.
[skip ci]
, [ci skip]
, [no ci]
, [skip actions]
or [actions skip]
. If PR last commit message contains [skip ci]
, then merge commit also skip.[skip ci]
or [ci skip]
. If PR last commit message contains [skip ci]
, then merge commit also skip.***NO_CI***
, [skip ci]
or [ci skip]
, or others.[skip ci]
or any expression w/pipeline like SCM Skip | Jenkins plugin.GitHub Actions use Build artifacts to share files between jobs in a workflow and also download artifacts from completed workflows.
store_artifacts
step, however you need call API to download stored artifacts. There are not retention period for upload artifact.PublishPipelineArtifact
task, and download via DownloadPipelineArtifact
task. There are not retention period for upload artifact.archiveArtifacts
step, however you need call API to download stored artifacts. There are not retention period for upload artifact.Use Context to retrive job id, name and others system info.
Make sure you can not refer gitHub
context in script.
see: Context and expression syntax for GitHub Actions - GitHub Help
# .github/workflows/context_github.yaml
name: "context github"
on:
issue_comment:
types: [created]
push:
branches: ["main"]
tags: ["*"]
pull_request:
branches: ["main"]
types: [opened, synchronize, reopened, closed]
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
jobs:
context:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- name: job
run: echo ${{ github.job }}
- name: ref
run: echo ${{ github.ref }}
- name: sha
run: echo ${{ github.sha }}
- name: repository
run: echo ${{ github.repository }}
- name: repository_owner
run: echo ${{ github.repository_owner }}
- name: actor
run: echo ${{ github.actor }}
- name: run_id
run: echo ${{ github.run_id }}
- name: workflow
run: echo ${{ github.workflow }}
- name: event_name
run: echo ${{ github.event_name }}
- name: event.ref
run: echo ${{ github.event.ref }}
- name: action
run: echo ${{ github.action }}
JSON output
Use toJson(<CONTEXT>)
To show context values in json.
# .github/workflows/dump_context.yaml
name: dump context
on:
issue_comment:
types: [created]
push:
branches: ["main"]
tags: ["*"]
pull_request:
branches: ["main"]
types: [opened, synchronize, reopened, closed]
pull_request_target:
branches: ["main"]
types: [opened, synchronize, reopened, closed]
schedule:
- cron: "0 0 * * *"
workflow_dispatch:
jobs:
dump:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
- name: Dump environment
run: env
- name: Dump GitHub context
run: echo "$CONTEXT"
env:
CONTEXT: ${{ toJson(github) }}
- name: Dump job context
run: echo "$CONTEXT"
env:
CONTEXT: ${{ toJson(job) }}
- name: Dump steps context
run: echo "$CONTEXT"
env:
CONTEXT: ${{ toJson(steps) }}
- name: Dump runner context
run: echo "$CONTEXT"
env:
CONTEXT: ${{ toJson(runner) }}
- name: Dump strategy context
run: echo "$CONTEXT"
env:
CONTEXT: ${{ toJson(strategy) }}
- name: Dump matrix context
run: echo "$CONTEXT"
env:
CONTEXT: ${{ toJson(matrix) }}
set environment variables for next step explains how to set environment variables for next step.
This syntax can be write in the script, let's see .github/scripts/setenv.sh
.
# .github/scripts/setenv.sh
#!/bin/bash
set -eux
while [ $# -gt 0 ]; do
case $1 in
--ref) GITHUB_REF=$2; shift 2; ;;
*) shift ;;
esac
done
echo BRANCH_SCRIPT=${GITHUB_REF} | tee -a "$GITHUB_ENV"
echo branch=${GITHUB_REF} | tee -a "$GITHUB_OUTPUT"
Call this script from workflow.
# .github/workflows/env_with_script.yaml
name: env with script
on:
workflow_dispatch:
push:
branches: ["main"]
pull_request:
branches: ["main"]
env:
BRANCH_NAME: ${{ startsWith(github.event_name, 'pull_request') && github.head_ref || github.ref_name }}
jobs:
bash:
strategy:
matrix:
runs-on: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.runs-on }}
timeout-minutes: 3
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v4
- name: Add ENV and OUTPUT by shell
id: shell
run: |
echo "BRANCH=${{ env.BRANCH_NAME }}" | tee -a "$GITHUB_ENV"
echo "branch=${{ env.BRANCH_NAME }}" | tee -a "$GITHUB_OUTPUT"
- name: Show ENV and OUTPUT
run: |
echo ${{ env.BRANCH }}
echo ${{ steps.shell.outputs.branch }}
- name: Add ENV and OUTPUT by Script
id: script
run: bash ./.github/scripts/setenv.sh --ref "${{ env.BRANCH_NAME }}"
- name: Show Script ENV and OUTPUT
run: |
echo ${{ env.BRANCH_SCRIPT }}
echo ${{ steps.script.outputs.branch }}
- name: Add PATH
run: echo "$HOME/foo/bar" | tee -a "$GITHUB_PATH"
- name: Show PATH
run: echo "$PATH"
powershell:
strategy:
matrix:
runs-on: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.runs-on }}
timeout-minutes: 3
defaults:
run:
shell: pwsh
steps:
- uses: actions/checkout@v4
- name: Add ENV and OUTPUT by shell
id: shell
run: |
echo "BRANCH=${{ env.BRANCH_NAME }}" | Tee-Object -Append -FilePath "${env:GITHUB_ENV}"
echo "branch=${{ env.BRANCH_NAME }}" | Tee-Object -Append -FilePath "${env:GITHUB_OUTPUT}"
- name: Show ENV and OUTPUT
run: |
echo "${{ env.BRANCH }}"
echo "${{ steps.shell.outputs.branch }}"
- name: Add ENV and OUTPUT by Script
id: script
run: ./.github/scripts/setenv.ps1 -Ref "${{ env.BRANCH_NAME }}"
- name: Show Script ENV and OUTPUT
run: |
echo "${{ env.BRANCH_SCRIPT }}"
echo "${{ steps.script.outputs.branch }}"
- name: Add PATH
run: echo "$HOME/foo/bar" | Tee-Object -Append -FilePath "${env:GITHUB_PATH}"
- name: Show PATH
run: echo "${env:PATH}"
cmd:
strategy:
matrix:
runs-on: [windows-latest]
runs-on: ${{ matrix.runs-on }}
timeout-minutes: 3
defaults:
run:
shell: cmd
steps:
- uses: actions/checkout@v4
# cmd must not use quotes!!
- name: Add ENV and OUTPUT by shell
id: shell
run: |
echo BRANCH=${{ env.BRANCH_NAME }} >> %GITHUB_ENV%
echo branch=${{ env.BRANCH_NAME }} >> %GITHUB_OUTPUT%
- name: Show ENV and OUTPUT
run: |
echo ${{ env.BRANCH }}
echo ${{ steps.shell.outputs.branch }}
- name: Add ENV and OUTPUT by Script
id: script
run: .github/scripts/setenv.bat --ref "${{ env.BRANCH_NAME }}"
- name: Show Script ENV and OUTPUT
run: |
echo ${{ env.BRANCH_SCRIPT }}
echo ${{ steps.script.outputs.branch }}
- name: Add PATH
run: echo "%UserProfile%\foo\bar" >> %GITHUB_PATH%
- name: Show PATH
run: echo %PATH%
GitHub Actions allow if
condition for step
.
You can refer any context inside if
condition.
You don't need add ${{}}
to context reference. but I do recomment add it for easier read.
NOTE:
matrix
cannot refer withjob.if
.Solved: What is the correct if condition syntax for checki/././. /- GitHub Community Forum
# .github/workflows/if_and_context.yaml
name: if and context reference
on:
workflow_dispatch:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
matrix_reference:
strategy:
matrix:
sample: ["hoge", "fuga"]
env:
APP: hoge
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
# env context reference
- run: echo "this is env if for hoge"
if: ${{ env.APP == matrix.sample }}
- run: echo "this is env if for fuga"
if: ${{ env.APP == matrix.sample }}
# github context reference
- run: echo "this is github if event push"
if: ${{ github.event_name == 'push' }}
# matrix context reference
- run: echo "this is matrix if for hoge"
if: ${{ matrix.sample == 'hoge' }}
- run: echo "this is matrix if for fuga"
if: ${{ matrix.sample == 'fuga' }}
You can handle Job dependency with needs
.
Basic usage is needs: <job_name>
. Let's check official example.
Requiring successful dependent jobs
Following example shows require successful dependent jobs.job2
will run after job1
is success, and job3
will run after job1
and job2
are success. It means job2
& job3
never run when job1
failed, job3
never run when job2
failed. In result, jobs are run seqientially in order of job1
-> job2
-> job3
.
jobs:
job1:
job2:
needs: job1
job3:
needs: [job1, job2]
See actual sample.
# .github/workflows/needs_require_success.yaml
name: needs require success
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
workflow_dispatch:
jobs:
A:
runs-on: ubuntu-latest
steps:
- run: echo "a"
B:
needs: [A]
runs-on: ubuntu-latest
steps:
- run: echo "b"
# Run only if A and B success
C:
needs: [A, B]
runs-on: ubuntu-latest
steps:
- run: echo "c"
Not requiring successful dependent jobs
job3
uses the always()
conditional expression. So thatjob3
will run regardless of job1
and job2
job result is success or failure. Because of needs
section, jobs are run seqientially in order of job1
-> job2
-> job3
.
jobs:
job1:
job2:
needs: job1
job3:
if: ${{ always() }}
needs: [job1, job2]
See actual sample.
# .github/workflows/needs_not_require_success.yaml
name: needs not require success
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
workflow_dispatch:
jobs:
A:
runs-on: ubuntu-latest
steps:
- run: echo "a"
B:
needs: [A]
runs-on: ubuntu-latest
steps:
- run: echo "b"
# always run without A and B result
C:
needs: [A, B]
if: ${{ always() }}
runs-on: ubuntu-latest
steps:
- run: echo "c"
Job needs
can be used for skip handling. However skipping dependent job cause trouble.
Following workflow expected to run D
when C
is invoked. But skipping A
and B
cause D
skip.
# .github/workflows/needs_skip_no_handling.yaml
name: needs skip no handling
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
workflow_dispatch:
jobs:
A:
if: ${{ false }}
runs-on: ubuntu-latest
steps:
- run: echo "a"
B:
if: ${{ false }}
runs-on: ubuntu-latest
steps:
- run: echo "b"
C:
runs-on: ubuntu-latest
steps:
- run: echo "c"
# D will always skip because A and B is skipped
D:
needs: [A, B, C]
runs-on: ubuntu-latest
steps:
- run: echo "d"
To handle D
to run when C
is invoked, you need to add if
condition to D
. Also handle when no conditional C
invokation, A
, B
and C
is success, then D
must run.
# .github/workflows/needs_skip_handling.yaml
name: needs skip handling
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
workflow_dispatch:
inputs:
only-c:
description: 'Run only Job C'
required: false
default: false
type: boolean
jobs:
A:
if: ${{ !inputs.only-c }}
runs-on: ubuntu-latest
steps:
- run: echo "a"
B:
if: ${{ !inputs.only-c }}
runs-on: ubuntu-latest
steps:
- run: echo "b"
C:
runs-on: ubuntu-latest
steps:
- run: echo "c"
# D will run when "C is success" or "all the jobs are success".
D:
needs: [A, B, C]
if: ${{ inputs.only-c && needs.C.result == 'success' || success() }}
runs-on: ubuntu-latest
steps:
- run: echo "d"
GitHub Actions supports specify permissions for each job or workflow.
You can turn all permission off with permissions: {}
.
Workflow permission can be done with root permissions:
.
# .github/workflows/permissions_workflow.yaml
name: permissions
on:
pull_request:
branches: ["main"]
permissions:
# actions: write
# checks: write
contents: read
# deployments: write
# discussions: write
# id-token: write
# issues: write
# packages: write
# pages: write
# pull-requests: write
# repository-projects: write
# security-events: write
# statuses: write
jobs:
job:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
job permission can be done with job.<job_name>.permissions
.
# .github/workflows/permissions_job.yaml
name: permissions job
on:
pull_request:
branches: ["main"]
jobs:
job:
permissions:
# actions: write
# checks: write
contents: read
# deployments: write
# discussions: write
# id-token: write
# issues: write
# packages: write
# pages: write
# pull-requests: write
# repository-projects: write
# security-events: write
# statuses: write
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
The most important permission is id-tokens: write
. It enables job to use OIDC like AWS, Azure and GCP.
To reuse local job, create local composite action is easiest way to do, this is calls composite actions
.
Create yaml file inside local action path, then declare using: "composite"
in local action.yaml.
.github/actions/YOUR_DIR/action.yaml
# .github/actions/local-composite-actions/action.yaml
name: YOUR ACTION NAME
description: |
Desctiption of your action
inputs:
foo:
description: thi is foo input
default: FOO
required: false
runs:
using: "composite" # this is key point
steps:
- name: THIS IS STEP1
shell: bash # this is key point
run: echo ${{ inputs.foo }}
# .github/workflows/reuse_local_actions.yaml
name: reuse local action
on:
workflow_dispatch:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
job:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
- name: use local action
uses: ./.github/actions/local-composite-actions
with:
foo: BAR
To reuse local job, create local node action is another way to do, this is calls node actions
.
Create yaml file inside local action path, then declare using: "node12"
in local action.yaml.
Next place your Node.js source files inside actions directory, you may require index.js
for entrypoint.
TIPS: You may find it is useful when you are running on GHE and copy GitHub Actions to your local.
.github/actions/YOUR_DIR/actions.yaml
# .github/actions/local-node-actions/action.yaml
name: "Hello World"
description: |
Desctiption of your action
runs:
using: "node20"
main: "index.js"
.github/actions/YOUR_DIR/*.js
.// .github/actions/local-node-actions/index.js
console.log("Hello, World!");
# .github/workflows/reuse_local_actions_node.yaml
name: reuse local action node
on:
workflow_dispatch:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
job:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
- name: use local action
uses: ./.github/actions/local-node-actions
GitHub Actions allow call workflow from workflow. You can call local workflow of the same repository (Private repository), and remote workflow of the Public repository.
detail: Reusing workflows - GitHub Docs
There are limitations on Reusable workflow caller.
with
inputs.
jobs:
bad:
runs-on: ubuntu-latest
steps:
uses: ./.github/workflows/dummy.yaml
with:
value: ${{ env.FOO }} # caller can not use `env.` in with block.
secrets: inherit
.github/workflows/
. Otherwise caller treated as calling public workflow.
$ ls -l ./.github/workflows/
env:
FOO: foo # Reusable workflow callee cannot refer this env.
jobs:
bad:
runs-on: ubuntu-latest
steps:
uses: ./.github/workflows/dummy.yaml
Place Reusable workflow yaml file under .github/workflows/
then set on.workflow_call
trigger, you are ready for reusable workflow.
Any inputs
, secrets
and outputs
should define onder on.workflow_call.
# .github/workflows/_reusable_workflow_called.yaml
name: _reusable workflow called
on:
workflow_call:
inputs:
username:
required: true
description: username to show
type: string
is-valid:
required: true
description: username to show
type: boolean
outputs:
firstword:
description: "The first output string"
value: ${{ jobs.reusable_workflow_job.outputs.output1 }}
secondword:
description: "The second output string"
value: ${{ jobs.reusable_workflow_job.outputs.output2 }}
env:
FOO: foo
jobs:
reusable_workflow_job:
runs-on: ubuntu-latest
outputs:
output1: ${{ steps.step1.outputs.firstword }}
output2: ${{ steps.step2.outputs.secondword }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} # checkout PR HEAD commit instead of merge commit
- name: (Limitation) Callee can not refer caller environment variable.
run: echo "caller environment. ${{ env.CALLER_VALUE }}"
- name: called username
run: echo "called username. ${{ inputs.username }}"
- name: called is-valid
run: echo "called is-valid. ${{ inputs.is-valid }}"
- name: called secret
run: echo "called secret. ${{ secrets.APPLES }}"
- name: called env (global)
run: echo "called global env. ${{ env.FOO }}"
- name: set variable (GITHUB_ENV)
run: echo "IS_VALID=${{ inputs.is-valid }}" >> "$GITHUB_ENV"
- name: called env (GITHUB_ENV)
run: echo "called env. ${{ env.IS_VALID }}"
- name: output step1
id: step1
run: echo "firstword=hello" >> "$GITHUB_OUTPUT"
- name: output step2
id: step2
run: echo "secondword=world" >> "$GITHUB_OUTPUT"
To call Reusable workflow, use uses: ./.github/workflows/xxxx.yaml
.
When you want pass boolean
type of input from workflow_dispatch to workflow_call, use fromJson(inputs.YOUR_BOOLEAN_PARAMETER)
.
See Type converter with fromJson for the detail.
# .github/workflows/reusable_workflow_caller_internal.yaml
name: reusable workflow caller (internal)
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
workflow_dispatch:
# (Limitation) Callee can not refer caller environment variable.
env:
CALLER_VALUE: caller
jobs:
call-workflow-passing-data:
uses: ./.github/workflows/_reusable_workflow_called.yaml
with:
username: "foo"
is-valid: true
secrets: inherit
job2:
runs-on: ubuntu-latest
needs: call-workflow-passing-data
steps:
- run: echo ${{ needs.call-workflow-passing-data.outputs.firstword }} ${{ needs.call-workflow-passing-data.outputs.secondword }}
Yo call public repository's reusable workflow, use uses: GITHUB_OWNER/REPOSITORY/.github/workflows/xxxx.yaml@<ref>
.
Warning: To call private repository's reusable workflow, you must use absolute path of self repository.
# .github/workflows/reusable_workflow_public_caller.yaml
name: reusable workflow caller (public)
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
workflow_dispatch:
jobs:
call-workflow-passing-data:
uses: guitarrapc/githubactions-lab/.github/workflows/_reusable_workflow_called.yaml@main
with:
username: foo
is-valid: true
secrets: inherit
job2:
runs-on: ubuntu-latest
needs: call-workflow-passing-data
steps:
- run: echo ${{ needs.call-workflow-passing-data.outputs.firstword }} ${{ needs.call-workflow-passing-data.outputs.secondword }}
Reusable Workflow caller cannot use matrix, but callee can use matrix. (see limitation.)
# .github/workflows/reusable_workflow_caller_matrix.yaml
name: reusable workflow caller (matrix)
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
workflow_dispatch:
jobs:
call-matrix-workflow:
strategy:
matrix:
username: [foo, bar]
uses: ./.github/workflows/_reusable_workflow_called.yaml
with:
username: ${{ matrix.username }}
is-valid: true
secrets: inherit
Reusalbe workflow can call other reusable workflow, it means nested call is supported.
# .github/workflows/_reusable_workflow_nest.yaml
name: _reusable workflow nest
on:
workflow_call:
inputs:
username:
required: true
description: username to show
type: string
is-valid:
required: true
description: username to show
type: boolean
# nested call is supported
jobs:
call-workflow-passing-data:
uses: ./.github/workflows/_reusable_workflow_called.yaml
with:
username: ${{ inputs.username }}
is-valid: ${{ inputs.is-valid }}
secrets: inherit
to accomplish sequential job run inside workflow, use needs:
for which you want the job to depends on.
this enforce job to be run when only previous job is success.
# .github/workflows/sequential_run.yaml
name: sequential jobs
on:
workflow_dispatch:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- run: echo "$COMMIT_MESSAGES"
env:
COMMIT_MESSAGES: ${{ toJson(github.event.commits.*.message) }}
publish:
needs: [build]
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- run: echo run when only build success
job-status-check-functions /- Context and expression syntax for GitHub Actions /- GitHub Help
use if:
you want set step to be run on particular status.
# .github/workflows/status_step.yaml
name: status step
on:
workflow_dispatch:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
job:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- run: echo "$COMMIT_MESSAGES"
env:
COMMIT_MESSAGES: ${{ toJson(github.event.commits.*.message) }}
- run: echo "success() run when none of previous steps have failed or been canceled"
if: ${{ success() }}
- run: echo "always() run even cancelled. it runs only when critical failure prevents the task."
if: ${{ always() }}
- run: echo "cancelled() run when Workflow cancelled."
if: ${{ cancelled() }}
- run: echo "failure() run when any previous step of a job fails."
if: ${{ failure() }}
There are many place to support multiline.
run
Use run: |
to write run
statement in multiline.
# .github/workflows/multiline_run.yaml
name: multiline run
on:
workflow_dispatch:
push:
branches: ["main"]
jobs:
push:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- run: |
echo "foo"
echo "bar"
if
Use if: >-
to write if
statement in multiline.
# .github/workflows/multiline_if.yaml
name: multiline if
on:
workflow_dispatch:
push:
branches: ["main"]
jobs:
push:
if: >-
github.event_name == 'push' || github.event.forced == false
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- run: echo "push"
workflow_dispatch:
if: >-
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- run: echo "workflow_dispatch"
matrix cannot reference secret
context, so pass secret key in matrix then dereference secret with secrets[matrix.SECRET_KEY]
.
let's set secrets in settings.
# .github/workflows/matrix_secret.yaml
name: matrix secret
on:
workflow_dispatch:
push:
branches: ["main"]
pull_request:
branches: ["main"]
env:
fruit: APPLES
jobs:
dereference:
strategy:
matrix:
org: [apples, bananas, carrots] #Array of org mnemonics to use below
include:
# includes a new variable for each org (this is effectively a switch statement)
- org: apples
secret: APPLES
- org: bananas
secret: BANANAS
- org: carrots
secret: CARROTS
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- run: echo "org:${{ matrix.org }} secret:${{ secrets[matrix.secret] }}"
- run: echo "org:${{ matrix.org }} secret:${{ secrets[env.secret] }}"
env:
secret: ${{ matrix.secret }}
- run: echo "env:${{ env.fruit }} secret:${{ secrets[env.fruit] }}"
you can refer matrix in job's env:
section before steps.
However you cannot use expression, you must evaluate in step.
# .github/workflows/matrix_envvar.yaml
name: matrix envvar
on:
workflow_dispatch:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
echo:
strategy:
matrix:
org: [apples, bananas, carrots]
runs-on: ubuntu-latest
timeout-minutes: 3
env:
ORG: ${{ matrix.org }}
# you can not use expression. do it on step.
# output on step is -> ci-`date '+%Y%m%d-%H%M%S'`+${GITHUB_SHA:0:6}
# GIT_TAG: "ci-`date '+%Y%m%d-%H%M%S'`+${GITHUB_SHA:0:6}"
steps:
- run: echo "${ORG}"
- run: echo "${NEW_ORG}"
env:
NEW_ORG: new-${{ env.ORG }}
You can set timeout for both job
and steps
.
default timeout is 360min. (6hours)
It is better set much more shorten timeout like 15min or 30min to prevent spending a lot build time.
# .github/workflows/timeout.yaml
name: timeout
on:
workflow_dispatch:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
my-job:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- run: echo "done before timeout"
timeout-minutes: 1 # each step
GitHub Actions built in concurrency control prevent you to run CI at same time. This help you achieve serial build pipeline control.
You can use build context like github.head_ref
or others. This means you can control with commit, branch, workflow and any.
# .github/workflows/concurrency_control.yaml
name: "concurrency control"
# only ${{ github }} context is available
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
on:
workflow_dispatch:
jobs:
long_job:
runs-on: ubuntu-latest
steps:
- run: sleep 60s
Specify cancel-in-progress: true
will cancel parallel build.
# .github/workflows/concurrency_control_cancel_in_progress.yaml
name: "concurrency control cancel in progress"
# only ${{ github }} context is available
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
on:
workflow_dispatch:
jobs:
long_job:
runs-on: ubuntu-latest
steps:
- run: sleep 60s
GitHub Actions offer workflow_dispatch
event to execute workflow manually from Web UI.
Also you can use action inputs to specify value trigger on manual trigger.
# .github/workflows/manual_trigger.yaml
name: manual trigger
on:
workflow_dispatch:
inputs:
branch:
description: "branch name to clone"
required: true
default: "main"
logLevel:
description: "Log level"
required: true
default: "warning"
tags:
description: "Test scenario tags"
required: false
jobs:
printInputs:
runs-on: ubuntu-latest
timeout-minutes: 3
env:
BRANCH: ${{ inputs.branch }}
LOGLEVEL: ${{ inputs.logLevel }}
TAGS: ${{ inputs.tags }}
steps:
- run: echo ${{ env.BRANCH }} ${{ env.LOGLEVEL }} ${{ env.TAGS }}
- uses: actions/checkout@v4
with:
ref: ${{ inputs.branch }}
- name: dump github context
run: echo "$CONTEXT"
env:
CONTEXT: ${{ toJson(github) }}
- name: dump inputs context
run: echo "$CONTEXT"
env:
CONTEXT: ${{ toJson(github.event.inputs) }}
- name: Show Input value
run: |
echo "Log level: ${{ inputs.logLevel }}"
echo "Tags: ${{ inputs.tags }}"
- name: INPUT_ is not generated automatcally
run: |
echo "${INPUT_TEST_VAR}"
echo "${TEST_VAR}"
- name: Add PATH
run: echo "/path/to/dir" | tee -a "$GITHUB_PATH"
- name: Set inputs to Environment Variables
run: |
echo "INPUT_LOGLEVEL=${{ inputs.logLevel }}" | tee -a "$GITHUB_ENV"
echo "INPUT_TAGS=${{ inputs.tags }}" | tee -a "$GITHUB_ENV"
- name: Show Input value
run: |
echo "Log level: ${{ env.INPUT_LOGLEVEL }}"
echo "Tags: ${{ env.INPUT_TAGS }}"
- name: Show Environment Variables
run: env
Even if you specify action inputs, input value will not store as ENV var INPUT_{INPUTS_ID}
as usual.
Workflow dispatch supported input type.
true
or false
and Web UI offers checkbox.# .github/workflows/workflow_dispatch_mixed_inputs.yaml
name: workflow dispatch mixed inputs
on:
workflow_dispatch:
inputs:
name:
type: choice
description: "name: Who to greet"
required: true
options:
- monalisa
- cschleiden
message:
description: "mnessage: add message"
required: true
use-emoji:
type: boolean
description: "use-emoji: Include 🎉🤣 emojis"
required: true
environment:
type: environment
description: "environment: Select environment"
required: true
jobs:
greet:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- name: Send greeting (github.event.inputs)
run: |
echo "message: ${{ github.event.inputs.message }}"
echo "name: ${{ github.event.inputs.name }}"
echo "use-emoji (string): ${{ github.event.inputs.use-emoji == 'true' }}"
echo "use-emoji (bool): ${{ github.event.inputs.use-emoji == true }}"
- name: Send greeting (inputs)
run: |
echo "message: ${{ inputs.message }}"
echo "name: ${{ inputs.name }}"
echo "use-emoji (string): ${{ inputs.use-emoji == 'true' }}"
echo "use-emoji (bool): ${{ inputs.use-emoji == true }}"
- name: Emoji
run: echo "🥳 😊"
Note Consider using Workflow Concurrency control instead of redundant control.
Creating PR emmits two events, push
and pull_request/synchronize
. This means duplicate build began and wastes build time.
Redundant build may trouble when you are runnning Private Repository, bacause there are build time limits. In other words, you don't need mind build comsume time when repo is Public.
Avoid push on pull_request trigger on same repo
In this example push
will trigger only when main
, default branch. This means push will not run when pull_request
synchronize event was emmited.
Simple enough for almost usage.
# .github/workflows/push_and_pr_avoid_redundant.yaml
name: push and pull_request avoid redundant
on:
# prevent push run on pull_request
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
my-job:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- run: echo push and pull_request trigger
redundant build cancel
Cancel duplicate workflow and mark CI failure.
# .github/workflows/cancel_redundantbuild.yaml
name: cancel redundant build
# when pull_request, both push and pull_request (synchronize) will trigger.
# this action sample will prevent duplicate run, but run only 1 of them.
on:
workflow_dispatch:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
cancel:
runs-on: ubuntu-latest
steps:
# no check for main and tag
- uses: rokroskar/workflow-run-cleanup-action@v0.3.3
if: ${{ !startsWith(github.ref, 'refs/tags/') && github.ref != 'refs/heads/main' }}
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
Better using manual gh.
[TBD]
You can detect which file was changed with push or pull_request by GitHub actions. This is useful when you want to use path-filter
, but require further file handling. Following 3 actions are available and can use same way.
Recommented
tj-actions/changed-files
is still actively developed. Usage is simple and output is static.# .github/workflows/file_change_detect_tj.yaml
name: file change detect tj
on:
workflow_dispatch:
pull_request:
branches: ["main"]
push:
branches: ["main"]
jobs:
job:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # push required 2 or 0 to detect last commit change
# see: https://github.com/tj-actions/changed-files
- id: changed-files
uses: tj-actions/changed-files@v45 # this action force fetch base branch and compare.
with:
separator: "," # default ' '
- name: Changed file list
run: echo "${{ steps.changed-files.outputs.all_modified_files }}"
- name: Is changed files include .github/workflows?
run: echo "${{ contains(steps.changed-files.outputs.all_modified_files, '.github/workflows')}}"
- name: Is changed files include .github/dummy?
run: echo "${{ contains(steps.changed-files.outputs.all_modified_files, '.github/dummy')}}"
# space separated
- id: changed-files2
uses: tj-actions/changed-files@v45
if: ${{ github.event.pull_request.changed_files < 100 }} # when changed files less than 100
- name: List all changed files
env:
CHANGED_FILES: ${{ steps.changed-files2.outputs.all_changed_files }}
run: |
for file in ${CHANGED_FILES}; do
echo "$file was changed"
done
# json separated
- id: changed-files3
uses: tj-actions/changed-files@v45 # this action force fetch base branch and compare.
with:
json: "true"
- name: Changed file list
run: echo "${{ steps.changed-files3.outputs.all_modified_files }}"
Not recommended
dorny/paths-filter
is still actively developed. However it's output is quite dynamic and hard to handle static lint like actionlint.
# .github/workflows/file_change_detect_dorny.yaml
name: file change detect dorny
on:
pull_request:
branches: ["main"]
push:
branches: ["main"]
jobs:
job:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
# see: https://github.com/dorny/paths-filter/blob/master/README.md
- id: changed-files
uses: dorny/paths-filter@v3
with:
base: ${{ github.event_name == 'push' && github.ref || '' }}
list-files: csv # default 'none'. Disables listing of matching files.
filters: |
foo:
- '**'
- name: Is any change happen on some filters?
run: echo "${{ steps.changed-files.outputs.changes }}"
- name: Is change happen on foo filter?
run: echo "${{ steps.changed-files.outputs.foo }}"
- name: Changed file list for foo filter
run: echo "${{ steps.changed-files.outputs.foo_files }}"
- name: Is foo filter changed files include .github/workflows?
run: echo "${{ contains(steps.changed-files.outputs.foo_files, '.github/workflows')}}"
- name: Is foo filter changed files include .github/dummy?
run: echo "${{ contains(steps.changed-files.outputs.foo_files, '.github/dummy')}}"
# space separated
- id: changed-files2
uses: dorny/paths-filter@v3
if: ${{ github.event.pull_request.changed_files < 100 }} # when changed files less than 100
with:
base: ${{ github.event_name == 'push' && github.ref || '' }}
list-files: shell
filters: |
foo:
- '**'
- name: List all changed files
env:
CHANGED_FILES: ${{ steps.changed-files2.outputs.foo_files }}
run: |
for file in ${CHANGED_FILES}; do
echo "$file was changed"
done
# json separated
- id: changed-files3
uses: dorny/paths-filter@v3
with:
base: ${{ github.event_name == 'push' && github.ref || '' }}
list-files: json
filters: |
foo:
- '**'
- name: Changed file list for foo filter
run: echo "${{ steps.changed-files3.outputs.foo_files }}"
trilom/file-changes-action
stopped development, so I will quit using it.
# .github/workflows/file_change_detect_dorny.yaml
name: file change detect dorny
on:
pull_request:
branches: ["main"]
push:
branches: ["main"]
jobs:
job:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
# see: https://github.com/dorny/paths-filter/blob/master/README.md
- id: changed-files
uses: dorny/paths-filter@v3
with:
base: ${{ github.event_name == 'push' && github.ref || '' }}
list-files: csv # default 'none'. Disables listing of matching files.
filters: |
foo:
- '**'
- name: Is any change happen on some filters?
run: echo "${{ steps.changed-files.outputs.changes }}"
- name: Is change happen on foo filter?
run: echo "${{ steps.changed-files.outputs.foo }}"
- name: Changed file list for foo filter
run: echo "${{ steps.changed-files.outputs.foo_files }}"
- name: Is foo filter changed files include .github/workflows?
run: echo "${{ contains(steps.changed-files.outputs.foo_files, '.github/workflows')}}"
- name: Is foo filter changed files include .github/dummy?
run: echo "${{ contains(steps.changed-files.outputs.foo_files, '.github/dummy')}}"
# space separated
- id: changed-files2
uses: dorny/paths-filter@v3
if: ${{ github.event.pull_request.changed_files < 100 }} # when changed files less than 100
with:
base: ${{ github.event_name == 'push' && github.ref || '' }}
list-files: shell
filters: |
foo:
- '**'
- name: List all changed files
env:
CHANGED_FILES: ${{ steps.changed-files2.outputs.foo_files }}
run: |
for file in ${CHANGED_FILES}; do
echo "$file was changed"
done
# json separated
- id: changed-files3
uses: dorny/paths-filter@v3
with:
base: ${{ github.event_name == 'push' && github.ref || '' }}
list-files: json
filters: |
foo:
- '**'
- name: Changed file list for foo filter
run: echo "${{ steps.changed-files3.outputs.foo_files }}"
Schedule job will offer Last commit on default branch
.
schedule workflow should merge to default branch to apply workflow change.
Pass branch info when you want run checkout on non-default branch.
Don't forget pretend refs/heads/
to your branch.
# .github/workflows/schedule_job.yaml
name: schedule job
on:
schedule:
- cron: "0 0 * * *"
jobs:
job:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- name: Dump GitHub context
run: echo "$CONTEXT"
env:
CONTEXT: ${{ toJson(github) }}
- uses: actions/checkout@v4
with:
ref: refs/heads/some-branch
You can create release and upload assets through GitHub Actions.
Multiple assets upload is supported by running running actions/upload-release-asset
for each asset.
# .github/workflows/create_release.yaml
name: create release
concurrency: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
on:
push:
tags:
- "[0-9]+.[0-9]+.[0-9]+*"
pull_request:
branches: ["main"]
workflow_dispatch:
inputs:
tag:
description: "tag: git tag you want create. (1.0.0)"
required: true
delete-release:
description: "delete-release: delete release after creation. (true/false)"
required: false
default: false
type: boolean
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
jobs:
create-release:
if: ${{ github.actor != 'dependabot[bot]' }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Setup tag
id: tag
run: echo "value=${{ inputs.tag || (github.event_name == 'pull_request' && '0.1.0-test' || github.ref_name) }}" | tee -a "$GITHUB_OUTPUT"
# Create Tag
- uses: actions/checkout@v4
- name: Create Tag and push if not exists
run: |
if ! git ls-remote --tags | grep ${{ steps.tag.outputs.value }}; then
git tag ${{ steps.tag.outputs.value }}
git push origin ${{ steps.tag.outputs.value }}
git ls-remote --tags
fi
# set release tag(*.*.*) to version string
- run: echo "foo" > "foo.${{ steps.tag.outputs.value }}.txt"
- run: echo "hoge" > "hoge.${{ steps.tag.outputs.value }}.txt"
- run: echo "fuga" > "fuga.${{ steps.tag.outputs.value }}.txt"
- run: ls -l
# Create Releases
- name: Create Release
run: gh release create ${{ steps.tag.outputs.value }} --draft --verify-tag --title "Ver.${{ steps.tag.outputs.value }}" --generate-notes
- name: Upload file to release
run: gh release upload ${{ steps.tag.outputs.value }} hoge.${{ steps.tag.outputs.value }}.txt fuga.${{ steps.tag.outputs.value }}.txt
- name: Upload additional file to release
run: gh release upload ${{ steps.tag.outputs.value }} foo.${{ steps.tag.outputs.value }}.txt
# Clean up
- name: Clean up (Wait 60s and delete Release)
run: |
if gh release list | grep Draft | grep ${{ steps.tag.outputs.value }}; then
sleep 60
gh release delete ${{ steps.tag.outputs.value }} --yes --cleanup-tag
fi
if: ${{ failure() || inputs.delete-release || (github.event_name == 'pull_request' || github.event_name == 'push') }}
If you want run job only when push to branch, and not for tag push.
# .github/workflows/branch_push_only.yaml
name: branch push only
on:
push:
branches: ["main"]
tags:
- "!*" # not a tag push
jobs:
aws:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- run: echo not run on tag
# .github/workflows/trigger_ci.yaml
name: trigger ci commit
on:
push:
branches: ["main"]
jobs:
job:
if: ${{ contains(toJSON(github.event.commits.*.message), '[build]') }}
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- run: echo "$COMMIT_MESSAGES"
env:
COMMIT_MESSAGES: ${{ toJson(github.event.commits.*.message) }}
If you want run job only when push to tag, and not for branch push.
# .github/workflows/tag_push_only.yaml
name: tag push only
on:
push:
tags:
- "**" # only tag
jobs:
job:
runs-on: ubuntu-latest
steps:
- run: echo not run on branch push
You can use pattern on on.push.tags
, but you can't on step.if
.
This pattern will match following.
not for below.
# .github/workflows/tag_push_only_pattern.yaml
name: tag push only pattern
on:
push:
tags:
- "[0-9]+.[0-9]+.[0-9]+*" # only tag with pattern match
jobs:
job:
runs-on: ubuntu-latest
steps:
- run: echo not run on branch push
pull_request
event contains tags and you can use it to filter step execution.
${{ contains(github.event.pull_request.labels.*.name, 'hoge') }}
will return true
if tag contains hoge
.
# .github/workflows/pr_label_get.yaml
name: pr label get
on:
pull_request:
types:
- labeled
- opened
- reopened
- synchronize
jobs:
changes:
runs-on: ubuntu-latest
timeout-minutes: 3
env:
IS_HOGE: "false"
steps:
- run: echo "${{ toJson(github.event.pull_request.labels.*.name) }}"
- run: echo "IS_HOGE=${{ contains(github.event.pull_request.labels.*.name, 'hoge') }}" >> "$GITHUB_ENV"
- run: echo "${IS_HOGE}"
- run: echo "run!"
if: ${{ env.IS_HOGE == 'true' }}
original pull_request
event will invoke when activity type is opened
, synchronize
, or reopened
.
# .github/workflows/skip_ci_pr_title.yaml
name: skip ci pr title
on: ["pull_request"]
jobs:
skip:
if: ${{ !(contains(github.event.pull_request.title, '[skip ci]') || contains(github.event.pull_request.title, '[ci skip]')) }}
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- run: echo "$GITHUB_CONTEXT"
env:
GITHUB_CONTEXT: ${{ toJson(github) }}
- run: echo "$TITLE"
env:
TITLE: ${{ toJson(github.event.pull_request.title) }}
build:
runs-on: ubuntu-latest
timeout-minutes: 3
needs: skip
steps:
- run: echo run when not skipped
default pull_request
event trigger from even fork repository, however fork pr could not read secrets
and may fail PR checks.
To control job to be skip from fork but run on self pr or push, use if
conditions.
# .github/workflows/skip_pr_from_fork.yaml
name: skip pr from fork
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
types:
- opened
- synchronize
jobs:
build:
# push & my repo will trigger
# pull_request on my repo will trigger
if: ${{ (github.event_name == 'push' && github.repository_owner == 'guitarrapc') || startsWith(github.event.pull_request.head.label, 'guitarrapc:') }}
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- run: echo build
You can skip job and steps if Pull Request is Draft.
Unfortunately GitHub Webhook v3 event not provide draft pr type, but event.pull_request.draft
shows true
when PR is draft.
# .github/workflows/skip_draft_pr.yaml
name: skip draft pr
on:
pull_request:
push:
branches: ["main"]
jobs:
job:
if: ${{ ! github.event.pull_request.draft }}
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
You can control behaviour with PR label.
# .github/workflows/skip_draft_but_label_pr.yaml
name: skip draft pr but label
on:
pull_request:
types:
- synchronize
- opened
- reopened
- ready_for_review
jobs:
build:
# RUN WHEN
# 1. PR has label 'draft_but_ci'
# 2. Not draft, `push` and `non-draft pr`.
if: ${{ (contains(github.event.pull_request.labels.*.name, 'draft_but_ci')) || !(github.event.pull_request.draft) }}
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
You cannot use ${{ env. }}
in env:
section.
Following is invalid with error.
The workflow is not valid. .github/workflows/env_refer_env.yaml (Line: 12, Col: 16): Unrecognized named-value: 'env'. Located at position 1 within expression: env.global_env
name: you can not refer env in env
on: ["push"]
env:
global_env: global
jobs:
build:
runs-on: ubuntu-latest
env:
job_env: ${{ env.global_env }}
steps:
- run: echo "${{ env.global_env }}"
- run: echo "${{ env.job_env }}"
Advanced tips.
You can use Dependabot to update GitHub Actions version automatically. Dependabot create pull request to keep your actions up to date, and you can merge it manually or automatically.
To enable Dependabot for GitHub Actions update, add .github/dependabot.yml
to your repository.
# .github/dependabot.yaml
# ref: https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly" # Check for updates to GitHub Actions every week
Customize dependabot.yaml
There are some configuration options for dependabot.yaml file.
Accessing secrets on dependabot action
When a Dependabot event triggers a workflow, the only secrets available to the workflow are Dependabot secrets. GitHub Actions secrets are not available.
Therefore I recommend not to use secret for Dependabot triggered workflows. If you need secrets, then put same named secret to Dependabot secret.
GitHub Actions actions/upload-artifact and actions/download-artifact offer artifact handling between jobs. You can upload and download artifact to/from GitHub Actions.
file
# .github/workflows/artifacts_file.yaml
name: artifacts (file)
on:
workflow_dispatch:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
# single file
upload-file:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- name: output
run: |
echo "hoge" > ./hoge.txt
- uses: actions/upload-artifact@v4
with:
name: hoge.txt
path: ./hoge.txt
retention-days: 1
download-file:
needs: [upload-file]
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/download-artifact@v4
with:
name: hoge.txt
path: .
- name: ls
run: ls -lR
- name: cat hoge.txt
run: cat hoge.txt
directory
# .github/workflows/artifacts_directory.yaml
name: artifacts (directory)
on:
workflow_dispatch:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
# directory
upload-directory:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- name: output
run: |
mkdir -p ./directory/bin
echo "hoge" > ./directory/hoge.txt
echo "fuga" > ./directory/fuga.txt
echo "foo" > ./directory/bin/foo.txt
echo "bar" > ./directory/bin/bar.txt
- uses: actions/upload-artifact@v4
with:
name: directory
path: ./directory/
retention-days: 1
download-directory:
needs: [upload-directory]
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/download-artifact@v4
with:
name: directory
path: ./directory
- name: ls
run: ls -lR
- name: cat hoge.txt
run: cat directory/hoge.txt
.tar.gz
# .github/workflows/artifacts_targz.yaml
name: artifacts (tar.gz)
on:
workflow_dispatch:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
# tar.gz
upload-targz:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- name: output
run: |
mkdir -p ./output/bin
echo "hoge" > ./output/hoge.txt
echo "fuga" > ./output/fuga.txt
echo "foo" > ./output/bin/foo.txt
echo "bar" > ./output/bin/bar.txt
tar -zcvf output.tar.gz ./output/
- uses: actions/upload-artifact@v4
with:
name: output.tar.gz
path: ./output.tar.gz
retention-days: 1
download-targz:
needs: [upload-targz]
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
# specify path: . to download tar.gz to current directory
- uses: actions/download-artifact@v4
with:
name: output.tar.gz
path: .
- name: ls
run: ls -lR
- name: expand
run: tar -zxvf output.tar.gz
- name: ls
run: ls -lR
- name: cat hoge.txt
run: cat ./output/hoge.txt
- name: cat foo.txt
run: cat ./output/bin/foo.txt
actions/checkout supports both shallow-clone and sparse checkout which is quite useful for monorepository. Typically, monorepository contains many folders and files, but you may want to checkout only specific folder or files.
shallow-clone
offers faster checkout by limiting depth of clone to latest 1 or N commits.sparse checkout
offers faster checkout by limiting checkout files and folders.Note: actions/checkout supports
git sparse-checkout
, since 2023/June.
Let's see what is difference between shallow-clone
and sparse-checkout
.
Shallow clone
Shallow clones use the --depth=<N>
parameter in git clone
to truncate the commit history. Typically, --depth=1 signifies that we only care about the most recent commits. This drastically reduces the amount of data that needs to be fetched, leading to faster clones and less storage of shallow history.
ref: https://github.blog/2020-12-21-get-up-to-speed-with-partial-clone-and-shallow-clone/
Sparse checkout
Sparse checkout use the git sparse-checkout set <PATH>
before git clone
to truncate the checkout files and folders. This amazingly reduces the amount of data that needs to be fetched, leading to faster checkout and less storage of limited paths.
ref: https://github.blog/2020-01-17-bring-your-monorepo-down-to-size-with-sparse-checkout/
Sparce checkout has 2 modes, git sparse-checkout
and git sparse-checkout --cone
. You can specify cone
or not with sparse-checkout-cone-mode
option. So what the difference between cone
and not cone
? Normally sparse-checkout-cone-mode: true
is faster than sparse-checkout-cone-mode: false
. But cone
mode has some limitation, you cannot exclude specific folder. So you need to choose which mode is better for you.
sparse checkout: src
& sparse-checkout-cone-mode: true
, checkout src
folder and root files.sparse checkout: src/*
& sparse-checkout-cone-mode: false
, checkout src
folder only.sparse checkout: !src
& sparse-checkout-cone-mode: true
, you can not use sparse-checkout-cone-mode: true
with exclude folder.sparse checkout: !src/*
& sparse-checkout-cone-mode: false
, you can exlude src
folder from checkout, but you need specify which folder you want to checkout.Sparse checkout
Checkout "src/*" and root files, but not checkout any not specified folders.
# .github/workflows/git_sparsecheckout.yaml
name: git sparsecheckout
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
sparse-checkout:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
src
- name: list root folders
run: ls -la
- name: list src folders
run: ls -laR ./src
Result is selected src
folder and root files will checkout.
$ ls -la
total 104
drwxr-xr-x 4 runner docker 4096 Jun 14 10:23 .
drwxr-xr-x 3 runner docker 4096 Jun 14 10:23 ..
-rw-r--r-- 1 runner docker 3557 Jun 14 10:23 .editorconfig
drwxr-xr-x 8 runner docker 4096 Jun 14 10:23 .git
-rw-r--r-- 1 runner docker 103 Jun 14 10:23 .gitattributes
-rw-r--r-- 1 runner docker 5 Jun 14 10:23 .gitignore
-rw-r--r-- 1 runner docker 1083 Jun 14 10:23 LICENSE.md
-rw-r--r-- 1 runner docker 70249 Jun 14 10:23 README.md
drwxr-xr-x 8 runner docker 4096 Jun 14 10:23 src
$ ls -laR ./src
./src:
total 32
drwxr-xr-x 8 runner docker 4096 Jun 14 10:23 .
drwxr-xr-x 4 runner docker 4096 Jun 14 10:23 ..
drwxr-xr-x 5 runner docker 4096 Jun 14 10:23 dotnet
drwxr-xr-x 2 runner docker 4096 Jun 14 10:23 json
drwxr-xr-x 6 runner docker 4096 Jun 14 10:23 k8s
drwxr-xr-x 2 runner docker 4096 Jun 14 10:23 mermaid
drwxr-xr-x 2 runner docker 4096 Jun 14 10:23 shellscript
drwxr-xr-x 2 runner docker 4096 Jun 14 10:23 txt
.... others
Sparse checkout and specify which file to checkout
Checkout only "src/*" path. All other files and folders will not checkout.
# .github/workflows/git_sparsecheckout_nocorn.yaml
name: git sparsecheckout (no corn)
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
sparse-checkout:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
src/*
sparse-checkout-cone-mode: false # required for ! entry to work
- name: list root folders
run: ls -la
- name: list src folders
run: ls -laR ./src
Result is selected src
folder and root files will checkout.
$ ls -la
total 16
drwxr-xr-x 4 runner docker 4096 Jun 14 10:23 .
drwxr-xr-x 3 runner docker 4096 Jun 14 10:23 ..
drwxr-xr-x 8 runner docker 4096 Jun 14 10:23 .git
drwxr-xr-x 8 runner docker 4096 Jun 14 10:23 src
$ ls -laR ./src
./src:
total 32
drwxr-xr-x 8 runner docker 4096 Jun 14 10:23 .
drwxr-xr-x 4 runner docker 4096 Jun 14 10:23 ..
drwxr-xr-x 5 runner docker 4096 Jun 14 10:23 dotnet
drwxr-xr-x 2 runner docker 4096 Jun 14 10:23 json
drwxr-xr-x 6 runner docker 4096 Jun 14 10:23 k8s
drwxr-xr-x 2 runner docker 4096 Jun 14 10:23 mermaid
drwxr-xr-x 2 runner docker 4096 Jun 14 10:23 shellscript
drwxr-xr-x 2 runner docker 4096 Jun 14 10:23 txt
.... others
Sparse checkout exclude path
Checkout except "src/" path. All other files and folders will checkout by `/`.
# .github/workflows/git_sparsecheckout_exclude.yaml
name: git sparsecheckout nocorn
on:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
sparse-checkout:
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v4
with:
sparse-checkout: |
!src/*
/*
sparse-checkout-cone-mode: false # required for ! entry to work
- name: list root folders
run: ls -la
- name: list .github folders
run: ls -laR ./.github
Result is exclude src
folder and all other files are checkout.
$ ls -la
total 108
drwxr-xr-x 5 runner docker 4096 Jun 14 10:23 .
drwxr-xr-x 3 runner docker 4096 Jun 14 10:23 ..
-rw-r--r-- 1 runner docker 3557 Jun 14 10:23 .editorconfig
drwxr-xr-x 8 runner docker 4096 Jun 14 10:23 .git
-rw-r--r-- 1 runner docker 103 Jun 14 10:23 .gitattributes
drwxr-xr-x 5 runner docker 4096 Jun 14 10:23 .github
-rw-r--r-- 1 runner docker 5 Jun 14 10:23 .gitignore
-rw-r--r-- 1 runner docker 1083 Jun 14 10:23 LICENSE.md
-rw-r--r-- 1 runner docker 70249 Jun 14 10:23 README.md
drwxr-xr-x 5 runner docker 4096 Jun 14 10:23 samples
$ ls -laR ./.github
./.github:
total 24
drwxr-xr-x 5 runner docker 4096 Jun 14 10:23 .
drwxr-xr-x 5 runner docker 4096 Jun 14 10:23 ..
drwxr-xr-x 12 runner docker 4096 Jun 14 10:23 actions
-rw-r--r-- 1 runner docker 117 Jun 14 10:23 ban-words.txt
drwxr-xr-x 2 runner docker 4096 Jun 14 10:23 scripts
drwxr-xr-x 3 runner docker 4096 Jun 14 10:23 workflows
.... others
You can dispatch this repository to other repository via calling GitHub workflow_dispatch
event API.
You don't need use repository_dispatch
event API anymore.
Target repository workflow
Here's target repo testtest
workflow test.yaml
.
name: test
on:
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
Dispatcher workflow
This repo will dispatch event with Workflow Dispatch Action actions.
# .github/workflows/dispatch_changes_actions.yaml
name: dispatch changes actions
on:
workflow_dispatch:
jobs:
dispatch:
runs-on: ubuntu-latest
timeout-minutes: 5
strategy:
matrix:
repo: [guitarrapc/testtest] #Array of target repos
include:
- repo: guitarrapc/testtest
ref: main
workflow: test
steps:
- name: dispatch ${{ matrix.repo }}
uses: benc-uk/workflow-dispatch@v1.2
with:
repo: ${{ matrix.repo }}
ref: ${{ matrix.ref }}
workflow: ${{ matrix.workflow }}
token: ${{ secrets.SYNCED_GITHUB_TOKEN_REPO }}
One of GitHub's vulnerable point is Workflow. Editting Workflow shoulbe be requirement when using secrets
and authenticate some service on workflow.
Easiest and simple way is use pull_request
target and path filter, then detect PR is fork or not. There might be many ways to prevent file change. xalvarez/prevent-file-change-action
can guard change in the step. Using tj-actions/changed-files
, dorny/paths-filter
, or others will be flexible way to detect change and do what you want.
# .github/workflows/prevent_file_change1.yaml
name: prevent file change 1
on:
pull_request:
branches: ["main"]
paths:
- .github/**/*.yaml
jobs:
detect:
if: ${{ github.event.pull_request.head.repo.fork }} # is Fork
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- name: "Prevent file change"
run: exit 1
# .github/workflows/prevent_file_change2.yaml
name: prevent file change 2
on:
pull_request:
branches: ["main"]
paths:
- .github/**/*.yaml
permissions:
pull-requests: read # only read required
jobs:
detect:
if: ${{ github.actor != 'dependabot[bot]' }}
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- name: Prevent file change for github YAML files.
uses: xalvarez/prevent-file-change-action@v1
with:
githubToken: ${{ secrets.GITHUB_TOKEN }}
pattern: ^\.github\/.*.y[a]?ml$ # -> .github/**/*.yaml
trustedAuthors: ${{ github.repository_owner }} # , separated. allow repository owner to change
# .github/workflows/prevent_file_change3.yaml
name: prevent file change 3
on:
pull_request:
branches: ["main"]
paths:
- .github/**/*.yaml
jobs:
detect:
if: ${{ github.event.pull_request.head.repo.fork }} # is Fork
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 2 # To retrieve the preceding commit.
- name: Get changed files in the .github folder
id: changed-files
uses: tj-actions/changed-files@v45
with:
files: .github/**/*.{yml,yaml}
- name: Run step if any file(s) in the .github folder change
if: ${{ steps.changed-files.outputs.any_changed == 'true' }}
run: |
echo "One or more files has changed."
echo "List all the files that have changed: ${{ steps.changed-files.outputs.all_changed_files }}"
exit 1
# .github/workflows/prevent_file_change4.yaml
name: prevent file change 4
on:
pull_request:
branches: ["main"]
paths:
- .github/**/*.yaml
jobs:
detect:
if: ${{ github.event.pull_request.head.repo.fork }} # is Fork
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- name: Get changed files in the .github folder
uses: dorny/paths-filter@v3
id: changes
with:
filters: |
src:
- .github/**/*.yaml
- name: Run step if any file(s) in the .github folder change
if: ${{ steps.changes.outputs.src == 'true' }}
run: |
echo "One or more files has changed."
echo "List all the files that have changed: ${{ steps.changes.outputs.changes }}"
exit 1
You can lint GitHub Actions yaml via actionlint. If you don't need automated PR review, run actionlint is enough.
# .github/workflows/actionlint.yaml
name: actionlint
on:
workflow_dispatch:
pull_request:
branches: ["main"]
paths:
- ".github/workflows/**"
schedule:
- cron: "0 0 * * *"
jobs:
actionlint:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
- name: Install actionlint
run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
- name: Run actionlint
run: ./actionlint -color -oneline
If you need automated PR review, run actionlint with reviewdog.
# .github/workflows/actionlint_reviewdog.yaml
name: actionlint (reviewdog)
on:
workflow_dispatch:
pull_request:
branches: ["main"]
paths:
- ".github/workflows/**"
jobs:
actionlint:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
- name: actionlint
uses: reviewdog/action-actionlint@v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
reporter: github-pr-review
fail_on_error: true # workflow will fail when actionlint detect warning.
You have two choice.
Below use jwalton/gh-find-current-pr to retrieve PR info from merge commit.
# .github/workflows/pr_from_merge_commit.yaml
name: pr from merge commit
on:
push:
branches: ["main"]
jobs:
get:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- uses: actions/checkout@v4
- uses: jwalton/gh-find-current-pr@v1
id: pr
with:
state: closed
- if: ${{ success() && steps.pr.outputs.number }}
run: |
echo "PR #${PR_NUMBER}"
echo "PR Title: ${PR_TITLE}"
env:
PR_NUMBER: ${{ steps.pr.outputs.number }}
PR_TITLE: ${{ steps.pr.outputs.title }}
GitHub Actions runforesight/workflow-telemetry-action offers workflow telemetry. Telemetry indicate which step consume much Execution Time, CPU, Memory and Network I/O. Default settings post telemetry result to PR comment and JOB Summary.
To enable telemetry, set runforesight/workflow-telemetry-action@v1
on the first step of your job, then action collect telemetry for later steps.
# .github/workflows/actions_telemetry.yaml
name: actions telemetry
on:
workflow_dispatch:
push:
branches: ["main"]
pull_request:
branches: ["main"]
jobs:
dotnet:
runs-on: ubuntu-latest
timeout-minutes: 3
steps:
- name: Collect actions workflow telemetry
uses: runforesight/workflow-telemetry-action@v2
with:
theme: dark # or light. dark generate charts compatible with Github dark mode.
comment_on_pr: false # post telemetry to PR comment. It won't override existing comment, therefore too noisy for PR.
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: 8.0.x
- name: dotnet build
run: dotnet build ./src/dotnet -c Debug
- name: dotnet test
run: dotnet test ./src/dotnet -c Debug --logger GitHubActions --logger "console;verbosity=normal"
- name: dotnet publish
run: dotnet publish ./src/dotnet/ -c Debug
Telemetry is posted to Job Summary.
Also if workflow ran with pull_request
trigger, then you can enable PR comment by default or set comment_on_pr: true
.
GitHub Actions cheet sheet.
Follow to setup-foo
style.
Use Hyphen -
instead of Underscore _
.
setup-foo
setup_foo
action folder naming also follow this rule.
.github/actions/setup-foo
.github/actions/setup_foo
github.ref
context will return branch name, however it is unsafe to directly reference in ref. It is recommended to use through env.
${{ github.event.pull_request.head.ref }}
${{ github.ref }}
# .github/workflows/_reusable_dump_context.yaml#L20-L22
# PR should checkout HEAD ref instead of merge commit. -> github.head.ref
# PR close delete branch, so it should checkout BASE ref instead of HEAD ref. -> github.base_ref
# Tag ref is tag version, let's checkout default branch instead of ref. -> github.event.repository.default_branch
Trigger push with tag, then you have 2 choice.
echo "${{ github.ref_name }}"
echo "${GITHUB_REF##*/}"
refs/heads/xxxxx
-> xxxxx
refs/tags/v1.0.0
-> v1.0.0
# .github/workflows/tag_push_only_context.yaml
name: tag push context
on:
push:
tags:
- "**" # only tag
jobs:
ref:
runs-on: ubuntu-latest
steps:
- name: Use GITHUB_REF and GITHUB_OUTPUT
run: echo "GIT_TAG=${GITHUB_REF##*/}" >> "$GITHUB_OUTPUT"
id: CI_TAG
- name: Use GITHUB_REF and GITHUB_ENV
run: echo "GIT_TAG=${GITHUB_REF##*/}" >> "$GITHUB_ENV"
- name: Show tag value by GITHUB_REF
run: |
echo "${{ steps.CI_TAG.outputs.GIT_TAG }}"
echo "${{ env.GIT_TAG }}"
- name: Show tag value by github.ref_name
run: |
echo "${{ github.ref_name }}"
${{ github.workflow }}
${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
Use following Git config to commit as GitHub Actions icon.
git config user.name github-actions[bot]
git config user.email 41898282+github-actions[bot]@users.noreply.github.com
There are some cases you want convert string to other type.
Consider you want use boolean input is-valid
with workflow_dispatch, then pass it to workflow_call as boolean.
github.event.inputs
context treat all value as string
, so github.event.inputs.is-valid
isn't boolean any more.
fromJson
expression is the trick to convert type from string to boolean.
${{ github.event.inputs.foobar == "true" }} # true. type is string
${{ fromJson(github.event.inputs.foobar) == true }} # true. string convert to boolean
Other way is use inputs.foobar
context. inputs
have type information and pass exactly as is to other workflow calls.
${{ inputs.foobar == 'true' }} # false. type is not string
${{ inputs.foobar == true }} # true. type is boolean
There are several way to achieve it. Most simple and easy to understand is fork
boolean.
fork
boolean.# Fork
if: ${{ github.event.pull_request.head.repo.fork }}
# Not Fork
if: ${{ ! github.event.pull_request.head.repo.fork }}
full_name
is match to repo.# Fork
if: ${{ github.event.pull_request.head.repo.full_name != 'org/repo' }}
# Not Fork
if: ${{ github.event.pull_request.head.repo.full_name == 'org/repo' }}
# Fork
if: ${{ ! startsWith(github.event.pull_request.head.label, format('{0}:', github.repository_owner)) }}
# Not Fork
if: ${{ startsWith(github.event.pull_request.head.label, format('{0}:', github.repository_owner)) }}