Skip to content

publish with an explicit release credential, and prove it before building - #138

Merged
openipc-ai merged 3 commits into
masterfrom
ci/release-token
Aug 30, 2026
Merged

publish with an explicit release credential, and prove it before building#138
openipc-ai merged 3 commits into
masterfrom
ci/release-token

Conversation

@openipc-ai

Copy link
Copy Markdown
Contributor

Companion to OpenIPC/firmware#2339. This repo was next in line for the same failure and hasn't hit it yet only because the release train correctly refused to advance past firmware's failure.

The failure being pre-empted

Moving the nightly off schedule: onto a dispatch changes the identity the run executes as, and the default GITHUB_TOKEN's release-write goes with it. For an app/OAuth-backed identity the releases API answers:

HTTP 403: Resource not accessible by integration

even though the job declares contents: write and the run log confirms the token was granted it — which is why nothing in the workflow file looks wrong. OpenIPC/firmware's run 33273333359 built 102 boards over two hours before finding out; latest never moved and cameras stopped updating.

This repo publishes exactly the same way — gh release create plus a PATCH of refs/tags/latest — so it would have failed identically, after building all 107 devices.

Changes

1. publish uses secrets.RELEASE_TOKEN (Contents: read and write). Also makes publishing independent of who or what starts the run.

2. preflight proves the credential first — draft release created and deleted; invisible, no tag, two API calls. Checking the secret is merely non-empty would not have caught the firmware incident: that token existed and was simply refused, so the probe attempts the write.

Skipped on pull_request, which neither publishes nor has the secret.

Before merging

RELEASE_TOKEN must exist as a repo secret here, or the next non-PR run fails fast in preflight — intended, but red rather than silently unpublished.

Verification

  • Workflow YAML parses; .github/scripts/ci-matrix.py --self-test passes (111 devices, 15 smoke, 39 cases).
  • Both probe failure paths exercised locally: unset token and refused API call each produce a clear ::error:: and exit 1. Neither passes silently.

…ding

This repo was next in line for the failure OpenIPC/firmware hit on
2026-08-29. Moving the nightly off `schedule:` onto a dispatch changes
the identity the run executes as, and the default GITHUB_TOKEN's
release-write goes with it: a scheduled run executes as the repository's
scheduling identity, a dispatched one as whoever dispatched it, and for
an app/OAuth-backed identity the releases API answers 403 "Resource not
accessible by integration".

It does so even though the job declares `contents: write` and the run
log confirms the token was granted it, which is why nothing in the
workflow file looks wrong. firmware's run 33273333359 built 102 boards
over two hours before finding out. This repo builds 107 devices and
publishes the same way -- `gh release create` plus a PATCH of
refs/tags/latest -- so it would have failed identically the first time
the train reached it. It never got that far only because the train
correctly refused to advance past firmware's failure.

The publish step takes RELEASE_TOKEN instead of the default token, and
preflight proves that credential can create a release before the matrix
is allowed to spend hours on it, by creating a draft release and
deleting it: invisible, no tag, two API calls.

Asserting the secret is merely non-empty would not have caught the
firmware incident. The token that failed existed and was simply refused,
so the probe has to attempt the write.
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Validate explicit release credentials before firmware builds

🐞 Bug fix ⚙️ Configuration changes 🕐 Less than 10 minutes

Grey Divider

AI Description

• Uses RELEASE_TOKEN for release creation, uploads, and rolling tag updates.
• Verifies release-write access before expensive non-PR device matrices begin.
• Skips credential probing for pull requests that cannot publish.
Diagram

graph TD
  Trigger["Workflow trigger"] --> Event{"Pull request?"}
  Event -- "Yes" --> Matrix["Device matrix"]
  Event -- "No" --> Probe["Credential probe"] --> API["GitHub Releases API"]
  API -- "Write verified" --> Matrix --> Publish["Publish releases"]
  Publish -- "Release writes" --> API
Loading
High-Level Assessment

