Skip to content

chore: update staging deploy outcome to include auto production PR st… #17

chore: update staging deploy outcome to include auto production PR st…

chore: update staging deploy outcome to include auto production PR st… #17

Workflow file for this run

# **`push`** **`main`**: **Quality** (**`quality-reusable.yml`**) then **staging** deploy + optional **main → production** PR.
#
# Operators: **`bun run github:sync:staging`** syncs **`staging`** + repo policy from **`config/github.policy.ts`**.
name: Main
env:
TURBO_TELEMETRY_DISABLED: "1"
on:
push:
branches: [main]
concurrency:
group: staging-deploy
cancel-in-progress: false
permissions:
contents: read
jobs:
quality:
uses: ./.github/workflows/quality-reusable.yml
secrets: inherit
with:
concurrency_group_suffix: ${{ github.workflow }}-${{ github.ref }}
quality_ruleset_gate:
name: Quality checks
needs: quality
if: always()
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Require reusable Quality workflow succeeded
env:
QUALITY_RESULT: ${{ needs.quality.result }}
run: |
set -euo pipefail
if [[ "$QUALITY_RESULT" != "success" ]]; then
echo "::error::Reusable Quality workflow finished with result ${QUALITY_RESULT}."
exit 1
fi
echo "Quality gate: reusable workflow succeeded."
deploy:
name: Deploy to staging
needs: quality_ruleset_gate
outputs:
did_staging_deploy: ${{ steps.deploy_result.outputs.did_staging_deploy }}
auto_production_pr: ${{ steps.deploy_result.outputs.auto_production_pr }}
runs-on: ubuntu-latest
environment: staging
steps:
- uses: actions/checkout@v6
with:
ref: ${{ github.sha }}
- uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.13
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Deploy preflight
id: preflight
env:
CI: true
STAGE: staging
DEPLOY_ENABLED: ${{ vars.DEPLOY_ENABLED }}
ALCHEMY_PASSWORD: ${{ secrets.ALCHEMY_PASSWORD }}
ALCHEMY_STATE_TOKEN: ${{ secrets.ALCHEMY_STATE_TOKEN }}
CHATROOM_INTERNAL_SECRET: ${{ secrets.CHATROOM_INTERNAL_SECRET }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
run: bun run deploy:preflight:staging
- name: Turbo deploy (staging)
id: deploy
if: steps.preflight.outputs.deploy_enabled == 'true'
env:
CI: true
STAGE: staging
ALCHEMY_PASSWORD: ${{ secrets.ALCHEMY_PASSWORD }}
ALCHEMY_STATE_TOKEN: ${{ secrets.ALCHEMY_STATE_TOKEN }}
CHATROOM_INTERNAL_SECRET: ${{ secrets.CHATROOM_INTERNAL_SECRET }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ vars.CLOUDFLARE_ACCOUNT_ID }}
WEB_DOMAINS: ${{ vars.WEB_DOMAINS }}
WEB_ROUTES: ${{ vars.WEB_ROUTES }}
WEB_ZONE_ID: ${{ vars.WEB_ZONE_ID }}
WEB_DOMAIN_OVERRIDE_EXISTING_ORIGIN: ${{ vars.WEB_DOMAIN_OVERRIDE_EXISTING_ORIGIN }}
run: |
set -o pipefail
bun run deploy:staging 2>&1 | tee deploy-output.txt
deploy_status=${PIPESTATUS[0]}
URL_FILE="$GITHUB_WORKSPACE/.alchemy/ci/web-deploy-url.txt"
WEB_URL=""
if [[ -f "$URL_FILE" ]]; then
IFS= read -r WEB_URL <"$URL_FILE" || true
fi
echo "web_url=$WEB_URL" >> "$GITHUB_OUTPUT"
exit "$deploy_status"
- name: Deploy summary
if: always()
env:
WORKFLOW_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
WEB_URL_FROM_STEP: ${{ steps.deploy.outputs.web_url }}
run: |
PF_OUT="${{ steps.preflight.outcome }}"
DE_OUT="${{ steps.deploy.outcome }}"
EN="${{ steps.preflight.outputs.deploy_enabled }}"
WEB_URL="$WEB_URL_FROM_STEP"
URL_FILE="$GITHUB_WORKSPACE/.alchemy/ci/web-deploy-url.txt"
if [[ -z "$WEB_URL" && -f "$URL_FILE" ]]; then
IFS= read -r WEB_URL <"$URL_FILE" || true
fi
{
echo "## Staging deploy"
echo ""
if [[ "$PF_OUT" == "failure" ]]; then
echo "**Status:** preflight failed — see **Deploy preflight**."
elif [[ "$PF_OUT" == "skipped" ]]; then
echo "**Status:** skipped before preflight."
elif [[ "$EN" != "true" ]]; then
echo "**Status:** skipped — \`DEPLOY_ENABLED\` is not \`true\` on GitHub Environment **staging**."
elif [[ "$DE_OUT" == "success" ]]; then
echo "**Status:** success"
if [[ -n "$WEB_URL" ]]; then
echo "**App (workers.dev):** [$WEB_URL]($WEB_URL)"
fi
elif [[ "$DE_OUT" == "failure" ]]; then
echo "**Status:** Turbo deploy failed — inspect logs."
else
echo "**Status:** inspect [run]($WORKFLOW_URL)."
fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Record staging deploy outcome
id: deploy_result
if: always()
env:
AUTO_PRODUCTION_PR: ${{ vars.AUTO_PRODUCTION_PR }}
run: |
pf="${{ steps.preflight.outputs.deploy_enabled }}"
de="${{ steps.deploy.outcome }}"
if [[ "${pf:-}" == "true" && "${de:-}" == "success" ]]; then
echo "did_staging_deploy=true" >> "$GITHUB_OUTPUT"
else
echo "did_staging_deploy=false" >> "$GITHUB_OUTPUT"
fi
if [[ "${AUTO_PRODUCTION_PR:-}" == "true" ]]; then
echo "auto_production_pr=true" >> "$GITHUB_OUTPUT"
else
echo "auto_production_pr=false" >> "$GITHUB_OUTPUT"
fi
maybe_production_pr:
name: Open or reuse production PR
needs: deploy
if: >-
needs.deploy.outputs.did_staging_deploy == 'true' &&
needs.deploy.outputs.auto_production_pr == 'true'
runs-on: ubuntu-latest
environment: staging
permissions:
contents: read
pull-requests: write
checks: write
steps:
- name: Create or note existing PR (main → production)
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PRODUCTION_PR_HEAD_RAW: ${{ vars.PRODUCTION_PR_HEAD }}
run: |
set -euo pipefail
REPO="${{ github.repository }}"
REPO_OWNER="${REPO%%/*}"
PROD_PR_HEAD="${PRODUCTION_PR_HEAD_RAW:-main}"
PROD_PR_HEAD_FILTER="${REPO_OWNER}:${PROD_PR_HEAD}"
RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
# The **Production merge source** required check is normally produced by
# restrict-production-pr-source.yml on pull_request. Events raised with
# GITHUB_TOKEN (including PRs opened by Actions) do not start new workflow
# runs, so that check never appears. Recreate the same check conclusion via
# the Checks API so production rulesets can merge.
post_production_merge_source_check() {
local sha
sha="$(gh pr list --repo "${REPO}" --base production --head "${PROD_PR_HEAD_FILTER}" --state open --json headRefOid --jq '.[0].headRefOid // empty' 2>/dev/null || true)"
if [[ -z "${sha}" ]]; then
echo "::warning::Production merge source: could not resolve PR head SHA; check run not posted."
return 0
fi
gh api --method POST "/repos/${REPO}/check-runs" \
-f name="Production merge source" \
-f head_sha="${sha}" \
-f status="completed" \
-f conclusion="success"
}
if ! gh api "repos/${REPO}/branches/production" --silent 2>/dev/null; then
{
echo "## Production PR"
echo ""
echo "**Skipped:** remote branch \`production\` does not exist yet."
echo "Create it once (for example push \`main\` to \`production\`), then rerun this workflow or open a PR manually."
} >> "$GITHUB_STEP_SUMMARY"
exit 0
fi
EXISTING="$(gh pr list --repo "${REPO}" --base production --head "${PROD_PR_HEAD_FILTER}" --state open --json url --jq '.[0].url // empty' || true)"
if [[ -n "${EXISTING}" ]]; then
{
echo "## Production PR"
echo "**Already open:** [${EXISTING}](${EXISTING})"
echo ""
echo "Merge this PR to deploy **production**."
} >> "$GITHUB_STEP_SUMMARY"
post_production_merge_source_check
exit 0
fi
BODY="$(printf '%s\n\n%s' "Staging deploy succeeded: ${RUN_URL}" "Merge this PR to deploy **production** (see \`.github/workflows/prod-deploy.yml\`).")"
gh pr create --repo "${REPO}" --base production --head "${PROD_PR_HEAD}" \
--title "Deploy staging to production" \
--body "${BODY}"
PR_URL="$(gh pr list --repo "${REPO}" --base production --head "${PROD_PR_HEAD_FILTER}" --state open --json url --jq '.[0].url // empty' || true)"
{
echo "## Production PR"
if [[ -n "${PR_URL}" ]]; then
echo "**Opened:** [${PR_URL}](${PR_URL}) (**${PROD_PR_HEAD}** → **production**)."
else
echo "Opened **${PROD_PR_HEAD}** → **production** (PR URL unavailable — check the **Pull requests** tab)."
fi
echo ""
echo "Merge this PR to deploy **production**."
} >> "$GITHUB_STEP_SUMMARY"
post_production_merge_source_check