Reusable GitHub composite actions for projects.
| Action | Description |
|---|---|
build |
Builds a Docker image with Buildx and optional registry-based layer caching |
push |
Tags and pushes a local Docker image to an ECR registry |
deploy-ecs |
Deploys a new image tag to an ECS service and updates SSM tag |
Consuming repositories need these secrets. They are configured per-repo in the terraform repo under github/vars/root.tfvars:
| Secret | Description |
|---|---|
AWS_ACCOUNT_ID |
Target AWS account ID for the repo |
AWS_ROLE_ARN |
IAM role ARN for OIDC authentication |
The build action accepts both build-args and secrets. Use the right one to avoid Docker layer cache invalidation.
build-args are baked into image layers. If a value changes between builds, all layers after the ARG declaration miss cache. Use for values that are stable or that sit below layers you expect to rebuild anyway.
secrets (via --mount=type=secret) are injected at runtime and excluded from the layer cache key. Use for credentials and tokens that rotate between builds — the layer will stay cached even when the secret value changes.
In the Dockerfile, consume secrets like this:
RUN --mount=type=secret,id=MY_SECRET \
export MY_SECRET=$(cat /run/secrets/MY_SECRET) && \
some-commandAnd pass them from the workflow:
- uses: Rhodri-Morgan/github-workflows/build@main
with:
image-repo: my-project
secrets: |
MY_SECRET=${{ env.MY_SECRET }}| Scenario | Use | Why |
|---|---|---|
| Auth tokens that rotate every build | secrets |
Avoids cache bust on every build |
| Database URLs with credentials | secrets |
Keeps credentials out of docker history |
Static config (e.g. NODE_ENV) |
build-args |
Value is stable, fine to bake in |
Before setting up build workflows, note the following:
- If your project needs different images for dev and prod (e.g. statically replaced variables, build-time validation that requires environment-specific values), use a matrix strategy so dev and prod builds run in parallel.
- If you have a monorepo, use separate jobs per image so they build concurrently on tag push.
- Validate your Dockerfile layer caching. Check each layer for cache-busting pitfalls: changing commit SHAs baked into build args, rotating secrets passed as build args instead of
--mount=type=secret, non-deterministic package installs (missing lockfiles), timestamps in generated files, andCOPY . .placed before dependency installation layers. - Registry caching is optional. The
aws-region,account-id, androle-arninputs on thebuildaction are only required if you want ECR-based registry layer caching. Without them, the build still works but won't cache layers to or from a remote registry. - Set
image-tagonly when you need a custom tag. If omitted, the build action falls back to the first 6 characters ofGITHUB_SHA. - Set
targetonly when you need a named Docker stage. If omitted, Docker builds the default final stage in the Dockerfile.
Example tag-triggered build and push workflow:
name: Build & Push to ECR
on:
push:
tags:
- "v*.*.*"
- "v*.*.*-*"
permissions:
id-token: write
contents: read
run-name: Build and push image ${{ github.ref_name }}
jobs:
build-push:
runs-on: ubuntu-latest
name: Build and push
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build image
id: build
uses: Rhodri-Morgan/github-workflows/build@main
with:
image-repo: your-ecr-repository
image-tag: ${{ github.ref_name }}
target: build
aws-region: eu-west-1
account-id: ${{ secrets.AWS_ACCOUNT_ID }}
role-arn: ${{ secrets.AWS_ROLE_ARN }}
- name: Run tests in built image
run: docker run --rm local/your-ecr-repository:${{ steps.build.outputs.image-tag }} make ci-test
- name: Push
uses: Rhodri-Morgan/github-workflows/push@main
with:
image-repo: your-ecr-repository
image-tag: ${{ steps.build.outputs.image-tag }}
aws-region: eu-west-1
account-id: ${{ secrets.AWS_ACCOUNT_ID }}
role-arn: ${{ secrets.AWS_ROLE_ARN }}Replace your-ecr-repository with your ECR repository name. Adjust the tag trigger, region, and test command to match your project.
Use target when you need to build a named Docker stage instead of the default final stage from the Dockerfile. In the example above, build is only the stage name used by that Dockerfile.
This action deploys a new image tag to an ECS service by registering a new task definition with the updated image tag. The SSM parameter is only updated after a successful deployment. On failure, the action automatically rolls back to the previous task definition.
Example deploy workflow (manual dispatch):
name: Deploy
on:
workflow_dispatch:
inputs:
deploy-tag:
description: "Image Tag:"
default: v2026.MM.DD-n
required: true
type: string
permissions:
id-token: write
contents: read
run-name: Deploy ${{ inputs.deploy-tag }}
concurrency:
group: deploy-my-service
cancel-in-progress: false
jobs:
deploy:
runs-on: ubuntu-latest
name: Deploy
steps:
- name: Deploy to ECS
uses: Rhodri-Morgan/github-workflows/deploy-ecs@main
with:
image-tag: ${{ inputs.deploy-tag }}
aws-region: eu-west-1
role-arn: ${{ secrets.AWS_ROLE_ARN }}
ecs-cluster: my-cluster
ecs-service: my-service
image-ssm-parameter: my-service-image-tag
deployment-timeout: "600"By default only the task definition's first container moves to the new tag. A task running several images (sidecars
from the same build, say) lists the ECR repositories whose containers should move in image-repos; containers from
anywhere else (a public redis image) keep theirs.
pre-deploy-task-family names a task definition, kept in Terraform, to run once on the new tag before the service
rolls, typically database migrations. It retags the same way, runs in the service's own subnets and security groups,
and if any of its containers exits non-zero the deploy stops there, with the service still on its old revision. The
GitHub Actions role needs ecs:RunTask on that family.
- name: Deploy to ECS
uses: Rhodri-Morgan/github-workflows/deploy-ecs@main
with:
image-tag: ${{ inputs.deploy-tag }}
aws-region: eu-west-1
role-arn: ${{ secrets.AWS_ROLE_ARN }}
ecs-cluster: my-cluster
ecs-service: my-service
image-ssm-parameter: my-service-image-tag
image-repos: my-service-api my-service-frontend
pre-deploy-task-family: my-service-migrateHelper scripts to use alongside these workflows.
| Script | Description |
|---|---|
scripts/tag-and-push.sh |
Creates and pushes a date-based git tag in vYYYY.MM.DD-n format |
Creates a git tag using the standard date format vYYYY.MM.DD-n, with an incrementing suffix for multiple releases on the same day (for example v2026.03.11, v2026.03.11-1, v2026.03.11-2). Pushing a tag in this format can be used to trigger tag-based build workflows.
Then trigger a build and push with:
yarn bumpAdd a Makefile target that fetches the script via the GitHub API:
.PHONY: tag-and-push
tag-and-push:
@gh api 'repos/Rhodri-Morgan/github-workflows/contents/scripts/tag-and-push.sh?ref=main' --jq '.content' | base64 -d > /tmp/tag-and-push.sh
@sh /tmp/tag-and-push.shThen trigger a build and push with:
make tag-and-push