The create-and-delete draft probe is the appropriate strategy because it validates effective release-write access, not merely secret presence or nominal permissions. Read-only permission inspection was considered but would not reproduce the integration-specific 403 this change prevents.

Files changed (1) +49 / -1

Bug fix (1) +49 / -1
master.ymlPreflight and publish with an explicit release credential +49/-1

Preflight and publish with an explicit release credential

• Adds a non-PR preflight probe that creates and deletes a draft release, failing before the device matrix when 'RELEASE_TOKEN' is missing or unauthorized. Replaces the publish job's default 'GITHUB_TOKEN' with the same explicit credential so dispatched releases are independent of the triggering identity.

.github/workflows/master.yml

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Aug 30, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Probe misclassifies transient failures ✓ Resolved 🐞 Bug ☼ Reliability
Description
The preflight performs its release POST only once and reports any API failure as an invalid
RELEASE_TOKEN, so a transient 403/429 or service error blocks the entire build even when the
credential is valid. This contradicts the publish path, which explicitly retries the same class of
GitHub API mutations six times because those transient failures are expected.
Code

.github/workflows/master.yml[R159-162]

+          if ! id=$(gh api -X POST "repos/${GH_REPO}/releases" \
+                      -f tag_name="$probe" -f name="$probe" -F draft=true \
+                      --jq .id); then
+            echo "::error::RELEASE_TOKEN cannot create releases in ${GH_REPO}."\
Evidence
The probe invokes gh api directly and exits immediately on any POST failure, while the same
workflow documents transient 403/429 mutations and wraps publication mutations in exponential retry
logic.

.github/workflows/master.yml[159-166]
.github/workflows/master.yml[481-486]
.github/workflows/master.yml[510-522]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The release credential probe treats every one-shot GitHub API failure as a credential failure, allowing transient API errors or rate limiting to abort a valid release run.
## Issue Context
The publishing code already defines retry/backoff behavior specifically for transient 403/429 responses, but the preflight POST and DELETE bypass it.
## Fix Focus Areas
- .github/workflows/master.yml[159-166]
- .github/workflows/master.yml[510-522]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Probe drafts leak on interruption ✓ Resolved 🐞 Bug ☼ Reliability
Description
The probe creates a draft release and relies on the immediately following command as its only
cleanup, so cancellation or process termination between those commands leaves the draft permanently
in the repository. Repeated interrupted runs accumulate stale credential-probe releases because no
later run searches for or removes them.
Code

.github/workflows/master.yml[R158-161]

+          probe="ci-release-credential-probe-${{ github.run_id }}"
+          if ! id=$(gh api -X POST "repos/${GH_REPO}/releases" \
+                      -f tag_name="$probe" -f name="$probe" -F draft=true \
+                      --jq .id); then
Evidence
The workflow creates the draft at lines 158-161, stores its ID only in the running shell, and has a
single subsequent DELETE at line 166 with no independent cleanup or recovery path.

.github/workflows/master.yml[158-166]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The credential probe can leave stale draft releases when the step is interrupted after creation but before deletion.
## Issue Context
Cleanup currently consists only of the next shell command; there is no trap, always-run cleanup step, or stale-probe recovery.
## Fix Focus Areas
- .github/workflows/master.yml[158-166]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Tip of the day
💡 Did you know, you can enable the Remediation agent and Qodo fixes findings in a dedicated fix PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread .github/workflows/master.yml Outdated
Comment thread .github/workflows/master.yml Outdated
Review caught a real defect in the credential probe. It deleted its
draft with a single fail-fast call, so an interrupted or cancelled job
left the draft behind -- and because github.run_id is stable across
re-runs, that leftover would collide with its own re-run and report a
perfectly good credential as broken. A guard that fails wrongly is worse
than no guard, because it blocks good runs and teaches people to ignore
it.

Three changes. The probe tag now carries run_attempt as well as run_id,
so a re-run can never collide with its own leftovers. Deletion moved
into a trap, so a cancelled job still cleans up. And a best-effort sweep
removes probe drafts stranded by earlier runs, because cleanup.yml does
not know this tag pattern and they would otherwise accumulate forever.

