Skip to content

fix(release): upload release assets by id so a spent API quota stops reading as "release not found" - #344

Merged
0x73746F66 merged 1 commit into
mainfrom
fix/release-upload-quota
Nov 13, 2026
Merged

0x73746F66 merged 1 commit into
mainfrom
fix/release-upload-quota

Conversation

@0x73746F66

Copy link
Copy Markdown
Member

Why

On v4.24.8, v4.24.32 and v4.24.34, provenance and publish-tea failed in the same run. Both died at gh release upload <tag> with release not found, minutes after the release was published and on different runners. Re-running the failed jobs passed. For v4.24.34, this left Homebrew, Scoop and the flake on v4.24.33, because canary needs provenance.

The release was never missing:

  • It was not a draft.
  • It was not immutable (immutable: false).
  • It was visible by tag over REST afterwards.

The cause is how gh handles a rate-limit error:

  • Since Allow downloading release assets without authentication cli/cli#13723, FetchRelease looks a release up two ways at once: by tag over REST, and as a draft over GraphQL.
  • When both lookups fail, it prefers to report release not found (pkg/cmd/release/shared/fetch.go).
  • PACKAGES_TOKEN is a personal token, and its 5,000 REST requests an hour are shared with everything else that uses it.
  • Once that quota is spent, the REST lookup is refused with a 403/429. The GraphQL lookup has its own quota, and it correctly finds no draft. So a published release reads as missing.

The timing matches. On the day v4.24.32 was released, the quota was at 0 at 16:08:18 (#342), and the provenance upload failed at 16:16:10. While this fix was being prepared, the same token had 0 REST requests left but 4,997 GraphQL requests left.

This was not GitHub's eventual consistency. The uploads ran 60 to 100 s after the release was created in passing runs and in failing runs alike, and publish-tea failed seven minutes after creation.

What changes

  • scripts/release-asset-upload.sh uploads to a release by its id. The id comes from the caller, or is resolved over GraphQL. The script:
    • replaces an existing asset with the same name, so re-runs work;
    • waits for a spent quota to reset, for up to 65 minutes in total;
    • honours Retry-After on a secondary limit;
    • retries 404, 5xx and connection failures a few times;
    • names the HTTP status in every failure.
  • The release job:
    • asks GraphQL whether the release exists, instead of gh release view;
    • uploads by id;
    • outputs release_id.
  • slsa-provenance.yml:
    • takes an optional release_id input, so belai's existing call still works by tag;
    • runs the upload script from its own commit (the job_workflow_sha OIDC claim);
    • uploads the slsa-provenance artifact before the release asset (with overwrite: true), so a failed release upload no longer loses the hand-off;
    • has a timeout of 80 minutes, to allow for a quota wait.
  • publish-tea:
    • needs provenance to succeed;
    • accepts a skipped update-packages only when the release is not the newest. Previously, a provenance failure skipped canary and update-packages, and publish-tea ran anyway. That could permanently publish a TEA release without its bundle, while the package managers were never updated.
    • no longer uses continue-on-error on the bundle download.
  • docs/PUBLISHING.md describes the new behaviour.

Checks

  • actionlint 1.7.12 with shellcheck 0.10.0:

    • no new findings in release.yml: the same 6 pre-existing info/warning notes as on main;
    • slsa-provenance.yml is clean;
    • the script passes shellcheck.
  • gh actions-lock --ignore-local-actions --verify-local reports all 12 workflows covered (actions/checkout@v7 added for slsa-provenance.yml).

  • The script was run offline against scripted responses. Each case behaved as expected:

    Response Result
    403 or 429 with a spent quota waited for the reset, then uploaded
    secondary limit with Retry-After waited, then uploaded
    422 (asset already exists) replaced the asset
    persistent 404 failed after retries, naming HTTP 404
    permission 403 failed at once
    quota wait longer than the budget failed, saying to re-run

@github-actions

Copy link
Copy Markdown
Contributor

Vulnetix config plan

Manager gitops:vulnetix/cli, prune off, plan hash a8bd984f1753c68b180298051dd9e2524ab644d2ebe9e4c7c811ff4126753b5f

Object Action Changes
~ QualityGate/default update (adopt) severity: null -> "high"

0 to create, 1 to update, 0 to delete, 0 unchanged

…reading as "release not found"

The provenance and publish-tea jobs failed together on v4.24.8, v4.24.32
and v4.24.34 with "release not found" from `gh release upload <tag>`,
minutes after the release was published, on different runners; a re-run
of the failed jobs passed. The release was never missing, a draft or
immutable. gh (since cli/cli#13723) looks a release up by tag over REST
and as a draft over GraphQL at once, and when both fail it prefers to
report "release not found". With PACKAGES_TOKEN's REST quota spent, the
REST lookup is refused with 403/429 and the GraphQL lookup, which has its
own quota, correctly finds no draft, so a published release reads as
missing. That token is a personal token whose 5,000 requests an hour are
shared with everything else using it; at 16:08:18 on the day of v4.24.32
the quota was at 0 (see #342), and the provenance upload failed at
16:16:10.

- scripts/release-asset-upload.sh uploads to a release by id (the
  caller's, or resolved over GraphQL), replaces an existing asset of the
  same name, waits a spent quota out until its reset (65 minutes at most),
  honours Retry-After for a secondary limit, retries 404/5xx a few times,
  and names the HTTP status of every failure.
- The release job asks GraphQL whether the release exists, uploads by id,
  and outputs release_id; the provenance workflow takes it as an optional
  input (belai's call still works by tag) and runs the script from its own
  commit (job_workflow_sha), so a caller in another repository runs it too.
- The slsa-provenance artifact is uploaded before the release asset, so a
  failed release upload no longer loses the hand-off.
- publish-tea needs provenance to succeed, and accepts a skipped
  update-packages only for a release older than the newest. Before, a
  provenance failure skipped canary and update-packages, and publish-tea
  still ran: it would have put v4.24.34 in the transparency log, which
  cannot be unpublished, without its bundle and with Homebrew, Scoop and
  the flake left on v4.24.33.
@0x73746F66
0x73746F66 force-pushed the fix/release-upload-quota branch from 3c0698e to c15a9e8 Compare November 13, 2026 20:56
@0x73746F66
0x73746F66 merged commit aca2bfe into main Nov 13, 2026
36 checks passed
@0x73746F66
0x73746F66 deleted the fix/release-upload-quota branch November 13, 2026 21: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