Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions .github/workflows/build-candidate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# DRAFT — promotion pipeline skeleton (see docs/promotion-pipeline.md).
# Builds ONE environment-neutral image from the tip of main, tagged
# candidate-<n> and sha-<gitsha>. The same image is later promoted to
# production unchanged; nothing environment-specific may be baked in here.
name: Build Candidate

on:
workflow_call:
inputs:
workflow-ref:
type: string
required: false
default: main
description: Branch, tag or commit used when calling the workflow.
target:
type: string
required: true
description: Runtime target (ecs, lambda, cloudrun). Selects registry + build identity.
outputs:
project-name:
description: Project name
value: ${{ jobs.build.outputs.project-name }}
candidate:
description: Candidate tag (candidate-<build-number>)
value: ${{ jobs.build.outputs.candidate }}
build-number:
description: Build number
value: ${{ jobs.build.outputs.build-number }}

jobs:
build:
name: Build candidate image
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
id-token: write
contents: read
outputs:
project-name: ${{ steps.env.outputs.project-name }}
candidate: candidate-${{ steps.build-number.outputs.build-number }}
build-number: ${{ steps.build-number.outputs.build-number }}
steps:
- name: Checkout Cru Actions & Workflows
uses: actions/checkout@v7
with:
repository: CruGlobal/.github
ref: ${{ inputs.workflow-ref }}
path: cru-github-actions

# TODO(draft): environment is passed ONLY to resolve the build role and
# secrets namespace (candidates are prod-bound). It must never influence
# image contents. Open question 1: introduce a `shared` BUILD namespace.
- name: Setup Build environment
uses: ./cru-github-actions/actions/setup-env
id: env
with:
environment: production
role-suffix: ${{ inputs.target == 'ecs' && 'TaskRole' || 'GitHubRole' }}

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v6
with:
aws-region: us-east-1
role-to-assume: ${{ steps.env.outputs.role-arn }}

- name: Generate Build Number
uses: ./cru-github-actions/actions/build-number
id: build-number

# TODO(draft): GCP auth + Artifact Registry login when target == cloudrun
# (mirrors build-cloudrun.yml: google-github-actions/auth@v3 via
# vars.GCP_WORKLOAD_IDENTITY_PROVIDER + vars.GCP_SERVICE_ACCOUNT).
- name: Login to Amazon ECR
if: inputs.target != 'cloudrun'
uses: aws-actions/amazon-ecr-login@v2
id: ecr
with:
mask-password: true

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4

- name: Export BUILD secrets
uses: ./cru-github-actions/actions/secrets
with:
type: BUILD

- name: Checkout project
uses: actions/checkout@v7
with:
ref: main
path: project

# Env-neutral: no ENVIRONMENT build arg, no env-specific tags. The image
# gets candidate-<n> and sha-<gitsha> and is pushed once.
- name: Build & push candidate
working-directory: project
run: ./build.sh
env:
DOCKER_ARGS: >-
--tag ${{ steps.ecr.outputs.registry }}/${{ steps.env.outputs.project-name }}:candidate-${{ steps.build-number.outputs.build-number }}
--tag ${{ steps.ecr.outputs.registry }}/${{ steps.env.outputs.project-name }}:sha-${{ github.sha }}
--push

- run: echo "::notice title=Candidate Built::${{ steps.env.outputs.project-name }} candidate-${{ steps.build-number.outputs.build-number }} (sha-${{ github.sha }})"
108 changes: 108 additions & 0 deletions .github/workflows/promote.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# DRAFT — promotion pipeline skeleton (see docs/promotion-pipeline.md).
# Promotes the EXACT image currently running on stage to production:
# resolve the stage digest, deploy that digest to prod (no rebuild),
# then tag it release-<r> as the rollback point.
#
# Intended caller: cru-deploy (workflow_dispatch wrapper). The `production`
# GitHub Environment there provides the human gate (required reviewers);
# the concurrency group below is the shared production lock.
name: Promote to Production

on:
workflow_call:
inputs:
workflow-ref:
type: string
required: false
default: main
project-name:
description: Project Name
required: true
type: string
deploy-type:
description: Runtime target (ecs, lambda, cloudrun)
required: true
type: string
project:
description: GCP Project (cloudrun only)
required: false
type: string
secrets:
datadog-api-key:
description: DataDog API Key
required: true

jobs:
promote:
name: Promote ${{ inputs.project-name }} stage image to production
runs-on: ubuntu-latest
environment: production
# The single production lock: a promote, a rollback, and a hotfix for the
# same project can never overlap. Never cancel an in-flight production action.
concurrency:
group: production-${{ inputs.project-name }}
cancel-in-progress: false
permissions:
id-token: write
contents: write
env:
DD_API_KEY: ${{ secrets.datadog-api-key }}
steps:
- name: Checkout Cru Actions & Workflows
uses: actions/checkout@v7
with:
repository: CruGlobal/.github
ref: ${{ inputs.workflow-ref }}
path: cru-github-actions

