This repository holds most reusable workflow for my own projects.
- Primary workflows
- Pull request validation: run on pull request
- Continuous deployment: after merging into
mainbranch- Deploy to GitHub Pages
- If merge is on prerelease, deploy prerelease to NPM
- If merge in on production, deploy to GitHub Release
- Production deployment: after release is created
- Download and deploy to NPM
- Pull request to bump to next prepatch
- One step, one command
- Do not put many commands into a single step, it's not easy to know which command failed the whole step
- Smaller steps limit exposure of token/env
- Programmatically build job matrix using
fromJSONecho matrix=jq -cnr '["package-1", "package-2"]' >> $GITHUB_OUTPUTmatrix: ${{ fromJSON(needs.prepare.outputs.matrix) }}
- When should jobs be split
- Job can be rerun
- When you want to rerun the job, it is the place where you want to split jobs
- One job, one thing to publish/deploy
- Limiting permissions
- Always think about job rerunnability
- Running
npm publishtwice will fail, should check if the package exists
- Running
- Increase atomicity by following build-first-deploy-later model
- Build everything and upload their artifacts first
- Then, converge all jobs (
needs: [build-package, build-pages]) into an approval job - Then, do all deployment after the approval job (
needs: approval) - Benefits: don't deploy when some builds failed
- Benefits: rerun failed builds/deployments
- If you want to work with commits/tags
actions/checkout@mainwithfetch-depth: 0
- Eliminate CD workflow flakiness
- Continuous deployment workflow is unlikely to be monitored
- Do not run tests during continuous deployment
- If inputs can be derived from repo
- Build a prepare step and prepare the inputs as
strategy.matrix - Other jobs will
- Use the matrix as
strategy.matrix: ${{ fromJSON(needs.prepare.outputs.matrix) }} - Use the input as
echo The input is ${{ matrix.input-value }}
- Use the matrix as
- Build a prepare step and prepare the inputs as
- Inputs vs. environment variables
- Inputs are auto-expanded for readability, preferred if it don't introduce too much complexity
npm version 1.2.3-abc.0123will change version to1.2.3-abc.123, leading zeroes in prerelease tags may be removed1.2.3-beta.00123->1.2.3-beta.123(segmented part are all numeric)1.2.3-beta-00123->1.2.3-beta-00123(hyphens do not segment)1.2.3-beta.00123abc->1.2.3-beta.00123abc(segmented part are not all numeric)- After
npm version 1.2.3-xyz, read it again and use that version from that point of time npx semverdon't remove leading zeroes in prerelease tags
- Semantic versioning will sort prerelease tags alphabetically and ignore build identifiers
1.2.3-beta.1>1.2.3-beta.01.2.3-beta+1~=1.2.3-beta+0- If prerelease version is not committed to repo, it is recommended to add
main.%Y%m%d-%H%M%S.commitishto the prerelease tag
- Pack as tarball before publish
- Upload tarball to artifacts as "evidence"
- Publish in another job without checkout
- Very clean and definitive publish
- Don't overwrite release asset
gh release upload --clobberis nice for job rerun, but it will wipe out the upload time so no one know when it is being updated
- Consider how workflow run when forked, they will run with no settings/secrets
if: ${{ secrets.something }}will not work, should useif: ${{ github.repository_owner == 'compulim' }}or something else
- GitHub mandates every job must have at least 1 step
- For debuggability, uses
echo abc=123 | tee --append $GITHUB_OUTPUT, instead ofecho abc=123 >> $GITHUB_OUTPUT- Only use
>> $GITHUB_OUTPUTfor secrets - Note that
teewill always exit with 0, should useset -eo pipefailto fail early
- Only use
- When using
bash, always setshell: bash- It will use
set -eo pipefail, otherwise, it'sset -e
- It will use
- CD should not release to
release/vnext- GitHub workflow permission issues
- When CD is on
pushevent instead ofworkflow_dispatch, it does not haveworkflow: writepermission - It will almost certainly fail until we run the CD once manually
- When CD is on
git pull --allwill always say we have a conflict
- GitHub workflow permission issues
xargsnot easily working on Windows because CRLF- Windows using actions with
path: /tmp/..., then reading files inbash- Actions may run on Windows natively, writing to
D:\ - Latter
bashwill not be able to access those files - Better with
path: ./tmp/...so it write to source folder, which is accessible across OS
- Actions may run on Windows natively, writing to
actions/checkoutwill remove files that are in.gitignore- Run
actions/checkoutas early as possible
- Run
- Shell naming convention
- Don't use
-because it is difficult to use inbash - Uppercase for things from environment/exports
- Lowercase for things in current shell
- Don't use
- Use
$(< some-file.txt)than`cat some-file.txt` - To use
bashfor-loop to loop through an array,for i in $(cat file.json | jq -cr '.[]') - Don't use
if: ${{ true }}, it will be always true, useif: trueinstead - Use
{ echo abc=123; echo xyz=789; } | tee --append $GITHUB_OUTPUTfor simplicity - Triggering workflow
github.tokenwon't work- Fine-grained PAT only work with "push" event, but won't work with "create release" event
- GitHub App token work, use this action
- More details at this article
cat filenames.txt | jq -nR 'reduce inputs as $i ([]; . + [$i])' | tee filenames.json$ cat filenames.txt
abc.txt
def.txt
xyz.txt
$ cat filenames.txt | jq -nR 'reduce inputs as $i ([]; . + [$i])' | tee filenames.json
[
"abc.txt",
"def.txt",
"xyz.txt"
]Given names.json and versions.json are two JSON arrays of strings.
jq -n --argfile names names.json --argfile versions versions.json '($names | to_entries) + ($versions | to_entries) | group_by(.key) | map({ name: .[0].value, version: .[1].value })'[
{
"name": "abc",
"version": "1.2.3"
},
{
"name": "def",
"version": "2.3.4"
},
{
"name": "xyz",
"version": "7.8.9"
}
]The outputs of the step will return "true" or "false", and bailout if network or credentials error.
- id: package-existence
name: Check if package already present
run: |
EXIST=`npm view my-package@1.2.3 --json 2>/dev/null | jq -r 'if .error then if .error.code == "E404" then false else halt_error(1) end else true end'` && true || exit 1
echo exist=$EXIST >> $GITHUB_OUTPUTIn contrast:
echo abc=`jq -nr 'halt_error(1)'` # always return 0Build the pages package as part of npm run build --workspaces, then extract the tarball to use with actions/upload-pages-artifact@main with all default settings.
build-pages:
name: 'Build: GitHub Pages'
needs:
- build
runs-on: ubuntu-latest
steps:
- name: Download packages
uses: actions/download-artifact@main
with:
name: packages
- name: Extract pages package
run: |
mkdir ./_site/
tar --directory=./_site/ --extract --file=`ls -1 pages-[0-9]*.tgz` --strip-component=2 --verbose package/public
ls -la ./_site/
- name: Upload pages artifact
uses: actions/upload-pages-artifact@mainThis will get the latest commitish of everything under the current folder. It is useful to know if anything changed under this folder.
Set actions/checkout@main with fetch-depth: 0 to fetch all commits.
COMMITTER_DATE=`git log --date=format:%Y%m%d-%H%M%S --pretty=format:'%cd' -1 ./`
LONG_COMMITISH=`git log --pretty=format:'%H' -1 ./`
SHORT_COMMITISH=`git log --pretty=format:'%h' -1 ./`To bump prerelease tag:
BRANCH=`git branch --show-current` # main
VERSION_SUFFIX=`git log --date=format:%Y%m%d-%H%M%S --pretty=format:'%cd.%h' -1 ./` # 20230816-084809.a1b2c3
VERSION=`npx semver --increment prerelease -n false --preid $BRANCH.$VERSION_SUFFIX` # 0.0.0-main.20230816-084809.a1b2c3There is a bug in npm@9.5.1 or semver@7.5.4. After running npm version, the next run of semver -n false will still append .0 to the prerelease tag.
npm version 1.2.3-alpha
npx semver --increment -n false --preid beta # will produce "1.2.3-beta.0"