Skip to content

CD

CD #214

Workflow file for this run

# 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