DashboardpublishSession 0FSl746ahuuJ

Session 0FSl746ahuuJ

Full ID: 0FSl746ahuuJXxKq3
Messages: 85
Distillations: 17
Time range: 2026-09-10 11:47:28 — 2026-09-10 12:26:29

Conversation (85 messages)

Perform a READ-ONLY, substantive adversarial security review of the exact current worktree at /home/byk/Code/getsentry/publish against base commit 7c60ddb7f43040fe8fbfea70efc833f689c04e75 (origin/main). DO NOT edit, format, stage, stash, commit, or otherwise mutate any file or repository state. Inspect every changed tracked file (staged and unstaged together) and every untracked file; use git diff HEAD plus untracked reads, not only git diff. Treat GitHub event payloads/issue content as attacker-controlled. Review approval authorization/provenance, request snapshot binding, accepted/ci-ready event binding, canonical title/workspace/revision parsing, poller trusted-code execution/manual recovery, CI transitions/races, exact-revision checkout, path/state containment, secret exposure, action/container pinning, final pre-Craft fence, and terminal cleanup/reconciliation. Accepted scope decision: SENTRY_INTERNAL_APP_PRIVATE_KEY is an existing organization-level secret available broadly; do not block this change solely because it cannot be migrated here, but report any NEW widening introduced by this diff. Preserve these invariants in your assessment: direct bot labels never fall through to collaborator permission; requester cannot self-approve; renamed/body/dry-run/re-approved/revision-changed releases never reach ci-ready in the stale cycle; poller removes stale ci-ready, revalidates, and always re-adds ci-ready; workflow_dispatch manual recovery executes only trusted default-branch controller code. Run read-only tests/checks if useful. Compute an initial and final fingerprint that covers HEAD, tracked diff, staged diff, and untracked file paths+contents; verify unchanged. Findings must come first, ordered by severity, each with current file:line evidence and classification MUST-FIX, CONCERN, or PASS. Explicitly state every changed file was inspected. If blocked, begin with BLOCKED and name the exact tool/error. ...

gateway · 2026-09-10 11:47:28
result
<path>/home/byk/Code/getsentry/publish</path>
<type>directory</type>
<entries>
.eslintrc.js
.git/
.github/
.gitignore
.lore.md
AGENTS.md
auto-approve-repos.txt
docs/
LICENSE
node_modules/
package.json
README.md
scripts/
src/
vitest.config.js
yarn.lock

(16 entries)
</entries>
gateway · 2026-09-10 11:48:37
result
HEAD 7c60ddb7f43040fe8fbfea70efc833f689c04e75
TRACKED_HEAD_DIFF_SHA256 679ed3e87d137e0bc55b2d8fd9ffde9efa900cd91b63c382447e750d6766aaaa  -
UNSTAGED_TRACKED_DIFF_SHA256 16c8e3d564c98865ee0d3c1412f21e156a09d8790b579ba8b8092d5b246a21f4  -
STAGED_DIFF_SHA256 8e4530c4a3b36b00445028365ab293870bc3d19520ef829cb92b6c60eefe187d  -
UNTRACKED_MANIFEST_SHA256 a3cd906e09f8c3c7f7c380a66742ab595bbe49391649c96478d69a831b990bc7  -
UNTRACKED_COUNT 23
STATUS
MM .github/workflows/auto-approve.yml
MM .github/workflows/ci-poller.yml
M  .github/workflows/cocoapods-keepalive.yml
MM .github/workflows/publish.yml
M  .github/workflows/test.yml
M  AGENTS.md
M  README.md
M  docs/key_rotation_vault_changes.md
M  docs/rfc.md
M  src/libs/__tests__/github.js
M  src/libs/github.js
 M src/modules/__tests__/ci-poller-input.js
 M src/modules/__tests__/ci-poller-workflow.js
MM src/modules/__tests__/details-from-context.js
 M src/modules/__tests__/process-end-state.js
 M src/modules/__tests__/publish-location.js
 M ...
