Skip to content

Status Truth

Status Truth #29

Workflow file for this run

---
name: Status Truth
on:
schedule:
- cron: '0 21 * * 0' # Every Sunday at 21:00 UTC
workflow_dispatch:
inputs:
dry_run:
description: 'Dry-run mode: count planned actions but open no issues'
required: false
default: 'true'
type: choice
options:
- 'true'
- 'false'
open_prs:
description: Execute correction PR actions (requires STATUS_TRUTH_PRS_ENABLED variable)
required: false
default: 'false'
type: choice
options:
- 'false'
- 'true'
# Default permissions: read-only. The open job overrides to issues: write.
permissions:
contents: read
# Serialize runs; never cancel an in-progress run so a detect-to-open sequence
# is never interrupted halfway through proposal planning.
concurrency:
group: status-truth
cancel-in-progress: false
jobs:
# ---------------------------------------------------------------------------
# Detect job: read-only scan of public status-truth claims.
# Emits a structured report artifact and counts-only summary.
# No write credentials; no issue mutations.
# ---------------------------------------------------------------------------
detect:
name: Detect status-truth drift
runs-on: ubuntu-latest
timeout-minutes: 15
# Detect job needs read access to contents, issues, and pull-requests for
# API-backed claim resolution (PR/issue/release state lookups).
permissions:
contents: read
issues: read
pull-requests: read
steps:
- name: ⤵ Checkout Branch
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 1
- name: 📦 Setup
uses: ./.github/actions/setup
# Best-effort metadata overlay for parity with workflow consumers. The detect
# step does not load private-token metadata; public writes are gated in `open`.
- name: ⤵ Overlay metadata from data branch
run: |
set -euo pipefail
if git ls-remote --exit-code origin data >/dev/null 2>&1; then
git fetch --no-tags origin data
git checkout origin/data -- metadata/
else
echo "data branch not yet established; skipping metadata overlay."
fi
# Best-effort rollout snapshot for rollout-tracker-status resolution.
# Raw snapshot output is streamed directly into jq and never echoed to logs.
- name: 📸 Load rollout snapshot (best-effort)
id: rollout-snapshot
env:
# Same credential class as gateway-rollout-tracker; scoped to this read-only snapshot step.
GH_TOKEN: ${{ secrets.FRO_BOT_PAT }}
ROLLOUT_TRACKER_SNAPSHOT_PATH: ${{ runner.temp }}/rollout-snapshot.json
# Only .snapshot is consumed here; an empty body skips the ~1 MB tracker-comment fetch.
ROLLOUT_TRACKER_COMMENT_BODY: ''
run: |
set -euo pipefail
snapshot_path="$ROLLOUT_TRACKER_SNAPSHOT_PATH"
tmp_snapshot="${snapshot_path}.tmp"
snapshot_error="${RUNNER_TEMP}/rollout-snapshot-error.log"
trap 'rm -f "$tmp_snapshot" "$snapshot_error"' EXIT
if node scripts/rollout-tracker-snapshot.ts 2>"$snapshot_error" | jq -e '.snapshot' > "$tmp_snapshot"; then
mv "$tmp_snapshot" "$snapshot_path"
item_count=$(jq '.items | length' "$snapshot_path" 2>/dev/null || echo 0)
echo "rollout-snapshot: loaded snapshot with ${item_count} item(s)"
echo "snapshot_path=${snapshot_path}" >> "$GITHUB_OUTPUT"
else
echo "rollout-snapshot: snapshot command unavailable; rollout-tracker claims will be unresolved"
fi
- name: 🔍 Detect status-truth drift
id: detect
env:
# Read-only GITHUB_TOKEN: detect step resolves current-repo PR/issue/release
# state. Cross-repo references are classified as unavailable (unresolved).
# Without a token, all API claims are unresolved (no fake clean report).
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITHUB_REPOSITORY: ${{ github.repository }}
STATUS_TRUTH_REPORT_PATH: ${{ runner.temp }}/status-truth-report.json
# Snapshot path from the pre-detect step (empty string if unavailable).
# loadRolloutSnapshot in the script treats empty string as absent.
STATUS_TRUTH_ROLLOUT_SNAPSHOT_PATH: ${{ steps.rollout-snapshot.outputs.snapshot_path || '' }}
# Scheduled runs default to dry-run=true; only workflow_dispatch can set false.
STATUS_TRUTH_DRY_RUN: ${{ github.event.inputs.dry_run || 'true' }}
run: node scripts/status-truth-detect.ts
- name: 📤 Upload report artifact
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: status-truth-report
# Artifact is workflow-internal (detect → open). It may contain public
# finding fields needed by the open step (e.g. fingerprints, verdicts,
# proposal-eligible flags). It must not contain private identity, source
# snippets, API responses, credentials, or rendered proposal bodies.
path: ${{ runner.temp }}/status-truth-report.json
retention-days: 7
- name: 📋 Write detect summary
if: always()
env:
STATUS_TRUTH_REPORT_PATH: ${{ runner.temp }}/status-truth-report.json
run: |
set -euo pipefail
report="$STATUS_TRUTH_REPORT_PATH"
{
echo "## Status Truth Detect Summary"
echo ""
echo "| Field | Value |"
echo "| --- | --- |"
if [[ -s "$report" ]]; then
echo "| Status | $(jq -r '.status // "unknown"' "$report") |"
echo "| Scan complete | $(jq -r '.scan_complete // false' "$report") |"
echo "| Total claims | $(jq '.counts.total // 0' "$report") |"
echo "| Current | $(jq '.counts.current // 0' "$report") |"
echo "| Drifted | $(jq '.counts.drifted // 0' "$report") |"
echo "| Unresolved | $(jq '.counts.unresolved // 0' "$report") |"
echo "| Unsafe | $(jq '.counts.unsafe // 0' "$report") |"
echo "| Proposal eligible | $(jq '.counts.proposal_eligible // 0' "$report") |"
else
echo "| Status | (report unavailable) |"
fi
} >> "$GITHUB_STEP_SUMMARY"
# ---------------------------------------------------------------------------
# Open job: consume the report artifact, validate it, and perform proposal
# issue actions. In dry-run mode (the default for scheduled runs) no issues
# are mutated and no app write token is minted.
# Separated from detect to enforce credential isolation.
# ---------------------------------------------------------------------------
open:
name: Open status-truth proposals
runs-on: ubuntu-latest
timeout-minutes: 15
needs: detect
if: always()
# Read-only GITHUB_TOKEN permissions for the open job.
# Live issue writes use the scoped app token minted in the "Mint write token"
# step (workflow_dispatch + dry_run=false only). Scheduled dry-runs never
# mint the app token and never mutate issues, so issues:write on the job
# token is not needed and would grant unnecessary privilege.
permissions:
contents: read
issues: read
steps:
- name: ⤵ Checkout Branch
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 1
- name: 📦 Setup
uses: ./.github/actions/setup
# Overlay metadata/ from the data branch so the privacy gate reads the
# authoritative private-repo list from metadata/repos.yaml. The overlay is
# best-effort; `loadPrivateTokensFromDisk()` is the fail-closed chokepoint.
- name: ⤵ Overlay metadata from data branch
run: |
set -euo pipefail
if git ls-remote --exit-code origin data >/dev/null 2>&1; then
git fetch --no-tags origin data
git checkout origin/data -- metadata/
else
echo "data branch not yet established; skipping metadata overlay."
fi
- name: 📥 Download report artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: status-truth-report
path: ${{ runner.temp }}
# Mint a scoped app token only for live writes. Scheduled runs default to dry-run
# and use the job token for read-only planning.
- name: 🔑 Mint write token
if: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run == 'false' }}
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.APPLICATION_ID }}
private-key: ${{ secrets.APPLICATION_PRIVATE_KEY }}
# Scope to this repo only — smallest blast radius.
repositories: ${{ github.event.repository.name }}
# Phase 1 permissions: issue-write + read-only content access.
permission-issues: write
permission-contents: read
- name: 📬 Open status-truth proposals
id: open
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token || github.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
STATUS_TRUTH_REPORT_PATH: ${{ runner.temp }}/status-truth-report.json
STATUS_TRUTH_OPEN_RESULT_PATH: ${{ runner.temp }}/status-truth-open-result.json
# Scheduled runs default to dry-run=true; only workflow_dispatch can set false.
STATUS_TRUTH_DRY_RUN: ${{ github.event.inputs.dry_run || 'true' }}
run: node scripts/status-truth-proposals.ts
- name: 📋 Write open summary
if: always()
env:
STATUS_TRUTH_OPEN_RESULT_PATH: ${{ runner.temp }}/status-truth-open-result.json
run: |
set -euo pipefail
result="$STATUS_TRUTH_OPEN_RESULT_PATH"
{
echo "## Status Truth Open Summary"
echo ""
echo "| Field | Value |"
echo "| --- | --- |"
if [[ -s "$result" ]]; then
echo "| Dry run | $(jq -r 'if .dryRun then "true" else "false" end' "$result") |"
echo "| Label gate failed | $(jq -r 'if .labelGateFailed then "true" else "false" end' "$result") |"
echo "| Opened | $(jq '.counts.opened // 0' "$result") |"
echo "| Updated | $(jq '.counts.updated // 0' "$result") |"
echo "| Reopened | $(jq '.counts.reopened // 0' "$result") |"
echo "| Closed | $(jq '.counts.closed // 0' "$result") |"
echo "| Suppressed | $(jq '.counts.suppressed // 0' "$result") |"
echo "| Failed | $(jq '.counts.failed // 0' "$result") |"
echo "| Same-run deduplicated | $(jq '.plannedCounts.sameRunDeduplicated // 0' "$result") |"
echo "| Version rejected | $(jq '.plannedCounts.versionRejected // 0' "$result") |"
echo "| Blocked | $(jq '(.plannedCounts.blocked // 0)' "$result") |"
echo "| Overflowed | $(jq '(.plannedCounts.overflowed // 0)' "$result") |"
echo "| Needs outcome cooldown | $(jq '(.plannedCounts.needsOutcomeCooldown // 0)' "$result") |"
# Outcome counts (read-model: current state of all proposal issues)
if jq -e '.outcomeCounts' "$result" >/dev/null 2>&1; then
echo ""
echo "### Proposal outcome counts"
echo ""
echo "| Outcome | Count |"
echo "| --- | --- |"
echo "| Proposed pending | $(jq '.outcomeCounts.proposedPending // 0' "$result") |"
echo "| Accepted | $(jq '.outcomeCounts.explicitAccepted // 0' "$result") |"
echo "| Rejected | $(jq '.outcomeCounts.explicitRejected // 0' "$result") |"
echo "| False positive | $(jq '.outcomeCounts.falsePositive // 0' "$result") |"
echo "| Resolved positive | $(jq '.outcomeCounts.resolvedPositive // 0' "$result") |"
echo "| Superseded | $(jq '.outcomeCounts.superseded // 0' "$result") |"
echo "| Needs outcome | $(jq '.outcomeCounts.needsOutcome // 0' "$result") |"
echo "| Conflicting labels | $(jq '.outcomeCounts.conflictingLabels // 0' "$result") |"
echo "| Malformed outcome | $(jq '.outcomeCounts.malformedOutcome // 0' "$result") |"
fi
# Planned per-kind counts (from planner; counts-only, no paths or fingerprints)
kind_count=$(jq '.plannedCountsByKind | length' "$result" 2>/dev/null || echo 0)
if [[ "$kind_count" -gt 0 ]]; then
echo ""
echo "### Planned counts by claim kind"
echo ""
echo "| Kind | Opened | Updated | Reopened | Closed | Suppressed |"
echo "| --- | --- | --- | --- | --- | --- |"
jq -r '
.plannedCountsByKind
| to_entries[]
| "| \(.key) | \(.value.opened // 0) | \(.value.updated // 0) | \(.value.reopened // 0) | \(.value.closed // 0) | \(.value.suppressed // 0) |"
' "$result"
fi
# Proposal outcomes by claim kind (read-model; counts-only, no paths or fingerprints)
outcome_kind_count=$(jq '.outcomeCountsByKind | length' "$result" 2>/dev/null || echo 0)
if [[ "$outcome_kind_count" -gt 0 ]]; then
echo ""
echo "### Proposal outcomes by claim kind"
echo ""
echo "| Kind | Pending | Accepted | Rejected | False positive | Resolved | Superseded | Needs outcome | Conflicting | Malformed |"
echo "| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |"
jq -r '
.outcomeCountsByKind
| to_entries[]
| "| \(.key) | \(.value.proposedPending // 0) | \(.value.explicitAccepted // 0) | \(.value.explicitRejected // 0) | \(.value.falsePositive // 0) | \(.value.resolvedPositive // 0) | \(.value.superseded // 0) | \(.value.needsOutcome // 0) | \(.value.conflictingLabels // 0) | \(.value.malformedOutcome // 0) |"
' "$result"
fi
else
echo "| Status | (open result unavailable) |"
fi
} >> "$GITHUB_STEP_SUMMARY"
# ---------------------------------------------------------------------------
# PRs job: Bounded correction PR execution, gated by three independent keys.
#
# This job only produces real PR actions when ALL of the following hold:
# 1. The repository variable STATUS_TRUTH_PRS_ENABLED is set to 'true'.
# 2. At least one claim kind has been added to GRADUATED_CLAIM_KINDS in
# scripts/status-truth-prs.ts via a reviewed repo change.
# 3. A workflow_dispatch run explicitly sets the open_prs input to 'true'.
#
# Any single key alone produces zero PR actions. Scheduled runs are
# structurally excluded (event-type check) and never open PRs. The script
# re-checks all three keys at startup as defense in depth — the workflow
# if: condition below is not the only enforcement point.
#
# Safety constraints (independently enforced in scripts/status-truth-prs.ts):
# - Ungraduated or disarmed candidates downgrade to proposal-only (no PR actions).
# - Forbidden paths (workflows, hooks, metadata, knowledge, persona, etc.) → proposal-only.
# - Corrected content must re-resolve as current before any write; failure downgrades.
# - At most one open PR per fingerprint; at most one new PR opened per run.
# - Every write independently re-validates the single-file diff and the
# bot-owned correction-branch naming pattern before it happens.
# - Drift-cleared or terminal-labeled proposals close their own PR and delete
# the correction branch; merged PRs are never touched.
# - No merge, approve, automerge, force-push, or retarget actions are possible.
# - Titles, branches, bodies, and comments carry only opaque fingerprint
# digests and normalized fields, and pass the public-output gate.
# - Workflow summaries and logs stay counts-only for PR actions.
# ---------------------------------------------------------------------------
prs:
name: Execute status-truth correction PRs
runs-on: ubuntu-latest
timeout-minutes: 10
needs: [detect, open]
if: ${{ vars.STATUS_TRUTH_PRS_ENABLED == 'true' && github.event_name == 'workflow_dispatch' && github.event.inputs.open_prs == 'true' }}
# Read-only job token; write authority is minted separately below,
# scoped at mint time to this repo and this run only.
permissions:
contents: read
steps:
- name: ⤵ Checkout Branch
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 1
- name: 📦 Setup
uses: ./.github/actions/setup
- name: 📥 Download report artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: status-truth-report
path: ${{ runner.temp }}
# Mint a scoped app token only inside this job, only on armed dispatches.
# Adds contents:write and pull-requests:write for branch/commit/PR
# mutations. issues:write is required because executeStatusTruthPrActions
# closes stale/terminal correction PRs via issues.createComment (PR
# comments are Issues API comments); pull-requests:write alone cannot
# create the closure comment.
- name: 🔑 Mint write token
id: app-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.APPLICATION_ID }}
private-key: ${{ secrets.APPLICATION_PRIVATE_KEY }}
# Scope to this repo only — smallest blast radius.
repositories: ${{ github.event.repository.name }}
permission-contents: write
permission-pull-requests: write
permission-issues: write
# Mint a separate, strictly read-only app token for fetch/list calls
# (existing-PR discovery, terminal-fingerprint issue scan). Keeps the
# write token's blast radius limited to the write calls that actually
# need it — the fetch path never needs write scope.
- name: 🔑 Mint fetch token (read-only)
id: fetch-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.APPLICATION_ID }}
private-key: ${{ secrets.APPLICATION_PRIVATE_KEY }}
# Scope to this repo only — smallest blast radius.
repositories: ${{ github.event.repository.name }}
permission-contents: read
permission-pull-requests: read
permission-issues: read
- name: 🛠 Execute status-truth correction PRs
id: prs
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
STATUS_TRUTH_FETCH_TOKEN: ${{ steps.fetch-token.outputs.token }}
GITHUB_REPOSITORY: ${{ github.repository }}
STATUS_TRUTH_REPORT_PATH: ${{ runner.temp }}/status-truth-report.json
STATUS_TRUTH_PRS_RESULT_PATH: ${{ runner.temp }}/status-truth-prs-result.json
STATUS_TRUTH_DRY_RUN: ${{ github.event.inputs.dry_run || 'true' }}
STATUS_TRUTH_PRS_ENABLED: ${{ vars.STATUS_TRUTH_PRS_ENABLED }}
STATUS_TRUTH_PRS_DISPATCH_INPUT: ${{ github.event.inputs.open_prs || 'false' }}
run: node scripts/status-truth-prs.ts
- name: 📋 Write PR execution summary
if: always()
env:
STATUS_TRUTH_PRS_RESULT_PATH: ${{ runner.temp }}/status-truth-prs-result.json
run: |
set -euo pipefail
result="$STATUS_TRUTH_PRS_RESULT_PATH"
{
echo "## Status Truth PR Execution Summary"
echo ""
echo "| Field | Value |"
echo "| --- | --- |"
if [[ -s "$result" ]]; then
echo "| Armed | $(jq -r 'if .armed then "true" else "false" end' "$result") |"
echo "| Dry run | $(jq -r 'if .dryRun then "true" else "false" end' "$result") |"
echo "| Planned | $(jq '.plannedCounts.prActionsPlanned // 0' "$result") |"
echo "| Downgraded to proposal-only | $(jq '.plannedCounts.downgradedToProposalOnly // 0' "$result") |"
echo "| Path forbidden | $(jq '.plannedCounts.pathForbidden // 0' "$result") |"
echo "| Privacy gate blocked | $(jq '.plannedCounts.privacyGateBlocked // 0' "$result") |"
echo "| Overflow | $(jq '.plannedCounts.overflow // 0' "$result") |"
echo "| Version rejected | $(jq '.plannedCounts.versionRejected // 0' "$result") |"
echo "| Opened | $(jq '.executedCounts.opened // 0' "$result") |"
echo "| Closed | $(jq '.executedCounts.closed // 0' "$result") |"
echo "| Rediscovered | $(jq '.executedCounts.rediscovered // 0' "$result") |"
echo "| Downgraded (execution) | $(jq '.executedCounts.downgraded // 0' "$result") |"
echo "| Safety refused | $(jq '.executedCounts.safetyRefused // 0' "$result") |"
echo "| Failed | $(jq '.executedCounts.failed // 0' "$result") |"
echo "| Branch delete failed | $(jq '.executedCounts.branchDeleteFailed // 0' "$result") |"
echo "| Would open (dry-run) | $(jq '.executedCounts.wouldOpen // 0' "$result") |"
echo "| Would close (dry-run) | $(jq '.executedCounts.wouldClose // 0' "$result") |"
echo "| Would rediscover (dry-run) | $(jq '.executedCounts.wouldRediscover // 0' "$result") |"
echo "| Terminal fingerprints skipped | $(jq '.terminalFingerprintsSkipped // 0' "$result") |"
echo "| Terminal fingerprint duplicates | $(jq '.terminalFingerprintDuplicates // 0' "$result") |"
echo "| Error | $(jq -r '.error // "none"' "$result") |"
else
echo "| Status | (PR execution result unavailable) |"
fi
} >> "$GITHUB_STEP_SUMMARY"