- name: Setup Deploy environment
uses: ./cru-github-actions/actions/setup-env
id: env
with:
project-name: ${{ inputs.project-name }}
environment: production

- name: Record deploy start
run: echo "DEPLOY_STARTED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_ENV"

# TODO(draft): per deploy-type, query what is ACTUALLY running on stage
# and resolve it to an immutable digest:
# ecs: aws ecs describe-services / describe-task-definition -> container image
# lambda: aws lambda get-function -> Code.ResolvedImageUri (already a digest)
# cloudrun: gcloud run services describe -> spec image + status digest
# Refusing to promote by tag makes this race-free: what was reviewed is
# what ships, even if a newer candidate landed on stage mid-review.
- name: Resolve stage image digest
id: stage
run: |
echo "TODO(draft): resolve digest for ${{ inputs.deploy-type }}"
echo "digest=sha256:TODO" >> "$GITHUB_OUTPUT"

# TODO(draft): requires the deploy actions to accept an explicit `image`
# input (docs/promotion-pipeline.md, required change 1). Auth per target:
# GitHubDeployECS / GitHubDeployLambda roles, cru-deploy@<project> SA.
- name: Deploy stage digest to production
run: echo "TODO(draft): deploy ${{ steps.stage.outputs.digest }} via deploy-${{ inputs.deploy-type }} action"

# TODO(draft): allocate release number (monotonic per project — reuse the
# DynamoDB counter pattern) and add release-<r> to the promoted digest.
# Needs registry tag-write on the deploy identities OR a release ledger
# instead (docs/promotion-pipeline.md, open question 3).
- name: Tag release
id: release
run: echo "TODO(draft): tag ${{ steps.stage.outputs.digest }} as release-<r>"

# Pinned major + continue-on-error: telemetry must never fail a
# production action (see the deployment-mark incident, PR #425).
- name: Record Deployment in Datadog
continue-on-error: true
run: >-
npx @datadog/datadog-ci@5 dora deployment
--service "$PROJECT_NAME"
--env production
--version "${{ steps.stage.outputs.digest }}"
--started-at "$DEPLOY_STARTED_AT"
--finished-at "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
--skip-git

- run: echo "::notice title=Promoted::$PROJECT_NAME stage image promoted to production"
92 changes: 92 additions & 0 deletions .github/workflows/rollback.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# DRAFT — promotion pipeline skeleton (see docs/promotion-pipeline.md).
# Redeploys a previous release's image to production. No rebuild, ever.
# Shares the production lock and human gate with promote.yml. This same
# workflow is the execution target for severity-gated AUTO-rollback later —
# automation would invoke it only for releases stamped rollback-safe.
name: Rollback Production

on:
workflow_call:
inputs:
workflow-ref:
type: string
required: false
default: main
project-name:
description: Project Name
required: true
type: string
deploy-type:
description: Runtime target (ecs, lambda, cloudrun)
required: true
type: string
release:
description: Release tag to redeploy (defaults to the previous release)
required: false
type: string
project:
description: GCP Project (cloudrun only)
required: false
type: string
secrets:
datadog-api-key:
description: DataDog API Key
required: true

jobs:
rollback:
name: Rollback ${{ inputs.project-name }} to ${{ inputs.release || 'previous release' }}
runs-on: ubuntu-latest
environment: production
concurrency:
group: production-${{ inputs.project-name }}
cancel-in-progress: false
permissions:
id-token: write
contents: write
env:
DD_API_KEY: ${{ secrets.datadog-api-key }}
steps:
- name: Checkout Cru Actions & Workflows
uses: actions/checkout@v7
with:
repository: CruGlobal/.github
ref: ${{ inputs.workflow-ref }}
path: cru-github-actions

- name: Setup Deploy environment
uses: ./cru-github-actions/actions/setup-env
id: env
with:
project-name: ${{ inputs.project-name }}
environment: production

- name: Record deploy start
run: echo "DEPLOY_STARTED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_ENV"

# TODO(draft): resolve the target release — the given release tag, or the
# release immediately before the one currently deployed — to a digest.
- name: Resolve release digest
id: target
run: |
echo "TODO(draft): resolve ${{ inputs.release || 'previous release' }} for ${{ inputs.deploy-type }}"
echo "digest=sha256:TODO" >> "$GITHUB_OUTPUT"

- name: Deploy release digest to production
run: echo "TODO(draft): deploy ${{ steps.target.outputs.digest }} via deploy-${{ inputs.deploy-type }} action"

# dora deployment has no --is-rollback; tag it instead. Pinned major +
# continue-on-error per the deployment-mark incident (PR #425).
- name: Record Rollback in Datadog
continue-on-error: true
run: >-
npx @datadog/datadog-ci@5 dora deployment
--service "$PROJECT_NAME"
--env production
--version "${{ steps.target.outputs.digest }}"
--started-at "$DEPLOY_STARTED_AT"
--finished-at "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
--skip-git
--custom-tags "rollback:true"

- run: echo "::notice title=Rolled Back::$PROJECT_NAME production rolled back to ${{ inputs.release || 'previous release' }}"
Loading