Skip to content
Merged
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
22 changes: 22 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Code owners for centrifuge/github-actions-lib
#
# This library is on the critical path that builds and publishes production
# app code, with the consumer repos' secrets. Paths that change deployment
# behavior, use secrets, or govern the security controls require review from
# a designated owner. Any listed owner can satisfy the required review.

# --- Deployment behavior & secret usage ---
/.github/workflows/app-build-deploy-dev.yml @gpmayorga @hieronx @gregory-kf
/.github/workflows/app-build-deploy-release.yml @gpmayorga @hieronx @gregory-kf
/.github/workflows/app-promote-production.yml @gpmayorga @hieronx @gregory-kf
/.github/workflows/app-rollback.yml @gpmayorga @hieronx @gregory-kf
/actions/build-app/ @gpmayorga @hieronx @gregory-kf
/actions/deploy-app/ @gpmayorga @hieronx @gregory-kf

# --- Security controls & governance ---
# lib-ci enforces SHA-pinning and lint; .pinact.yaml defines pin exceptions;
# CODEOWNERS and CLAUDE.md govern who/what may change the above.
/.github/workflows/lib-ci.yml @gpmayorga @hieronx @gregory-kf
/.pinact.yaml @gpmayorga @hieronx @gregory-kf
/.github/CODEOWNERS @gpmayorga @hieronx @gregory-kf
/CLAUDE.md @gpmayorga @hieronx @gregory-kf
128 changes: 94 additions & 34 deletions .github/workflows/app-build-deploy-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,32 @@ name: App Build & Deploy Release
# prereleased → mainnet build (bundle attached to the release) → staging
# (`versions upload --preview-alias staging`, tagged with the
# release tag) → optionally public-demo
# released → promote the staged version to production
# (`versions deploy <id>@100%` looked up by tag)
# released → notify Slack that the staged version is ready; an
# authorized deployer promotes it by dispatching the app's
# promote-production.yml caller (app-promote-production.yml)
#
# A tag MUST be prereleased before it can be released: production promotes
# the version staging uploaded, and the bundle only exists after the
# prerelease build.
# Production promotion is deliberately NOT automated on the release event.
# Promotion goes through the allowlist-gated dispatch workflow
# (app-promote-production.yml): actor must be in the caller repo's
# AUTHORIZED_DEPLOYERS variable, dispatched from main, audited as a workflow
# run, failed attempts announced to Slack. That is a process control, not a
# hard control: anyone holding a token that can deploy Worker versions can
# still promote, and repo admins can edit the allowlist. It removes the
# automatic, unattended path and ties promotion to a GitHub identity — it
# does not remove the capability.
#
# A tag MUST be prereleased before it can be released: promotion targets the
# version staging uploaded, and the bundle only exists after the prerelease
# build.
#
# Release bundles are immutable: re-running the prerelease build for a tag
# whose bundle already exists fails. Cut a new prerelease for new code, or
# delete the asset from the release page to rebuild the same tag.
#
# GitHub Environments (`staging`, `public-demo`, `production`) resolve
# against the caller's repo. Caller permissions ceiling: contents: write
# (release bundle upload), deployments: write.
# GitHub Environments (`staging`, `public-demo`) resolve against the caller's
# repo. Caller permissions ceiling: contents: write (release bundle upload),
# deployments: write (staging deployment record). The released path only
# reads and posts to Slack, so it needs no extra scopes.

on:
workflow_call:
Expand Down Expand Up @@ -60,21 +72,26 @@ on:
type: boolean
default: false
production-url:
description: 'Fixed production URL for the GitHub deployment record (falls back to the wrangler deployment URL)'
description: 'Public production URL, surfaced in the promotion notification'
required: false
type: string
default: ''
production-worker-name:
description: 'Cloudflare Worker name for the prod environment (wrangler.toml [env.prod]); used in the promotion notification commands and dashboard link'
required: false
type: string
default: ''
secrets:
cloudflare-api-token:
description: 'Cloudflare API token'
required: true
slack-webhook-url:
description: 'Slack incoming-webhook URL for the production-promotion notification'
required: true
outputs:
staging-url:
description: 'Staging deployment URL (prereleased only)'
value: ${{ jobs.deploy-staging.outputs.deployment-url }}
production-url:
description: 'Production deployment URL (released only)'
value: ${{ jobs.deploy-production.outputs.deployment-url }}

jobs:
# One build per release, shared via `needs: build`. Artifact names are
Expand Down Expand Up @@ -161,31 +178,74 @@ jobs:
cloudflare-account-id: ${{ inputs.cloudflare-account-id }}
github-token: ${{ github.token }}