The sweep is capped at the 10 most recent releases and swallows every
error. That cap is not arbitrary: listing 100 releases in these repos
returns HTTP 504 because each carries hundreds of assets, measured at
~2s for 10 and ~7s for 30. Housekeeping must never be the thing that
fails a build.

Verified end to end against the live repo with a real token: creates the
draft, reports "release credential OK", the trap removes it, no tag is
created and no published release is touched. Both failure paths still
fail loudly -- unset token, and a credential the API refuses.
Review caught that the probe treated any one-shot POST failure as a bad
credential. GitHub answers 403 for secondary rate limiting as well as
for refusal -- the publish step retries six times for exactly that
reason -- and these repos return 504 on ordinary release listings often
enough that one was hit while writing this change. A blip would have
failed the nightly with "the token is wrong", sending whoever read it
off to reissue a credential that was never the problem.

The POST now retries three times with 5s and 10s backoff, and the error
says "after 3 attempts ... if this is not a transient API error". Three
rather than the publish step`s six because the value here is failing
fast: a genuine misconfiguration still surfaces in well under a minute
instead of after the whole matrix.

Testing the retry then exposed a second defect, in the sweep added by
the previous commit. gh writes its error body to STDOUT, so a failed
list feeds the cleanup loop lines of JSON instead of release ids, and
the log filled with `removing stranded probe draft {"status": "401"}`.
The deletes were harmless -- they simply failed -- but a step whose
entire purpose is to be believed must not narrate confident nonsense.
Only numeric ids are acted on now.

Verified against the live repo: happy path creates the draft, reports
OK, the trap removes it, nothing is left behind; a refused credential
retries three times and fails with the honest message; a failing list
produces zero bogus removal lines.
@openipc-ai

Copy link
Copy Markdown
Contributor Author

Both findings addressed. The transient-failure one was right, and sharper than it looks.

1. Probe misclassifies transient failures — fixed in 0f0229d

Correct, and I'd already seen evidence for it without connecting it: listing releases in these repos returned HTTP 504 twice while I was developing this change. GitHub also answers 403 for secondary rate limiting, which is precisely why the publish step below retries six times. So a one-shot POST reporting "RELEASE_TOKEN cannot create releases" would send whoever read that log off to reissue a credential that was never the problem — a misdiagnosis baked into the error text.

The POST now retries 3× with 5s/10s backoff, and the message reads "could not create a release after 3 attempts. If this is not a transient API error, the token needs Contents: read and write."

Three rather than the publish step's six on purpose: the value of this probe is failing fast. A genuine misconfiguration still surfaces in well under a minute instead of after 107 devices have built.

2. Probe drafts leak on interruption — fixed in c5930f9

Already addressed before this review landed (Qodo marks it resolved). Tag now carries run_attempt as well as run_idrun_id is stable across re-runs, so a leftover would otherwise collide with its own re-run and report a valid credential as broken. Deletion moved into a trap, plus a best-effort sweep for drafts stranded by earlier runs, since cleanup.yml doesn't know this tag pattern.

A third one, found by testing the fix for the first

Exercising the retry path surfaced a defect in that sweep: gh writes its error body to stdout, so a failed list fed the cleanup loop lines of JSON instead of release ids, and the log filled with:

removing stranded probe draft {"status": "401"}

The deletes were harmless — they simply failed — but a step whose entire purpose is to be believed must not narrate confident nonsense. Only numeric ids are acted on now.

Verification

Against the live repo, not just reasoned about:

  • happy path → creates draft, release credential OK, trap removes it, 0 drafts left, no tag created, published releases untouched
  • refused credential → retries 3×, fails with the honest message, exit 1
  • failing list → 0 bogus removal lines

@openipc-ai
openipc-ai merged commit 2b619b9 into master Aug 30, 2026
20 checks passed
@openipc-ai
openipc-ai deleted the ci/release-token branch August 30, 2026 08:00
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