CD #214
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Using multiple workflow .yaml files | |
# https://stackoverflow.com/questions/64009546/how-to-run-multiple-github-actions-workflows-from-sub-directories | |
# TODO | |
# Preventing concurrent workflows (e.g. multiple merges to master at once) | |
# https://github.blog/changelog/2021-04-19-github-actions-limit-workflow-run-or-job-concurrency/ | |
# From: https://github.community/t/how-to-limit-concurrent-workflow-runs/16844/ | |
# | |
# Further split sub-directories' actions/workflows for more granular control. | |
# - https://stackoverflow.com/questions/64009546/how-to-run-multiple-github-actions-workflows-from-sub-directories | |
# If we decide to use Docker - Using local Dockerfile in pipeline: | |
# steps: | |
# - name: Check out code | |
# uses: actions/checkout@v3 | |
# - name: Build docker images | |
# run: docker build -t local < .devcontainer/Dockerfile # .devcontainer is the local path | |
# - name: Run tests | |
# run: docker run -it -v $PWD:/srv -w/srv local make test | |
# OR | |
# - name: Build docker images | |
# run: docker-compose build | |
# - name: Run tests | |
# run: docker-compose run test | |
# Ref: https://stackoverflow.com/questions/61154750/use-local-dockerfile-in-a-github-action | |
name: CD | |
on: | |
workflow_dispatch: | |
inputs: | |
clientVersion: | |
description: "Sets (or increments if 'true') client app version" | |
required: false | |
default: "" | |
release: | |
types: [ published ] | |
workflow_run: | |
workflows: [ 'CI' ] | |
branches: [ master ] | |
types: [ completed ] | |
defaults: | |
run: | |
shell: bash | |
working-directory: ./ | |
# Set GitHub user info for ease of use of Git CLI commands. | |
# | |
# See: | |
# - https://docs.npmjs.com/cli/v9/commands/npm-run-script#ignore-scripts | |
# - https://stackoverflow.com/questions/59471962/how-does-npm-behave-differently-with-ignore-scripts-set-to-true | |
# - https://github.com/tschaub/gh-pages#optionsuser | |
# - https://github.com/actions/checkout/issues/13 | |
env: | |
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
gitUserName: ${{ github.actor }} | |
gitUserEmail: ${{ github.actor }}@users.noreply.github.com | |
jobs: | |
# cd-init: | |
# runs-on: ubuntu-latest | |
# steps: | |
# - name: Checkout repository branch | |
# uses: actions/checkout@v3 | |
# | |
# - name: CD Client build | |
# # Workflows require at least one job that has no dependencies. | |
# # However, we can still use the `uses` block for "reusable workflows" | |
# # | |
# # See: | |
# # - Reusable workflows `uses`: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_iduses | |
# uses: ./.github/workflows/ci.yaml | |
# | |
# cd-init: | |
# runs-on: ubuntu-latest | |
# steps: | |
# - name: Checkout repository branch | |
# uses: actions/checkout@v3 | |
# | |
# cd-client-build: | |
# runs-on: ubuntu-latest | |
# steps: | |
# - name: Client CD - Download CI output | |
# id: cd-download-artifacts | |
# needs: [ cd-init ] | |
# # Workflows require at least one job that has no dependencies. | |
# # However, we can still use the `uses` block for "reusable workflows" | |
# # | |
# # See: | |
# # - Reusable workflows `uses`: https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_iduses | |
# # uses: ./.github/workflows/ci.yaml | |
# # uses: actions/download-artifact@v3 | |
# with: | |
# name: ci-build-output | |
# path: | | |
# dist | |
cd-build: | |
runs-on: ubuntu-latest | |
# Only run on merge to master, but not on PR to master since PRs are just drafts, not officially prod-ready code. | |
# | |
# See: | |
# - https://github.community/t/depend-on-another-workflow/16311/3 | |
# - https://stackoverflow.com/questions/66205887/only-run-github-actions-step-if-not-a-pull-request/66206183#66206183 | |
if: ${{ github.event_name != 'pull_request' && (github.event.pull_request.merged || github.ref == 'refs/heads/master') }} | |
# Grant the permissions required for deployments to GitHub Pages. | |
# | |
# See: | |
# - https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs | |
permissions: | |
pages: write # Grant write permissions to deploy to the `gh-pages` (or whatever is specified in "Settings") branch | |
id-token: write # to verify the deployment originates from an appropriate source | |
deployments: write | |
packages: write | |
actions: write | |
contents: write | |
steps: | |
- name: Checkout repository branch | |
uses: actions/checkout@v3 | |
- name: Set NodeJS version | |
uses: actions/setup-node@v3 | |
with: | |
node-version: 16 | |
# Native GitHub `actions/download-artifact@v3` doesn't allow sharing between workflows. | |
# - Issue: https://github.com/actions/toolkit/issues/501 | |
# Once it does, we can use: | |
# | |
# - name: Client CD - Download CI output | |
# id: cd-download-artifacts | |
# uses: actions/download-artifact@v3 | |
# with: | |
# name: ci-build-output | |
# path: | | |
# dist | |
# | |
# Note that simply adding a `needs`/`uses` block for my-workflow.yaml file doesn't suffice until this is fixed; | |
# adding said block for my-action.yaml would (since it's an action and actions are reusable while workflows aren't | |
# despite what GitHub claims) but only if that action covers all your needs. | |
# | |
# We can work around this via: | |
# | |
# 1. | |
# Use a third-party download-artifact action. | |
# - Good example: https://stackoverflow.com/questions/60355925/share-artifacts-between-workflows-github-actions/65049722#65049722 | |
# | |
# - name: Client CD - Download CI output | |
# id: cd-download-artifacts | |
# # needs: [ ci-build-output ] | |
# # needs: [ ci-build-and-upload-artifacts ] | |
# uses: dawidd6/action-download-artifact@v2 | |
# with: | |
# name: ci-build-output-artifacts | |
# branch: master | |
# github_token: ${{ secrets.GITHUB_TOKEN }} | |
# if_no_artifact_found: fail | |
# workflow_conclusion: success | |
# | |
# 2. Use our own custom CLI action to manually download the artifact files. | |
# This is the only reliable option at the moment for sharing artifacts between workflow files | |
# without uploading them in Release files. | |
# Do so via: | |
# - Get latest CI workflow ID via: | |
# gh run list --limit 1 --workflow CI | tail -n +1 | awk '{ print $(NF - 2) }' | |
# - Download all files from that workflow into an arbitrary dir (`ci-workflow-artifact-output` in this case). | |
# - Create the dir we actually want to use (`dist` in this case). | |
# - Copy all nested files/directories from the downloaded dir to the desired dir. | |
# - Delete the original temp dir. | |
# Notes: | |
# - `github.run_id` == Current workflow run ID, not the ID of the run we want (previous workflow run). | |
# - GitHub CLI docs: https://cli.github.com/manual/gh_help_reference | |
# - GitHub workflows - Storing artifacts: https://docs.github.com/en/actions/using-workflows/storing-workflow-data-as-artifacts | |
# - GitHub workflows - Using `gh` CLI: https://docs.github.com/en/actions/using-workflows/using-github-cli-in-workflows | |
# - GitHub workflows - `github` context vars available: https://docs.github.com/en/actions/learn-github-actions/contexts#github-context | |
# | |
# TODO: Try the cache instead of storing artifacts manually: | |
# - Related - Cache: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows | |
# - name: Client CD - Download CI artifacts | |
# env: | |
# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
# WORKFLOW_RUN_ID: ${{ github.run_id }} | |
# run: | | |
# gh run download --dir ci-workflow-artifact-output --pattern '*' $(gh run list --limit 1 --workflow CI | tail -n +1 | awk '{ print $(NF - 2) }') | |
# mkdir dist | |
# cp -R ci-workflow-artifact-output/*/* dist | |
# rm -rf ci-workflow-artifact-output | |
# | |
# # Only necessary if running npm scripts in CD, which we are for `deploy` | |
# - name: Client CD - Install | |
# id: cd-client-install | |
# shell: bash | |
# working-directory: ./ | |
# run: | | |
# npm install | |
- name: Client CD Build - Download CI cache | |
id: client-cd-build-download-cache | |
uses: actions/cache/restore@v3 | |
env: | |
cache-name: ci-cache | |
with: | |
path: | | |
node_modules | |
dist | |
package.json | |
package-lock.json | |
# ( declare origIFS="$IFS"; declare IFS=$'\n'; declare fileHashes=(); for file in $(find src/ -type f); do fileHashes+=("$(sha256sum "$file")"); done; declare fileHashesStr="$(printf "%s\n" "${_fileHashes[@]}")"; fileHashesStr="${fileHashesStr/%\n}"; declare fileHashesSortedByFilename="$(echo "$fileHashesStr" | sort -V -k 2)"; declare dirHash="$(echo -n "$fileHashesSortedByFilename" | sha256sum | awk '{ print $1 }')"; echo "$dirHash"; ) | |
key: ${{ env.cache-name }}-${{ runner.os }}-${{ hashFiles('package.json', './src/**', './test/**', './tests/**', './config/**', './mocks/**') }} | |
continue-on-error: false | |
# Only run next step if cache-hit failed. | |
# For some reason, the recommended logic from the docs doesn't work: | |
# - if: ${{ steps.ci-cache.outputs.cache-hit != 'true' }} | |
# Instead, use `failure()` as suggested here: http://chamindac.blogspot.com/2020/08/how-to-run-github-actions-step-when.html#:~:text=run%20on%20failure-,if%3A%20%24%7B%7B%20failure()%20%7D%7D,-run%3A%20%7C | |
- name: Client CD - Generate CI artifacts (failed CI cache) | |
if: ${{ failure() || steps.client-cd-build-download-cache.outputs.cache-hit != 'true' }} | |
run: | | |
git config --global user.name ${{ env.gitUserName }} | |
git config --global user.email ${{ env.gitUserEmail }} | |
git pull | |
npm install | |
# Consolidate any source of the new client version into one place, `env.clientVersion`, for ease of use | |
# throughout all other workflows/jobs/actions/steps. | |
# | |
# Since the only `release` event we listen to is `publish`, we can safely assume the ref-name is the tag name. | |
# | |
# See: | |
# - "Release" event and object info: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#release | |
- name: CD:Client - Set version number | |
run: | | |
if [[ -n "${{ env.clientVersion }}" ]] || [[ -n "${{ inputs.clientVersion }}" ]] || [[ "$GITHUB_EVENT_NAME" == 'release' ]]; then | |
if [[ "$GITHUB_EVENT_NAME" == 'release' ]]; then | |
echo "clientVersion=$GITHUB_REF_NAME" >> $GITHUB_ENV | |
elif [[ -n "${{ inputs.clientVersion }}" ]]; then | |
echo "clientVersion=${{ inputs.clientVersion }}" >> $GITHUB_ENV | |
fi | |
fi | |
# Separate this step from `CI:Verify` to ensure linting, type-checking, etc. pass before using resources for building the app. | |
# We need to either cache the build output after the version is incremented, or re-run | |
# `npm run build` so the version is injected into the code where needed. | |
# | |
# See: | |
# - Cache GitHub Workflow action docs: https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#example-using-the-cache-action | |
# - Cache-restore between workflows: https://github.com/actions/cache#example-cache-workflow | |
- name: Client CD Deploy - Upgrade app version | |
# Not necessarily required since `npm version` returns the version string (with "v" in it), but for reference: | |
# - Normal: npm version patch; git commit --amend -m "Patch to v$(jq -r '.version' package.json)" | |
# - Without git commit or tag: newAppVersion=$(npm version --no-git-tag-version patch); git commit -am "Patch to ${newAppVersion}" | |
# | |
# run: | |
# newAppVersion=$(npm version --no-git-tag-version patch) | |
# git commit -am "Patch to ${newAppVersion}" | |
run: | | |
git config --global user.name ${{ env.gitUserName }} | |
git config --global user.email ${{ env.gitUserEmail }} | |
newAppVersion="${{ env.clientVersion }}" | |
if [[ -z "$newAppVersion" ]] || [[ "$newAppVersion" == "v$(jq -r '.version' package.json)" ]]; then | |
npm version --no-git-tag-version patch -m "Upgrade version to %s" | |
else | |
npm version --no-git-tag-version "$newAppVersion" -m "Upgrade version to %s" | |
fi | |
newAppVersion="$(jq -r '.version' package.json)" | |
git commit -am "Update version to v${newAppVersion}" | |
git push | |
npm run build | |
echo "Created new app version: $(jq '.version' package.json)" | |
- name: Client CD Build - Upload CD cache | |
id: client-cd-build-upload-cache | |
uses: actions/cache/save@v3 | |
env: | |
cache-name: ci-cache | |
with: | |
path: | | |
node_modules | |
dist | |
package.json | |
package-lock.json | |
# ( declare origIFS="$IFS"; declare IFS=$'\n'; declare fileHashes=(); for file in $(find src/ -type f); do fileHashes+=("$(sha256sum "$file")"); done; declare fileHashesStr="$(printf "%s\n" "${_fileHashes[@]}")"; fileHashesStr="${fileHashesStr/%\n}"; declare fileHashesSortedByFilename="$(echo "$fileHashesStr" | sort -V -k 2)"; declare dirHash="$(echo -n "$fileHashesSortedByFilename" | sha256sum | awk '{ print $1 }')"; echo "$dirHash"; ) | |
key: ${{ env.cache-name }}-${{ runner.os }}-${{ hashFiles('package.json', './src/**', './test/**', './tests/**', './config/**', './mocks/**') }} | |
cd-deploy: | |
runs-on: ubuntu-latest | |
needs: [ cd-build ] | |
permissions: | |
pages: write # Grant write permissions to deploy to the `gh-pages` (or whatever is specified in "Settings") branch | |
id-token: write # to verify the deployment originates from an appropriate source | |
deployments: write | |
packages: write | |
actions: write | |
contents: write | |
steps: | |
- name: Checkout repository branch | |
uses: actions/checkout@v3 | |
- name: Set NodeJS version | |
uses: actions/setup-node@v3 | |
with: | |
node-version: 16 | |
- name: Client CD Deploy - Update branch with CD changes | |
id: client-cd-deploy-download-branch | |
shell: bash | |
run: | | |
git config --global user.name ${{ env.gitUserName }} | |
git config --global user.email ${{ env.gitUserEmail }} | |
git pull | |
- name: Client CD Deploy - Download CD cache | |
id: client-cd-deploy-download-cache | |
uses: actions/cache/restore@v3 | |
env: | |
cache-name: ci-cache | |
with: | |
path: | | |
node_modules | |
dist | |
package.json | |
package-lock.json | |
# ( declare origIFS="$IFS"; declare IFS=$'\n'; declare fileHashes=(); for file in $(find src/ -type f); do fileHashes+=("$(sha256sum "$file")"); done; declare fileHashesStr="$(printf "%s\n" "${_fileHashes[@]}")"; fileHashesStr="${fileHashesStr/%\n}"; declare fileHashesSortedByFilename="$(echo "$fileHashesStr" | sort -V -k 2)"; declare dirHash="$(echo -n "$fileHashesSortedByFilename" | sha256sum | awk '{ print $1 }')"; echo "$dirHash"; ) | |
key: ${{ env.cache-name }}-${{ runner.os }}-${{ hashFiles('package.json', './src/**', './test/**', './tests/**', './config/**', './mocks/**') }} | |
continue-on-error: false | |
# Ignore pre-/post- npm scripts via `npm run --ignore-scripts <my-script>`. | |
# This could be useful for, e.g. scripts like `deploy` since `predeploy` (`npm run build`) | |
# was already run in CI. | |
# | |
# Set `user.name` and `user.email` for ~/.gitconfig inline via the `--user` flag for `gh-pages`: | |
# npm run --ignore-scripts deploy -- --user "${{ env.gitUserName }} <${{ env.gitUserEmail }}>" | |
- name: CD - Deploy application | |
shell: bash | |
# We need to either cache the build output after the version is incremented, or re-run | |
# `npm run build` so the version is injected into the code where needed. | |
# | |
# Run `git config user.<info>` and `git pull` so we can push to a branch other than the | |
# one cloned via `actions/checkout`, e.g. cloning `master` and pushing to `gh-pages`. | |
run: | | |
git config --global user.name ${{ env.gitUserName }} | |
git config --global user.email ${{ env.gitUserEmail }} | |
# gh-pages requires this in CI environments for some reason | |
# See: | |
# - https://github.com/tschaub/gh-pages/issues/384 | |
# - https://github.com/tschaub/gh-pages/issues/359 | |
git remote set-url origin "$(echo "${{ github.repositoryUrl }}" | sed -E 's|git://|https://x-access-token:${{ secrets.GITHUB_TOKEN }}@|')" | |
git pull | |
newAppVersion="$(jq -r '.version' package.json)" | |
echo "Deploying version ${newAppVersion}..." | |
npm run --ignore-scripts deploy | |
# git checkout gh-pages | |
# | |
# ( for path in $(ls --ignore=dist --ignore=.git --ignore=package.json); do echo "$path"; rm -rf "$path"; done; ) | |
# | |
# cp -R dist/* . | |
# | |
# ( for path in dist/*; do git add "$(echo "$path" | sed -E 's/dist/./')"; done; ) | |
# | |
# rm -rf dist | |
# | |
# git commit -am "Update version to v${newAppVersion}" | |
# git push |