Skip to content

ci: use docker build workflow #1332

ci: use docker build workflow

ci: use docker build workflow #1332

Workflow file for this run

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-runners
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-runners
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