# `released` promotes the version already staged for this tag; deploy-app
# skips download-artifact for prod, so no `needs: build`.
deploy-production:
# `released` does not deploy — it announces that the version staged for
# this tag is ready and posts the gated promotion instructions. The link
# assumes the caller repo names its promote caller `promote-production.yml`
# (documented in the README). No Cloudflare token, no wrangler, no
# artifact; needs only network access to post Slack.
notify-production-promotion:
runs-on: ubuntu-latest
if: github.event.action == 'released'
environment:
name: production
url: ${{ inputs.production-url || steps.deploy.outputs.deployment-url }}
permissions:
contents: read
deployments: write
outputs:
deployment-url: ${{ steps.deploy.outputs.deployment-url }}
steps:
- name: '📥 Checkout Code'
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false
- name: '📣 Request production promotion'
shell: bash
env:
SLACK_WEBHOOK_URL: ${{ secrets.slack-webhook-url }}
APP_NAME: ${{ inputs.app-name }}
WORKER_NAME: ${{ inputs.production-worker-name }}
ACCOUNT_ID: ${{ inputs.cloudflare-account-id }}
PRODUCTION_URL: ${{ inputs.production-url }}
TAG: ${{ github.ref_name }}
ACTOR: ${{ github.actor }}
SERVER_URL: ${{ github.server_url }}
REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail

- name: '🚀 Deploy App to Production'
id: deploy
uses: centrifuge/github-actions-lib/actions/deploy-app@main
with:
environment: prod
app-name: ${{ inputs.app-name }}
cloudflare-api-token: ${{ secrets.cloudflare-api-token }}
cloudflare-account-id: ${{ inputs.cloudflare-account-id }}
github-token: ${{ github.token }}
# Fall back to a readable placeholder so the message is still
# coherent if a caller omits the worker name.
WORKER="${WORKER_NAME:-<worker from wrangler.toml [env.prod]>}"
RELEASE_URL="${SERVER_URL}/${REPOSITORY}/releases/tag/${TAG}"
PROMOTE_URL="${SERVER_URL}/${REPOSITORY}/actions/workflows/promote-production.yml"
DASH_URL="https://dash.cloudflare.com/${ACCOUNT_ID}/workers/services/view/${WORKER}/production/deployments"
GH_COMMAND="gh workflow run promote-production.yml --repo ${REPOSITORY} --ref main -f tag=${TAG}"

# printf per line keeps the message free of YAML/heredoc indentation.
TEXT=$(printf '%s\n' \
"🚀 *${APP_NAME}* — release *${TAG}* is staged, not receiving production traffic yet." \
"Staging already serves this version. Promotion is gated: a deployer listed in AUTHORIZED_DEPLOYERS must dispatch the promote workflow from main." \
"" \
"• Release: <${RELEASE_URL}|${TAG}> — by ${ACTOR}" \
"" \
"*To promote:*" \
'```' \
"${GH_COMMAND}" \
'```' \
"Or via <${PROMOTE_URL}|Promote Production> → Run workflow." \
"Emergency fallback: a Cloudflare admin can promote from the <${DASH_URL}|dashboard>.")
if [ -n "$PRODUCTION_URL" ]; then
TEXT="${TEXT}"$'\n'"Once promoted, live at ${PRODUCTION_URL}"
fi

PAYLOAD=$(jq -n --arg text "$TEXT" '{text: $text}')
HTTP_CODE=$(curl -sS -o /tmp/slack_resp.txt -w '%{http_code}' \
-X POST -H 'Content-type: application/json' \
--data "$PAYLOAD" "$SLACK_WEBHOOK_URL")
echo "Slack responded: ${HTTP_CODE} $(cat /tmp/slack_resp.txt)"

# Fail loudly: a swallowed notification means nobody promotes.
if [ "$HTTP_CODE" != "200" ]; then
echo "::error::Slack notification failed (HTTP ${HTTP_CODE}). Promote via: ${GH_COMMAND}"
exit 1
fi

{
echo "### 🚀 Production promotion required"
echo "Release [\`${TAG}\`](${RELEASE_URL}) is staged. An authorized deployer must promote it:"
echo ""
echo "\`\`\`${GH_COMMAND}\`\`\`"
echo ""
echo "Or dispatch [Promote Production](${PROMOTE_URL})."
} >> "$GITHUB_STEP_SUMMARY"
Loading
Loading