chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:58:18 +08:00
commit 6d5d58c1a9
18293 changed files with 3502153 additions and 0 deletions
+634
View File
@@ -0,0 +1,634 @@
name: "Showcase: Promote (staging → prod)"
# Promotes the staging-tested digest of one or more services to prod.
# Workflow_dispatch only. Humans trigger. No automatic prod promotes.
#
# Order:
# 0. resolve-targets → expand the workflow_dispatch `service`
# input (SSOT key, dispatch_name, or
# 'all') into the canonical services_csv
# consumed by every downstream job.
# 1. verify-staging-precondition → live-probe staging for the target service(s).
# Refuse on red (matches bin/railway
# --require-staging-green default).
# 2. promote → bin/railway promote <service>; runs the
# spec §7 hardening (P1..P6).
# 3. verify-prod → verify-deploy.ts --env prod for the
# target service(s). Feature-level probes,
# not naked 200. Then the prod equivalence
# re-sweep gate (§6.2) and the pinned-ness
# gate (lint-prod, §8.2 — every prod service
# must be on an immutable @sha256: digest).
# 4. notify → Slack #oss-alerts on any red. Never #engr.
# success → #team-showcase.
on:
workflow_dispatch:
inputs:
service:
description: "Service to promote (dispatch_name or SSOT key). 'all' = whole fleet. Leave the placeholder to abort."
required: true
type: choice
# >>> BEGIN GENERATED service options (showcase/scripts/sync-promote-service-options.ts) — DO NOT EDIT
default: __select_a_service__
options:
- __select_a_service__
- all
- ag2
- agno
- built-in-agent
- claude-sdk-python
- claude-sdk-typescript
- crewai-crews
- google-adk
- langgraph-fastapi
- langgraph-python
- langgraph-typescript
- langroid
- llamaindex
- mastra
- ms-agent-dotnet
- ms-agent-harness-dotnet
- ms-agent-python
- pydantic-ai
- shell
- shell-dashboard
- shell-docs
- shell-dojo
- showcase-aimock
- showcase-harness
- showcase-pocketbase
- spring-ai
- starter-adk
- starter-agno
- starter-crewai-crews
- starter-langgraph-fastapi
- starter-langgraph-js
- starter-langgraph-python
- starter-llamaindex
- starter-mastra
- starter-ms-agent-framework-dotnet
- starter-ms-agent-framework-python
- starter-pydantic-ai
- starter-strands-python
- strands
- strands-typescript
- webhooks
# <<< END GENERATED service options
digest:
description: "Optional digest override (default: snapshot from staging)"
required: false
type: string
# Serialize ALL promotes on a single input-agnostic group so concurrent runs
# (e.g. `all` + a single-service promote) can't pin the same Railway service
# at once; `cancel-in-progress: false` ensures an in-flight prod promote is
# never cancelled by a newer run queued behind it.
concurrency:
group: showcase-promote
cancel-in-progress: false
permissions:
contents: read
jobs:
resolve-targets:
runs-on: ubuntu-latest
timeout-minutes: 3
permissions:
contents: read
outputs:
# Leaf-set CSV — the BACKWARD-COMPAT promote target the promote job still
# drives off (Phase 1 behavior is unchanged).
services_csv: ${{ steps.resolve.outputs.services_csv }}
# Tier-ordered promote closure (requested transitive runtimeDeps
# Tier-1 verification, §4.2). Phase 1 SURFACES it (operators see the
# closure in the step summary) but the actual promote still uses
# services_csv; U4 enforces tier ordering / dependent-gating off these.
closure_csv: ${{ steps.resolve.outputs.closure_csv }}
# Machine-readable `tier:name` form of the closure for U4 to consume.
closure_plan: ${{ steps.resolve.outputs.closure_plan }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22.x
- name: Generate SSOT artifact
working-directory: showcase/scripts
# The emitted JSON is EPHEMERAL here: resolve-promote-targets.sh reads it
# with jq to resolve which service(s) to promote and it is NEVER
# committed, so oxfmt-canonical formatting is irrelevant. This job's
# `npm ci` runs in showcase/scripts only and does NOT install the
# repo-root oxfmt binary the committed-artifact path shells out to, so
# leave it canonical-free via EMIT_SKIP_OXFMT (else spawnSync ENOENT
# fails the step and blocks EVERY promote). The committed-artifact path
# (static_quality.yml) keeps oxfmt REQUIRED + fail-loud.
env:
EMIT_SKIP_OXFMT: "1"
run: |
npm ci
npx tsx emit-railway-envs-json.ts
- name: Resolve target service set
id: resolve
# Resolution + the tiered-closure derivation live in the bats-tested
# showcase/scripts/resolve-promote-targets.sh so they can't drift from
# their test (see __tests__/resolve-promote-targets.bats), mirroring how
# promote-fleet.sh / verify-prod-display.sh were extracted from this same
# workflow. The script preserves the existing guards (--digest+all
# reject, empty-`all` fail-loud, unknown/ambiguous/not-prod-eligible) and
# emits services_csv (leaf, backward-compat) + closure_csv/closure_plan
# (tiered closure) into $GITHUB_OUTPUT, plus the closure plan + skips into
# $GITHUB_STEP_SUMMARY.
env:
INPUT: ${{ inputs.service }}
DIGEST: ${{ inputs.digest }}
GENERATED: showcase/scripts/railway-envs.generated.json
run: showcase/scripts/resolve-promote-targets.sh
verify-staging-precondition:
needs: [resolve-targets]
runs-on: ubuntu-latest
timeout-minutes: 15
environment: railway
permissions:
contents: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22.x
- working-directory: showcase/scripts
run: npm ci
- name: Live-probe staging for promote precondition
working-directory: showcase/scripts
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
SERVICES_CSV: ${{ needs.resolve-targets.outputs.services_csv }}
run: |
if [ -z "$RAILWAY_TOKEN" ]; then
echo "::error::RAILWAY_TOKEN is not set"
exit 1
fi
# Spec §7.2 P3: the live staging probe at promote time is
# authoritative. CI verify history is a leading indicator only.
# Run from showcase/scripts (where `npm ci` installed tsx) so npx
# uses the local install instead of network-fetching it.
#
# --skip-ineligible: SERVICES_CSV is the PROMOTE target set, which
# for `service=all` legitimately includes services that are
# promotable but NOT staging-probe-eligible (the starter-* fleet
# carries probe.staging=false in the SSOT). Those are an expected
# state here, not a fault — verify-deploy SKIPS them with a clear
# status line and probes only the eligible services, rather than
# hard-crashing the whole precondition on the first starter-*. This
# flag does NOT relax green for eligible services, nor does it
# tolerate an unknown (non-SSOT) name — a typo is still a hard error.
npx tsx verify-deploy.ts --env staging --services "$SERVICES_CSV" --skip-ineligible
promote:
needs: [resolve-targets, verify-staging-precondition]
# Do NOT hard-depend on verify-staging-precondition's RESULT. That job
# probes the FULL requested set all-or-nothing, so a single staging-red
# service (e.g. a chronically-broken integration in `all`) fails it and
# — under the default `if: success()` — would SKIP promote entirely,
# re-blocking the whole fleet. bin/railway enforces staging-green
# PER-SERVICE (spec §7 P2 = latest staging deploy must be SUCCESS, P3 =
# live staging probe, default-on), so a red service is refused on its own
# and lands in promote-fleet.sh's failed set (reds the run) without
# taking the greens down with it. verify-staging-precondition therefore
# stays as an advisory early signal surfaced in the notify payload, not a
# hard blocker. `!cancelled()` keeps a human-cancelled run from promoting;
# mirrors the verify-prod gate idiom below.
if: ${{ !cancelled() && needs.resolve-targets.result == 'success' }}
runs-on: ubuntu-latest
# promote-fleet.sh now fans out promotes WITHIN a tier (PROMOTE_FANOUT, cap
# 5) instead of running the whole fleet serially. Even so, the largest tier
# (~30 integration leaves) at cap 5 with bin/railway's ~300s/service
# verify_serving_digest! is ~6 batches * ~5 min ≈ 30 min, so 20 min cancelled
# a `service=all` promote mid-fleet. 35 min leaves headroom above the
# worst-case tier without masking a genuinely stuck promote.
timeout-minutes: 35
environment: railway
permissions:
contents: read
packages: read
outputs:
# CSV of the services that ACTUALLY promoted (best-effort: a partial
# failure still exposes the succeeded subset). verify-prod scopes its
# prod verification to exactly this set so a single failed service can't
# red the verification of the services that did promote.
succeeded_csv: ${{ steps.promote.outputs.succeeded_csv }}
# Aggregated staging-drift payload: non-empty when any promoted service's
# staging RUNNING digest differed from the current GHCR :latest (i.e.
# staging was NOT serving :latest). Folded into the Slack notify payload
# so operators see "what shipped to prod is not current :latest".
staging_drift: ${{ steps.promote.outputs.staging_drift }}
# Base64-encoded results JSON (schema_version=1) carrying BOTH the
# succeeded[] and failed[] sets, consumed by the three-variant Slack
# renderer (showcase_promote_notify.yml). promote-fleet.sh is the SSOT
# for the result set; the notify job enriches this blob with run-context
# (run_id, trigger, operator, elapsed, pre_staging) before dispatch.
# Empty when the promote step did not run (the notify job synthesizes a
# total-failure blob in that case).
results_b64: ${{ steps.promote.outputs.results_b64 }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- uses: ruby/setup-ruby@d45b1a4e94b71acab930e56e79c6aa188764e7f9 # v1.316.0
with:
ruby-version: "3.3"
bundler-cache: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22.x
- working-directory: showcase/scripts
# Same EPHEMERAL regeneration as resolve-targets: bin/railway reads this
# JSON to derive EXPECTED_DOMAINS and it is never committed, so skip the
# oxfmt-canonical pass (repo-root oxfmt is not installed by this job's
# showcase/scripts-scoped `npm ci`; an ENOENT here would abort promote).
env:
EMIT_SKIP_OXFMT: "1"
run: |
npm ci
npx tsx emit-railway-envs-json.ts
- name: bin/railway promote
id: promote
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Tier-ordered closure plan (U4): promote-fleet.sh prefers CLOSURE_PLAN
# over SERVICES_CSV, promoting BY TIER (0->1->2) with dependent-tier
# gating AND within-tier parallel fan-out (PROMOTE_FANOUT). A serial
# `service=all` fleet overran the job timeout; tiered fan-out cuts the
# wall-clock so the whole fleet completes inside timeout-minutes.
CLOSURE_PLAN: ${{ needs.resolve-targets.outputs.closure_plan }}
# Retained as the backward-compat fallback: promote-fleet.sh uses this
# ONLY when CLOSURE_PLAN is empty (e.g. an older resolve step).
SERVICES_CSV: ${{ needs.resolve-targets.outputs.services_csv }}
DIGEST: ${{ inputs.digest }}
run: |
set -euo pipefail
if [ -z "$RAILWAY_TOKEN" ]; then
echo "::error::RAILWAY_TOKEN is not set"
exit 1
fi
# promote-fleet.sh runs each service in turn BEST-EFFORT: a single
# red service (e.g. a chronically-broken integration in the `all`
# set) must not abort promotion of the rest of the fleet. The script
# attempts every service, aggregates the succeeded/failed sets, emits
# a step summary, and exits non-zero iff ANY service failed (so the
# notify job still fires) — but only AFTER attempting all of them.
# bin/railway itself handles spec §7 preconditions (P1..P6);
# --require-staging-green is default-on and the prior job already
# established staging is green (defense in depth). We deliberately do
# NOT pass --confirm-divergence: WARN-divergence refusals are a real
# signal that must fail the run.
RAILWAY_BIN="showcase/bin/railway" \
showcase/scripts/promote-fleet.sh
verify-prod:
needs: [resolve-targets, promote]
# Run whenever promote actually RAN — success OR partial failure — so the
# services that DID promote still get prod verification. `!cancelled()`
# excludes a human-cancelled run; `needs.promote.result != 'skipped'`
# excludes the case where promote never ran (e.g. an upstream abort/skip).
# Under the default `if: success()` this job was SKIPPED on any partial
# promote failure, leaving the promoted services with zero prod
# verification — that is the bug this gate fixes.
if: ${{ !cancelled() && needs.promote.result != 'skipped' }}
runs-on: ubuntu-latest
timeout-minutes: 20
environment: railway
permissions:
contents: read
outputs:
# Distinguishes a job that actually PROBED prod (`success`) from one that
# SKIPPED probing because nothing promoted (`skipped`). The GitHub job
# `result` is `success` in BOTH cases (the skip path exits 0), so the
# notify job must read THIS output — not `needs.verify-prod.result` — to
# avoid reporting a misleading `verify-prod=success` when prod was never
# touched. A real probe failure / contract violation exits non-zero, so
# the job `result` becomes `failure` and this output is never written
# (notify falls back to the job result for that case).
status: ${{ steps.verify.outputs.status }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22.x
- working-directory: showcase/scripts
run: npm ci
- name: Run verify-deploy --env prod
id: verify
working-directory: showcase/scripts
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
# Scope verification to the services that ACTUALLY promoted (from the
# promote job's best-effort succeeded set), NOT the full requested set
# (resolve-targets) — verifying a service that failed to promote would
# guarantee a red verify and mask the health of the ones that did
# promote.
SERVICES_CSV: ${{ needs.promote.outputs.succeeded_csv }}
# The promote job's result, so the empty-CSV branch can tell a genuine
# all-failed run (empty succeeded set is expected) apart from a
# contract violation (promote reported success yet emitted no CSV).
PROMOTE_RESULT: ${{ needs.promote.result }}
run: |
set -euo pipefail
if [ -z "$RAILWAY_TOKEN" ]; then
echo "::error::RAILWAY_TOKEN is not set"
exit 1
fi
# If NOTHING promoted (every requested service failed, so the
# succeeded set is empty), there is nothing to verify — skip with a
# clear log line rather than calling verify-deploy.ts with an empty
# --services (which would either error or vacuously pass). The promote
# job already exited non-zero in that case, so the overall run is red
# via the notify state machine regardless.
#
# BUT: an empty succeeded set is only legitimate when promote did NOT
# succeed. If promote reported success and STILL emitted no CSV, the
# "promote already failed" assumption that justifies the vacuous skip
# is violated — fail loud instead of silently exiting 0.
if [ -z "$SERVICES_CSV" ]; then
if [ "$PROMOTE_RESULT" = "success" ]; then
echo "::error::promote reported success but succeeded_csv is empty — contract violation"
exit 1
fi
echo "::notice::succeeded_csv is empty (no services promoted, or promote did not run); skipping prod verification. The run is red via the promote job result if anything failed."
# Record that prod was SKIPPED, not verified. The job still exits 0
# (its `result` is `success`), so the notify job reads this `status`
# output to render `verify-prod=skipped` instead of a misleading
# `verify-prod=success`.
echo "status=skipped" >> "$GITHUB_OUTPUT"
exit 0
fi
# Run from showcase/scripts (where `npm ci` installed tsx) so npx uses
# the local install instead of network-fetching it. A non-zero exit
# here fails the step (job `result` = failure) and `status` is never
# written, so notify falls back to the job result.
npx tsx verify-deploy.ts --env prod --services "$SERVICES_CSV"
# Prod was actually probed and passed.
echo "status=success" >> "$GITHUB_OUTPUT"
# ── Prod equivalence gate (UNIT U10, spec §6.2) ──────────────────────
# After the closure pins (promote job) AND the per-service prod probe
# (above), re-sweep the promoted integration closure on the PROD control
# plane (prod harness scheduler + prod harness-workers, or scheduler
# INLINE fallback when workers are unprovisioned — §4.4) and run U9's
# equivalence gate over the FRESH prod rows vs current staging. Promote
# success flips to the equivalence definition (fail only on
# staging-green / prod-not-green, gray/stale excluded, one-directional).
#
# GATED ON CONFIGURATION: the equivalence gate needs prod + staging
# PocketBase creds and the prod Railway environment id. Those are an
# out-of-PR operational prerequisite (§8.1) — until the secrets are wired
# the step ANNOTATES "not configured" and is a no-op, so this job's
# existing per-service prod verification (above) remains the gate. Once
# configured, a gate FAILURE fails the step (and the run).
# Install the pnpm workspace ONLY when the equivalence gate is configured
# (the gate step's enqueue dynamically imports the harness producer graph,
# which lives in the pnpm workspace — `npm ci` in showcase/scripts alone
# does not resolve it). `--ignore-scripts` skips postinstall/Playwright
# browser downloads: the host only ENQUEUES + POLLS prod PocketBase; the
# prod harness-workers own the browser. Gated on the same config check as
# the gate step so the no-op (unconfigured) path stays cheap.
- name: Set up pnpm (equivalence gate only)
if: ${{ steps.verify.outputs.status == 'success' && vars.SHOWCASE_PROD_POCKETBASE_URL != '' && vars.SHOWCASE_STAGING_POCKETBASE_URL != '' && vars.SHOWCASE_RAILWAY_ENV_ID_PROD != '' }}
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
- name: pnpm install (equivalence gate only)
if: ${{ steps.verify.outputs.status == 'success' && vars.SHOWCASE_PROD_POCKETBASE_URL != '' && vars.SHOWCASE_STAGING_POCKETBASE_URL != '' && vars.SHOWCASE_RAILWAY_ENV_ID_PROD != '' }}
run: pnpm install --ignore-scripts
- name: Prod equivalence re-sweep gate
if: ${{ steps.verify.outputs.status == 'success' }}
working-directory: showcase/scripts
env:
# The promoted closure subset that ACTUALLY pinned (best-effort) —
# the set to re-sweep + compare. Same scope as the prod probe above.
PROMOTED_CLOSURE_CSV: ${{ needs.promote.outputs.succeeded_csv }}
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
RAILWAY_PROJECT_ID: ${{ vars.SHOWCASE_RAILWAY_PROJECT_ID }}
RAILWAY_ENVIRONMENT_ID_PROD: ${{ vars.SHOWCASE_RAILWAY_ENV_ID_PROD }}
PROD_POCKETBASE_URL: ${{ vars.SHOWCASE_PROD_POCKETBASE_URL }}
PROD_POCKETBASE_SUPERUSER_EMAIL: ${{ secrets.SHOWCASE_PROD_POCKETBASE_SUPERUSER_EMAIL }}
PROD_POCKETBASE_SUPERUSER_PASSWORD: ${{ secrets.SHOWCASE_PROD_POCKETBASE_SUPERUSER_PASSWORD }}
STAGING_POCKETBASE_URL: ${{ vars.SHOWCASE_STAGING_POCKETBASE_URL }}
STAGING_POCKETBASE_SUPERUSER_EMAIL: ${{ secrets.SHOWCASE_STAGING_POCKETBASE_SUPERUSER_EMAIL }}
STAGING_POCKETBASE_SUPERUSER_PASSWORD: ${{ secrets.SHOWCASE_STAGING_POCKETBASE_SUPERUSER_PASSWORD }}
# §4.4 annotation: flip to "false" once prod harness-workers are
# provisioned-but-degraded is the default-true full-throughput path.
PROD_HARNESS_WORKERS_PROVISIONED: ${{ vars.SHOWCASE_PROD_HARNESS_WORKERS_PROVISIONED }}
run: |
set -euo pipefail
# OUT-OF-PR PREREQ GATE: the equivalence gate is inert until the prod
# + staging PocketBase + prod Railway env-id config is wired (§8.1).
# Absence is NOT a failure — the per-service prod probe above already
# gated the run; this step just annotates that the equivalence gate
# was skipped for lack of configuration.
if [ -z "${PROD_POCKETBASE_URL:-}" ] || [ -z "${STAGING_POCKETBASE_URL:-}" ] || [ -z "${RAILWAY_ENVIRONMENT_ID_PROD:-}" ]; then
echo "::notice::Prod equivalence gate not configured (prod/staging PocketBase + prod Railway env-id absent) — skipping the re-sweep gate. This is the §8.1 out-of-PR prerequisite; the per-service prod probe remains the gate."
exit 0
fi
if [ -z "${PROMOTED_CLOSURE_CSV:-}" ]; then
echo "::notice::no promoted closure to re-sweep — equivalence gate vacuously passes."
exit 0
fi
# A gate FAILURE (a genuine staging-green / prod-not-green regression)
# or a re-sweep timeout (REFUSE) exits non-zero → fails this step and
# the run, exactly like the per-service probe above. `pnpm exec` (not
# `npx`) so the dynamically-imported harness producer graph resolves
# through the workspace node_modules the install step above created.
pnpm exec tsx verify-prod-resweep.ts
# ── Prod pinned-ness gate (UNIT U12, spec §8.2) ──────────────────────
# COMPLEMENTS the equivalence gate above — it asserts PINNED-NESS, not
# equivalence. After the promote pins the closure and prod is verified
# healthy + equivalent, assert that EVERY prod service is on an immutable
# `@sha256:` digest, not a mutable `:latest` tag. A born-on-:latest prod
# service (deploy-to-railway.ts provisions an unpinned source.image, spec
# R-E) can be healthy AND equivalent while still floating on a mutable tag
# — a latent rollback/repro hazard the other gates do not catch. The gate
# logic lives in the bats-tested showcase/scripts/lint-prod-gate.sh (a thin
# `bin/railway lint-prod` wrapper) so it can't drift from its test (see
# __tests__/lint-prod-gate.bats). Runs whenever prod was actually probed
# (same `status == 'success'` guard as the equivalence gate); a non-zero
# exit (an unpinned prod service, or a hard lint-prod error) fails the step
# and the run. No `--exit-zero`: an unpinned prod service must red the run.
- name: Prod pinned-ness gate (lint-prod)
if: ${{ steps.verify.outputs.status == 'success' }}
env:
RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
run: |
set -euo pipefail
if [ -z "$RAILWAY_TOKEN" ]; then
echo "::error::RAILWAY_TOKEN is not set"
exit 1
fi
showcase/scripts/lint-prod-gate.sh
notify:
# Single Slack message per run via the three-variant aggregated renderer
# (.github/workflows/showcase_promote_notify.yml): success ✅ / partial ⚠️
# (per-service Failed bullets, cross-posts #oss-alerts) / total ❌. This job
# REPLACES the old inline two-state notify (success/failure-only, which
# dumped the full requested CSV and mislabeled a partial promote as a blanket
# "Failed [all 39]"). It builds the results JSON (from the promote job's
# results_b64, enriched with this run's context) and DISPATCHES the renderer.
#
# Best-effort promote invariant (do NOT reintroduce a PRE gate):
# resolve-targets = HARD precondition (must succeed).
# verify-staging-precondition = ADVISORY only — surfaced in the Slack
# payload (`pre_staging`), never gates
# promote or run success.
# promote = best-effort (exits non-zero iff a
# service failed).
# verify-prod = verifies the succeeded subset.
needs: [resolve-targets, verify-staging-precondition, promote, verify-prod]
if: always()
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
# The job's own GITHUB_TOKEN cannot start NEW workflow runs (Actions'
# recursion-prevention drops workflow_dispatch events authenticated with
# GITHUB_TOKEN), so the renderer dispatch goes through the devops-bot App
# token minted below — mirroring canary.yml's cross-workflow dispatch.
contents: read
# actions:read lets the payload step query this run's own metadata
# (created_at) to compute real wall-clock elapsed for the Slack message.
actions: read
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
persist-credentials: false
- name: Build notify payload
id: payload
env:
INPUT: ${{ inputs.service }}
# promote-fleet's results JSON (schema_version=1, succeeded[]+failed[]).
# Empty when the promote step did not run (upstream abort/skip/cancel).
RESULTS_B64: ${{ needs.promote.outputs.results_b64 }}
RESOLVE: ${{ needs.resolve-targets.result }}
PRE: ${{ needs.verify-staging-precondition.result }}
PROMOTE: ${{ needs.promote.result }}
PROD: ${{ needs.verify-prod.result }}
# Operator identity for the renderer's `operator_mention`. The renderer
# prefers a Slack users.lookupByEmail on operator_email; we have no
# reliable email for github.actor, so pass the actor as the git-name
# fallback (the renderer degrades to it when the email lookup is empty).
ACTOR: ${{ github.actor }}
# gh api (run metadata lookup for real wall-clock elapsed) needs a token.
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
# SKIP cases that must NOT post a Slack message (parity with the old
# neutral-state arms): a deliberate no-pick abort, or a human cancel.
if [ "$INPUT" = "__select_a_service__" ]; then
echo "::notice::no service selected (deliberate no-op abort); skipping Slack notify."
echo "dispatch=0" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$RESOLVE" = "cancelled" ] || [ "$PRE" = "cancelled" ] || [ "$PROMOTE" = "cancelled" ] || [ "$PROD" = "cancelled" ]; then
echo "::notice::run cancelled by a human; skipping Slack notify (no red page)."
echo "dispatch=0" >> "$GITHUB_OUTPUT"
exit 0
fi
# Map the ADVISORY verify-staging-precondition result onto the
# renderer's pre_staging glyph vocabulary (green/amber/red/skipped).
case "$PRE" in
success) PRE_STAGING="green" ;;
failure) PRE_STAGING="amber" ;; # advisory red → amber (not a gate)
*) PRE_STAGING="skipped" ;;
esac
# 6-char lowercase hex run_id — the renderer's HARD CONTRACT
# (^[0-9a-f]{6}$; see its run-name directive). Derive it from this
# run's own id so it is stable + greppable, never random.
RUN_ID=$(printf '%06x' "$(( GITHUB_RUN_ID % 16777216 ))")
# Base results JSON: prefer promote-fleet's emitted blob (the SSOT for
# succeeded[]/failed[]). When promote never ran (no results_b64), the
# run aborted upstream — synthesize a total-failure blob so the
# renderer posts the ❌ variant instead of nothing.
if [ -n "${RESULTS_B64:-}" ]; then
if ! printf '%s' "$RESULTS_B64" | base64 -d > /tmp/results-base.json 2>/dev/null; then
echo "::error::failed to decode promote results_b64"
exit 1
fi
else
jq -nc '{schema_version:1, abort_reason:"fleet-preflight", succeeded:[], failed:[]}' > /tmp/results-base.json
fi
# Real wall-clock elapsed: this notify job runs last (needs: [...],
# if: always()), so (now - run.created_at) is the run's end-to-end
# duration. Query this run's own metadata via gh api (actions:read).
# Fall back to 0 only if the lookup fails or yields a non-sane value,
# in which case the renderer omits the "in {elapsed}" phrasing.
ELAPSED=0
created_at=$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" \
--jq '.created_at' 2>/dev/null || echo "")
if [ -n "$created_at" ]; then
start_epoch=$(date -u -d "$created_at" +%s 2>/dev/null || echo "")
if [ -n "$start_epoch" ]; then
now_epoch=$(date -u +%s)
delta=$(( now_epoch - start_epoch ))
# Guard against clock skew / parse glitches producing a negative.
if [ "$delta" -ge 0 ]; then
ELAPSED="$delta"
fi
fi
fi
# Enrich the base blob with this run's context (run_id, trigger,
# operator, elapsed, pre_staging). trigger=workflow (the renderer maps
# it to the `showcase_promote.yml` label). elapsed_seconds is the real
# wall-clock computed above; the renderer formats it as "Nm SSs".
ENRICHED_B64=$(jq -c \
--arg run_id "$RUN_ID" \
--arg trigger "workflow" \
--arg actor "$ACTOR" \
--arg pre "$PRE_STAGING" \
--argjson elapsed "$ELAPSED" \
'. + {run_id:$run_id, trigger:$trigger, operator_git_name:$actor, elapsed_seconds:$elapsed, pre_staging:$pre}' \
/tmp/results-base.json | base64 | tr -d '\n')
{
echo "dispatch=1"
echo "run_id=$RUN_ID"
echo "results_b64=$ENRICHED_B64"
} >> "$GITHUB_OUTPUT"
- name: Mint devops-bot token
id: app-token
if: steps.payload.outputs.dispatch == '1'
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: 1108748
private-key: ${{ secrets.DEVOPS_BOT_PRIVATE_KEY }}
# actions=write is the ONLY scope needed: dispatch the sibling renderer
# workflow. The default GITHUB_TOKEN cannot start new workflow runs.
permission-actions: write
- name: Dispatch showcase_promote_notify.yml
if: steps.payload.outputs.dispatch == '1'
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
RESULTS_B64: ${{ steps.payload.outputs.results_b64 }}
RUN_ID: ${{ steps.payload.outputs.run_id }}
run: |
set -euo pipefail
# Dispatch the three-variant renderer. It owns ALL Slack posting
# (#team-showcase init + thread, #oss-alerts cross-post on
# partial/total). Pass trigger=workflow + the 6-hex run_id (which is
# also the renderer's run-name for the CLI's gh-run-list polling
# contract). Dispatched on the default branch (no --ref) so the
# renderer's reviewed main-branch definition runs.
gh workflow run showcase_promote_notify.yml \
--repo "$GITHUB_REPOSITORY" \
-f results="$RESULTS_B64" \
-f trigger=workflow \
-f run_id="$RUN_ID"
echo "dispatched showcase_promote_notify.yml (run_id=$RUN_ID)"