Skip to content

fix(ci): estate-rescan — fail fast on 403, publish via branch+PR (token grant still required)#641

Merged
hyperpolymath merged 6 commits into
mainfrom
fix/estate-rescan-fail-fast
Jul 21, 2026
Merged

fix(ci): estate-rescan — fail fast on 403, publish via branch+PR (token grant still required)#641
hyperpolymath merged 6 commits into
mainfrom
fix/estate-rescan-fail-fast

Conversation

@hyperpolymath

Copy link
Copy Markdown
Owner

Context

verisimdb-data is the estate fact-store — 302 repos of panic-attack scan output that the
whole neurosymbolic pipeline (PatternRegistry → TriangleRouter → FleetDispatcher) reasons
over. It stopped updating on 2026-07-06 and was still stale on 2026-07-21 (15 days).

Nothing alerted, because estate-rescan.yml was busy retrying the failure.

Root cause

The push step retried any push failure with backoff (2/4/8/16s), on the reasonable
assumption of writer contention. But the actual failure was:

remote: Permission to hyperpolymath/verisimdb-data.git denied to hyperpolymath.
fatal: unable to access '...' The requested URL returned error: 403

HYPATIA_DISPATCH_PAT has read on verisimdb-data (the clone step at line 141 succeeds)
but not Contents:write. A permission error can never clear by retrying — so the loop
converted a hard, actionable auth failure into five rounds of flaky-looking noise, and the
fact-store froze silently.

This is a monitoring failure as much as a permissions one: the retry hid the signal.

Changes

.github/workflows/estate-rescan.yml — narrow the retry rather than remove it. Contention
retry is still correct and is preserved; auth/permission now fails fast with a ::error
naming the exact missing grant.

Verification

Exercised with a stubbed git on PATH covering all four paths:

Scenario Attempts Exit vs before
403 permission denied 1 78 was 5 attempts, exit 128
Contention (! [rejected]) 5 1 unchanged
Immediate success 1 0 unchanged
Transient ×2 then success 3 0 unchanged — still self-heals

shellcheck clean; workflow YAML re-parsed (jobs: ['rescan']).

⚠️ This makes the failure legible — it does not fix it

Action still required: grant HYPATIA_DISPATCH_PAT Contents: write on
hyperpolymath/verisimdb-data. Until that lands, this workflow will now fail fast and
clearly
on the first attempt instead of silently wasting 30s and going stale.

After the grant, verify recovery with:

gh workflow run estate-rescan.yml -R hyperpolymath/hypatia
gh api repos/hyperpolymath/verisimdb-data --jq .pushed_at   # must advance past 2026-07-06

Related

Companion to #640 (arangodb health check). Both are Phase 0 foundation repair: the estate
audit cannot be trusted while hypatia's own tests are red and its fact-store is frozen.

🤖 Generated with Claude Code

…-store

The push step retried ANY push failure with backoff (2/4/8/16s) on the
assumption of writer contention. But `HYPATIA_DISPATCH_PAT` lacks
Contents:write on hyperpolymath/verisimdb-data, so every attempt returned:

    remote: Permission to hyperpolymath/verisimdb-data.git denied to hyperpolymath
    fatal: ... The requested URL returned error: 403

A permission error can never clear by retrying. The loop turned a hard,
actionable auth failure into five rounds of flaky-looking noise, and the
estate fact-store silently went stale: verisimdb-data pushed_at was stuck
at 2026-07-06 and was still stale on 2026-07-21 (15 days) while the
neurosymbolic pipeline kept reasoning over the frozen snapshot.

Retry remains correct for genuine contention, so this narrows rather than
removes it: classify stderr, fail fast with an actionable ::error on
auth/permission, keep the backoff for everything else.

NOTE: this makes the failure legible; it does not grant the permission.
The PAT still needs Contents:write on verisimdb-data (read already works —
the clone step succeeds — so the token is valid but under-scoped).

Verified with a stubbed `git` on PATH, all four paths:
  403 denied            -> 1 attempt,  exit 78   (was 5 attempts, exit 128)
  contention reject     -> 5 attempts, exit 1    (unchanged)
  immediate success     -> 1 attempt,  exit 0    (unchanged)
  transient x2 then ok  -> 3 attempts, exit 0    (unchanged, still self-heals)
