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
59 changes: 59 additions & 0 deletions .github/workflows/add-to-kanban.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,6 +9,11 @@ on:
jobs:
add-to-project:
runs-on: ubuntu-latest
# NO GITHUB_TOKEN AT ALL (saadqbal, #2181). Every call in this job authenticates
# as the App, so the workflow token needs nothing -- and an empty grant is the
# only version of that claim a reader can check. Free, and it means the least-
# privilege story covers both credentials in the job rather than just the loud one.
permissions: {}
steps:
# Board writes authenticate as the tracebloc-release-train App (backend#2036),
# not a human's PAT. `owner:` yields an ORG-scoped installation token; a
Expand All@@ -25,6 +30,60 @@ jobs:
app-id: ${{ secrets.RELEASE_TRAIN_APP_ID }}
private-key: ${{ secrets.RELEASE_TRAIN_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
# SCOPED TO THIS REPO, or the two reads below land org-wide (saadqbal,
# #2181). `owner:` alone does not narrow anything -- run 32239403796 says
# so in as many words: "Input 'repositories' is not set. Creating token for
# all repositories owned by tracebloc." A token calling itself
# least-privilege while carrying issue+PR read across all 19 installed
# repos is the claim this PR exists to stop making.
#
# `organization_projects` is an ORG-level permission and is not affected by
# repo scoping, so the board write should be unchanged -- but that is an
# assumption, and it is the same class of assumption that broke the first
# attempt, so the verification run is what settles it rather than this
# comment. If it is wrong the failure is LOUD (see below), which is what
# makes trying it cheap.
repositories: ${{ github.event.repository.name }}
# Least privilege (backend#2166): without any `permission-*` the token
# carries the App's FULL installation grant. actions/add-to-project needs
# THREE scopes, not one: it must RESOLVE the triggering issue/PR node
# before it can add it to the board, so it needs read on both content
# types (this workflow fires on `issues` and `pull_request`) in addition
# to the project write. Projects-write alone leaves the node unresolvable
# -- the add fails with "Could not resolve to a node with the global id".
#
# WHAT IS ACTUALLY DEMONSTRATED, and what is not. Stated narrowly because
# two earlier versions of this paragraph each overclaimed in a different
# direction, and this text is copied verbatim into 17 repos -- a wrong
# argument here is a wrong argument 17 times, in a byte-compared file
# nobody re-derives.
#
# DEMONSTRATED: a MISSING READ scope fails loudly. Run 32239403796 on this
# branch, at commit 218f0b13 (projects-write only), errored with
# `Could not resolve to a node with the global id` and the job went RED --
# `add-to-project` routes GraphQL errors through `setFailed`.
#
# NOT DEMONSTRATED: the case the FIRST version of this comment described --
# the token resolving the node fine and then 403ing the BOARD WRITE. No run
# has ever produced it. So "fails loudly" is proven for the read scopes and
# is an expectation, not a measurement, for the write.
#
# AND ONE RUN THAT LOOKED LIKE EVIDENCE IS NOT (aptracebloc). The previous
# wording cited run 32237283072 as a second scope failure. It is not one:
# it ran on `develop`, whose file at that sha passes NO `permission-*` at
# all (the App's full grant), and it failed on
# `Could not resolve to a node with the global id of I_kwDONNfQt88...` --
# a node a fully-privileged token also cannot see, i.e. an issue that no
# longer exists (this workflow fires on `issues: transferred`). Run
# 32237067262, the SAME develop sha, succeeded 2m34s earlier. A dead node,
# not a permission.
#
# The proof this is right is therefore still a LANDED CARD, not a passing
# mint: a mint can succeed with a scope the board write then needs and
# lacks, and that is the one path nothing here has exercised.
permission-issues: read
permission-pull-requests: read
permission-organization-projects: write

- uses: actions/add-to-project@5afcf98fcd03f1c2f92c3c83f58ae24323cc57fd # v2.0.0
with:
Expand Down
76 changes: 72 additions & 4 deletions .github/workflows/backend-fixtures-drift.yml
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,12 +19,24 @@ name: Backend fixtures drift
# org Actions secret. Until that secret exists the job SKIPS with a warning
# instead of failing: the gate is inert, not red (same activation model as
# the public PII gate's denylist secret).
#
# THAT ACTIVATION PHASE IS OVER. The secret has existed since 2026-08-05 and
# this check is being armed as a required context (backend#2212), so the
# skip-with-a-warning path is now a fail-open: a *required* check that exits 0
# when it cannot run is worse than an advisory one, because it also looks
# solved. It now FAILS instead -- see the step below for the three-way split
# and why a public repo needs an override label rather than a silent pass.

on:
push:
branches: [develop, main]
pull_request:
branches: [develop, main]
# labeled/unlabeled so the skip-fixtures-drift override actually re-runs the
# gate. The default types are opened/synchronize/reopened, so without these
# applying the label would change nothing until the next push -- exactly the
# bug Bugbot caught on version-bump-gate-caller.yml's skip-version-gate.
types: [opened, synchronize, reopened, ready_for_review, labeled, unlabeled]
workflow_dispatch:

permissions:
Expand All@@ -42,13 +54,69 @@ jobs:
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

# Three-way, because this repo is PUBLIC and the two reasons the token can
# be missing are not the same thing:
#
# token present -> run the check (the normal path)
# absent, Dependabot PR -> PASS with a ::notice::. Dependabot runs get
# DEPENDABOT-scoped secrets, never Actions
# secrets, so the token is empty on them
# however correctly it is set -- and unlike a
# fork, head.repo.fork is FALSE, so they would
# otherwise land in the misconfiguration branch
# below and block every security bump (Bugbot,
# averaging-service#367). Safe to defer here:
# a dependency bump cannot alter
# internal/api/testdata/*.json, and if it did,
# internal/api/contracts_test.go replays every
# fixture through the real decode paths under
# the REQUIRED `Test` check, with no token.
# Divergence from the pinned backend ref is
# re-checked by the push run on develop/main,
# where Actions secrets are available. Set the
# token as a Dependabot secret too and this
# branch stops being reached.
# absent, fork PR -> FAIL. GitHub does not expose repo secrets to
# forks by design, so the check genuinely
# cannot run. A maintainer verifies the
# fixtures by hand and applies
# `skip-fixtures-drift`, which is a permanent
# artifact on the PR -- the same model as
# skip-fr-gate. Passing forks silently would
# fail open on exactly the contributions that
# warrant the most scrutiny.
# absent, not a fork -> FAIL. The secret was removed, rotated or
# expired. That is a misconfiguration, and it
# used to read as a clean run.
#
# Every ${{ }} goes through env:, never into the run: body.
- name: scripts/sync-backend-fixtures.sh --check
env:
BACKEND_CONTRACTS_TOKEN: ${{ secrets.BACKEND_CONTRACTS_TOKEN }}
IS_FORK: ${{ github.event.pull_request.head.repo.fork || false }}
# The PR author, not github.actor: stable across re-runs of the same PR.
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
OVERRIDE: ${{ contains(github.event.pull_request.labels.*.name, 'skip-fixtures-drift') }}
run: |
if [ -z "$BACKEND_CONTRACTS_TOKEN" ]; then
echo "::warning::BACKEND_CONTRACTS_TOKEN is not set — skipping the backend fixtures drift check." \
"Add a read-only (Contents: read) token for tracebloc/backend as a repo/org Actions secret to activate this gate."
set -uo pipefail

if [ "$OVERRIDE" = "true" ]; then
echo "::warning title=Backend fixtures drift OVERRIDDEN::The skip-fixtures-drift label is applied, so this gate did NOT verify internal/api/testdata/*.json against tracebloc/backend. Whoever applied the label is asserting they checked the fixtures by hand. The label stays on the PR as the record."
exit 0
fi
./scripts/sync-backend-fixtures.sh --check

if [ -n "$BACKEND_CONTRACTS_TOKEN" ]; then
exec ./scripts/sync-backend-fixtures.sh --check
fi

if [ "$PR_AUTHOR" = "dependabot[bot]" ]; then
echo "::notice title=Backend fixtures drift deferred::Dependabot runs receive Dependabot-scoped secrets, not Actions secrets, so BACKEND_CONTRACTS_TOKEN is empty here by design. A dependency bump cannot alter internal/api/testdata/*.json, and internal/api/contracts_test.go replays every fixture through the real decode paths under the required Test check without needing a token. Drift against the pinned backend ref is re-checked by the push run on develop/main. Set BACKEND_CONTRACTS_TOKEN as a Dependabot secret too for full coverage here."
exit 0
fi

if [ "$IS_FORK" = "true" ]; then
echo "::error title=Backend fixtures drift could not run::This PR is from a fork, and GitHub does not expose BACKEND_CONTRACTS_TOKEN to forks, so the vendored fixtures could not be checked against tracebloc/backend. A maintainer must verify internal/api/testdata/*.json by hand and apply the 'skip-fixtures-drift' label. Refusing to report green on a check that did not run (backend#2212)."
else
echo "::error title=BACKEND_CONTRACTS_TOKEN is missing::The secret is not readable on this run, so the backend fixtures drift check did not execute. It is expected on develop/main PRs from this repo -- if it was rotated or removed, restore a read-only (Contents: read) token for tracebloc/backend as a repo/org Actions secret. This step used to exit 0 here, which reported a check that never ran as a passing one (backend#2212)."
fi
exit 1
Loading