gateway · 2026-09-10 11:48:54
result
BRANCH main
UPSTREAM origin/main
ORIGIN_MAIN 7c60ddb7f43040fe8fbfea70efc833f689c04e75
MERGE_BASE_HEAD_ORIGIN_MAIN 7c60ddb7f43040fe8fbfea70efc833f689c04e75
RECENT_LOG
7c60ddb (HEAD -> main, origin/main, origin/HEAD) feat: accept workspace publish requests
83d210b (opencode/tidy-orchid) build(deps): bump js-yaml from 4.3.0 to 4.3.1 (#9174)
5268a23 feat: Add Vercel creds
92eba3f feat: Add CLOUDFLARE_API_TOKEN (#9122)
ada6147 build(deps): bump js-yaml from 4.1.1 to 4.3.0 (#8731)
a4f961a fix: Add GITHUB_API_TOKEN for commit-on-git target (#9069)
bf10f82 build(deps): bump postcss from 8.5.15 to 8.5.23 (#9017)
837edc0 docs: fix possessive apostrophe placement (#9015)
4da4a25 chore: replace deprecated trimRight() with trimEnd() (#9012)
22fd64d docs: fix typo 'acces' -> 'access' (#9011)
24b9ce3 docs: fix missing article in rfc.md (#9014)
c1bded6 docs: fix run-on sentence in README (#9010)
HEAD_TO_ORIGIN_MAIN_COUNTS 0	0
gateway · 2026-09-10 11:49:39
result
COMBINED_DIFF_STAT
 .github/workflows/auto-approve.yml                |  30 ++-
 .github/workflows/ci-poller.yml                   | 232 +++++++++++++++---
 .github/workflows/cocoapods-keepalive.yml         |   1 -
 .github/workflows/publish.yml                     | 271 +++++++++++++++++++---
 .github/workflows/test.yml                        |   6 +-
 AGENTS.md                                         |   6 +
 README.md                                         |   4 +-
 docs/key_rotation_vault_changes.md                |  40 ++--
 docs/rfc.md                                       |   9 +-
 src/libs/__tests__/github.js                      |   2 +-
 src/libs/github.js                                |   6 +-
 src/modules/__tests__/ci-poller-input.js          |  22 +-
 src/modules/__tests__/ci-poller-workflow.js       | 207 ++++++++++++++---
 src/modules/__tests__/details-from-context.js     |  22 +-
 src/modules/__tests__/process-end-state.js        |  27 +++
 ...
gateway · 2026-09-10 11:49:49
result
<path>/home/byk/Code/getsentry/publish/.github/workflows/auto-approve.yml</path>
<type>file</type>
<content>
1: name: auto-approve non-sdks
2: on:
3:   issues:
4:     types: [opened]
5: 
6: permissions:
7:   contents: read
8:   issues: write
9: 
10: jobs:
11:   auto-approve:
12:     runs-on: ubuntu-latest
13:     environment: production
14:     if: |
15:       (github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') &&
16:       startsWith(github.event.issue.title, 'publish: ')
17:     steps:
18:       - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
19:         with:
20:           persist-credentials: false
21:       - name: Get auth token
22:         id: token
23:         uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
24:         with:
25:           client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
26:           private-key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }}
27:       - name: Bind request snapshot
28:   ...
gateway · 2026-09-10 11:49:59
result
<path>/home/byk/Code/getsentry/publish/.github/workflows/ci-poller.yml</path>
<type>file</type>
<content>
1: name: CI Status Poller
2: 
3: on:
4:   schedule:
5:     - cron: "*/5 * * * *"
6:   repository_dispatch:
7:     types: [ci-poller]
8:   workflow_run:
9:     workflows: [Run CI Status Poller]
10:     types: [completed]
11: 
12: permissions:
13:   contents: read
14:   issues: write
15: 
16: jobs:
17:   check-ci:
18:     runs-on: ubuntu-latest
19:     environment: production
20:     # Skip entirely (no runner provisioned) when there's nothing to check.
21:     # Set to "true" by ci-pending.yml, reset to "false" here when done.
22:     # repository_dispatch and a successful main-branch run of the protected
23:     # workflow_dispatch relay are the trusted manual-recovery paths.
24:     if: >-
25:       vars.CI_POLLER_HAS_PENDING == 'true'
26:       || github.event_name == 'repository_dispatch'
27:       || (
28:       github.event_name == 'workflow_run'
29:       && ...
gateway · 2026-09-10 11:50:10
result
<path>/home/byk/Code/getsentry/publish/.github/workflows/publish.yml</path>
<type>file</type>
<content>
1: name: Publish
2: on:
3:   issues:
4:     types: [labeled]
5: 
6: concurrency:
7:   # Use the issue title (e.g. "publish: getsentry/foo@1.2.3") so duplicate
8:   # issues for the same repo@version share a concurrency group.
9:   group: ${{ github.event.issue.title }}
10:   cancel-in-progress: false
11: 
12: permissions:
13:   contents: read
14:   issues: write
15:   packages: write
16: 
17: jobs:
18:   # When accepted is added to a publish issue:
19:   #   - Add ci-pending (and remove ci-failed if retrying)
20:   #   - Enable the poller via CI_POLLER_HAS_PENDING=true
21:   #   - Comment on the issue
22:   #   - Trigger the poller immediately so we don't wait for the next cron tick
23:   # The publish job below requires ci-ready, so it will not fire until the
24:   # poller flips ci-pending → ci-ready (which also prevents publishing without
25:   # CI verification in the ...
gateway · 2026-09-10 11:50:23
result
<path>/home/byk/Code/getsentry/publish/.github/workflows/ci-poller-dispatch.yml</path>
<type>file</type>
<content>
1: name: Run CI Status Poller
2: 
3: on:
4:   workflow_dispatch:
5: 
6: permissions: {}
7: 
8: jobs:
9:   dispatch:
10:     # production only permits deployments from main. Repository secrets must
11:     # never be used by this workflow; protected environment secrets enforce
12:     # that an arbitrary dispatched ref cannot add them.
13:     environment: production
14:     runs-on: ubuntu-latest
15:     steps:
16:       - run: ":"

(End of file - total 16 lines)
</content>
gateway · 2026-09-10 11:50:36
result
<path>/home/byk/Code/getsentry/publish/.github/workflows/cocoapods-keepalive.yml</path>
<type>file</type>
<content>
1: name: CocoaPods Token Keep-Alive
2: 
3: on:
4:   schedule:
5:     # CocoaPods sessions currently expire after 3 days of inactivity (VALIDITY_LENGTH)
6:     # This runs every day at midnight UTC to keep the current token alive
7:     # See: https://github.com/CocoaPods/trunk.cocoapods.org/blob/a1869790e9ae2229b6985b6af4532da814202558/app/models/session.rb#L9
8:     - cron: "0 0 * * *" # Runs every day at midnight UTC
9: 
10: permissions:
11:   contents: read
12: 
13: jobs:
14:   keepalive:
15:     name: Refresh CocoaPods Session
16:     runs-on: macos-15
17:     environment: production
18:     env:
19:       COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
20: 
21:     steps:
22:       - name: Install CocoaPods
23:         run: gem install cocoapods
24:       - name: Print CocoaPods Version
25:         run: pod --version
26:       - name: Refresh CocoaPods ...
gateway · 2026-09-10 11:50:47
result
<path>/home/byk/Code/getsentry/publish/.github/workflows/test.yml</path>
<type>file</type>
<content>
1: name: Test
2: on:
3:   push:
4:     branches:
5:       - main
6:   pull_request:
7: 
8: permissions:
9:   contents: read
10: 
11: defaults:
12:   run:
13:     shell: bash
14: jobs:
15:   unit-test:
16:     runs-on: ubuntu-latest
17:     name: unit tests
18:     steps:
19:       - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
20:       - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38
21:         with:
22:           node-version: 24
23:       - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
24:         id: cache
25:         with:
26:           path: node_modules
27:           key: ${{ runner.os }}-node_modules-${{ hashFiles('package.json', 'yarn.lock') }}
28: 
29:       - name: Install Dependencies
30:         if: steps.cache.outputs.cache-hit != 'true'
31:         run: yarn install --frozen-lockfile
32: 
33:       - name: Test
34:    ...
gateway · 2026-09-10 11:51:55
result
/home/byk/Code/getsentry/publish/src/publish/request-digest-from-event.js
/home/byk/Code/getsentry/publish/src/publish/update-issue.js
/home/byk/Code/getsentry/publish/src/publish/post-workflow-details.js
/home/byk/Code/getsentry/publish/src/publish/post-result.js
/home/byk/Code/getsentry/publish/src/publish/inputs.js
/home/byk/Code/getsentry/publish/src/publish/resolve-release-revision.js
/home/byk/Code/getsentry/publish/src/publish/resolve-location.js
/home/byk/Code/getsentry/publish/src/publish/resolve-ci-poller-input.js
/home/byk/Code/getsentry/publish/src/publish/discover-location.js
/home/byk/Code/getsentry/publish/src/publish/record-auto-approval-attestation.js
/home/byk/Code/getsentry/publish/src/publish/current-accepted-event.js
/home/byk/Code/getsentry/publish/src/publish/record-ci-ready-attestation.js
/home/byk/Code/getsentry/publish/src/publish/validate-approval-attestation.js
/home/byk/Code/getsentry/publish/src/publish/__tests__/post-result.js
/home/byk/Code/getsentry/pub...
gateway · 2026-09-10 11:52:02
result
<path>/home/byk/Code/getsentry/publish/src/modules/approval-attestation.js</path>
<type>file</type>
<content>
1: const crypto = require("crypto");
2: 
3: const APPROVAL_ATTESTATION_PREFIX = "<!-- publish-approval ";
4: const AUTO_APPROVAL_ATTESTATION_PREFIX = "<!-- publish-auto-approval ";
5: const CI_READY_ATTESTATION_PREFIX = "<!-- publish-ci-ready ";
6: const ATTESTATION_SUFFIX = " -->";
7: 
8: function createAttestation(prefix, value) {
9:   return `${prefix}${Buffer.from(JSON.stringify(value)).toString(
10:     "base64url"
11:   )}${ATTESTATION_SUFFIX}`;
12: }
13: 
14: function requestDigest({ body, labels, title }) {
15:   if (
16:     typeof body !== "string" ||
17:     !Array.isArray(labels) ||
18:     typeof title !== "string"
19:   ) {
20:     throw new Error("Invalid publish request");
21:   }
22: 
23:   return crypto
24:     .createHash("sha256")
25:     .update(
26:       JSON.stringify({
27:         body,
28:         dryRun: labels.some((label) => label.name === ...
gateway · 2026-09-10 11:52:09
result
<path>/home/byk/Code/getsentry/publish/src/modules/approval-authorizer.js</path>
<type>file</type>
<content>
1: const ALLOWED_ROLE_NAMES = new Set(["write", "maintain", "admin"]);
2: const { parsePublishTitle } = require("./details-from-context.js");
3: 
4: const AUTO_APPROVAL_LABELER = "sentry-internal-app[bot]";
5: const AUTO_APPROVERS = new Set(["getsantry[bot]", "sentry-release-bot[bot]"]);
6: 
7: function isAutoApprovedRepository({
8:   actor,
9:   autoApprovedRepositories,
10:   issueTitle,
11: }) {
12:   if (!AUTO_APPROVERS.has(actor)) {
13:     return false;
14:   }
15: 
16:   const title = parsePublishTitle(issueTitle);
17: 
18:   if (!title) {
19:     return false;
20:   }
21: 
22:   return autoApprovedRepositories.has(
23:     `getsentry/${title.repo}${title.path || ""}`
24:   );
25: }
26: 
27: async function authorizeApproval({ actor, issueTitle, getPermission }) {
28:   const title = parsePublishTitle(issueTitle);
29: 
30:   if (!title) {
31:     return { authorized: ...
gateway · 2026-09-10 11:52:19
result
<path>/home/byk/Code/getsentry/publish/src/modules/details-from-context.js</path>
<type>file</type>
<content>
1: const { parse: parsePublishIssueTitle } = require("./publish-issue-title");
2: const { getReleaseRevisionDetails } = require("./release-revision");
3: const { isPublishPath } = require("./publish-location");
4: const {
5:   isPublishRepository,
6:   isReleaseVersion,
7: } = require("./publish-issue-validation");
8: 
9: /**
10:  * Matches the entire "Targets" section of a github publish issue body.
11:  */
12: const TARGETS_SECTION_PARSER_REGEX =
13:   /^(?!### Targets$\s)(?: *- \[[ xX]\] \S+\s*$(?:\r?\n)?)+/m;
14: 
15: /**
16:  * Matches all targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`.
17:  * The "id" of the targets is captured within a capture group. ...
gateway · 2026-09-10 11:52:31
result
<path>/home/byk/Code/getsentry/publish/src/modules/ci-poller-input.js</path>
<type>file</type>
<content>
1: const { parse: parsePublishIssueTitle } = require("./publish-issue-title");
2: const {
3:   getReleaseRevision,
4:   updateReleaseRevision,
5: } = require("./release-revision");
6: const {
7:   isPublishRepository,
8:   isReleaseVersion,
9: } = require("./publish-issue-validation");
10: const { isPublishPath } = require("./publish-location");
11: const { requestDigest } = require("./approval-attestation");
12: 
13: function getCiPollerInput({ title, issueBody, labels, revision }) {
14:   let parsedTitle;
15:   try {
16:     parsedTitle = parsePublishIssueTitle(title);
17:   } catch {
18:     throw new Error(`Invalid publish issue title: '${title}'`);
19:   }
20:   const { repo, version } = parsedTitle;
21:   if (!isPublishRepository(repo)) {
22:     throw new Error(`Invalid publish issue repository: '${repo}'`);
23:   }
24:   if (!isReleaseVersion(version)) {
25:     throw new ...
gateway · 2026-09-10 11:52:45
result
<path>/home/byk/Code/getsentry/publish/src/modules/release-revision.js</path>
<type>file</type>
<content>
1: const { parse } = require("./publish-issue-title");
2: 
3: function isRevision(revision) {
4:   return /^[0-9a-f]{40}$/.test(revision);
5: }
6: 
7: function getReleaseRevisionDetails({ issueBody, repo }) {
8:   if (parse(issueBody, { startRule: "CheckRunsLinkCount" }) !== 1) {
9:     throw new Error(
10:       `Expected exactly one View check runs link in Quick links for getsentry/${repo}.`
11:     );
12:   }
13: 
14:   let details;
15:   try {
16:     details = parse(issueBody, { startRule: "ReleaseRevision" });
17:   } catch {
18:     throw new Error(
19:       `Expected a View check runs link for getsentry/${repo} in the publish issue body.`
20:     );
21:   }
22: 
23:   if (details.repo !== repo) {
24:     throw new Error(
25:       `Expected a View check runs link for getsentry/${repo} in the publish issue body.`
26:     );
27:   }
28: 
29:   return details;
30: }
31: 
32: ...
gateway · 2026-09-10 11:52:58
result
<path>/home/byk/Code/getsentry/publish/src/modules/publish-location.js</path>
<type>file</type>
<content>
1: function resolvePublishLocation({ path, workspaceNames }) {
2:   if (!isPublishPath(path)) {
3:     throw new Error("Invalid publish path.");
4:   }
5: 
6:   if (!workspaceNames.every(isWorkspaceName)) {
7:     throw new Error(
8:       "Craft workspace discovery returned an invalid workspace list."
9:     );
10:   }
11: 
12:   if (path === ".") {
13:     return { path };
14:   }
15: 
16:   const workspace = path.slice(2);
17:   if (workspaceNames.includes(workspace)) {
18:     return { path: ".", workspace };
19:   }
20: 
21:   return { path };
22: }
23: 
24: function isPublishPath(path) {
25:   return (
26:     typeof path === "string" &&
27:     (path === "." ||
28:       (path.startsWith("./") &&
29:         path.slice(2).split("/").every(isSafeWorkspaceSegment)))
30:   );
31: }
32: 
33: function isWorkspaceName(name) {
34:   return (
35:     typeof name === "string" && ...
gateway · 2026-09-10 11:53:07
result
<path>/home/byk/Code/getsentry/publish/src/modules/process-end-state.js</path>
<type>file</type>
<content>
1: const Sentry = require("@sentry/node");
2: 
3: async function processEndState({ context, octokit, inputs = {}, status }) {
4:   const { repo, version } = inputs;
5:   const { repo: publishRepo, runId: run_id } = context;
6:   const { number: issue_number } = context.payload.issue;
7: 
8:   // Validate the status before applying any terminal state transition.
9:   sentryInfoFromDetails({ status, repo });
10: 
11:   if (status === "success") {
12:     await octokit.rest.issues.update({
13:       ...publishRepo,
14:       issue_number,
15:       state: "closed",
16:     });
17:   }
18: 
19:   const workflowInfo = (
20:     await octokit.rest.actions.getWorkflowRun({
21:       ...publishRepo,
22:       run_id,
23:     })
24:   ).data;
25: 
26:   const details = {
27:     repo,
28:     version,
29:     publishRepo,
30:     run_id,
31:     issue_number,
32:     workflowInfo,
33:     ...
gateway · 2026-09-10 11:53:17
result
<path>/home/byk/Code/getsentry/publish/src/modules/post-workflow-details.js</path>
<type>file</type>
<content>
1: async function postWorkflowDetails({ context, octokit }) {
2:   const { repo: publishRepo, runId: run_id } = context;
3:   const { number: issue_number } = context.payload.issue;
4: 
5:   const workflowInfo = (
6:     await octokit.rest.actions.getWorkflowRun({
7:       ...publishRepo,
8:       run_id,
9:     })
10:   ).data;
11: 
12:   return octokit.rest.issues.createComment({
13:     ...publishRepo,
14:     issue_number,
15:     body: `Publishing: [run#${run_id}](${workflowInfo.html_url})`,
16:   });
17: }
18: 
19: module.exports = postWorkflowDetails;

(End of file - total 19 lines)
</content>
gateway · 2026-09-10 11:53:44
result
<path>/home/byk/Code/getsentry/publish/src/publish/authorize-approval.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3:   createApprovalAttestation,
4:   currentAcceptedEvent,
5:   hasAutoApprovalAttestation,
6:   requestDigest,
7: } = require("../modules/approval-attestation.js");
8: const {
9:   AUTO_APPROVAL_LABELER,
10:   AUTO_APPROVERS,
11:   authorizeApproval,
12:   isAutoApprovedRepository,
13: } = require("../modules/approval-authorizer.js");
14: 
15: function getAutoApprovedRepositories() {
16:   return new Set(
17:     fs
18:       .readFileSync("auto-approve-repos.txt", "utf8")
19:       .split(/\r?\n/)
20:       .filter(Boolean)
21:   );
22: }
23: 
24: async function getPermission({ owner, repository, username }) {
25:   const response = await getGitHubResponse(
26:     `repos/${encodeURIComponent(owner)}/${encodeURIComponent(
27:       repository
28:     )}/collaborators/${encodeURIComponent(username)}/permission`
29:   );
30: 
31:   if ...
gateway · 2026-09-10 11:53:57
result
<path>/home/byk/Code/getsentry/publish/src/publish/validate-approval-attestation.js</path>
<type>file</type>
<content>
1: const {
2:   currentAcceptedEvent,
3:   currentCiReadyEvent,
4:   hasApprovalAttestation,
5:   hasCiReadyAttestation,
6:   requestDigest,
7: } = require("../modules/approval-attestation.js");
8: 
9: async function getGitHubResponse(path) {
10:   return fetch(`https://api.github.com/${path}`, {
11:     headers: {
12:       Accept: "application/vnd.github+json",
13:       Authorization: `Bearer ${process.env.APPROVAL_TOKEN}`,
14:       "X-GitHub-Api-Version": "2026-03-10",
15:     },
16:   });
17: }
18: 
19: async function getIssue({ repository, issueNumber }) {
20:   const response = await getGitHubResponse(
21:     `repos/${repository}/issues/${issueNumber}`
22:   );
23: 
24:   if (!response.ok) {
25:     throw new Error(
26:       `Could not retrieve issue #${issueNumber}: GitHub returned ${response.status}`
27:     );
28:   }
29: 
30:   return ...
gateway · 2026-09-10 11:54:10
result
<path>/home/byk/Code/getsentry/publish/src/publish/current-accepted-event.js</path>
<type>file</type>
<content>
1: const { currentAcceptedEvent } = require("../modules/approval-attestation.js");
2: const { getAllPages } = require("./validate-approval-attestation.js");
3: 
4: async function getCurrentAcceptedEvent({
5:   getIssueEvents,
6:   issueNumber,
7:   repository,
8: }) {
9:   const event = currentAcceptedEvent(
10:     await getIssueEvents({ repository, issueNumber })
11:   );
12: 
13:   if (!event) {
14:     throw new Error("The issue has no current accepted label event");
15:   }
16: 
17:   return event;
18: }
19: 
20: async function main() {
21:   for (const name of [
22:     "APPROVAL_TOKEN",
23:     "APPROVAL_ISSUE_NUMBER",
24:     "APPROVAL_ISSUE_REPOSITORY",
25:   ]) {
26:     if (!process.env[name]) {
27:       throw new Error(`No "${name}" environment variable found`);
28:     }
29:   }
30: 
31:   const event = await getCurrentAcceptedEvent({
32:     getIssueEvents: ({ ...
gateway · 2026-09-10 11:54:15
result
<path>/home/byk/Code/getsentry/publish/src/publish/record-auto-approval-attestation.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3:   createAutoApprovalAttestation,
4:   requestDigest,
5: } = require("../modules/approval-attestation.js");
6: const { getIssue } = require("./validate-approval-attestation.js");
7: 
8: async function recordAutoApprovalAttestation({
9:   autoApprover,
10:   expectedRequestDigest,
11:   getIssue,
12:   issueNumber,
13:   issueTitle,
14:   repository,
15: }) {
16:   const issue = await getIssue({ repository, issueNumber });
17: 
18:   if (
19:     issue.state !== "open" ||
20:     issue.title !== issueTitle ||
21:     requestDigest(issue) !== expectedRequestDigest ||
22:     issue.user?.login?.toLowerCase() !== autoApprover.toLowerCase()
23:   ) {
24:     throw new Error("The automated approval request changed before approval");
25:   }
26: 
27:   return createAutoApprovalAttestation({ autoApprover, issue });
28: }
29: 
30: ...
gateway · 2026-09-10 11:54:26
result
<path>/home/byk/Code/getsentry/publish/src/publish/record-ci-ready-attestation.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3:   createCiReadyAttestation,
4:   currentAcceptedEvent,
5:   requestDigest,
6: } = require("../modules/approval-attestation.js");
7: const {
8:   getAllPages,
9:   getGitHubResponse,
10:   getIssue,
11: } = require("./validate-approval-attestation.js");
12: 
13: async function getAuthenticatedLogin() {
14:   const response = await getGitHubResponse("user");
15: 
16:   if (!response.ok) {
17:     throw new Error(
18:       `Could not retrieve the authenticated user: GitHub returned ${response.status}`
19:     );
20:   }
21: 
22:   const { login } = await response.json();
23: 
24:   if (typeof login !== "string") {
25:     throw new Error("GitHub returned no authenticated user login");
26:   }
27: 
28:   return login;
29: }
30: 
31: async function recordCiReadyAttestation({
32:   expectedAcceptedEvent,
33:   ...
gateway · 2026-09-10 11:54:35
result
<path>/home/byk/Code/getsentry/publish/src/publish/request-digest-from-event.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const { requestDigest } = require("../modules/approval-attestation.js");
3: 
4: function requestDigestFromEvent(event) {
5:   if (!event || typeof event.issue !== "object" || event.issue === null) {
6:     throw new Error("The GitHub event has no issue snapshot");
7:   }
8: 
9:   return requestDigest(event.issue);
10: }
11: 
12: function main() {
13:   if (!process.env.GITHUB_EVENT_PATH) {
14:     throw new Error('No "GITHUB_EVENT_PATH" environment variable found');
15:   }
16: 
17:   if (!process.env.GITHUB_OUTPUT) {
18:     throw new Error('No "GITHUB_OUTPUT" environment variable found');
19:   }
20: 
21:   const event = JSON.parse(
22:     fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8")
23:   );
24:   fs.appendFileSync(
25:     process.env.GITHUB_OUTPUT,
26:     `request_digest=${requestDigestFromEvent(event)}\n`
27:   );
28: ...
gateway · 2026-09-10 11:55:20
result
<path>/home/byk/Code/getsentry/publish/src/publish/discover-location.js</path>
<type>file</type>
<content>
1: const { execFileSync } = require("child_process");
2: const { existsSync } = require("fs");
3: const path = require("path");
4: 
5: const core = require("@actions/core");
6: 
7: const { resolvePublishLocation } = require("../modules/publish-location");
8: 
9: const CRAFT_IMAGE =
10:   "getsentry/craft@sha256:9a4a5d5efa44a00c2215078ead39800d4aaa5a97908b94f45a64d7d506d6e14b";
11: 
12: function getWorkspaceNames({
13:   repositoryDirectory,
14:   exists = existsSync,
15:   execFile = execFileSync,
16: }) {
17:   if (!exists(path.join(repositoryDirectory, ".craft.yml"))) {
18:     return [];
19:   }
20: 
21:   const output = execFile(
22:     "docker",
23:     [
24:       "run",
25:       "--rm",
26:       "--volume",
27:       `${path.resolve(repositoryDirectory)}:/github/workspace/__repo__`,
28:       "--workdir",
29:       "/github/workspace/__repo__",
30:       ...
gateway · 2026-09-10 11:55:37
result
<path>/home/byk/Code/getsentry/publish/src/publish/inputs.js</path>
<type>file</type>
<content>
1: const github = require("@actions/github");
2: const core = require("@actions/core");
3: const { detailsFromContext } = require("../modules/details-from-context");
4: 
5: async function inputs() {
6:   const result = await detailsFromContext({
7:     context: github.context,
8:   });
9:   core.setOutput("result", JSON.stringify(result));
10: }
11: inputs();

(End of file - total 11 lines)
</content>
gateway · 2026-09-10 11:55:56
result
<path>/home/byk/Code/getsentry/publish/src/publish/post-result.js</path>
<type>file</type>
<content>
1: const processEndState = require("../modules/process-end-state.js");
2: const { getGitHubToken } = require("../libs/github.js");
3: const github = require("@actions/github");
4: 
5: async function main({
6:   context = github.context,
7:   octokit = github.getOctokit(getGitHubToken()),
8:   report = processEndState,
9:   status = process.argv.slice(2)[0],
10: } = {}) {
11:   let inputs = {};
12: 
13:   if (process.env.PUBLISH_ARGS) {
14:     try {
15:       inputs = JSON.parse(process.env.PUBLISH_ARGS);
16:     } catch {
17:       console.warn("Could not parse publish inputs; reporting without them");
18:     }
19:   }
20: 
21:   await report({ context, octokit, inputs, status });
22: }
23: 
24: if (require.main === module) {
25:   main().catch((error) => {
26:     console.error(error);
27:     process.exitCode = 1;
28:   });
29: }
30: 
31: module.exports = { main };

(End of file - ...
gateway · 2026-09-10 11:56:08
result
<path>/home/byk/Code/getsentry/publish/src/publish/post-workflow-details.js</path>
<type>file</type>
<content>
1: const postWorkflowDetails = require("../modules/post-workflow-details.js");
2: const { getGitHubToken } = require("../libs/github");
3: const github = require("@actions/github");
4: 
5: const context = github.context;
6: const octokit = github.getOctokit(getGitHubToken());
7: 
8: postWorkflowDetails({ context, octokit });

(End of file - total 8 lines)
</content>
gateway · 2026-09-10 11:56:18
result
<path>/home/byk/Code/getsentry/publish/src/publish/resolve-ci-poller-input.js</path>
<type>file</type>
<content>
1: const { getCiPollerInput } = require("../modules/ci-poller-input");
2: const { readFileSync } = require("fs");
3: 
4: const issueBody = process.env.PUBLISH_ISSUE_BODY_FILE
5:   ? readFileSync(process.env.PUBLISH_ISSUE_BODY_FILE, "utf8")
6:   : process.env.PUBLISH_ISSUE_BODY || "";
7: 
8: process.stdout.write(
9:   JSON.stringify(
10:     getCiPollerInput({
11:       issueBody,
12:       labels: JSON.parse(process.env.PUBLISH_ISSUE_LABELS || ""),
13:       title: process.env.PUBLISH_TITLE || "",
14:       revision: process.env.PUBLISH_REVISION || "",
15:     })
16:   )
17: );

(End of file - total 17 lines)
</content>
gateway · 2026-09-10 11:56:24
result
<path>/home/byk/Code/getsentry/publish/src/publish/resolve-release-revision.js</path>
<type>file</type>
<content>
1: const core = require("@actions/core");
2: const { getReleaseRevision } = require("../modules/release-revision");
3: 
4: function resolveReleaseRevision() {
5:   const { repo } = JSON.parse(process.env.PUBLISH_ARGS || "{}");
6:   if (!repo) {
7:     throw new Error("Publish input must define a repository.");
8:   }
9: 
10:   core.setOutput(
11:     "revision",
12:     getReleaseRevision({
13:       issueBody: process.env.PUBLISH_ISSUE_BODY || "",
14:       repo,
15:     })
16:   );
17: }
18: 
19: resolveReleaseRevision();

(End of file - total 19 lines)
</content>
gateway · 2026-09-10 11:56:32
result
<path>/home/byk/Code/getsentry/publish/src/publish/update-issue.js</path>
<type>file</type>
<content>
1: const { updateIssue } = require("../modules/update-issue.js");
2: const { getGitHubToken } = require("../libs/github");
3: const github = require("@actions/github");
4: 
5: async function main() {
6:   const context = github.context;
7:   const octokit = github.getOctokit(getGitHubToken());
8:   let inputs;
9: 
10:   if (process.env.PUBLISH_ARGS) {
11:     try {
12:       inputs = JSON.parse(process.env.PUBLISH_ARGS);
13:     } catch {
14:       console.warn("Could not parse publish inputs; skipping target update");
15:     }
16:   }
17: 
18:   await updateIssue({ context, octokit, inputs });
19: }
20: 
21: if (require.main === module) {
22:   main().catch((error) => {
23:     console.error(error);
24:     process.exitCode = 1;
25:   });
26: }
27: 
28: module.exports = { main };

(End of file - total 28 lines)
</content>
gateway · 2026-09-10 11:56:41
result
<path>/home/byk/Code/getsentry/publish/src/libs/github.js</path>
<type>file</type>
<content>
1: /**
2:  * This is a helper function that throws a useful error message if the
3:  * workflow environment is not configured correctly.
4:  *
5:  * @returns string
6:  */
7: function getGitHubToken() {
8:   if (!process.env.GITHUB_TOKEN) {
9:     throw new Error(
10:       'No "GITHUB_TOKEN" environment variable found. ' +
11:         "Please ensure the workflow is configured correctly"
12:     );
13:   }
14:   return process.env.GITHUB_TOKEN;
15: }
16: 
17: module.exports = {
18:   getGitHubToken,
19: };

(End of file - total 19 lines)
</content>
gateway · 2026-09-10 11:56:55
result
<path>/home/byk/Code/getsentry/publish/src/modules/publish-issue-validation.js</path>
<type>file</type>
<content>
1: function isPublishRepository(name) {
2:   return (
3:     /^[A-Za-z0-9_.-]+$/.test(name) &&
4:     name !== "." &&
5:     name !== ".." &&
6:     name !== "__proto__" &&
7:     !name.startsWith("-")
8:   );
9: }
10: 
11: function isReleaseVersion(version) {
12:   return /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-?([0-9a-z-]+(?:\.[0-9a-z-]+)*))?(?:\+([0-9a-z-]+(?:\.[0-9a-z-]+)*))?$/i.test(
13:     version
14:   );
15: }
16: 
17: module.exports = { isPublishRepository, isReleaseVersion };

(End of file - total 17 lines)
</content>
gateway · 2026-09-10 11:57:04
result
<path>/home/byk/Code/getsentry/publish/src/modules/publish-issue-title.js</path>
<type>file</type>
<content>
1: /* eslint-disable */
2: // @generated by Peggy 5.1.0.
3: //
4: // https://peggyjs.org/
5: 
6: "use strict";
7: 
8: class peg$SyntaxError extends SyntaxError {
9:   constructor(message, expected, found, location) {
10:     super(message);
11:     this.expected = expected;
12:     this.found = found;
13:     this.location = location;
14:     this.name = "SyntaxError";
15:   }
16: 
17:   format(sources) {
18:     let str = "Error: " + this.message;
19:     if (this.location) {
20:       let src = null;
21:       const st = sources.find((s) => s.source === this.location.source);
22:       if (st) {
23:         src = st.text.split(/\r\n|\n|\r/g);
24:       }
25:       const s = this.location.start;
26:       const offset_s =
27:         this.location.source &&
28:         typeof this.location.source.offset === "function"
29:           ? this.location.source.offset(s)
30:          ...
gateway · 2026-09-10 11:57:15
result
<path>/home/byk/Code/getsentry/publish/src/modules/update-issue.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3:   TARGETS_SECTION_PARSER_REGEX,
4:   TARGETS_PARSER_REGEX,
5: } = require("./details-from-context");
6: 
7: async function updateTargets({
8:   octokit,
9:   stateFilePath,
10:   publishRepo,
11:   issue_number,
12: }) {
13:   if (!stateFilePath || !fs.existsSync(stateFilePath)) {
14:     return;
15:   }
16: 
17:   const issueRequest = octokit.rest.issues.get({
18:     ...publishRepo,
19:     issue_number,
20:   });
21: 
22:   const craftStateRequest = fs.promises
23:     .readFile(stateFilePath, { encoding: "utf-8" })
24:     .then((data) => JSON.parse(data));
25: 
26:   const [{ data: issue }, craftState] = await Promise.all([
27:     issueRequest,
28:     craftStateRequest,
29:   ]);
30: 
31:   const newIssueBody = transformIssueBody(craftState, issue.body);
32: 
33:   await octokit.rest.issues.update({
34:     ...publishRepo,
35:     ...
gateway · 2026-09-10 11:57:31
result
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/approval-attestation.js</path>
<type>file</type>
<content>
1: import { describe, expect, test } from "vitest";
2: 
3: const {
4:   createApprovalAttestation,
5:   createCiReadyAttestation,
6:   currentAcceptedEvent,
7:   hasApprovalAttestation,
8:   hasCiReadyAttestation,
9:   parseApprovalAttestation,
10:   requestDigest,
11: } = require("../approval-attestation.js");
12: 
13: function issue(title, { body = "", dryRun = false } = {}) {
14:   return {
15:     body,
16:     labels: dryRun ? [{ name: "dry-run" }] : [],
17:     title,
18:   };
19: }
20: 
21: describe("approval attestations", () => {
22:   test("matches the latest accepted event to a trusted attestation", () => {
23:     const title = "publish: getsentry/relay/py@1.2.3";
24:     const publishIssue = issue(title);
25:     const attestation = createApprovalAttestation({
26:       actor: "contractor",
27:       eventId: "200",
28:       issue: publishIssue,
29:     ...
gateway · 2026-09-10 11:57:43
result
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/approval-authorizer.js</path>
<type>file</type>
<content>
1: import { describe, expect, test, vi } from "vitest";
2: 
3: const { authorizeApproval } = require("../approval-authorizer.js");
4: 
5: const issueTitle = "publish: getsentry/sentry-javascript@10.0.0";
6: 
7: describe("authorizeApproval", () => {
8:   test.each(["getsantry[bot]", "sentry-release-bot[bot]"])(
9:     "rejects direct approval by %s without a target repository lookup",
10:     async (actor) => {
11:       const getPermission = vi.fn();
12: 
13:       await expect(
14:         authorizeApproval({
15:           actor,
16:           issueTitle,
17:           getPermission,
18:           autoApprovedRepositories: new Set(["getsentry/sentry-javascript"]),
19:         })
20:       ).resolves.toEqual({
21:         authorized: false,
22:         repository: "getsentry/sentry-javascript",
23:       });
24: 
25:       ...
gateway · 2026-09-10 11:57:57
result
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/authorize-approval.js</path>
<type>file</type>
<content>
1: import { afterEach, describe, expect, test, vi } from "vitest";
2: import fs from "fs";
3: const {
4:   createAutoApprovalAttestation,
5:   requestDigest,
6: } = require("../../modules/approval-attestation.js");
7: const {
8:   getAutoApprovedRepositories,
9:   main,
10: } = require("../authorize-approval.js");
11: 
12: const originalEnvironment = { ...process.env };
13: 
14: afterEach(() => {
15:   process.env = { ...originalEnvironment };
16:   vi.restoreAllMocks();
17:   vi.resetModules();
18: });
19: 
20: function jsonResponse(json) {
21:   return { ok: true, json: vi.fn().mockResolvedValue(json) };
22: }
23: 
24: async function runAuthorization({ actor, issueTitle, responses }) {
25:   process.env.GITHUB_OUTPUT = "/tmp/github-output";
26:   process.env.APPROVAL_TOKEN = "release-bot-token";
27:   process.env.APPROVAL_ACTOR = actor;
28:   ...
gateway · 2026-09-10 11:58:12
result
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/validate-approval-attestation.js</path>
<type>file</type>
<content>
1: import { afterEach, describe, expect, test, vi } from "vitest";
2: 
3: const {
4:   validateApprovalAttestation,
5: } = require("../validate-approval-attestation.js");
6: const {
7:   createApprovalAttestation,
8:   createCiReadyAttestation,
9:   requestDigest,
10: } = require("../../modules/approval-attestation.js");
11: 
12: afterEach(() => {
13:   vi.restoreAllMocks();
14: });
15: 
16: function jsonResponse(json) {
17:   return { ok: true, json: vi.fn().mockResolvedValue(json) };
18: }
19: 
20: function issue(
21:   title,
22:   { body = "", labels = [{ name: "accepted" }], state = "open" } = {}
23: ) {
24:   return { body, labels, state, title };
25: }
26: 
27: describe("validateApprovalAttestation", () => {
28:   test("accepts the current accepted event and its trusted attestation", async () => {
29:     const title = "publish: ...
gateway · 2026-09-10 11:58:40
result
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/record-auto-approval-attestation.js</path>
<type>file</type>
<content>
1: import { describe, expect, test, vi } from "vitest";
2: 
3: const {
4:   parseAutoApprovalAttestation,
5:   requestDigest,
6: } = require("../../modules/approval-attestation.js");
7: const {
8:   recordAutoApprovalAttestation,
9: } = require("../record-auto-approval-attestation.js");
10: 
11: describe("recordAutoApprovalAttestation", () => {
12:   test("binds an automated requester to the live publish request", async () => {
13:     const title = "publish: getsentry/sentry-javascript@10.0.0";
14: 
15:     await expect(
16:       recordAutoApprovalAttestation({
17:         autoApprover: "getsantry[bot]",
18:         expectedRequestDigest: requestDigest({
19:           body: "Merge target: main",
20:           labels: [],
21:           title,
22:         }),
23:         getIssue: vi.fn().mockResolvedValue({
24:           body: "Merge target: main",
25:   ...
gateway · 2026-09-10 11:58:46
result
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/record-ci-ready-attestation.js</path>
<type>file</type>
<content>
1: import { describe, expect, test, vi } from "vitest";
2: 
3: const {
4:   parseCiReadyAttestation,
5:   requestDigest,
6: } = require("../../modules/approval-attestation.js");
7: const {
8:   recordCiReadyAttestation,
9: } = require("../record-ci-ready-attestation.js");
10: 
11: describe("recordCiReadyAttestation", () => {
12:   test("binds the current approval to the app that will add ci-ready", async () => {
13:     const title = "publish: getsentry/sentry-javascript@10.0.0";
14: 
15:     await expect(
16:       recordCiReadyAttestation({
17:         expectedRequestDigest: requestDigest({
18:           body: "Merge target: main",
19:           labels: [{ name: "accepted" }],
20:           title,
21:         }),
22:         getAuthenticatedLogin: vi
23:           .fn()
24:           .mockResolvedValue("sentry-internal-app[bot]"),
25:         getIssue: ...
gateway · 2026-09-10 11:59:04
result
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/current-accepted-event.js</path>
<type>file</type>
<content>
1: import { describe, expect, test, vi } from "vitest";
2: 
3: const { getCurrentAcceptedEvent } = require("../current-accepted-event.js");
4: 
5: describe("getCurrentAcceptedEvent", () => {
6:   test("returns the latest accepted-label event", async () => {
7:     await expect(
8:       getCurrentAcceptedEvent({
9:         getIssueEvents: vi.fn().mockResolvedValue([
10:           {
11:             actor: { login: "contractor" },
12:             event: "labeled",
13:             id: "100",
14:             label: { name: "accepted" },
15:           },
16:           {
17:             actor: { login: "contractor" },
18:             event: "labeled",
19:             id: "200",
20:             label: { name: "accepted" },
21:           },
22:         ]),
23:         issueNumber: "123",
24:         repository: "getsentry/publish",
25:       })
26:     ).resolves.toEqual({ ...
gateway · 2026-09-10 11:59:10
result
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/request-digest-from-event.js</path>
<type>file</type>
<content>
1: import { describe, expect, test } from "vitest";
2: 
3: const { requestDigest } = require("../../modules/approval-attestation.js");
4: const { requestDigestFromEvent } = require("../request-digest-from-event.js");
5: 
6: describe("requestDigestFromEvent", () => {
7:   test("digests the exact issue event snapshot", () => {
8:     const issue = {
9:       body: "Merge target: main",
10:       labels: [{ name: "accepted" }, { name: "dry-run" }],
11:       title: "publish: getsentry/relay@1.2.3",
12:     };
13: 
14:     expect(requestDigestFromEvent({ issue })).toBe(requestDigest(issue));
15:   });
16: 
17:   test("rejects events without a complete issue snapshot", () => {
18:     expect(() => requestDigestFromEvent({})).toThrow(
19:       "The GitHub event has no issue snapshot"
20:     );
21:     expect(() =>
22:       requestDigestFromEvent({ issue: { body: ...
gateway · 2026-09-10 11:59:36
result
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/workflow-action-pinning.js</path>
<type>file</type>
<content>
1: import { readFileSync, readdirSync } from "fs";
2: import { describe, expect, test } from "vitest";
3: 
4: const workflowPaths = readdirSync(".github/workflows")
5:   .filter((path) => path.endsWith(".yml"))
6:   .map((path) => `.github/workflows/${path}`);
7: const workflows = workflowPaths.map((path) => ({
8:   path,
9:   contents: readFileSync(path, "utf8"),
10: }));
11: const SECRET_BEARING_JOBS = {
12:   ".github/workflows/auto-approve.yml": ["auto-approve"],
13:   ".github/workflows/ci-poller.yml": ["check-ci"],
14:   ".github/workflows/cocoapods-keepalive.yml": ["keepalive"],
15:   ".github/workflows/publish.yml": ["waiting-for-ci", "publish"],
16: };
17: 
18: function jobContents(workflow, jobName) {
19:   const header = `  ${jobName}:`;
20:   const jobStart = workflow.indexOf(header);
21:   const nextJobOffset = workflow
22:     .slice(jobStart + ...
gateway · 2026-09-10 11:59:59
result
Found 18 matches
/home/byk/Code/getsentry/publish/.github/workflows/test.yml:
  Line 19:       - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803

  Line 20:       - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38

  Line 23:       - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830


/home/byk/Code/getsentry/publish/.github/workflows/publish.yml:
  Line 36:         uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803

  Line 42:         uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1

  Line 56:         uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1

  Line 167:         uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1

  Line 220:         uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1

  Line 256:         uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38

  Line 284:         uses: ...
gateway · 2026-09-10 12:00:18
result
No files found
gateway · 2026-09-10 12:00:32
result
/home/byk/Code/getsentry/publish/.github/workflows/test.yml
/home/byk/Code/getsentry/publish/.github/workflows/publish.yml
/home/byk/Code/getsentry/publish/.github/workflows/cocoapods-keepalive.yml
/home/byk/Code/getsentry/publish/.github/workflows/ci-poller.yml
/home/byk/Code/getsentry/publish/.github/workflows/auto-approve.yml
/home/byk/Code/getsentry/publish/.github/workflows/ci-poller-dispatch.yml
gateway · 2026-09-10 12:00:54
result
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/ci-poller-workflow.js</path>
<type>file</type>
<content>
1: import {
2:   mkdtempSync,
3:   mkdirSync,
4:   readFileSync,
5:   readdirSync,
6:   rmSync,
7:   writeFileSync,
8: } from "fs";
9: import { tmpdir } from "os";
10: import { join } from "path";
11: import { spawnSync } from "child_process";
12: 
13: import { afterEach, expect, test } from "vitest";
14: 
15: const temporaryDirectories = [];
16: const INITIAL_REVISION = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
17: const UPDATED_REVISION = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
18: 
19: function getPollerScript() {
20:   const workflow = readFileSync(
21:     join(__dirname, "../../../.github/workflows/ci-poller.yml"),
22:     "utf8"
23:   );
24:   const section = workflow.match(
25:     / {6}- name: Check CI status for ci-pending issues[\s\S]*? {8}run: \|\n(?<script>[\s\S]*?)\n {6}- name: Check for remaining pending issues/
26:   );
27: 
28:   if ...
gateway · 2026-09-10 12:01:09
result
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/ci-poller-workflow.js</path>
<type>file</type>
<content>
1: import { readFileSync } from "fs";
2: import { describe, expect, test } from "vitest";
3: 
4: const workflow = readFileSync(".github/workflows/ci-poller.yml", "utf8");
5: const manualDispatchWorkflow = readFileSync(
6:   ".github/workflows/ci-poller-dispatch.yml",
7:   "utf8"
8: );
9: 
10: describe("CI poller workflow", () => {
11:   test("checks out trusted code before requesting app tokens", () => {
12:     expect(workflow).toMatch(
13:       /- name: Get publish code\n\s+uses: actions\/checkout@[a-f0-9]{40}\n\s+with:\n(?:\s+#.*\n)*\s+ref: \$\{\{ github\.event\.repository\.default_branch \}\}/
14:     );
15: 
16:     expect(workflow.indexOf("Get publish code")).toBeLessThan(
17:       workflow.indexOf("Get auth token")
18:     );
19:   });
20: 
21:   test("uses a protected workflow dispatch relay for manual recovery", () => {
22:     ...
gateway · 2026-09-10 12:01:24
result
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/publish-workflow.js</path>
<type>file</type>
<content>
1: import { spawnSync } from "child_process";
2: import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
3: import { tmpdir } from "os";
4: import { join } from "path";
5: 
6: import { afterEach, expect, test } from "vitest";
7: 
8: const { resolvePublishLocation } = require("../publish-location.js");
9: 
10: const temporaryDirectories = [];
11: 
12: function getWorkflow() {
13:   return readFileSync(
14:     join(__dirname, "../../../.github/workflows/publish.yml"),
15:     "utf8"
16:   );
17: }
18: 
19: function getSetTargetsScript() {
20:   const workflow = getWorkflow();
21:   const section = workflow.match(
22:     / {6}- name: Set targets[\s\S]*? {8}run: \|\n(?<script>[\s\S]*?)\n {6}- name: Revalidate approval attestation/
23:   );
24:   if (!section?.groups?.script) {
25:     throw new Error("Missing Set targets workflow script");
26:   }
27:   ...
gateway · 2026-09-10 12:01:45
result
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/publish-workflow.js</path>
<type>file</type>
<content>
1: import { readFileSync } from "fs";
2: import { describe, expect, test } from "vitest";
3: 
4: const workflow = readFileSync(".github/workflows/publish.yml", "utf8");
5: 
6: describe("publish workflow", () => {
7:   test("binds approval and publication to the triggering issue snapshot", () => {
8:     expect(workflow).toContain("node src/publish/request-digest-from-event.js");
9:     expect(
10:       workflow.match(
11:         /EXPECTED_REQUEST_DIGEST: \$\{\{ steps\.request-digest\.outputs\.request_digest \}\}/g
12:       )
13:     ).toHaveLength(4);
14:   });
15: 
16:   test("revalidates approval immediately before Craft", () => {
17:     const fence = workflow.indexOf("Revalidate approval attestation");
18: 
19:     expect(fence).toBeGreaterThan(workflow.indexOf("Set targets"));
20:     expect(fence).toBeLessThan(workflow.indexOf("Publish using Craft"));
21:     ...
gateway · 2026-09-10 12:02:17
result
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/post-result.js</path>
<type>file</type>
<content>
1: import { afterEach, describe, expect, test, vi } from "vitest";
2: 
3: const { main } = require("../post-result.js");
4: 
5: const originalEnvironment = { ...process.env };
6: 
7: afterEach(() => {
8:   process.env = { ...originalEnvironment };
9: });
10: 
11: describe("post-result entry point", () => {
12:   test.each([undefined, "not-json"])(
13:     "reports terminal state when publish inputs are %s",
14:     async (publishArgs) => {
15:       const report = vi.fn().mockResolvedValue(undefined);
16:       if (publishArgs === undefined) {
17:         delete process.env.PUBLISH_ARGS;
18:       } else {
19:         process.env.PUBLISH_ARGS = publishArgs;
20:       }
21: 
22:       await main({
23:         context: {},
24:         octokit: {},
25:         report,
26:         status: "failure",
27:       });
28: 
29:       expect(report).toHaveBeenCalledWith(
30:         ...
gateway · 2026-09-10 12:02:25
result
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/auto-approval-workflow.js</path>
<type>file</type>
<content>
1: import { readFileSync } from "fs";
2: import { describe, expect, test } from "vitest";
3: 
4: const workflow = readFileSync(".github/workflows/auto-approve.yml", "utf8");
5: 
6: describe("auto-approval workflow", () => {
7:   test("checks out the code used to record the attestation", () => {
8:     const checkout = workflow.slice(
9:       workflow.indexOf("uses: actions/checkout@"),
10:       workflow.indexOf("Record automated approval attestation")
11:     );
12: 
13:     expect(checkout).not.toContain("sparse-checkout:");
14:   });
15: 
16:   test("records a request-bound attestation before adding accepted", () => {
17:     expect(workflow).toContain("node src/publish/request-digest-from-event.js");
18:     expect(workflow).toContain(
19:       "EXPECTED_REQUEST_DIGEST: ${{ steps.request-digest.outputs.request_digest }}"
20:     );
21:     ...
gateway · 2026-09-10 12:02:42
result
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/ci-poller-input.js</path>
<type>file</type>
<content>
1: import { readFileSync } from "fs";
2: import { join } from "path";
3: 
4: import { expect, test } from "vitest";
5: 
6: const { getCiPollerInput } = require("../ci-poller-input.js");
7: const { requestDigest } = require("../approval-attestation.js");
8: 
9: const REVISION = "7e5ca7ed5581552de066e2a8bc295b8306be38ac";
10: const labels = [{ name: "accepted" }, { name: "dry-run" }];
11: const issueBody = `Requested by: @byk
12: 
13: Merge target: (default)
14: 
15: Quick links:
16: - [View changes](https://github.com/getsentry/toolkit/compare/1.2.2...release/1.2.3)
17: - [View check runs](https://github.com/getsentry/toolkit/commit/${REVISION}/checks/)`;
18: 
19: test("parses root and full-path publish titles", () => {
20:   const rootTitle = "publish: getsentry/toolkit/cli@1.2.3";
21:   expect(
22:     getCiPollerInput({
23:       issueBody,
24:       labels,
25:       ...
gateway · 2026-09-10 12:02:58
result
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/release-revision.js</path>
<type>file</type>
<content>
1: import { expect, test } from "vitest";
2: 
3: const {
4:   getReleaseRevision,
5:   updateReleaseRevision,
6: } = require("../release-revision.js");
7: 
8: const REVISION = "7e5ca7ed5581552de066e2a8bc295b8306be38ac";
9: 
10: function requestBody(quickLinks) {
11:   return `Requested by: @byk
12: 
13: Merge target: (default)
14: 
15: Quick links:
16: ${quickLinks}`;
17: }
18: 
19: function canonicalQuickLinks(revision = REVISION) {
20:   return `- [View changes](https://github.com/getsentry/toolkit/compare/1.2.2...release/1.2.3)
21: - [View check runs](https://github.com/getsentry/toolkit/commit/${revision}/checks/)`;
22: }
23: 
24: test("gets the CI-approved revision from the request header", () => {
25:   expect(
26:     getReleaseRevision({
27:       repo: "toolkit",
28:       issueBody: requestBody(canonicalQuickLinks()),
29:     })
30:   ).toBe(REVISION);
31: ...
gateway · 2026-09-10 12:03:28
result
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/publish-location.js</path>
<type>file</type>
<content>
1: import { expect, test } from "vitest";
2: 
3: const { resolvePublishLocation } = require("../publish-location.js");
4: 
5: test("classifies an exact full workspace path without normalizing it", () => {
6:   expect(
7:     resolvePublishLocation({
8:       path: "./packages/CLI",
9:       workspaceNames: ["packages/cli", "packages/CLI"],
10:     })
11:   ).toStrictEqual({ path: ".", workspace: "packages/CLI" });
12: });
13: 
14: test("keeps a non-workspace suffix as a checkout path", () => {
15:   expect(
16:     resolvePublishLocation({
17:       path: "./packages",
18:       workspaceNames: ["cli"],
19:     })
20:   ).toStrictEqual({ path: "./packages" });
21: });
22: 
23: test("keeps a multi-segment suffix that is not an exact workspace path", () => {
24:   expect(
25:     resolvePublishLocation({
26:       path: "./packages/cli",
27:       workspaceNames: ...
gateway · 2026-09-10 12:03:48
result
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/discover-location.js</path>
<type>file</type>
<content>
1: import { afterEach, expect, test, vi } from "vitest";
2: 
3: const {
4:   discoverLocation,
5:   getWorkspaceNames,
6: } = require("../discover-location.js");
7: 
8: afterEach(() => {
9:   vi.restoreAllMocks();
10: });
11: 
12: test("retains checkout-path behavior when the root config is absent", () => {
13:   expect(
14:     discoverLocation({
15:       input: { path: "./packages/cli" },
16:       repositoryDirectory: "__repo__",
17:       exists: () => false,
18:     })
19:   ).toEqual({ path: "./packages/cli" });
20: });
21: 
22: test("discovers exact workspace paths using the released Craft image", () => {
23:   const execFile = vi.fn(() => '["packages/CLI"]');
24: 
25:   expect(
26:     discoverLocation({
27:       input: { path: "./packages/CLI" },
28:       repositoryDirectory: "__repo__",
29:       exists: () => true,
30:       execFile,
31:     })
32:   ...
gateway · 2026-09-10 12:04:05
result
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/details-from-context.js</path>
<type>file</type>
<content>
1: import { test, expect } from "vitest";
2: 
3: const {
4:   detailsFromContext,
5:   parsePublishTitle,
6: } = require("../details-from-context.js");
7: 
8: const inputsArgs = {
9:   context: {
10:     repo: { owner: "getsentry", repo: "publish" },
11:     payload: {
12:       issue: {
13:         number: "223",
14:         title: "publish: getsentry/sentry@21.3.1",
15:         body: `Requested by: @BYK
16: 
17: Merge target: custom-branch
18: 
19: Quick links:
20: - [View changes](https://github.com/getsentry/sentry/compare/21.3.0...refs/heads/releases/21.3.1)
21: - [View check runs](https://github.com/getsentry/sentry/commit/7e5ca7ed5581552de066e2a8bc295b8306be38ac/checks/)
22: 
23: Assign the **accepted** label to this issue to approve the release. ...
gateway · 2026-09-10 12:04:22
result
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/process-end-state.js</path>
<type>file</type>
<content>
1: import { vi, describe, test, expect } from "vitest";
2: 
3: vi.mock("fs");
4: 
5: const processEndState = require("../process-end-state.js");
6: 
7: describe("publish failed", () => {
8:   const failureArgs = {
9:     status: "failure",
10:     inputs: { repo: "sentry", version: "21.3.1" },
11:     context: {
12:       runId: "1234",
13:       repo: { owner: "getsentry", repo: "publish" },
14:       payload: { issue: { number: "211" } },
15:     },
16:     octokit: {
17:       rest: {
18:         actions: {
19:           getWorkflowRun: async () => ({
20:             data: {
21:               html_url: "https://github.com/getsentry/sentry/actions/runs/1234",
22:             },
23:           }),
24:         },
25:         issues: {
26:           createComment: vi.fn(),
27:         },
28:       },
29:     },
30:     Sentry: {
31:       Scope: class Scope {
32:         ...
gateway · 2026-09-10 12:04:37
result
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/update-issue.js</path>
<type>file</type>
<content>
1: import { vi, describe, test, expect, beforeAll, beforeEach, it } from "vitest";
2: import fs from "fs";
3: 
4: const { updateIssue, transformIssueBody } = require("../update-issue.js");
5: 
6: let mockExistsSync;
7: 
8: const updateTargetsArgs = {
9:   inputs: { repo: "sentry", version: "21.3.1" },
10:   context: {
11:     runId: "1234",
12:     repo: { owner: "getsentry", repo: "publish" },
13:     payload: { issue: { number: "211" } },
14:   },
15:   octokit: {
16:     rest: {
17:       actions: {
18:         getWorkflowRun: async () => ({
19:           data: {
20:             html_url: "https://github.com/getsentry/sentry/actions/runs/1234",
21:           },
22:         }),
23:       },
24:       issues: {
25:         get: vi.fn(),
26:         update: vi.fn(),
27:         removeLabel: vi.fn(),
28:       },
29:     },
30:   },
31:   Sentry: {
32:     Scope: class Scope ...
gateway · 2026-09-10 12:04:51
result
<path>/home/byk/Code/getsentry/publish/src/libs/__tests__/github.js</path>
<type>file</type>
<content>
1: import { vi, describe, test, expect } from "vitest";
2: 
3: vi.mock("fs");
4: 
5: const { getGitHubToken } = require("../github.js");
6: 
7: describe("getGitHubToken", () => {
8:   test("throw if no token is defined", async () => {
9:     delete process.env.GITHUB_TOKEN;
10:     expect(() => getGitHubToken()).toThrow(
11:       'No "GITHUB_TOKEN" environment variable found.'
12:     );
13:   });
14: 
15:   test("return token if defined", async () => {
16:     process.env.GITHUB_TOKEN = "Example Token";
17:     expect(getGitHubToken()).toEqual("Example Token");
18:   });
19: });

(End of file - total 19 lines)
</content>
gateway · 2026-09-10 12:05:09
result
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/resolve-release-revision.js</path>
<type>file</type>
<content>
1: import { spawnSync } from "child_process";
2: import { expect, test } from "vitest";
3: 
4: const script = new URL("../resolve-release-revision.js", import.meta.url)
5:   .pathname;
6: 
7: test("rejects missing workflow input through release revision validation", () => {
8:   const result = spawnSync(process.execPath, [script], {
9:     encoding: "utf8",
10:     env: {
11:       ...process.env,
12:       PUBLISH_ARGS: "",
13:       PUBLISH_ISSUE_BODY: "",
14:     },
15:   });
16: 
17:   expect(result.status).not.toBe(0);
18:   expect(result.stderr).toContain("Publish input must define a repository.");
19:   expect(result.stderr).not.toContain("Unexpected end of JSON input");
20: });

(End of file - total 20 lines)
</content>
gateway · 2026-09-10 12:05:22
result
<path>/home/byk/Code/getsentry/publish/README.md</path>
<type>file</type>
<content>
1: # Sentry Publish 🏠
2: 
3: This is a meta/control repository that implements the [Central Publish Repository](docs/rfc.md) RFC
4: 
5: ## Quick Start
6: 
7: [craft quick start](https://craft.sentry.dev/github-actions/)
8: 
9: ## Release Flow
10: 
11: ```mermaid
12: flowchart TD
13:     A[Developer triggers release workflow] --> B["SDK Repo: craft prepare"]
14:     B --> C[Build artifacts & create release branch]
15:     C --> D[Upload artifacts to GitHub]
16:     D --> E["Create issue in getsentry/publish"]
17:     E --> F{Release Manager Review}
18:     F -->|"Add 'accepted' label"| G[Publish workflow triggers]
19:     G --> I[Download artifacts from GitHub]
20:     I --> J["craft publish to registries"]
21:     J --> K{Publish successful?}
22:     K -->|Yes| L[Issue closed - success]
23:     K -->|No| M[Issue updated with failure]
24: ```
25: 
26: ## Goals
27: 
28: 1. ...
gateway · 2026-09-10 12:05:39
result
<path>/home/byk/Code/getsentry/publish/docs/key_rotation_vault_changes.md</path>
<type>file</type>
<content>
1: # Key Rotation & Vault Changes
2: 
3: Currently there are three vaults that contain secrets:
4: 
5: - SDKs/OSS: both available to a range of employees, some of which departed
6: - Shared: available to all engineers, some of which departed
7: - Ops: available to operations, none of which departed
8: 
9: ## Goal
10: 
11: We do not want employees to publish through their own accounts, but we also do not want them to have access to the global credentials. We have enabled the first part of this process as the preparation of releases no longer requires credentials when [Craft](https://github.com/getsentry/craft) is used.
12: 
13: The actual publish via Craft however still requires access to the credentials. ...
gateway · 2026-09-10 12:06:09
result
<path>/home/byk/Code/getsentry/publish/docs/rfc.md</path>
<type>file</type>
<content>
1: # [RFC] Central Publish Repository
2: 
3: This design document is written to address the concerns mentioned in [Key Rotation & Vault Changes](key_rotation_vault_changes.md). Implementation pending feedback from engineering team members, specifically the SDK team.
4: 
5: # Problem Statement
6: 
7: 1. We need to be able to quickly recycle all publishing tokens (NPM, PyPI, Cargo, Docker, etc.) at will without disrupting our releases or our developer flow
8: 2. We need to limit the publishing tokens to authorized personnel only and not leak them in any way
9:    1. GitHub secrets is not enough as it lets anyone with write access to the repo to access the secrets and scuttle them out, even in a brand new PR. Example: [https://github.com/getsentry/sentry/pull/21930](https://github.com/getsentry/sentry/pull/21930)
10: 3. ...
gateway · 2026-09-10 12:06:41
result
<path>/home/byk/Code/getsentry/publish/AGENTS.md</path>
<type>file</type>
<content>
1: # AGENTS.md - Coding Agent Guidelines
2: 
3: This document provides guidelines for AI coding agents working in this repository.
4: 
5: ## Project Overview
6: 
7: This is the **Sentry Publish Repository** - a GitHub Actions-based approval system for publishing releases across multiple Sentry projects using [Craft](https://github.com/getsentry/craft). It's a pure JavaScript (Node.js 24.0.0) project using CommonJS modules.
8: 
9: ## Build/Lint/Test Commands
10: 
11: ### Package Manager
12: 
13: - **Yarn 1.22.22** (classic) - Do NOT use npm
14: - Version management via Volta (pinned in package.json)
15: 
16: ### Commands
17: 
18: ```bash
19: # Install dependencies
20: yarn install
21: 
22: # Run all tests
23: yarn test
24: 
25: # Run tests in watch mode
26: yarn test:watch
27: 
28: # Run a single test file
29: yarn test src/modules/__tests__/update-issue.js
30: 
31: # Run tests matching a pattern
32: ...
gateway · 2026-09-10 12:06:54
result
<path>/home/byk/Code/getsentry/publish/.lore.md</path>
<type>file</type>
<content>
1: <!-- Managed by lore (https://github.com/BYK/loreai) — manual edits are imported on next session. -->
2: 
3: ## Long-term Knowledge
4: 
5: ### Architecture
6: 
7: <!-- lore:01a043d7-502d-7de9-9416-66f3050c12b7 -->
8: * **approval-authorizer GitHub roles**: Chose live GitHub collaborator permission checks over private \`security-as-code\` because Publish is public and GitHub reflects effective target-repository access without exposing RBAC. Humans authorize only with \`write\`, \`maintain\`, or \`admin\`; \`triage\` never authorizes. \`getsantry\[bot]\` must never fall through to collaborator lookup: it authorizes only when its exact parsed repository/release path is in \`auto-approve-repos.txt\`, because bot repository access does not prove the event followed an allowlisted auto-approval path.
9: 
10: <!-- lore:01a082db-d8b9-7438-927a-07bf7aa5e369 -->
11: * **production environment secret boundary**: ...
gateway · 2026-09-10 12:07:21
result
<path>/home/byk/Code/getsentry/publish/src/publish/request-digest-from-event.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const { requestDigest } = require("../modules/approval-attestation.js");
3: 
4: function requestDigestFromEvent(event) {
5:   if (!event || typeof event.issue !== "object" || event.issue === null) {
6:     throw new Error("The GitHub event has no issue snapshot");
7:   }
8: 
9:   return requestDigest(event.issue);
10: }
11: 
12: function main() {
13:   if (!process.env.GITHUB_EVENT_PATH) {
14:     throw new Error('No "GITHUB_EVENT_PATH" environment variable found');
15:   }
16: 
17:   if (!process.env.GITHUB_OUTPUT) {
18:     throw new Error('No "GITHUB_OUTPUT" environment variable found');
19:   }
20: 
21:   const event = JSON.parse(
22:     fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8")
23:   );
24:   fs.appendFileSync(
25:     process.env.GITHUB_OUTPUT,
26:     `request_digest=${requestDigestFromEvent(event)}\n`
27:   );
28: ...
gateway · 2026-09-10 12:08:11
result
diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml
index d9763c7..5f86245 100644
--- a/.github/workflows/auto-approve.yml
+++ b/.github/workflows/auto-approve.yml
@@ -5,25 +5,47 @@ on:
 
 permissions:
   contents: read
+  issues: write
 
 jobs:
   auto-approve:
     runs-on: ubuntu-latest
+    environment: production
     if: |
       (github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') &&
       startsWith(github.event.issue.title, 'publish: ')
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
         with:
-          sparse-checkout: auto-approve-repos.txt
-          sparse-checkout-cone-mode: false
+          persist-credentials: false
       - name: Get auth token
         id: token
-        uses: actions/create-github-app-token@v3
+        uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
         with:
           ...
gateway · 2026-09-10 12:08:47
result
diff --git a/.github/workflows/ci-poller.yml b/.github/workflows/ci-poller.yml
index 487e9da..92ce6f0 100644
--- a/.github/workflows/ci-poller.yml
+++ b/.github/workflows/ci-poller.yml
@@ -3,16 +3,15 @@ name: CI Status Poller
 on:
   schedule:
     - cron: "*/5 * * * *"
-  workflow_dispatch:
-    inputs:
-      attempt:
-        description: Self-dispatch attempt counter (internal use)
-        required: false
-        default: "0"
+  repository_dispatch:
+    types: [ci-poller]
+  workflow_run:
+    workflows: [Run CI Status Poller]
+    types: [completed]
 
 permissions:
   contents: read
-  issues: read
+  issues: write
 
 jobs:
   check-ci:
@@ -20,18 +19,46 @@ jobs:
     environment: production
     # Skip entirely (no runner provisioned) when there's nothing to check.
     # Set to "true" by ci-pending.yml, reset to "false" here when done.
-    # Always allow workflow_dispatch for manual recovery.
-    if: vars.CI_POLLER_HAS_PENDING == 'true' || github.event_name == ...
gateway · 2026-09-10 12:09:22
result
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 2635ea6..88a4030 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -32,13 +32,90 @@ jobs:
       && github.event.issue.state == 'open'
       && startsWith(github.event.issue.title, 'publish: ')
     steps:
+      - name: Get publish code
+        uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
+        with:
+          persist-credentials: false
+
       - name: Get auth token
         id: token
-        uses: actions/create-github-app-token@v3
+        uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
         with:
           client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
           private-key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }}
 
+      - name: Bind request snapshot
+        id: request-digest
+        run: node src/publish/request-digest-from-event.js
+
+      # The release bot sees all target repositories. ...
gateway · 2026-09-10 12:09:58
result
diff --git a/src/modules/ci-poller-input.js b/src/modules/ci-poller-input.js
index ca08a72..bf2d23d 100644
--- a/src/modules/ci-poller-input.js
+++ b/src/modules/ci-poller-input.js
@@ -8,8 +8,9 @@ const {
   isReleaseVersion,
 } = require("./publish-issue-validation");
 const { isPublishPath } = require("./publish-location");
+const { requestDigest } = require("./approval-attestation");
 
-function getCiPollerInput({ title, issueBody, revision }) {
+function getCiPollerInput({ title, issueBody, labels, revision }) {
   let parsedTitle;
   try {
     parsedTitle = parsePublishIssueTitle(title);
@@ -28,12 +29,14 @@ function getCiPollerInput({ title, issueBody, revision }) {
     throw new Error(`Invalid publish issue path: '${path}'`);
   }
   const currentRevision = getReleaseRevision({ issueBody, repo });
+  const resolvedIssueBody = revision
+    ? updateReleaseRevision({ issueBody, repo, revision })
+    : issueBody;
 
   return {
-    ...(revision
-      ? ...
gateway · 2026-09-10 12:10:30
result
diff --git a/src/libs/github.js b/src/libs/github.js
index e4d506f..f1188c1 100644
--- a/src/libs/github.js
+++ b/src/libs/github.js
@@ -6,8 +6,10 @@
  */
 function getGitHubToken() {
   if (!process.env.GITHUB_TOKEN) {
-    throw new Error('No "GITHUB_TOKEN" environment variable found. ' +
-    'Please ensure the workflow is configured correctly');
+    throw new Error(
+      'No "GITHUB_TOKEN" environment variable found. ' +
+        "Please ensure the workflow is configured correctly"
+    );
   }
   return process.env.GITHUB_TOKEN;
 }
diff --git a/src/modules/post-workflow-details.js b/src/modules/post-workflow-details.js
index 278d23e..a02534a 100644
--- a/src/modules/post-workflow-details.js
+++ b/src/modules/post-workflow-details.js
@@ -1,4 +1,4 @@
-async function postWorkflowDetails({context, octokit}) {
+async function postWorkflowDetails({ context, octokit }) {
   const { repo: publishRepo, runId: run_id } = context;
   const { number: issue_number } = ...
gateway · 2026-09-10 12:10:59
result
diff --git a/src/modules/__tests__/process-end-state.js b/src/modules/__tests__/process-end-state.js
index 940f2d7..f983bfb 100644
--- a/src/modules/__tests__/process-end-state.js
+++ b/src/modules/__tests__/process-end-state.js
@@ -171,6 +171,33 @@ describe("publish success", () => {
       }
     `);
   });
+
+  test("closes the issue before workflow lookup and comments", async () => {
+    const update = vi.fn();
+    const getWorkflowRun = vi.fn().mockRejectedValue(new Error("unavailable"));
+
+    await expect(
+      processEndState({
+        ...successArgs,
+        octokit: {
+          rest: {
+            actions: { getWorkflowRun },
+            issues: { createComment: vi.fn(), update },
+          },
+        },
+      })
+    ).rejects.toThrow("unavailable");
+
+    expect(update).toHaveBeenCalledWith({
+      issue_number: "211",
+      owner: "getsentry",
+      repo: "publish",
+      state: "closed",
+    });
+    ...
gateway · 2026-09-10 12:11:11
result
diff --git a/AGENTS.md b/AGENTS.md
index cf473df..51bf3a8 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -273,3 +273,9 @@ This codebase interacts heavily with GitHub Actions. Entry points in `src/publis
 ## Code Owners
 
 All code is owned by `@getsentry/releng` (Release Engineering team).
+
+<!-- This section is maintained by the coding agent via lore (https://github.com/BYK/loreai) -->
+## Long-term Knowledge
+
+For long-term knowledge entries managed by [lore](https://github.com/BYK/loreai) (gotchas, patterns, decisions, architecture), see [`.lore.md`](.lore.md) in the project root.
+<!-- End lore-managed section -->
diff --git a/README.md b/README.md
index 4cbc9b9..82bc3f5 100644
--- a/README.md
+++ b/README.md
@@ -99,6 +99,8 @@ Automatic approvals are managed in the [`auto-approve.yml`](https://github.com/g
 
 The system uses [Craft](https://github.com/getsentry/craft) under the hood to prepare and publish releases. It uses tokens from [Sentry Release ...
gateway · 2026-09-10 12:11:29
result
<path>/home/byk/Code/getsentry/publish/README.md</path>
<type>file</type>
<content>
88: ## Approvals
89: 
90: Packages we release into the wider world that our customers install, require an explicit approval. This for instance applies to
91: `sentry-cli`, our SDKs or the `symbolicator` distributed utilities. Internal dependencies such as `arroyo` can be published
92: with an auto approval. The reasoning here is that the bump of the dependency requires an explicit approval again in Sentry
93: proper. In theory if an independent package gets sufficient independent use of Sentry we might want to reconsider an auto
94: approval process for such package as it might become an interesting target for an attacker.
95: 
96: Automatic approvals are managed in the [`auto-approve.yml`](https://github.com/getsentry/publish/blob/main/.github/workflows/auto-approve.yml) workflow.
97: 
98: ## Under the hood
99: 
100: The system uses [Craft](https://github.com/getsentry/craft) under the hood to prepare ...
gateway · 2026-09-10 12:11:45
result
<path>/home/byk/Code/getsentry/publish/package.json</path>
<type>file</type>
<content>
1: {
2:   "name": "publish",
3:   "private": true,
4:   "version": "0.0.1",
5:   "description": "Approval-based publishing system for Sentry",
6:   "main": "index.js",
7:   "repository": "git@github.com:getsentry/publish.git",
8:   "author": "Sentry Open Source <oss@sentry.io>",
9:   "license": "Apache-2.0",
10:   "volta": {
11:     "node": "24.0.0",
12:     "yarn": "1.22.22"
13:   },
14:   "resolutions": {
15:     "undici": "^6.23.0"
16:   },
17:   "scripts": {
18:     "generate": "node scripts/generate-publish-issue-title-parser.js",
19:     "check:generated": "node scripts/generate-publish-issue-title-parser.js --check",
20:     "test": "yarn check:generated && vitest run",
21:     "test:watch": "vitest",
22:     "lint": "eslint src .github --ignore-pattern '!.github'",
23:     "prettier": "prettier --write src"
24:   },
25:   "devDependencies": {
26:     "eslint": "^8.9.0",
27:     ...
gateway · 2026-09-10 12:14:08
result
Found 2 matches
/home/byk/Code/getsentry/publish/src/publish/validate-approval-attestation.js:
  Line 14:       "X-GitHub-Api-Version": "2026-03-10",


/home/byk/Code/getsentry/publish/src/publish/authorize-approval.js:
  Line 45:       "X-GitHub-Api-Version": "2026-03-10",
gateway · 2026-09-10 12:14:26
result
# API Versions

Learn how to specify which REST API version to use whenever you make a request to the REST API.

## About API versioning

The GitHub REST API is versioned. The API version name is based on the date when the API version was released. For example, the API version `2026-03-10` was released on Tue, 10 Mar 2026.

Breaking changes are changes that can potentially break an integration. Breaking changes will be released in a new API version. We will provide advance notice before releasing breaking changes. ...
gateway · 2026-09-10 12:14:40
result
# Events that trigger workflows

You can configure your workflows to run when specific activity on GitHub happens, at a scheduled time, or when an event outside of GitHub occurs.

## About events that trigger workflows

Workflow triggers are events that cause a workflow to run. For more information about how to use workflow triggers, see [Triggering a workflow](/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow).

Some events have multiple activity types. For these events, you can specify which activity types will trigger a workflow run. For more information about what each activity type means, see [Webhook events and payloads](/en/webhooks/webhook-events-and-payloads).

> \[!NOTE]
> Not all webhook events trigger workflows.

Like GitHub Actions workflows, agentic workflows can be triggered by repository events and schedules. ...
gateway · 2026-09-10 12:16:12
result
<path>/home/byk/Code/getsentry/publish/auto-approve-repos.txt</path>
<type>file</type>
<content>
1: getsentry/arroyo
2: getsentry/auto-type-annotate
3: getsentry/devenv
4: getsentry/infra-event-notifier
5: getsentry/jest-sentry-environment
6: getsentry/json-schema-diff
7: getsentry/js-source-scopes
8: getsentry/objectstore/clients
9: getsentry/ophio
10: getsentry/pdb
11: getsentry/pyo3-python-tracing-subscriber
12: getsentry/pytest-sentry
13: getsentry/relay/py
14: getsentry/responses
15: getsentry/rust-proguard
16: getsentry/rust-sourcemap
17: getsentry/rust-usage-accountant
18: getsentry/script-runner
19: getsentry/sentry-api-schema
20: getsentry/sentry-forked-djangorestframework-stubs
21: getsentry/sentry-forked-django-stubs
22: getsentry/sentry-forked-jsonnet
23: getsentry/sentry-infra-tools
24: getsentry/sentry-kafka-management
25: getsentry/sentry-kafka-schemas
26: getsentry/sentry-protos
27: getsentry/sentry-redis-tools
28: getsentry/service-registry
29: getsentry/skrooge
30: ...
gateway · 2026-09-10 12:19:15
result
# REST API endpoints for users

Use the REST API to get public and private information about authenticated users.

> [!NOTE]
> Most endpoints use `Authorization: Bearer <YOUR-TOKEN>` and `Accept: application/vnd.github+json` headers, plus `X-GitHub-Api-Version: 2026-03-10`. Curl examples below omit these standard headers for brevity.

## Get the authenticated user

```
GET /user
```

OAuth app tokens and personal access tokens (classic) need the read:user scope, or the broader user scope, for this endpoint to return the private user response. The private user response includes additional fields such as private_gists, total_private_repos, owned_private_repos, disk_usage, collaborators, and two_factor_authentication. Tokens without these scopes receive the public user response.
The private and public user response types are unrelated to the private profile setting. ...
gateway · 2026-09-10 12:23:38
result
yarn run v1.22.22
$ yarn check:generated && vitest run --printConsoleTrace src/modules/__tests__/approval-attestation.js src/modules/__tests__/approval-authorizer.js src/modules/__tests__/ci-poller-input.js src/modules/__tests__/ci-poller-workflow.js src/modules/__tests__/details-from-context.js src/modules/__tests__/publish-location.js src/modules/__tests__/publish-workflow.js src/modules/__tests__/release-revision.js src/publish/__tests__/authorize-approval.js src/publish/__tests__/auto-approval-workflow.js src/publish/__tests__/ci-poller-workflow.js src/publish/__tests__/current-accepted-event.js src/publish/__tests__/discover-location.js src/publish/__tests__/publish-workflow.js src/publish/__tests__/record-auto-approval-attestation.js src/publish/__tests__/record-ci-ready-attestation.js src/publish/__tests__/request-digest-from-event.js src/publish/__tests__/validate-approval-attestation.js src/publish/__tests__/workflow-action-pinning.js
$ node ...
gateway · 2026-09-10 12:26:29

Distillations (17)

Gen 0 2026-09-10 14:17:06 · 357 tokens

🔴 (12:26) Test command completed successfully in `/home/byk/Code/getsentry/publish`: `yarn check:generated && vitest run --printConsoleTrace src/modules/__tests__/approval-attestation.js src/modules/__tests__/approval-authorizer.js src/modules/__tests__/ci-poller-input.js src/modules/__tests__/ci-poller-workflow.js src/modules/__tests__/details-from-context.js src/modules/__tests__/publish-locat…

Gen 0 2026-09-10 14:13:04 · 1194 tokens

🔴 (12:16) GitHub Actions workflow triggers can originate from repository activity, schedules, or events outside GitHub; not all webhook events trigger workflows, and events with multiple activity types can be filtered by activity type. 🔴 (12:16) The `branch_protection_rule` workflow trigger supports activity types `created`, `edited`, and `deleted`; `GITHUB_SHA` is the last commit on the defaul…

Gen 0 2026-09-10 13:54:44 · 843 tokens

🔴 (12:14) `/home/byk/Code/getsentry/publish/package.json` defines the private package `publish` version `0.0.1`, described as “Approval-based publishing system for Sentry,” with entry point `index.js`, repository `git@github.com:getsentry/publish.git`, author `Sentry Open Source <oss@sentry.io>`, and license `Apache-2.0`. 🔴 (12:14) `/home/byk/Code/getsentry/publish/package.json` pins Volta tool…

Gen 0 2026-09-10 13:54:12 · 288 tokens

🔴 (12:11) `README.md` approval policy requires explicit approval for customer-installed public releases such as `sentry-cli`, SDKs, and `symbolicator` utilities; internal dependencies such as `arroyo` may receive automatic approval because their later dependency bump in Sentry proper requires another explicit approval. Automatic approvals are managed by `.github/workflows/auto-approve.yml`. 🔴 (…

Gen 0 2026-09-10 13:53:58 · 1708 tokens

🔴 (12:08) Working tree contains new `src/publish/request-digest-from-event.js`: `requestDigestFromEvent(event)` rejects events without an `issue` object, computes `requestDigest(event.issue)` via `src/modules/approval-attestation.js`, and `main()` reads JSON from `GITHUB_EVENT_PATH` and appends `request_digest=…` to `GITHUB_OUTPUT`; missing environment variables fail closed. 🔴 (12:08) `.github/…

Gen 0 2026-09-10 13:41:34 · 1278 tokens

🔴 (12:07) User requires the CI poller’s deliberate `ci-ready` transition to remain intact: after CI succeeds, remove any stale `ci-ready` label, revalidate the exact approved release snapshot, and always add `ci-ready` again so a fresh label event triggers publishing; adding the label must never be treated as an idempotent optimization. 🔴 (12:07) User requires the CI poller to revoke or stop pr…

Gen 0 2026-09-10 13:21:28 · 2840 tokens

🔴 (12:04) `/home/byk/Code/getsentry/publish/src/modules/__tests__/process-end-state.js` tests `processEndState()` for statuses `"failure"`, `"cancelled"`, `"success"`, and undefined using repository `sentry`, version `21.3.1`, run ID `"1234"`, and publish issue `"211"`. 🔴 (12:04) For status `"failure"`, `processEndState()` creates exactly 1 issue comment beginning `Failed to publish.` with run …

Gen 0 2026-09-10 13:19:45 · 3821 tokens

🔴 (12:01) `/home/byk/Code/getsentry/publish/src/publish/__tests__/ci-poller-workflow.js` reads `.github/workflows/ci-poller.yml` and `.github/workflows/ci-poller-dispatch.yml` and tests the CI poller workflow. 🔴 (12:01) The CI poller must check out trusted code at `github.event.repository.default_branch` in `"Get publish code"` before `"Get auth token"`. 🔴 (12:01) The CI poller uses `repositor…

Gen 0 2026-09-10 13:04:46 · 2799 tokens

🔴 (11:57) `src/modules/__tests__/approval-authorizer.js` contains tests for `authorizeApproval()` using issue title `publish: getsentry/sentry-javascript@10.0.0`. 🔴 (11:57) `authorizeApproval()` rejects direct approval by actors `"getsantry[bot]"` and `"sentry-release-bot[bot]"`, returns `{ authorized: false, repository: "getsentry/sentry-javascript" }`, and does not call `getPermission`, even …

Gen 0 2026-09-10 12:49:40 · 1052 tokens

🔴 (11:57) `updateTargets({ octokit, stateFilePath, publishRepo, issue_number })` in `src/modules/update-issue.js` returns without making API calls when `stateFilePath` is absent or `fs.existsSync(stateFilePath)` is false. 🔴 (11:57) When a state file exists, `updateTargets()` concurrently fetches the issue via `octokit.rest.issues.get({ ...publishRepo, issue_number })` and reads/parses the UTF-8…

Gen 0 2026-09-10 12:18:28 · 1341 tokens

🔴 (11:55) `src/publish/request-digest-from-event.js` defines `requestDigestFromEvent(event)`, which throws `The GitHub event has no issue snapshot` unless `event.issue` is a non-null object, then returns `requestDigest(event.issue)`. 🔴 (11:55) `main()` in `src/publish/request-digest-from-event.js` requires `GITHUB_EVENT_PATH` and `GITHUB_OUTPUT`, throwing `No "GITHUB_EVENT_PATH" environment var…

Gen 0 2026-09-10 12:17:35 · 4418 tokens

🔴 (11:51) `/home/byk/Code/getsentry/publish/.github/workflows/test.yml` defines workflow `Test`, triggered by pushes to `main` and all `pull_request` events, with `contents: read`, default `bash` shell, and one `unit-test` job named `unit tests` on `ubuntu-latest`. 🔴 (11:51) `.github/workflows/test.yml` runs ordered steps: 1. `actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803`; 2. `acti…

Gen 0 2026-09-10 12:15:04 · 334 tokens

🔴 (11:50) User’s `.github/workflows/ci-poller-dispatch.yml` defines the manually triggered `Run CI Status Poller` workflow with `permissions: {}` and one `dispatch` job running on `ubuntu-latest` in the protected `production` environment; its only step runs `":"`. 🔴 (11:50) User stated repository secrets must never be used by `.github/workflows/ci-poller-dispatch.yml`; protected `production` en…

Gen 0 2026-09-10 12:14:30 · 22 tokens

🔴 (11:50) User stated the CI poller always adds the `ci-ready` label.

Gen 0 2026-09-10 12:01:57 · 1124 tokens

🔴 (11:49) Repository was on branch `main`, tracking `origin/main`; `HEAD`, `ORIGIN_MAIN`, and `MERGE_BASE_HEAD_ORIGIN_MAIN` were all `7c60ddb7f43040fe8fbfea70efc833f689c04e75`, with `HEAD_TO_ORIGIN_MAIN_COUNTS 0	0`. 🔴 (11:49) Recent commit history included, in order: `7c60ddb feat: accept workspace publish requests`; `83d210b build(deps): bump js-yaml from 4.3.0 to 4.3.1 (#9174)` on `opencode/t…

Gen 0 2026-09-10 11:51:46 · 989 tokens

🔴 (11:48) Repository root `/home/byk/Code/getsentry/publish` contained 16 entries: `.eslintrc.js`, `.git/`, `.github/`, `.gitignore`, `.lore.md`, `AGENTS.md`, `auto-approve-repos.txt`, `docs/`, `LICENSE`, `node_modules/`, `package.json`, `README.md`, `scripts/`, `src/`, `vitest.config.js`, `yarn.lock`. 🔴 (11:48) Initial repository fingerprint reported `HEAD 7c60ddb7f43040fe8fbfea70efc833f689c04…

Gen 0 2026-09-10 11:51:00 · 689 tokens

🟡 (11:47) [requested-review] User requested a READ-ONLY, substantive adversarial security review of the exact current worktree at `/home/byk/Code/getsentry/publish` against base commit `7c60ddb7f43040fe8fbfea70efc833f689c04e75` (`origin/main`). 🔴 (11:47) [enforced-read-only-workflow] User prohibited editing, formatting, staging, stashing, committing, or otherwise mutating any file or repository…