shellcheck clean; workflow YAML re-parsed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@hyperpolymath
hyperpolymath marked this pull request as ready for review July 21, 2026 05:38
… via PR

Corrects the diagnosis in the previous commit on this branch. That commit
made the 403 fail fast instead of retrying it five times, which was right,
but it named the wrong cause and told the operator to grant the PAT
Contents:write. That would not have worked.

## Actual cause

`hyperpolymath/verisimdb-data` carries an active `Base` ruleset on
~DEFAULT_BRANCH whose rules include `pull_request` and
`required_signatures`, plus `deletion`, `non_fast_forward` and
`required_linear_history`. Its only bypass actor is RepositoryRole(id=5)
in mode `pull_request`. So `git push origin HEAD:main` is refused for
everyone, admin included, whatever the token can do.

Timeline, which is what identified it:

    Base ruleset created ............ 2026-04-12
    bot pushed fine (16 commits) .... through 2026-06-25 17:40
    Base ruleset UPDATED ............ 2026-06-26 08:57
    bot pushes since ................ none

The ruleset was updated ~15h after the last successful bot push.

## Two measurement traps this exposed

- The repo's `pushed_at` read 2026-07-21 and looked healthy, because
  humans kept committing. Bot-data freshness must be judged by the last
  commit AUTHORED BY THE BOT — which was 2026-06-25, i.e. 26 days stale,
  longer than the "frozen 2026-07-06" previously recorded.
- A hard permission wall retried five times with backoff is
  indistinguishable from flakiness in the log.

## Fix

Push a feature branch (unrestricted — the ruleset targets only the default
branch), open a PR, and merge it. That satisfies `pull_request`, and
satisfies `required_signatures` because GitHub signs the commits it creates
server-side, which a runner's raw `git push` can never do. No ruleset needs
weakening and no new token is needed.

`--rebase`, not `--squash`: squashing would collapse the scans commit and
the index commit into one, breaking the invariant regen-index.sh relies on
(its `last_updated` comes from the newest commit touching scans/, so the
index must land as a separate commit that does not touch scans/). Rebase
also satisfies `required_linear_history`.

Adds a postcondition asserting the PR actually reached MERGED. A refused
merge that exits 0 is precisely the silent-failure class that let this
store go stale for 26 days.

The retained 403 handler now covers the branch push only, where a denial
genuinely *is* a token problem, and says so.

Verified: YAML parses; zero live `push origin HEAD:main` lines remain;
shellcheck -S warning on the step body returns 0 with no findings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@hyperpolymath

Copy link
Copy Markdown
Owner Author

Correction: I named the wrong cause. It is a ruleset, not the token.

The first commit on this branch made the 403 fail fast instead of retrying it five times —
that part was right. But its error message told the operator to grant
HYPATIA_DISPATCH_PAT the Contents: write permission. That would not have worked, and
anyone who acted on it would have burned time regenerating a token for nothing.

What is actually blocking the push

hyperpolymath/verisimdb-data carries an active Base ruleset on ~DEFAULT_BRANCH:

rules pull_request, required_signatures, deletion, non_fast_forward, required_linear_history
bypass actors RepositoryRole(id=5) — in mode pull_request only

So nobody may push directly to main, admin included, regardless of token permissions.
estate-rescan.yml did exactly git push origin HEAD:main.

The timeline is what settled it:

event when
Base ruleset created 2026-04-12
bot pushing fine (16 bot commits) through 2026-06-25 17:40
Base ruleset updated 2026-06-26 08:57
bot pushes since none

Updated roughly fifteen hours after the last successful bot push.

Two measurement traps worth recording

  1. pushed_at lied. It read 2026-07-21 throughout and the repo looked healthy —
    because humans kept committing to it. Bot-data freshness has to be judged by the last
    commit authored by the bot, which was 2026-06-25. That makes the store 26 days
    stale, not the ~15 previously recorded.
  2. A hard permission wall retried five times with backoff is indistinguishable from
    flakiness
    in a log. That is how this stayed invisible.

The fix in this push

Push a feature branch (unrestricted — the ruleset targets only the default branch), open
a PR, and merge it:

  • satisfies pull_request
  • satisfies required_signatures, because GitHub signs the commits it creates
    server-side
    — something a runner's raw git push can never do
  • no ruleset needs weakening, and no new token is required

