Shared GitHub Actions workflows and composite actions for the AxeForging immutable release pipeline.
.
├── actions/
│ ├── build-push-image/ # Build Docker image, push to AR, return digest
│ ├── promote-artifact/ # Add new tag to existing digest (no rebuild)
│ ├── deploy-cloudrun/ # Deploy to Cloud Run by digest (enforces @sha256:)
│ ├── pr-title-lint/ # Validate PR title (Conventional Commits), sticky comment
│ └── sticky-pr-comment/ # Create/update one PR comment by marker (no duplicates)
│
├── .github/
│ └── workflows/
│ ├── docker-build-push.yml # Reusable: language-agnostic build + push golden artifact
│ ├── go-build-push.yml # Reusable: Go convenience (go test + build)
│ ├── promote-artifact.yml # Reusable: promote artifact tag
│ ├── cloudrun-deploy.yml # Reusable: deploy to Cloud Run
│ └── pr-title-lint.yml # Reusable: PR title lint (workflow_call)
│
├── docs/
│ └── immutable-pipeline/
│ └── README.md
│
├── scripts/
│ └── gcp-setup.sh
├── lefthook.yml
├── Makefile
└── README.md
Language-agnostic. Run your tests first, then call this. Works for Go, Node.js, Python, Java, or anything that has a Dockerfile.
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test # or: pytest / go test / mvn test / cargo test
build-golden:
needs: [test]
uses: AxeForging/reusable-workflows/.github/workflows/docker-build-push.yml@main
with:
image-name: my-service
registry: ${{ vars.AR_REGION }}-docker.pkg.dev/${{ vars.AR_PROJECT_ID }}/services
wif-provider: ${{ vars.WIF_PROVIDER }}
secrets:
wif-service-account: ${{ secrets.WIF_SERVICE_ACCOUNT }}Bundles go test + docker build + push in one call. For pure Go services that don't need a separate test job.
jobs:
build:
uses: AxeForging/reusable-workflows/.github/workflows/go-build-push.yml@main
with:
image-name: my-service
registry: ${{ vars.AR_REGION }}-docker.pkg.dev/${{ vars.AR_PROJECT_ID }}/services
wif-provider: ${{ vars.WIF_PROVIDER }}
secrets:
wif-service-account: ${{ secrets.WIF_SERVICE_ACCOUNT }}Promotes an image tag without rebuilding (golden → RC, RC → final). Language-agnostic.
jobs:
promote:
uses: AxeForging/reusable-workflows/.github/workflows/promote-artifact.yml@main
with:
registry: ${{ vars.AR_REGION }}-docker.pkg.dev/${{ vars.AR_PROJECT_ID }}/services
image: my-service
source-tag: git-abc1234
target-tag: v1.2.0-rc.1
wif-provider: ${{ vars.WIF_PROVIDER }}
secrets:
wif-service-account: ${{ secrets.WIF_SERVICE_ACCOUNT }}Deploys to Cloud Run using a digest reference. Enforces @sha256: — mutable tag references are rejected. Language-agnostic.
jobs:
deploy:
uses: AxeForging/reusable-workflows/.github/workflows/cloudrun-deploy.yml@main
with:
service: my-service
image: us-central1-docker.pkg.dev/my-project/services/my-service@sha256:abc123
region: ${{ vars.CLOUDRUN_REGION }}
project-id: ${{ vars.PROD_PROJECT_ID }}
environment: production
version: v1.2.0
commit: ${{ github.sha }}
wif-provider: ${{ vars.WIF_PROVIDER }}
secrets:
wif-service-account: ${{ secrets.WIF_SERVICE_ACCOUNT }}Validates PR titles against Conventional Commits and posts a single sticky comment explaining the expected format. No Jira ticket required by default; fully configurable.
on:
pull_request:
types: [opened, synchronize, reopened, edited]
jobs:
pr-title:
uses: AxeForging/reusable-workflows/.github/workflows/pr-title-lint.yml@main
# optional:
# with:
# extra-types: wip,hotfix
# custom-regex: '^[A-Z]+-\d+ - (feat|fix|chore): .+'
# expected-hint: "Use: <TICKET> - <type>: <description>"
# fail-on-error: 'false'The reusable workflow requests contents:read + pull-requests:write itself — the caller passes nothing else. See actions/pr-title-lint for all inputs and the direct-action form.
RC creation and final release both use workflow_dispatch (manual trigger). This is intentional:
- Production releases require explicit human approval — they are not automatic
- Manual dispatch creates an audit trail in GitHub Actions (who triggered, when, with what inputs)
- It matches the spec: "approved RC → create tag → deploy to production" — the approval is the act of triggering
- The RC version input prevents accidental promotions
Rollback and hotfix also use
workflow_dispatchfor the same reason.
| Action | Language | Description |
|---|---|---|
build-push-image |
any | docker build + push, returns digest |
promote-artifact |
any | Tag same digest with new label |
deploy-cloudrun |
any | Deploy to Cloud Run, enforces digest ref, sets observability labels |
pr-title-lint |
any | Validate PR title (configurable), post sticky comment, fail on mismatch |
sticky-pr-comment |
any | Create/update a single PR comment by marker |
Run the interactive setup script — it asks for the values, runs all commands in order, and writes the results to scripts/gcp-setup.env (gitignored):
bash scripts/gcp-setup.shOr follow the manual steps below.
Manual setup — individual gcloud commands
Replace the variables at the top, then run the blocks in order.
# ── Variables ────────────────────────────────────────────────────────────────
AR_PROJECT_ID="your-artifact-project-id" # Artifact Registry + WIF + CI SA
STG_PROJECT_ID="your-stg-project-id" # Cloud Run staging
PROD_PROJECT_ID="your-prod-project-id" # Cloud Run production
REGION="us-central1"
GITHUB_ORG="AxeForging"
SA_NAME="ci-pipeline"
SA_EMAIL="${SA_NAME}@${AR_PROJECT_ID}.iam.gserviceaccount.com"
WIF_POOL="github-pool"
WIF_PROVIDER="github-provider"
AR_REPO="services"1 — Enable required APIs
# artifact-project: AR, IAM, WIF
gcloud services enable \
artifactregistry.googleapis.com \
iam.googleapis.com \
iamcredentials.googleapis.com \
cloudresourcemanager.googleapis.com \
--project "$AR_PROJECT_ID"
# stg and prod: Cloud Run
for ENV_PROJECT in "$STG_PROJECT_ID" "$PROD_PROJECT_ID"; do
gcloud services enable \
run.googleapis.com \
iam.googleapis.com \
cloudresourcemanager.googleapis.com \
--project "$ENV_PROJECT"
done2 — Create the CI service account (lives in artifact-project)
gcloud iam service-accounts create "$SA_NAME" \
--display-name "CI Pipeline" \
--project "$AR_PROJECT_ID"3 — Create the Workload Identity Pool (lives in artifact-project)
gcloud iam workload-identity-pools create "$WIF_POOL" \
--location global \
--display-name "GitHub Actions Pool" \
--project "$AR_PROJECT_ID"4 — Create the OIDC provider (GitHub)
gcloud iam workload-identity-pools providers create-oidc "$WIF_PROVIDER" \
--location global \
--workload-identity-pool "$WIF_POOL" \
--display-name "GitHub provider" \
--issuer-uri "https://token.actions.githubusercontent.com" \
--attribute-mapping \
"google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner" \
--attribute-condition "assertion.repository_owner == '${GITHUB_ORG}'" \
--project "$AR_PROJECT_ID"5 — Bind the service account to the pool
WIF_POOL_RESOURCE=$(gcloud iam workload-identity-pools describe "$WIF_POOL" \
--location global \
--project "$AR_PROJECT_ID" \
--format "value(name)")
gcloud iam service-accounts add-iam-policy-binding "$SA_EMAIL" \
--role roles/iam.workloadIdentityUser \
--member "principalSet://iam.googleapis.com/${WIF_POOL_RESOURCE}/attribute.repository_owner/${GITHUB_ORG}" \
--project "$AR_PROJECT_ID"6 — Grant CI SA write access to Artifact Registry
gcloud projects add-iam-policy-binding "$AR_PROJECT_ID" \
--member "serviceAccount:${SA_EMAIL}" \
--role roles/artifactregistry.writer \
--condition None7 — Grant CI SA deploy access to stg and prod
for ENV_PROJECT in "$STG_PROJECT_ID" "$PROD_PROJECT_ID"; do
gcloud projects add-iam-policy-binding "$ENV_PROJECT" \
--member "serviceAccount:${SA_EMAIL}" \
--role roles/run.developer \
--condition None
gcloud projects add-iam-policy-binding "$ENV_PROJECT" \
--member "serviceAccount:${SA_EMAIL}" \
--role roles/iam.serviceAccountUser \
--condition None
done8 — Grant Cloud Run compute agents read access to Artifact Registry
# Cloud Run in stg/prod needs to pull images from artifact-project's AR
for ENV_PROJECT in "$STG_PROJECT_ID" "$PROD_PROJECT_ID"; do
PROJECT_NUMBER=$(gcloud projects describe "$ENV_PROJECT" --format "value(projectNumber)")
COMPUTE_SA="${PROJECT_NUMBER}-compute@developer.gserviceaccount.com"
gcloud projects add-iam-policy-binding "$AR_PROJECT_ID" \
--member "serviceAccount:${COMPUTE_SA}" \
--role roles/artifactregistry.reader \
--condition None
done9 — Create the Artifact Registry Docker repository
gcloud artifacts repositories create "$AR_REPO" \
--repository-format docker \
--location "$REGION" \
--description "Container images for all services" \
--project "$AR_PROJECT_ID"10 — Print the values you need for GitHub secrets/variables
echo "── GitHub Secrets ──────────────────────────────────────────────────────"
echo "WIF_SERVICE_ACCOUNT: ${SA_EMAIL}"
echo ""
echo "── GitHub Variables ────────────────────────────────────────────────────"
echo "WIF_PROVIDER:"
gcloud iam workload-identity-pools providers describe "$WIF_PROVIDER" \
--location global \
--workload-identity-pool "$WIF_POOL" \
--project "$AR_PROJECT_ID" \
--format "value(name)"
echo ""
echo "AR_PROJECT_ID: ${AR_PROJECT_ID}"
echo "AR_REGION: ${REGION}"
echo "STG_PROJECT_ID: ${STG_PROJECT_ID}"
echo "PROD_PROJECT_ID: ${PROD_PROJECT_ID}"
echo "CLOUDRUN_REGION: ${REGION}"
echo ""
echo "Artifact Registry: ${REGION}-docker.pkg.dev/${AR_PROJECT_ID}/${AR_REPO}"