Skip to content

delivery-snapshot

delivery-snapshot #68

# Delivery snapshot — regenerate the cockpit's delivery dashboard from real merge
# evidence on every merge to master, and FAIL CLOSED if the rendered snapshot
# would misreport delivery.
#
# Governance intent (registry/board-spec.yaml): GitHub Projects is a render
# target, not the truth. The truth is merged PRs and issue events. This job
# recomputes from that evidence so the dashboard cannot drift from what shipped.
name: delivery-snapshot
on:
push:
branches: [master]
paths:
- 'socioprophet-web/client-vue/**'
- '.github/workflows/delivery-snapshot.yml'
schedule:
# Refresh daily so the board reflects reality even in a quiet week.
- cron: '17 6 * * *'
workflow_dispatch:
permissions:
contents: write
pull-requests: write
issues: read
concurrency:
group: delivery-snapshot
cancel-in-progress: true
jobs:
regenerate:
runs-on: ubuntu-latest
defaults:
run:
working-directory: socioprophet-web/client-vue
steps:
# Prefer a minted, short-lived GitHub App installation token so the snapshot
# PR is AUTHORED BY THE APP. GitHub deliberately does not start workflow runs
# for events triggered by the default GITHUB_TOKEN (recursion guard), so a PR
# opened with github.token never triggers the required `gate / check` and can
# therefore never auto-merge. An App-authored PR triggers the gate normally.
# No long-lived PAT is stored — the token is minted at runtime and expires.
#
# Until the App is configured this falls back to github.token: the PR still
# opens and is fully gated (never a direct push, never fail-open), it just
# needs its gate triggered once (e.g. a maintainer close/reopen) to merge.
# One-time setup: create a GitHub App with contents:write + pull-requests:write
# on SocioProphet/socioprophet, install it, then set repo var
# DELIVERY_SNAPSHOT_APP_ID and repo secret DELIVERY_SNAPSHOT_APP_KEY.
- name: Mint scoped GitHub App token (optional)
id: apptoken
if: ${{ vars.DELIVERY_SNAPSHOT_APP_ID != '' }}
uses: actions/create-github-app-token@v1
with:
app-id: ${{ vars.DELIVERY_SNAPSHOT_APP_ID }}
private-key: ${{ secrets.DELIVERY_SNAPSHOT_APP_KEY }}
owner: SocioProphet
repositories: socioprophet
- uses: actions/checkout@v4
with:
fetch-depth: 0
# Persist whichever token authors the snapshot PR, so the push + PR are
# attributed to the App when configured (falls back to github.token).
token: ${{ steps.apptoken.outputs.token || github.token }}
# board-spec.yaml is NOT in this repo — its canonical home is
# SocioProphet/sociosphere (a public repo). Consume the single source of
# truth via a sparse cross-repo checkout rather than forking a copy in here;
# pointing --board-spec at a nonexistent path left "Board spec resolved"
# permanently unverified. Uses github.token: sociosphere is public, and the
# App token is scoped to socioprophet only.
- name: Fetch canonical board spec (sociosphere is the source of truth)
uses: actions/checkout@v4
with:
repository: SocioProphet/sociosphere
sparse-checkout: registry/board-spec.yaml
sparse-checkout-cone-mode: false
path: .sociosphere
token: ${{ github.token }}
- uses: actions/setup-node@v4
with:
node-version: '22'
- name: Regenerate snapshot from merge evidence
env:
GH_TOKEN: ${{ github.token }}
run: |
node scripts/generate-delivery-snapshot.mjs \
--orgs "SocioProphet,SourceOS-Linux" \
--days 14 \
--history-weeks 10 \
--board-spec "${GITHUB_WORKSPACE}/.sociosphere/registry/board-spec.yaml"
# The gate. A snapshot that could not collect live evidence, or that
# asserts a value with no evidence ref, must not be published as status.
# Integrity fails the build (the snapshot would misinform).
# Health findings are printed and PUBLISHED — suppressing the dashboard when
# flow degrades is exactly backwards.
- name: Verify snapshot integrity (fail closed)
run: node scripts/verify-delivery-snapshot.mjs
- name: Typecheck and test the surface
run: |
# client-vue commits a pnpm-lock.yaml, not a package-lock.json, so
# `npm ci` (which requires package-lock/npm-shrinkwrap) fails EUSAGE and
# the whole snapshot job dies before it can commit the refreshed data.
# Match the working convention of client-vue-product-build.yml, which
# installs with `npm install`.
npm install --no-audit --no-fund
npx vue-tsc --noEmit
npx vitest run src/__tests__/deliveryDashboard.test.ts src/__tests__/deliveryEconomics.test.ts
# Publish the refreshed snapshot through the SAME gate as everything else.
# master is protected (required check "gate / check"), so a direct push is —
# correctly — rejected (GH006). Pushing anyway, or making that push
# non-fatal, would be fail-open: it silently disables the refresh. Instead
# the snapshot arrives as a gated, auto-merging PR from a bot-owned branch.
- name: Open or update the snapshot PR
env:
# App token when configured (so the PR triggers the gate and auto-merges);
# github.token otherwise (PR opens and is gated, needs its gate triggered).
GH_TOKEN: ${{ steps.apptoken.outputs.token || github.token }}
SNAPSHOT: socioprophet-web/client-vue/src/data/deliverySnapshot.ts
BRANCH: bot/delivery-snapshot
run: |
cd "$GITHUB_WORKSPACE"
if git diff --quiet -- "$SNAPSHOT"; then
echo "snapshot unchanged — nothing to propose"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Carry exactly the regenerated snapshot onto a dedicated bot branch,
# rebuilt from the current master each run so the PR only ever diffs the
# snapshot file.
git checkout -B "$BRANCH"
git add "$SNAPSHOT"
git commit -m "chore(cockpit): refresh delivery snapshot from merge evidence"
git push --force origin "HEAD:$BRANCH"
# Open the PR once; later runs update it via the force-push above.
if gh pr view "$BRANCH" --json state --jq '.state' 2>/dev/null | grep -qx OPEN; then
echo "existing snapshot PR updated"
else
gh pr create \
--base master \
--head "$BRANCH" \
--title "chore(cockpit): refresh delivery snapshot from merge evidence" \
--body $'Automated refresh of the cockpit delivery snapshot from real merge evidence.\n\nThe workflow never pushes to master directly — branch protection correctly rejects that. This PR flows through the same `gate / check` as every other change, and auto-merge is enabled so it merges the moment the gate passes.'
fi
# Let it merge itself once the required gate passes — no standing write
# access to master, no fail-open. Enabling auto-merge is best-effort:
# the load-bearing outcome (an open, gated PR) is already achieved.
gh pr merge "$BRANCH" --squash --auto \
|| echo "auto-merge not set; PR remains open and gated for review/merge"