--rebase, deliberately not --squash: squashing would collapse the scans commit and
the index commit into one, breaking the invariant regen-index.sh depends on (its
last_updated is derived from the newest commit touching scans/, so the index must land
as a separate commit that does not touch it). Rebase also satisfies
required_linear_history.

Added a postcondition asserting the PR actually reached MERGED. A refused merge that
exits 0 is precisely the silent-failure class that let this store go stale for 26 days.

The retained 403 handler now guards only the branch push, where a denial genuinely
would be a token problem — and the message says so.

Verification

  • YAML parses; zero live push origin HEAD:main lines remain in the file
  • shellcheck -S warning on the step body: rc 0, no findings
  • End-to-end: dispatched on this branch as run 29841343819 — result to follow. I am not
    calling this fixed until that run has actually merged a PR into verisimdb-data.

Scope note

The same Base ruleset is active on hypatia, standards and gitbot-fleet. Any other
automation doing a direct push to a default branch is blocked identically and will show the
same misleading 403.

@hyperpolymath hyperpolymath changed the title fix(ci): estate-rescan retried a 403 five times, hiding a frozen fact-store fix(ci): verisimdb-data 403 is a ruleset, not the token — publish via branch+PR Jul 21, 2026
@hyperpolymath

Copy link
Copy Markdown
Owner Author

Re-correction: the original diagnosis was right. It IS the token.

I overturned this branch's first commit on circumstantial evidence and I was wrong. Deployed
and dispatched (run 29841343819); the result settles it.

What the run showed

The rewrite pushes a feature branch, which no ruleset on this repo covers. It still failed:

remote: Permission to hyperpolymath/verisimdb-data.git denied to hyperpolymath.
fatal: unable to access '.../verisimdb-data.git/': The requested URL returned error: 403

The differential test that settled it

Same operation, two different tokens:

token create refs/heads/… on verisimdb-data result
HYPATIA_DISPATCH_PAT (in-workflow) feature branch 403 denied
a plain repo-scope token probe-ruleset-test via git-refs API succeeded (deleted immediately)

A ruleset does not discriminate by token. HYPATIA_DISPATCH_PAT lacks write access to
hyperpolymath/verisimdb-data.
Full stop.

What misled me

The Base ruleset really was updated 2026-06-26 08:57, roughly fifteen hours after the
bot's last successful push at 2026-06-25 17:40. That is a striking correlation and I
read it as causation. A PAT expiring or being re-scoped in the same window fits the same
evidence exactly. Timeline correlation is not a cause; a differential test is.

Confidence levels, stated explicitly this time

  • MEASURED — the PAT cannot write to this repo, on any branch.
  • MEASURED — the ruleset does not block non-default branch creation.
  • INFERRED, NOT MEASURED — that the ruleset would also refuse a direct
    push origin HEAD:main even with a good token. Base does carry pull_request +
    required_signatures on ~DEFAULT_BRANCH with bypass only in pull_request mode, so it
    is likely — but testing it means pushing to the live fact-store, so it stays unproven.

Owner action IS required

Grant HYPATIA_DISPATCH_PAT access to hyperpolymath/verisimdb-data. If it is a
fine-grained PAT, repository access must include that repo, then:

  • Contents: Read and write
  • Pull requests: Read and write ← the PR route needs this too, not just Contents

Keeping the branch+PR code regardless

required_signatures is active on the default branch and a runner's git commit output is
unsigned, so a raw push to main would very likely be refused on signature grounds even
with a perfect token. Publishing through a PR lets GitHub author the merge commits
server-side. The postcondition asserting the PR reached MERGED will settle that
empirically the first time the token works, rather than assuming it.

So this branch stays as-is: the code is necessary but was never sufficient, and the first
commit's advice — grant the token write — was correct all along.

@hyperpolymath hyperpolymath changed the title fix(ci): verisimdb-data 403 is a ruleset, not the token — publish via branch+PR fix(ci): estate-rescan — fail fast on 403, publish via branch+PR (token grant still required) Jul 21, 2026
hyperpolymath and others added 4 commits July 21, 2026 16:15
…s scanning

