SRE-904: Split token minting from the release run - #9192
Conversation
Port the hashintel/.github#99 pattern to release.yml: only a new mint-token job holds id-token: write and runs no repository dependency code; the release job receives the App token as ciphertext and holds no OIDC permission, so compromised build/publish dependencies can no longer mint fresh Vault-signed App JWTs. Also drop the unused id-token grant from deploy.yml's setup job, bump hashintel/.github pins to the merge commit of that PR, and pass the now-required RENOVATE_TOKEN_ENC_KEY through to the housekeeping workflow. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HPkZcHmt5qmnGHtVB96d7
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #9192 +/- ##
==========================================
- Coverage 59.63% 59.63% -0.01%
==========================================
Files 1420 1420
Lines 138767 138767
Branches 6555 6555
==========================================
- Hits 82755 82754 -1
- Misses 54948 54949 +1
Partials 1064 1064 Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
PR SummaryHigh Risk Overview New composite actions (
Lint, test, bench, codspeed, deploy (sourcemaps), and canary-release each gain a dedicated Housekeeping bumps the Reviewed by Cursor Bugbot for commit 4191fe7. Bugbot is set up for automated code reviews on this repo. Configure here. |
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HPkZcHmt5qmnGHtVB96d7
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit f720252. Configure here.
The sccache bundle crosses as JSON with named keys, built with jq and parsed by key on the consuming side, instead of a positional newline string. Handoff inputs and outputs say what the ciphertext contains (encrypted-token, encrypted-sccache-credentials); only the generic encrypt-secret/decrypt-secret actions keep content-agnostic names. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HPkZcHmt5qmnGHtVB96d7
install-sccache no longer talks to Vault: it requires the encrypted credential bundle, and install-tools drops the Vault/Cloudflare inputs. Every sccache consumer — bench, codspeed, canary-release, and deploy's sourcemaps job — mints the bundle in its own sccache-credentials job, moving the fetch out of the dependency-running jobs; their remaining id-token grants serve only their other OIDC uses (legacy-Vault AWS upload, CodSpeed auth, Sentry). Fork pull requests skip the fetch and run uncached, as before. Also wraps the overlong needs list in test.yml that failed lint:format. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018HPkZcHmt5qmnGHtVB96d7
encrypt-secret and decrypt-secret speak in value/encrypted_value pairs, and the release handoff names its output encrypted-github-token.
Benchmark results
|
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 2002 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 1002 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 3314 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 1527 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 2078 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 1033 | Flame Graph |
policy_resolution_medium
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 102 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 52 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 269 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 108 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 133 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 63 | Flame Graph |
policy_resolution_none
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 2 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 2 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 8 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 3 | Flame Graph |
policy_resolution_small
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| resolve_policies_for_actor | user: empty, selectivity: high, policies: 52 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: empty, selectivity: medium, policies: 26 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: high, policies: 94 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: seeded, selectivity: medium, policies: 27 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: high, policies: 66 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: low, policies: 1 | Flame Graph | |
| resolve_policies_for_actor | user: system, selectivity: medium, policies: 29 | Flame Graph |
read_scaling_complete
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id;one_depth | 1 entities | Flame Graph | |
| entity_by_id;one_depth | 10 entities | Flame Graph | |
| entity_by_id;one_depth | 25 entities | Flame Graph | |
| entity_by_id;one_depth | 5 entities | Flame Graph | |
| entity_by_id;one_depth | 50 entities | Flame Graph | |
| entity_by_id;two_depth | 1 entities | Flame Graph | |
| entity_by_id;two_depth | 10 entities | Flame Graph | |
| entity_by_id;two_depth | 25 entities | Flame Graph | |
| entity_by_id;two_depth | 5 entities | Flame Graph | |
| entity_by_id;two_depth | 50 entities | Flame Graph | |
| entity_by_id;zero_depth | 1 entities | Flame Graph | |
| entity_by_id;zero_depth | 10 entities | Flame Graph | |
| entity_by_id;zero_depth | 25 entities | Flame Graph | |
| entity_by_id;zero_depth | 5 entities | Flame Graph | |
| entity_by_id;zero_depth | 50 entities | Flame Graph |
read_scaling_linkless
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id | 1 entities | Flame Graph | |
| entity_by_id | 10 entities | Flame Graph | |
| entity_by_id | 100 entities | Flame Graph | |
| entity_by_id | 1000 entities | Flame Graph | |
| entity_by_id | 10000 entities | Flame Graph |
representative_read_entity
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/block/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/book/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/building/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/organization/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/page/v/2
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/person/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/playlist/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/song/v/1
|
Flame Graph | |
| entity_by_id | entity type ID: https://blockprotocol.org/@alice/types/entity-type/uk-address/v/1
|
Flame Graph |
representative_read_entity_type
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| get_entity_type_by_id | Account ID: bf5a9ef5-dc3b-43cf-a291-6210c0321eba
|
Flame Graph |
representative_read_multiple_entities
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| entity_by_property | traversal_paths=0 | 0 | |
| entity_by_property | traversal_paths=255 | 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true | |
| entity_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=0 | 0 | |
| link_by_source_by_property | traversal_paths=255 | 1,resolve_depths=inherit:1;values:255;properties:255;links:127;link_dests:126;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:0;link_dests:0;type:false | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:0;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:0;properties:2;links:1;link_dests:0;type:true | |
| link_by_source_by_property | traversal_paths=2 | 1,resolve_depths=inherit:0;values:2;properties:2;links:1;link_dests:0;type:true |
scenarios
| Function | Value | Mean | Flame graphs |
|---|---|---|---|
| full_test | query-limited | Flame Graph | |
| full_test | query-unlimited | Flame Graph | |
| linked_queries | query-limited | Flame Graph | |
| linked_queries | query-unlimited | Flame Graph |

Requested by Tim Diekmann · Slack thread
🌟 What is the purpose of this PR?
Ports the token-minting split from hashintel/.github#99 to this repo's release workflow, and removes
id-token: writefrom a job that never consumes it. This is a supply-chain containment change: it narrows what compromised dependency code running in CI can reach.Before: the single
releasejob heldid-token: writewhile running the full dependency graph (mise/Rust toolchain install,yarn install, turbo builds,yarn changeset publishand every module those load). Any compromised build-time dependency in that job could not only read the HASH Release App installation token andNPM_TOKENfrom its own environment, but also use the job-wideACTIONS_ID_TOKEN_REQUEST_*credentials to mint fresh OIDC tokens and authenticate to any Vault role whose claim bindings the run satisfies — at minimum the sccache role, whose R2 credentials allow poisoning the shared compile cache that feeds every Rust build. Similarly,deploy.yml'ssetupjob grantedid-token: writeto a job that runsturbo query(dependency code) but consumes no OIDC at all.After: only a new
mint-tokenjob holdsid-token: write, and it runs nothing but pinned credential-minting actions (github-app-token,vault-actionfor the sccache bundle) — no dependency code executes there. Thereleasejob holds no OIDC permission: compromised dependency code there gets at most the hour-long installation token it is handed (which the split intentionally does not protect — the publish needs it), and can no longer sign fresh App JWTs in Vault or fetch sccache credentials. The dead grant indeploy.yml'ssetupjob is gone entirely.🔗 Related links
b7a5d7f)id-token+dependency-code combinations this assessment flagged🚫 Blocked by
RELEASE_TOKEN_ENC_KEYmust exist inhashintel/hashbefore merge (value:openssl rand -base64 32; repo-level secret). Without it the mint job fails at its: "${ENC_KEY:?}"guard on the next push tomain.SCCACHE_ENC_KEYmust exist inhashintel/hashbefore merge (sameopenssl rand -base64 32; repo-level secret). It keys the sccache credential handoff inrelease,lint, andtest; without it their credentials jobs fail at the:?guard.RENOVATE_TOKEN_ENC_KEY— the bumpedhousekeeping-dependencies.ymldeclares itrequired: true. Already created inhashintel/hashas part of the SRE-904: Split token minting from the Renovate run .github#99 rollout, so the passthrough works immediately.🔍 What does this change?
.github/workflows/release.yml— split into two jobs:mint-token(environment: mainkept — theci-hash-releaseVault role binds the OIDCenvironmentclaim):contents: read+id-token: writeonly; runs the existing pinnedgithub-app-tokenaction unchanged, then encrypts the token withopenssl enc -aes-256-cbc -pbkdf2 -salt -pass env:ENC_KEY -base64 -Akeyed by the newRELEASE_TOKEN_ENC_KEYsecret. Only ciphertext leaves the job (GitHub drops masked values from job outputs, so plaintext could not cross anyway).: "${VAR:?}"guards fail loud on missing inputs. The openssl encrypt/decrypt round-trip was verified against OpenSSL 3.0.13 (the ubuntu-latest version).release: noid-token; decrypts and::add-mask::s the plaintext directly after checkout, before anything else runs, then the existing checkout/install/changesets steps unchanged. sccache stays:mint-tokenalso fetches the R2 credentials and hands them over as a second ciphertext (keyed by the newSCCACHE_ENC_KEYsecret), decrypted insideinstall-sccache— so the job builds cached without holding OIDC. The Rust toolchain stays: publishing@blockprotocol/type-systembuilds the wasm artifact from the Rust crate (build:wasmvia wasm-pack;rust/pkg/is gitignored, so it cannot come from the checkout), sorust: falsewas not an option.environment: mainis kept on this job too so environment-scoped secrets remain reachable.NPM_TOKENhandling and the changesets config are untouched..github/workflows/deploy.yml—setupjob:id-token: write→contents: read. The job passesrust: falsetoinstall-tools, and the sccache credential fetch (gated on Rust) is the only OIDC consumer in that action — nothing in the job can use the grant, so it only widened what its dependency code (turbo query) could mint..github/workflows/{preflight,preflight-todo-comments,housekeeping}.yml+release.yml— bump everyhashintel/.githubpin from8c1a3b1tob7a5d7f651c1d6a862d5dd97aa6164125bedb6c5, the merge commit of SRE-904: Split token minting from the Renovate run .github#99. The bump crosses only Include messages within json_state output #98 (Renovate 43.288.0) and Fix behavior execution for Javascript runners #99 (the mint split); all referenced reusable workflows and thegithub-app-tokenaction exist unchanged at the new pin..github/workflows/housekeeping.yml— passRENOVATE_TOKEN_ENC_KEYthrough to the reusable workflow, which now requires it for the same encrypted job-boundary handoff..github/actions/{encrypt-secret,decrypt-secret,mint-sccache-credentials}(new) — the handoff as dedicated composite actions (plain bash + openssl; every plaintext line is masked before it is output), consumed via newinstall-tools/install-sccacheinputs. This is the only route:install-sccacheno longer talks to Vault, and every sccache consumer — release,lint.yml,test.yml,bench.yml,codspeed.yml,canary-release.yml, anddeploy.yml's sourcemaps job — mints the bundle in a per-workflowsccache-credentialsjob (contents: read+id-token: write, no dependency code). Jobs whoseid-tokengrant was sccache-only (lint'spackage/global, test'sunit-tests/integration-tests, canary's publish job) drop it; bench, codspeed, and deploy-sourcemaps keep theirs solely for their other OIDC uses (legacy-Vault AWS upload credentials, CodSpeed's own auth, the Sentry token mint). The dead grants and unused Vault inputs on the rust-lesssetupjobs are gone. On fork PRs the CF secret is empty, so the fetch is skipped and consumers run uncached, as before..github/workflows/canary-release.yml— its only OIDC consumer was the sccache fetch, so the publish job now consumes the handoff and holds noid-tokenat all; the workflow-level grant moved into itssccache-credentialsjob.Deliberately not changed:
publish-blocks-to-preview.ymlwas in scope (two sccache-onlyid-tokengrants alongsideyarn build+ unpinnednpx blockprotocol@latest), but SRE-908: Delete the block-publishing workflows #9187 deleted both block-publishing workflows before this PR — nothing left to fix.Pre-Merge Checklist 🚀
🚢 Has this modified a publishable library?
This PR:
📜 Does this require a change to the docs?
The changes in this PR:
🕸️ Does this require a change to the Turbo Graph?
The changes in this PR:
releasejob can still read it. The same goes for the sccache R2 credentials it is handed (as in every Rust CI job). What the split removes is the ability to mint: fresh App JWTs signed in Vault, or any other role reachable with a fresh OIDC token.NPM_TOKEN, thereleasejob will needid-token: writeback for the npm exchange — that use is inseparable from the publish step. The Vault/App-token mint should stay inmint-tokenregardless.🐾 Next steps
deploy.yml'ssourcemapsjob's remaining OIDC use (the Sentry token mint alongside dependency code — its sccache fetch already moved out); lower value, tracked separately.🛡 What tests cover this?
actionlinton all five changed files; the only findings are the pre-existingqueue:keys actionlint's schema does not know). Release workflows are only exercised by a real push tomain.❓ How to test this?
mainpush:mint-tokenshould complete in seconds and output only ciphertext;releaseshould decrypt, mask, and proceed through changesets exactly as before.releasejob shows noid-tokenin its permissions block and that sccache still starts, from the handed-over credentials. Same check for lint's and test's Rust jobs.RENOVATE_TOKEN_ENC_KEYsecret already exists; the requirement would fail fast at dispatch if it were missing).📹 Demo
Not applicable (CI-only change).