ci: use docker build workflow #1335
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
name: Docker Build | |
# This workflow builds Docker images for projects in the repository. | |
# It can be triggered by: | |
# - Labeling a pull request with "ci debug" | |
# - Being called from another workflow via `workflow_call` | |
# - Manual dispatch using `workflow_dispatch` | |
# The workflow uses matrix builds to parallelize the process and leverages caching | |
# to speed up dependency and final image builds. | |
on: | |
pull_request: | |
types: | |
- labeled | |
- synchronize | |
workflow_call: | |
inputs: | |
projects: | |
description: 'Comma-separated list of project names to build.' | |
type: string | |
required: true | |
build-args: | |
description: 'Additional build-args (newline separated)' | |
type: string | |
required: false | |
version: | |
description: 'The version to tag the image with (previously DOCKER_TAG)' | |
type: string | |
required: false | |
workflow_dispatch: | |
inputs: | |
projects: | |
description: 'Comma-separated list of project names to build.' | |
type: string | |
required: true | |
build-args: | |
description: 'Additional build-args (newline separated)' | |
type: string | |
required: false | |
version: | |
description: 'The version to tag the image with (previously DOCKER_TAG)' | |
type: string | |
required: false | |
concurrency: | |
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}-${{ github.event_name }} | |
cancel-in-progress: true | |
defaults: | |
run: | |
shell: bash -euo pipefail {0} | |
env: | |
# For build summaries after Docker build (uses docker.io image) | |
# DOCKER_BUILD_EXPORT_BUILD_IMAGE: our.private.ecr.aws/pullthrough-docker/dockereng/export-build | |
# Regsitry to for build images and cache | |
IMAGE_REGISTRY: ${{ vars.IMAGE_REGISTRY || 'localhost' }} | |
AWS_ECR_REPO_BASE: ${{ vars.AWS_ECR_REPO_BASE || 'docker.io' }} | |
NODE_IMAGE_VERSION: 20 | |
NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} | |
NX_TASKS_RUNNER: ci | |
# Registry without trailing `/docker` | |
ECR_PULLTHROUGH_CACHE: ${{ vars.ECR_PULLTHROUGH_CACHE || 'public.ecr.aws' }} | |
AWS_REGION: ${{ vars.AWS_REGION }} | |
# From [docs](https://github.com/moby/buildkit#s3-cache-experimental): | |
# name=<manifest>: specify name of the manifest to use (default: buildkit) | |
# Multiple manifest names can be specified at the same time, separated by ;. | |
# The standard use case is to use the git sha1 as name, and the branch name as duplicate, and load both with 2 import-cache commands. | |
AWS_CACHE_PREFIX: >- | |
type=s3,region=${{ vars.AWS_REGION }},bucket=${{ secrets.S3_DOCKER_CACHE_BUCKET }},prefix=docker-cache/,mode=max | |
AWS_CACHE_FROM_COMMON: | | |
type=s3,region=${{ vars.AWS_REGION }},bucket=${{ secrets.S3_DOCKER_CACHE_BUCKET }},prefix=docker-cache/,name=${{ github.sha }} | |
type=s3,region=${{ vars.AWS_REGION }},bucket=${{ secrets.S3_DOCKER_CACHE_BUCKET }},prefix=docker-cache/,name=${{ github.ref }} | |
type=s3,region=${{ vars.AWS_REGION }},bucket=${{ secrets.S3_DOCKER_CACHE_BUCKET }},prefix=docker-cache/,name=target-base | |
type=s3,region=${{ vars.AWS_REGION }},bucket=${{ secrets.S3_DOCKER_CACHE_BUCKET }},prefix=docker-cache/,name=target-deps | |
type=s3,region=${{ vars.AWS_REGION }},bucket=${{ secrets.S3_DOCKER_CACHE_BUCKET }},prefix=docker-cache/,name=target-output-base | |
type=s3,region=${{ vars.AWS_REGION }},bucket=${{ secrets.S3_DOCKER_CACHE_BUCKET }},prefix=docker-cache/,name=target-output-base-with-pg | |
type=s3,region=${{ vars.AWS_REGION }},bucket=${{ secrets.S3_DOCKER_CACHE_BUCKET }},prefix=docker-cache/,name=target-playwright-base | |
type=s3,region=${{ vars.AWS_REGION }},bucket=${{ secrets.S3_DOCKER_CACHE_BUCKET }},prefix=docker-cache/,name=target-static-base | |
AWS_CACHE_TO_COMMON: | | |
type=s3,region=${{ vars.AWS_REGION }},bucket=${{ secrets.S3_DOCKER_CACHE_BUCKET }},prefix=docker-cache/,name=${{ github.sha }};${{ github.ref }},mode=max | |
jobs: | |
prepare: | |
name: Prepare | |
if: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'ci debug') }} | |
runs-on: 'arc-runners' | |
outputs: | |
build-matrix: ${{ steps.build-matrix.outputs.matrix }} | |
deps-matrix: ${{ steps.deps-matrix.outputs.matrix }} | |
node-image-version: ${{ steps.args-prep.outputs.node-image-version }} | |
playwright-image-version: ${{ steps.args-prep.outputs.playwright-image-version }} | |
build-args: ${{ steps.args-prep.outputs.build-args }} | |
steps: | |
- uses: actions/checkout@v4 | |
- name: Prepare Docker build-args | |
id: args-prep | |
run: | | |
# Setters | |
node_image_version="$NODE_IMAGE_VERSION" | |
playwright_image_version="$(yarn info --json @playwright/test | jq -r '.children.Version')" | |
# Outputters | |
echo node-image-version="$node_image_version" | tee -a "$GITHUB_OUTPUT" | |
echo playwright-image-version="$playwright_image_version" | tee -a "$GITHUB_OUTPUT" | |
# Multi-line build-args (End-Of-Multiline) | |
echo build-args"<<EOF" | tee -a "$GITHUB_OUTPUT" | |
echo NODE_IMAGE_VERSION="$node_image_version" | tee -a "$GITHUB_OUTPUT" | |
echo PLAYWRIGHT_IMAGE_VERSION="$playwright_image_version" | tee -a "$GITHUB_OUTPUT" | |
echo PLAYWRIGHT_VERSION="$playwright_image_version" | tee -a "$GITHUB_OUTPUT" # Duplicate for compatability | |
echo DOCKER_IMAGE_REGISTRY="$ECR_PULLTHROUGH_CACHE" | tee -a "$GITHUB_OUTPUT" | |
echo EOF | tee -a "$GITHUB_OUTPUT" | |
- name: Create matrix from input (build) | |
id: build-matrix | |
env: | |
projects: ${{ inputs.projects }} | |
is_debug: ${{ contains(github.event.pull_request.labels.*.name, 'ci debug') }} | |
test_everything: ${{ contains(github.event.pull_request.labels.*.name, 'test everything') }} | |
base_ref: ${{ github.event.pull_request.base.ref }} | |
run: | | |
if [[ "$is_debug" == true ]] && [[ -z "$projects" ]] && [[ "$test_everything" != true ]]; then | |
echo "Using small subset for testing docker build (on $base_ref)" | |
# A representative sample of various docker build targets | |
projects="web,air-discount-scheme-backend,license-api" | |
fi | |
# Create a list of objects of the form: | |
# [ | |
# { | |
# "name": "services-my-service", | |
# "docker": "next|nest|mytype" | |
# }, | |
# ... | |
# ] | |
set -x | |
matrix="$(git ls-files --error-unmatch '**/project.json' | | |
xargs cat | | |
jq -s -c --arg projects "$projects" '{ include: [ | |
.[] | |
| select( .name | if ($projects | length > 0) then IN($projects | split(",") | .[]) else true end ) | |
| { | |
project: .name, | |
docker: (.targets | keys | map(select(startswith("docker-") and . != "docker-native")) | map(sub("^docker-"; "")) | .[]) | |
} | |
] | |
}')" | |
echo "matrix=$matrix" | tee -a "$GITHUB_OUTPUT" | |
- name: Create matrix from input (deps) | |
id: deps-matrix | |
# Find targets containing `base` and output matrix with targets | |
run: | | |
set -x | |
matrix="$(grep -oP '(?<= AS )\S*\bbase\b\S*$' ./scripts/ci/Dockerfile | | |
sort -u | | |
jq -cRne '{ | |
include: [ | |
inputs | |
| { | |
target: . | |
} | |
] | |
}' | |
)" | |
echo "matrix=$matrix"| tee -a "$GITHUB_OUTPUT" | |
- name: Debug outputs | |
env: | |
args_prep: ${{ toJson(steps.args-prep.outputs) }} | |
build_matrix: ${{ toJson(steps.build-matrix.outputs) }} | |
deps_matrix: ${{ toJson(steps.deps-matrix.outputs) }} | |
run: | | |
echo "---" | |
echo "${args_prep}" | jq | |
echo "${build_matrix}" | jq | |
echo "${deps_matrix}" | jq | |
echo "---" | |
codegen: | |
name: Codegen | |
runs-on: arc-runners | |
needs: prepare | |
steps: | |
- uses: actions/checkout@v4 | |
- uses: actions/setup-node@v4 | |
- name: Setup yarn | |
uses: ./.github/actions/setup-yarn | |
- name: Ensure generated files (fetch cache) | |
id: cache | |
uses: runs-on/cache/restore@v4 | |
with: | |
path: ${{ github.sha }} | |
key: generated-files-${{ github.base_ref }}-${{ github.head_ref }}-${{ github.sha }} | |
restore-keys: | | |
generated-files-${{ github.base_ref }}-${{ github.head_ref }}-${{ github.sha }} | |
generated-files-${{ github.base_ref }}-${{ github.head_ref }} | |
generated-files-${{ github.base_ref }} | |
generated-files | |
- name: Ensure generated files (codegen) | |
if: ${{ steps.cache.outputs.cache-hit != 'true' }} | |
id: gen-files | |
env: | |
SHA: ${{ github.sha }} | |
NX_TASKS_RUNNER: default | |
run: | | |
echo "generated files: $SHA" | |
node ./scripts/ci/generate-files.mjs "$SHA" | |
tar -xzvf "$SHA" | |
- name: Ensure generated files (save cache) | |
if: ${{ steps.gen-files.outcome == 'success' }} | |
uses: runs-on/cache/save@v4 | |
with: | |
path: ${{ github.sha }} | |
key: generated-files-${{ github.base_ref }}-${{ github.head_ref }}-${{ github.sha }} | |
deps: | |
name: Docker-build dependencies | |
runs-on: arc-docker | |
needs: prepare | |
strategy: | |
fail-fast: true | |
matrix: ${{ fromJson(needs.prepare.outputs.deps-matrix) }} | |
permissions: | |
id-token: write # This is required for requesting the JWT for AWS/ECR login | |
contents: read # This is required for actions/checkout | |
steps: | |
- name: Debug inputs | |
env: | |
matrix: ${{ toJson(matrix) }} | |
run: | | |
echo "---" | |
echo "$matrix" | jq | |
echo "---" | |
- name: Check out repo | |
uses: actions/checkout@v4 | |
- name: Debug AWS user/login | |
if: ${{ !github.event.localrun }} | |
run: | | |
echo "DEBUG AWS user/login" | |
aws sts get-caller-identity | xargs echo "STS caller identity:" | |
- name: Log in to Amazon ECR | |
if: ${{ !github.event.localrun }} | |
id: ecr-login | |
uses: aws-actions/amazon-ecr-login@v2 | |
- name: Log in Docker | |
if: ${{ steps.ecr-login.conclusion == 'success' }} | |
id: docker-login | |
uses: docker/login-action@v3 | |
with: | |
registry: ${{ steps.ecr-login.outputs.registry }} | |
- name: Set up Docker Buildx | |
id: buildx | |
uses: docker/setup-buildx-action@v3 | |
with: | |
driver: docker-container | |
# We get rate-limited if using docker.io's image for buildkit. | |
driver-opts: | | |
image=${{ env.AWS_ECR_REPO_BASE }}/moby/buildkit:buildx-stable-1 | |
install: true | |
use: true | |
# Build stable layers; only difference between these steps should be the `target` | |
- name: Docker build/cache dependencies (${{ matrix.target }}) | |
uses: docker/build-push-action@v6 | |
with: | |
target: ${{ matrix.target }} | |
context: . | |
builder: ${{ steps.buildx.outputs.name }} | |
file: ./scripts/ci/Dockerfile | |
push: false | |
build-args: ${{ needs.prepare.outputs.build-args }} | |
cache-from: | | |
${{ env.AWS_CACHE_FROM_COMMON }} | |
${{ env.AWS_CACHE_PREFIX }},name=target-${{ matrix.target }} | |
cache-to: | | |
${{ env.AWS_CACHE_PREFIX }},name=${{ github.sha }};${{ github.ref }};target-${{ matrix.target }} | |
build: | |
name: Docker-build ${{ matrix.project }} | |
runs-on: arc-docker | |
needs: | |
- prepare | |
- deps | |
- codegen | |
strategy: | |
fail-fast: false | |
matrix: ${{ fromJson(needs.prepare.outputs.build-matrix) }} | |
permissions: | |
id-token: write # This is required for requesting the JWT for AWS/ECR login | |
contents: read # This is required for actions/checkout | |
steps: | |
- name: Check out repo | |
uses: actions/checkout@v4 | |
- name: Prepare app specific build-args | |
id: app-args | |
env: | |
app_name: ${{ matrix.project }} | |
run: | | |
# Fetch source root from project files (but strip trailing /src, if any) | |
app_home="$( | |
git ls-files --error-unmatch '**/project.json' | | |
xargs cat | | |
# Some projects don't have `.sourceRoot`, and some have trailing whitespace in `.sourceRoot` | |
jq -sre --arg project "$app_name" '.[] | select(.name == $project and .sourceRoot) | .sourceRoot | sub("/src ?"; "")' | |
)" | |
# Multi-line output | |
echo "build-args<<EOF" | tee -a "$GITHUB_OUTPUT" | |
echo "APP=$app_name" | tee -a "$GITHUB_OUTPUT" | |
echo "APP_HOME=$app_home" | tee -a "$GITHUB_OUTPUT" | |
echo "APP_DIST_HOME=dist/$app_home" | tee -a "$GITHUB_OUTPUT" | |
echo "EOF" | tee -a "$GITHUB_OUTPUT" | |
- name: Log in to Amazon ECR | |
if: ${{ !github.event.localrun }} | |
id: ecr-login | |
uses: aws-actions/amazon-ecr-login@v2 | |
- name: Log in Docker | |
if: ${{ steps.ecr-login.conclusion == 'success' }} | |
id: docker-login | |
uses: docker/login-action@v3 | |
# The ecr-login action takes care of loading credentials, we only need to specify the registry | |
with: | |
registry: ${{ steps.ecr-login.outputs.registry }} | |
- name: Set up Docker Buildx | |
uses: docker/setup-buildx-action@v3 | |
with: | |
driver: docker-container | |
# We get rate-limited if using docker.io's image for buildkit. | |
driver-opts: | | |
image=${{ env.AWS_ECR_REPO_BASE }}/moby/buildkit:buildx-stable-1 | |
install: true | |
use: true | |
- name: Generate image metadata | |
id: meta | |
# This step requires a valid GitHub token to query the API for e.g. description of repository | |
if: ${{ !github.event.localrun }} | |
uses: docker/metadata-action@v5 | |
with: | |
images: | | |
${{ env.IMAGE_REGISTRY }}/${{ matrix.project }} | |
tags: | | |
# Edge without prefix should only be used on `main`, so as not to conflict with type=sha | |
# type=edge | |
type=edge,prefix=edge- | |
# SemVer by tag (e.g. v1.2.3) | |
type=semver,pattern={{version}} | |
type=pep440,pattern={{version}} | |
# PR branch/name | |
# type=ref,event=<branch|tag|pr> Defaults are good | |
# Git SHA | |
type=sha,format=short,prefix= | |
type=sha,format=long,prefix= | |
# The final sha for the merge commit | |
# mcgh: Merge Commit GitHub | |
type=raw,value=${{ inputs.version }},enable=${{ !!inputs.version }} | |
type=raw,priority=50,value=mcgh-${{ github.event.pull_request.merge_commit_sha }},enable=${{ !!github.event.pull_request.merge_commit_sha }} | |
- name: Ensure generated files (fetch cache) | |
uses: runs-on/cache/restore@v4 | |
id: restore-generated-files-cache | |
with: | |
fail-on-cache-miss: true | |
path: ${{ github.sha }} | |
key: generated-files-${{ github.base_ref }}-${{ github.head_ref }}-${{ github.sha }} | |
restore-keys: | | |
generated-files-${{ github.base_ref }}-${{ github.head_ref }}-${{ github.sha }} | |
generated-files-${{ github.base_ref }}-${{ github.head_ref }} | |
generated-files-${{ github.base_ref }} | |
generated-files | |
- name: Ensure generated files (unpack) | |
run: | | |
tar -xzvf "$GITHUB_SHA" | |
- name: Docker-build and push Docker image | |
uses: docker/build-push-action@v6 | |
with: | |
target: output-${{ matrix.docker }} | |
context: . | |
file: ./scripts/ci/Dockerfile | |
push: ${{ !github.event.localrun }} | |
labels: ${{ steps.meta.outputs.labels }} | |
tags: ${{ steps.meta.outputs.tags }} | |
secrets: | | |
nx_cloud_access_token=${{ secrets.NX_CLOUD_ACCESS_TOKEN }} | |
build-args: | | |
${{ needs.prepare.outputs.build-args }} | |
${{ steps.app-args.outputs.build-args }} | |
${{ inputs.build-args }} | |
# Caching final images doesn't make sense when this should only run if the project is affected (changed) | |
cache-from: | | |
${{ env.AWS_CACHE_FROM_COMMON }} | |
${{ env.AWS_CACHE_PREFIX }},name=target-output-${{ matrix.docker }},mode=min | |
cache-to: | | |
${{ env.AWS_CACHE_PREFIX }},name=target-output-${{ matrix.docker }};${{ github.sha }};${{ github.ref }},mode=min |