The write to verisimdb-data was the LAST step of this job, after a ~300-repo
panic-attack scan. Every time the token was wrong, the run burned the whole
scan and then died at the final step. That is a large part of why a hard
permission wall went unnoticed for 26 days: reaching the failure was
expensive, and it presented as a flaky tail-end push rather than as a
categorical "this cannot work".

Adds a first step that creates a ref on verisimdb-data and immediately
deletes it. No content, no commit, about a second. It is the same
differential test that finally identified the fault — the one thing that
distinguishes a token problem from a ruleset problem, since a ruleset
cannot discriminate by token.

Skipped when dry_run is set, since a dry run never publishes.

The error message names the specific grants required (Contents: Read and
write AND Pull requests: Read and write) and notes that a fine-grained PAT
must list the repository explicitly rather than inheriting account scope —
which is the trap that made the earlier "the token is valid, the clone
worked" reasoning misleading.

Verified: YAML parses, preflight is step 0, shellcheck -S warning returns 0
with no findings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The branch+PR route reached the merge and was refused. GitHub said why,
verbatim:

    GraphQL: Base branch requires signed commits.
    Rebase merges cannot be automatically signed by GitHub (mergePullRequest)

So my previous commit's claim — that a rebase merge would satisfy
`required_signatures` — was wrong. Measured, not inferred, this time.

## The merge method is fully determined

verisimdb-data's `Base` ruleset requires `pull_request`,
`required_signatures` and `required_linear_history` on main:

    direct push  -> refused (pull_request required)
    merge commit -> refused (required_linear_history)
    rebase merge -> refused (cannot be signed)   <- measured above
    squash merge -> GitHub authors one new commit server-side and signs it

Squash is the only method that satisfies all three.

## Why that forces TWO PRs

A single squash of "scans + index" breaks the index contract.
scripts/regen-index.sh line 44:

    last_updated="$(git log -1 --format=%cI -- "$SCAN_DIR")"

i.e. the commit time of the newest commit touching scans/. Squashing both
changes together produces ONE commit touching scans/ *and* index.json,
while index.json still carries a timestamp computed before that commit
existed — so index-freshness.yml, which reruns the same script at HEAD and
diffs, would fail.

Publishing as two squashes keeps them separate: PR 1 lands exactly one
commit touching scans/; PR 2 lands exactly one commit that does not. The
index is regenerated AGAINST MERGED main (fetch + reset --hard FETCH_HEAD
after PR 1 lands), so the timestamp it records is the real post-merge
scans commit.

Both PRs assert they reached MERGED before the step succeeds.

Verified: YAML parses; shellcheck -S warning rc 0, no findings; and the
live-code ordering (comments excluded) is
publish scans -> reset to merged main -> regen index -> publish index.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Run 29843933209 reported the publish step as failed. It had not failed:
verisimdb-data#54 merged at 15:37:35Z with a verified signature, and the
363-repo rescan is on main.

`gh pr merge --squash` is evaluated the instant the PR is created, before
the base-branch policy has settled, and returns

    not mergeable: the base branch policy prohibits the merge
    ...add the `--auto` flag

then the merge lands ~30s later regardless. So the step exited 1 on a
success, and — because it exited before opening its second PR — left
index.json stale on main while scans/ had moved. That is the exact
"reports failure, actually succeeded" inverse of the fake-gate problem.

Now: --auto queues the merge, and the step POLLS for state==MERGED rather
than inferring it from the exit code of the command that requested it.

Also proven while fixing the stale index by hand (verisimdb-data#55): an
index-only PR passes index-freshness cleanly under squash, because
regen-index.sh's `git log -1 -- scans/` is unaffected by a squash that
touches no file under scans/. So the two-PR split is sound and the
"squash vs regen-index" contradiction I reported does NOT exist.

Verified: YAML parses; shellcheck rc 0; verisimdb-data main is now
d81aec63 with last_updated=2026-07-21T16:37:34 and 498 scans, and the
index-freshness gate is green on main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@hyperpolymath
hyperpolymath merged commit f0b40ea into main Jul 21, 2026
36 of 38 checks passed
@hyperpolymath
hyperpolymath deleted the fix/estate-rescan-fail-fast branch July 21, 2026 16:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant