Skip to content

docs(reply-by-email): document SparkPost reply-by-email and dev limit… #435

docs(reply-by-email): document SparkPost reply-by-email and dev limit…

docs(reply-by-email): document SparkPost reply-by-email and dev limit… #435

Workflow file for this run

# Build container images and push to AWS ECR.
#
# On every push to main/production/dev/staging, builds the nginx and app
# images in parallel and tags them with the branch name plus a unique
# build tag (vX.Y.Z-<sha7>). On main, also computes the next semantic
# version with commitizen, tags the image with that version (and :latest),
# and — only after both image pushes succeed — commits and pushes the
# version bump back to main so a failed build never strands a bump.
on:
push:
branches:
- main
- production
- dev
- staging
- test
workflow_dispatch:
name: AWS ECR Build & Push
env:
ECR_REGISTRY: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.${{ secrets.AWS_REGION }}.amazonaws.com
permissions:
contents: write
id-token: write
jobs:
prepare-version:
name: Prepare version
runs-on: ubuntu-24.04-arm
# Skip only when main is being pushed by its own auto-generated bump
# commit — without this guard the bump push would recurse into another
# build/bump cycle. Bump: commits propagated to other branches
# (dev/production/staging) by ff-merge ARE a release on that environment
# and must still build, so the skip is scoped to main. workflow_dispatch
# never has a head_commit, so this expression is also safe under manual
# triggers.
if: ${{ github.event_name != 'push' || !(github.ref_name == 'main' && startsWith(github.event.head_commit.message, 'bump:')) }}
outputs:
version: ${{ steps.compute.outputs.version }}
version_tag: ${{ steps.compute.outputs.version_tag }}
build_tag: ${{ steps.compute.outputs.build_tag }}
branch: ${{ steps.compute.outputs.branch }}
app_env: ${{ steps.compute.outputs.app_env }}
bumped: ${{ steps.bump.outputs.bumped }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GH_PAT }}
- name: Set up Python
if: github.ref_name == 'main'
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Compute next version with commitizen (main only)
id: bump
if: github.ref_name == 'main'
env:
COMMITIZEN_VERSION: "4.16.2"
run: |
python3 -m pip install --user "commitizen==${COMMITIZEN_VERSION}"
CURRENT_VERSION=$(cat VERSION | tr -d '[:space:]')
set +e
NEXT_VERSION=$("$HOME/.local/bin/cz" bump --get-next 2>/dev/null)
EXIT_CODE=$?
set -e
if [ "$EXIT_CODE" = "0" ] && [ -n "$NEXT_VERSION" ] && [ "$NEXT_VERSION" != "$CURRENT_VERSION" ]; then
echo "bumped=true" >> "$GITHUB_OUTPUT"
echo "next_version=$NEXT_VERSION" >> "$GITHUB_OUTPUT"
echo "Will bump $CURRENT_VERSION -> $NEXT_VERSION"
else
# Exit 21 = NoCommitsFoundError; exit 0 with same version means
# nothing semantic to bump (e.g. only chore: / docs: commits).
echo "bumped=false" >> "$GITHUB_OUTPUT"
echo "next_version=$CURRENT_VERSION" >> "$GITHUB_OUTPUT"
echo "No bump needed (exit=$EXIT_CODE next=$NEXT_VERSION current=$CURRENT_VERSION)"
fi
- name: Compute version + build tag
id: compute
run: |
if [ "${{ steps.bump.outputs.bumped }}" = "true" ]; then
VERSION="${{ steps.bump.outputs.next_version }}"
else
VERSION=$(cat VERSION | tr -d '[:space:]')
fi
SHORT_SHA="$(git rev-parse --short=7 HEAD)"
# Branch-prefix the build_tag on non-main branches so each branch
# gets a uniquely-named docker tag — otherwise dev / staging /
# production all built at the same SHA would race for the same
# v<version>-<sha> tag and only the last push would keep it.
# Main stays as v<version>-<sha> because it's the canonical owner
# of the release version line; the branch name is communicated
# via the existing :main, :v<version>, and :latest tags.
if [ "${GITHUB_REF_NAME}" = "main" ]; then
BUILD_TAG="v${VERSION}-${SHORT_SHA}"
else
BUILD_TAG="${GITHUB_REF_NAME}-v${VERSION}-${SHORT_SHA}"
fi
# Map branch -> APP_ENV for the env-aware nginx template overlay.
# Known deployment branches map to their canonical env name; any
# other branch (including main, feature branches) is passed
# through verbatim — the Dockerfile gracefully skips when no
# matching config/<env>/nginx/templates dir exists.
case "${GITHUB_REF_NAME}" in
production|staging|dev|test) APP_ENV="${GITHUB_REF_NAME}" ;;
*) APP_ENV="${GITHUB_REF_NAME}" ;;
esac
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "version_tag=v${VERSION}" >> "$GITHUB_OUTPUT"
echo "build_tag=${BUILD_TAG}" >> "$GITHUB_OUTPUT"
echo "branch=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
echo "app_env=${APP_ENV}" >> "$GITHUB_OUTPUT"
build-nginx:
name: Build and Push Nginx
runs-on: ubuntu-24.04-arm
needs: prepare-version
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GH_PAT }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
with:
mask-password: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Compute image tags
id: tags
env:
REPO: ${{ env.ECR_REGISTRY }}/commons-wordpress-nginx
BRANCH: ${{ needs.prepare-version.outputs.branch }}
VERSION_TAG: ${{ needs.prepare-version.outputs.version_tag }}
BUILD_TAG: ${{ needs.prepare-version.outputs.build_tag }}
run: |
tags="${REPO}:${BRANCH},${REPO}:${BUILD_TAG}"
if [ "$BRANCH" = "main" ]; then
tags="${tags},${REPO}:${VERSION_TAG},${REPO}:latest"
fi
echo "tags=${tags}" >> "$GITHUB_OUTPUT"
- name: Build and push nginx image
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile.nginx
push: true
# Disable provenance/SBOM attestations — they get pushed as
# separate OCI artifacts that show up as untagged images in
# ECR (see aws/containers-roadmap#1596).
provenance: false
sbom: false
tags: ${{ steps.tags.outputs.tags }}
build-args: |
APP_VERSION=${{ needs.prepare-version.outputs.version }}
BUILD_TAG=${{ needs.prepare-version.outputs.build_tag }}
GIT_SHA=${{ github.sha }}
APP_BRANCH=${{ needs.prepare-version.outputs.branch }}
APP_ENV=${{ needs.prepare-version.outputs.app_env }}
cache-from: type=registry,ref=${{ env.ECR_REGISTRY }}/commons-wordpress-nginx:cache
cache-to: type=registry,ref=${{ env.ECR_REGISTRY }}/commons-wordpress-nginx:cache,mode=max
- name: Label nginx cache manifest
# Buildkit only applies :cache to the cache config manifest,
# which ECR renders as an untagged "Other" artifact in the
# console. Re-put the same manifest with two build-unique
# identifying tags (do-not-deploy-<id>, build-cache-<id>).
# Buildkit's :cache tag is preserved untouched so cache-from
# continues to resolve. Cache manifests accumulate one entry
# per build — prune via an ECR lifecycle policy on the
# do-not-deploy-*/build-cache-* prefixes if storage growth
# becomes an issue.
env:
REPO: commons-wordpress-nginx
BUILD_TAG: ${{ needs.prepare-version.outputs.build_tag }}
run: |
set -euo pipefail
MANIFEST=$(aws ecr batch-get-image \
--repository-name "$REPO" \
--image-ids imageTag=cache \
--query 'images[0].imageManifest' \
--output text)
MEDIA_TYPE=$(aws ecr batch-get-image \
--repository-name "$REPO" \
--image-ids imageTag=cache \
--query 'images[0].imageManifestMediaType' \
--output text)
for tag in "do-not-deploy-${BUILD_TAG}" "build-cache-${BUILD_TAG}"; do
aws ecr put-image \
--repository-name "$REPO" \
--image-manifest "$MANIFEST" \
--image-manifest-media-type "$MEDIA_TYPE" \
--image-tag "$tag" \
>/dev/null 2>&1 \
&& echo " + applied tag :$tag to nginx cache" \
|| echo " · tag :$tag already on this manifest (no-op)"
done
build-app:
name: Build and Push App
runs-on: ubuntu-24.04-arm
needs: prepare-version
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GH_PAT }}
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ secrets.AWS_REGION }}
- name: Login to Amazon ECR
id: login-ecr
uses: aws-actions/amazon-ecr-login@v2
with:
mask-password: true
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Compute image tags
id: tags
env:
REPO: ${{ env.ECR_REGISTRY }}/commons-wordpress-app
BRANCH: ${{ needs.prepare-version.outputs.branch }}
VERSION_TAG: ${{ needs.prepare-version.outputs.version_tag }}
BUILD_TAG: ${{ needs.prepare-version.outputs.build_tag }}
run: |
tags="${REPO}:${BRANCH},${REPO}:${BUILD_TAG}"
if [ "$BRANCH" = "main" ]; then
tags="${tags},${REPO}:${VERSION_TAG},${REPO}:latest"
fi
echo "tags=${tags}" >> "$GITHUB_OUTPUT"
- name: Build and push app image
uses: docker/build-push-action@v5
with:
context: .
file: Dockerfile.php
target: cloud
push: true
# Disable provenance/SBOM attestations — they get pushed as
# separate OCI artifacts that show up as untagged images in
# ECR (see aws/containers-roadmap#1596).
provenance: false
sbom: false
tags: ${{ steps.tags.outputs.tags }}
build-args: |
BASE_IMAGE=${{ env.ECR_REGISTRY }}/commons-wordpress-base:latest
APP_VERSION=${{ needs.prepare-version.outputs.version }}
BUILD_TAG=${{ needs.prepare-version.outputs.build_tag }}
GIT_SHA=${{ github.sha }}
APP_BRANCH=${{ needs.prepare-version.outputs.branch }}
cache-from: type=registry,ref=${{ env.ECR_REGISTRY }}/commons-wordpress-app:cache
cache-to: type=registry,ref=${{ env.ECR_REGISTRY }}/commons-wordpress-app:cache,mode=max
- name: Label app cache manifest
# See "Label nginx cache manifest" for rationale.
env:
REPO: commons-wordpress-app
BUILD_TAG: ${{ needs.prepare-version.outputs.build_tag }}
run: |
set -euo pipefail
MANIFEST=$(aws ecr batch-get-image \
--repository-name "$REPO" \
--image-ids imageTag=cache \
--query 'images[0].imageManifest' \
--output text)
MEDIA_TYPE=$(aws ecr batch-get-image \
--repository-name "$REPO" \
--image-ids imageTag=cache \
--query 'images[0].imageManifestMediaType' \
--output text)
for tag in "do-not-deploy-${BUILD_TAG}" "build-cache-${BUILD_TAG}"; do
aws ecr put-image \
--repository-name "$REPO" \
--image-manifest "$MANIFEST" \
--image-manifest-media-type "$MEDIA_TYPE" \
--image-tag "$tag" \
>/dev/null 2>&1 \
&& echo " + applied tag :$tag to app cache" \
|| echo " · tag :$tag already on this manifest (no-op)"
done
push-bump:
name: Commit and push version bump
runs-on: ubuntu-24.04-arm
needs: [prepare-version, build-nginx, build-app]
# Only fires when both image builds succeeded AND prepare-version
# decided a bump was warranted — guarantees we never leave a stranded
# bump on main from a build that didn't actually produce images.
if: needs.prepare-version.outputs.bumped == 'true'
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: main
token: ${{ secrets.GH_PAT }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Bump and push
env:
COMMITIZEN_VERSION: "4.16.2"
run: |
python3 -m pip install --user "commitizen==${COMMITIZEN_VERSION}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
"$HOME/.local/bin/cz" bump --yes
git push origin main --tags