DashboardpublishSession 1V5okji1pQaM

Session 1V5okji1pQaM

Full ID: 1V5okji1pQaM0xmaw
Messages: 130
Distillations: 5
Time range: 2026-09-10 15:56:27 — 2026-09-10 16:57:54

Conversation (130 messages)

Perform a substantive independent adversarial correctness and security review of the exact current worktree in /home/byk/Code/getsentry/publish. READ ONLY: do not edit files, run formatters, or mutate the tree. Review the full integrated diff against origin/main, not only the latest edits, and inspect every changed/untracked source, test, workflow, and relevant documentation file. Focus especially on approval provenance, latest label/unlabel event semantics and malformed event IDs, request digest/event snapshot binding, auto-approval identity, CI-poller trusted-code/secret boundaries, branch/check-suite API error handling, revision movement, ci-ready remove/revalidate/re-add and failed-add recovery, publication privilege fences, immutable pins, organization/project scoping where applicable, shell injection, race/TOCTOU behavior, and fail-open paths. Treat GitHub event and issue content as attacker-controlled. The exact tree has passed generated verification, 192 tests, lint, whitespace checks, and 10 focused stability runs, but do not rely on tests alone. Return a substantive report with current file:line evidence. Classify each point as PASS, CONCERN, or MUST-FIX. If any tool or scope blocks review, begin BLOCKED and name the exact blocker. End your response with exactly MERGE or DO-NOT-MERGE. Never return an empty response.

gateway · 2026-09-10 15:56:27
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 15:57:00
result
## main...origin/main
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 src/modules/__tests__/publish-workflow.js
 M src/modules/__tests__/release-revision.js
MM src/modules/__tests__/update-issue.js
 M src/modules/ci-poller-input.js
M  src/modules/details-from-context.js
M  src/modules/post-workflow-details.js
MM src/modules/process-end-state.js
 M src/modules/publish-location.js
 M src/modules/release-revision.js
 M src/modules/update-issue.js
 M src/publish/__tests__/discover-location.js
 M ...
gateway · 2026-09-10 15:57:18
result
7c60ddb7f43040fe8fbfea70efc833f689c04e75
7c60ddb7f43040fe8fbfea70efc833f689c04e75
7c60ddb7f43040fe8fbfea70efc833f689c04e75
a4cccb7ec721c017ed38b75686bea932b0e14c4c9298c240a9db9bd1c043fa11  -
570dcb023ae08f4fb8338923496a55bf1c2e012a944f9969650bc31feadd485c  .github/workflows/ci-poller-dispatch.yml
7a8b139f81784011cc5b0d2f217ded95b981897e1fd4fce52c43774074dba988  .lore.md
9051b3a78e4ceb1ef079c599abf9550d03ba8527ca8bbf229631c38f65375b29  src/modules/approval-attestation.js
c9ca690c7ca173c6cdc973e511dadb5479ff10cd958f9b0c4f10f83017e6ec55  src/modules/approval-authorizer.js
11a45a2ff4688586212aef5605db4c31d8d5a1d34735f2a5b7c1bf6bcbdd2a9b  src/modules/__tests__/approval-attestation.js
c6f64660b2fa5339b7b7936608ce90455518d5445c0ce3ff0298f1035e48a407  src/modules/__tests__/approval-authorizer.js
98e714ef8884479883e63861602e251b2bead1c788b418f88000062662990209  src/publish/authorize-approval.js
89327251626058674db523d8e541475bb9bf94cce2359adba06a85f99e359895  ...
gateway · 2026-09-10 15:57:31
result
M	.github/workflows/auto-approve.yml
M	.github/workflows/ci-poller.yml
M	.github/workflows/cocoapods-keepalive.yml
M	.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
M	src/modules/__tests__/details-from-context.js
M	src/modules/__tests__/process-end-state.js
M	src/modules/__tests__/publish-location.js
M	src/modules/__tests__/publish-workflow.js
M	src/modules/__tests__/release-revision.js
M	src/modules/__tests__/update-issue.js
M	src/modules/ci-poller-input.js
M	src/modules/details-from-context.js
M	src/modules/post-workflow-details.js
M	src/modules/process-end-state.js
M	src/modules/publish-location.js
M	src/modules/release-revision.js
M	src/modules/update-issue.js
M	src/publish/__tests__/discover-location.js
M	src/publish/__tests__/resolve-release-revision.js
M	s...
gateway · 2026-09-10 15:58:17
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 15:58:34
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 15:58:49
result
No files found
gateway · 2026-09-10 15:59:46
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 16:00:20
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 16:01:02
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 16:01:19
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 16:02:15
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 16:02:46
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 16:03:03
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 16:03:37
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 16:03:56
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 16:05:35
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 16:05:54
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 16:06:02
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 16:06:32
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 16:06:48
result
/home/byk/Code/getsentry/publish/src/modules/publish-issue-validation.js
/home/byk/Code/getsentry/publish/src/modules/publish-issue-title.peggy
/home/byk/Code/getsentry/publish/src/modules/publish-issue-title.js
gateway · 2026-09-10 16:06:58
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 16:07:12
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 16:07:19
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 16:07:39
result
<path>/home/byk/Code/getsentry/publish/src/modules/publish-issue-title.peggy</path>
<type>file</type>
<content>
1: {
2:   function join(characters) {
3:     return characters.join("");
4:   }
5: }
6: 
7: // BEGIN TITLE GRAMMAR
8: // Canonical grammar for publish issue titles. A path suffix is syntactic only:
9: // the controller resolves the complete suffix as a workspace after checking
10: // out the CI-approved revision.
11: PublishIssueTitle
12:   = "publish: " "getsentry/"? repo:Repository path:Path? "@" version:Version !. {
13:       return {
14:         repo,
15:         path: path || "",
16:         version,
17:       };
18:     }
19: 
20: Repository
21:   = characters:RepositoryCharacter+ { return join(characters); }
22: 
23: RepositoryCharacter
24:   = [A-Za-z0-9_.-]
25: 
26: Path
27:   = segments:("/" segment:PathSegment { return `/${segment}`; })+ { return join(segments); }
28: 
29: PathSegment
30:   = characters:RepositoryCharacter+ { return join(characters); }
31: 
32: ...
gateway · 2026-09-10 16:08:28
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 16:08:51
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 16:09:09
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 16:09:37
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 16:09:51
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 16:10:09
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 16:10:18
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 16:10:31
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 16:10:49
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 16:11:08
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 16:11:25
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 16:11:43
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 16:11:56
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 16:12:25
result
/home/byk/Code/getsentry/publish/src/publish/resolve-release-revision.js
gateway · 2026-09-10 16:12:39
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 16:12:58
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 16:14:12
result
diff --git a/.github/workflows/ci-poller.yml b/.github/workflows/ci-poller.yml
index 487e9da..746e187 100644
--- a/.github/workflows/ci-poller.yml
+++ b/.github/workflows/ci-poller.yml
@@ -1,324 +1,509 @@
 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:
     runs-on: ubuntu-latest
     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 16:14:22
result
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 2635ea6..0231505 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -1,318 +1,528 @@
 name: Publish
 on:
   issues:
     types: [labeled]
 
 concurrency:
   # Use the issue title (e.g. "publish: getsentry/foo@1.2.3") so duplicate
   # issues for the same repo@version share a concurrency group.
   group: ${{ github.event.issue.title }}
   cancel-in-progress: false
 
 permissions:
   contents: read
   issues: write
   packages: write
 
 jobs:
   # When accepted is added to a publish issue:
   #   - Add ci-pending (and remove ci-failed if retrying)
   #   - Enable the poller via CI_POLLER_HAS_PENDING=true
   #   - Comment on the issue
   #   - Trigger the poller immediately so we don't wait for the next cron tick
   # The publish job below requires ci-ready, so it will not fire until the
   # poller flips ci-pending → ci-ready (which also prevents publishing without
   # CI ...
gateway · 2026-09-10 16:15:04
result
<path>/home/byk/Code/getsentry/publish/.github/workflows</path>
<type>directory</type>
<entries>
auto-approve.yml
ci-poller-dispatch.yml
ci-poller.yml
cocoapods-keepalive.yml
publish.yml
test.yml

(6 entries)
</entries>
gateway · 2026-09-10 16:15:56
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 16:16:07
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 16:16:17
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 16:16:26
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 16:16:42
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 16:16:53
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 16:17:07
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 16:17:13
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 16:17:20
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 16:17:36
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 16:19:40
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 16:19:56
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 16:20:31
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 16:20:40
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 16:20:55
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 16:21:12
result
<path>/home/byk/Code/getsentry/publish/.lore.md</path>
<type>file</type>
<content>
241: <!-- lore:01a08b2d-aae8-771e-8c37-4b6a283ff28c -->
242: * **Use \`ci-ready\` as the sole publish trigger**: Assume the CI poller always adds \`ci-ready\`, and gate the publish job on \`ci-ready\` label events rather than \`accepted\`. Keep approval and CI-waiting flows separate so \`publish\` cannot race \`waiting-for-ci\`. Bind transitions to the current request and approval event, revalidate before adding or consuming \`ci-ready\`, and ensure renamed, changed, or re-approved releases cannot publish using stale authorization. On terminal outcomes, reliably remove active authorization labels (\`accepted\`, \`ci-pending\`, and \`ci-ready\`), closing the issue only on success. Protect these lifecycle rules with focused fail-first regression tests and mutation checks.
243: 
244: <!-- lore:01a08722-a96f-7f8c-b212-adbcce39fc05 -->
245: * **Use precise GitHub REST API documentation and preserve access ...
gateway · 2026-09-10 16:21:26
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 16:21:42
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 16:22:01
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 16:22:18
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
@@ -1,35 +1,57 @@
 name: auto-approve non-sdks
 on:
   issues:
     types: [opened]
 
 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: ...
gateway · 2026-09-10 16:22:59
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
@@ -1,17 +1,19 @@
 /**
  * This is a helper function that throws a useful error message if the
  * workflow environment is not configured correctly.
  *
  * @returns string
  */
 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;
 }
 
 module.exports = {
   getGitHubToken,
 };
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
@@ -1,42 +1,45 @@
 const { parse: parsePublishIssueTitle } = ...
gateway · 2026-09-10 16:23:34
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 16:23:53
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 16:24:25
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 16:24:37
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 16:24:46
result
File not found: /home/byk/Code/getsentry/publish/src/modules/__tests__/publish-issue-title.js
gateway · 2026-09-10 16:25:08
result
/home/byk/Code/getsentry/publish/src/modules/__tests__/generate-publish-issue-title-parser.js
gateway · 2026-09-10 16:25:20
result
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/generate-publish-issue-title-parser.js</path>
<type>file</type>
<content>
1: import { expect, test } from "vitest";
2: 
3: const {
4:   generateDocumentation,
5: } = require("../../../scripts/generate-publish-issue-title-parser.js");
6: 
7: test("requires generated title grammar markers in the documentation", () => {
8:   expect(() =>
9:     generateDocumentation({
10:       documentation: "# Publish Issue Format\n",
11:       titleGrammar: 'PublishIssueTitle = "publish: "',
12:     })
13:   ).toThrow("Could not find the generated title grammar in the documentation.");
14: });
15: 
16: test("rejects duplicate generated title grammar markers", () => {
17:   expect(() =>
18:     generateDocumentation({
19:       documentation: `<!-- BEGIN GENERATED TITLE GRAMMAR -->
20: old
21: <!-- END GENERATED TITLE GRAMMAR -->
22: <!-- BEGIN GENERATED TITLE GRAMMAR -->
23: old
24: <!-- END GENERATED TITLE GRAMMAR -->`,
25:       ...
gateway · 2026-09-10 16:25:50

Your prior response was empty and unusable. Retry the READ-ONLY review now. You MUST return non-empty substantive evidence or begin BLOCKED and name the exact tool/error that prevents review. Inspect the exact current worktree and full diff against origin/main. At minimum report current file:line evidence for: latest label/unlabel and malformed event-ID handling; request snapshot/attestation binding; special bot and auto-approval authorization; trusted workflow/ref and secret boundaries; branch-discovery fail-closed behavior; revision movement; ci-ready revalidation/add/recovery; pre-Craft fence and immutable dependencies; shell/input injection; and cleanup authorization. Classify every item PASS, CONCERN, or MUST-FIX. Do not edit or mutate anything. End exactly MERGE or DO-NOT-MERGE. An empty response is forbidden.

gateway · 2026-09-10 16:28:14

I’ll rerun the review against the exact worktree, inspect every changed and untracked file, run the required read-only checks, and verify the worktree fingerprint afterward. The required repo-setup skill is not available in this session, so I’m applying the checked-in AGENTS.md and package scripts directly.

gpt-5.6-sol · 2026-09-10 16:28:14 · ~66 tokens
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 16:28:43
result
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 src/modules/__tests__/publish-workflow.js
 M src/modules/__tests__/release-revision.js
MM src/modules/__tests__/update-issue.js
 M src/modules/ci-poller-input.js
M  src/modules/details-from-context.js
M  src/modules/post-workflow-details.js
MM src/modules/process-end-state.js
 M src/modules/publish-location.js
 M src/modules/release-revision.js
 M src/modules/update-issue.js
 M src/publish/__tests__/discover-location.js
 M ...
gateway · 2026-09-10 16:29:34
result
a4cccb7ec721c017ed38b75686bea932b0e14c4c9298c240a9db9bd1c043fa11  -
570dcb023ae08f4fb8338923496a55bf1c2e012a944f9969650bc31feadd485c  .github/workflows/ci-poller-dispatch.yml
7a8b139f81784011cc5b0d2f217ded95b981897e1fd4fce52c43774074dba988  .lore.md
9051b3a78e4ceb1ef079c599abf9550d03ba8527ca8bbf229631c38f65375b29  src/modules/approval-attestation.js
c9ca690c7ca173c6cdc973e511dadb5479ff10cd958f9b0c4f10f83017e6ec55  src/modules/approval-authorizer.js
11a45a2ff4688586212aef5605db4c31d8d5a1d34735f2a5b7c1bf6bcbdd2a9b  src/modules/__tests__/approval-attestation.js
c6f64660b2fa5339b7b7936608ce90455518d5445c0ce3ff0298f1035e48a407  src/modules/__tests__/approval-authorizer.js
98e714ef8884479883e63861602e251b2bead1c788b418f88000062662990209  src/publish/authorize-approval.js
89327251626058674db523d8e541475bb9bf94cce2359adba06a85f99e359895  src/publish/current-accepted-event.js
53638e4f38677b512e137138947cb0b69dc82cdaf5612a52a057fc4f2ac4eca3  ...
gateway · 2026-09-10 16:30:09

The worktree is large (34 tracked files plus 21 untracked files), with HEAD equal to origin/main. I recorded the initial patch hash as a4cccb7ec721c017ed38b75686bea932b0e14c4c9298c240a9db9bd1c043fa11; I’m now tracing the current implementation and tests file by file.

gpt-5.6-sol · 2026-09-10 16:30:09 · ~84 tokens
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 16:30:20
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 16:30:43
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 16:31:18
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 16:31:52
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 16:32:11
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 16:32:35
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 16:32:49
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 16:33:08
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 16:33:13
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 16:38:01
result
name: Publish
on:
  issues:
    types: [labeled]

concurrency:
  # Use the issue title (e.g. "publish: getsentry/foo@1.2.3") so duplicate
  # issues for the same repo@version share a concurrency group.
  group: ${{ github.event.issue.title }}
  cancel-in-progress: false

permissions:
  contents: read
  issues: write
  packages: write

jobs:
  # When accepted is added to a publish issue:
  #   - Add ci-pending (and remove ci-failed if retrying)
  #   - Enable the poller via CI_POLLER_HAS_PENDING=true
  #   - Comment on the issue
  #   - Trigger the poller immediately so we don't wait for the next cron tick
  # The publish job below requires ci-ready, so it will not fire until the
  # poller flips ci-pending → ci-ready (which also prevents publishing without
  # CI verification in the auto-approve race).
  waiting-for-ci:
    runs-on: ubuntu-latest
    name: Waiting for CI
    environment: production
    if: >-
      github.event.label.name == 'accepted'
      && ...
gateway · 2026-09-10 16:40:02
result
Found 52 matches
/home/byk/Code/getsentry/publish/.github/workflows/publish.yml:
  Line 29:     environment: production

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

  Line 45:           private-key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }}

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

  Line 59:           private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }}

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

  Line 170:           private-key: ${{ secrets.CI_POLLER_APP_PRIVATE_KEY }}

  Line 200:     environment: production

  Line 280:           GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

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

  Line 288:           private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }}

  Line 433:           DOCKER_GHCR_IO_PASSWORD: ${{ ...
gateway · 2026-09-10 16:40:07
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 285:         uses: ...
gateway · 2026-09-10 16:40:27
result
<path>/home/byk/Code/getsentry/publish/.github/workflows</path>
<type>directory</type>
<entries>
auto-approve.yml
ci-poller-dispatch.yml
ci-poller.yml
cocoapods-keepalive.yml
publish.yml
test.yml

(6 entries)
</entries>
gateway · 2026-09-10 16:40:50
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 16:40:57
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 16:41:04
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 16:41:28

I’ve confirmed the privileged workflows use the protected production environment and immutable action references. I’m now checking whether the code-level provenance gates and shell boundaries match those workflow claims, including every negative-path test.

gpt-5.6-sol · 2026-09-10 16:41:28 · ~43 tokens
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 16:41:34
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 16:41:52
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 16:42:25
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 16:42:35
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 16:42:46
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 16:43:02
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 16:43:14
result
<path>/home/byk/Code/getsentry/publish/src/modules/publish-issue-title.peggy</path>
<type>file</type>
<content>
1: {
2:   function join(characters) {
3:     return characters.join("");
4:   }
5: }
6: 
7: // BEGIN TITLE GRAMMAR
8: // Canonical grammar for publish issue titles. A path suffix is syntactic only:
9: // the controller resolves the complete suffix as a workspace after checking
10: // out the CI-approved revision.
11: PublishIssueTitle
12:   = "publish: " "getsentry/"? repo:Repository path:Path? "@" version:Version !. {
13:       return {
14:         repo,
15:         path: path || "",
16:         version,
17:       };
18:     }
19: 
20: Repository
21:   = characters:RepositoryCharacter+ { return join(characters); }
22: 
23: RepositoryCharacter
24:   = [A-Za-z0-9_.-]
25: 
26: Path
27:   = segments:("/" segment:PathSegment { return `/${segment}`; })+ { return join(segments); }
28: 
29: PathSegment
30:   = characters:RepositoryCharacter+ { return join(characters); }
31: 
32: ...
gateway · 2026-09-10 16:43:42
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 16:44:01
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 16:44:21
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 16:44:44
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 16:45:08
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 16:45:26
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 16:45:51
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 16:46:13
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 16:48:25
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 16:48:46
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 16:52:32
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 16:52:48
result
Found 100 matches (more matches available)
/home/byk/Code/getsentry/publish/src/publish/request-digest-from-event.js:
  Line 13:   if (!process.env.GITHUB_EVENT_PATH) {

  Line 17:   if (!process.env.GITHUB_OUTPUT) {

  Line 22:     fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8")

  Line 25:     process.env.GITHUB_OUTPUT,


/home/byk/Code/getsentry/publish/src/publish/update-issue.js:
  Line 10:   if (process.env.PUBLISH_ARGS) {

  Line 12:       inputs = JSON.parse(process.env.PUBLISH_ARGS);


/home/byk/Code/getsentry/publish/src/publish/post-result.js:
  Line 13:   if (process.env.PUBLISH_ARGS) {

  Line 15:       inputs = JSON.parse(process.env.PUBLISH_ARGS);


/home/byk/Code/getsentry/publish/src/publish/resolve-release-revision.js:
  Line 5:   const { repo } = JSON.parse(process.env.PUBLISH_ARGS || "{}");

  Line 13:       issueBody: process.env.PUBLISH_ISSUE_BODY || "",


/home/byk/Code/getsentry/publish/src/publish/resolve-location.js:
  Line 5:   const input = ...
gateway · 2026-09-10 16:52:58
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 16:53:20
result
Found 21 matches
/home/byk/Code/getsentry/publish/scripts/generate-publish-issue-title-parser.js:
  Line 65:   const generatedParser = prettier.format(`/* eslint-disable */\n${parser}`, {


/home/byk/Code/getsentry/publish/.lore.md:
  Line 73: * **publish Inform start gate**: Trap: leaving \`.github/workflows/publish.yml\`’s \`Inform start\` step unguarded looks reasonable because publication status should be announced, but transient workflow-run lookup or comment failures then block checkout and Craft. Fix: mark this informational step \`continue-on-error: true\`; authorization fences and publication remain mandatory, while comments and telemetry are best-effort.

  Line 94: * **Verify publish issue reconciliation**: Trap: one best-effort reconciliation before terminal reporting looks sufficient because it removes authorization labels promptly, but \`continue-on-error\` can silently leave stale approval state and later optional scripts may fail. ...
gateway · 2026-09-10 16:53:39
result
File not found: /home/byk/Code/getsentry/publish/.github/dependabot.yml
gateway · 2026-09-10 16:54:04
result
No files found
gateway · 2026-09-10 16:54:15
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 16:54:24
result
/home/byk/Code/getsentry/publish/docs/rfc.md
/home/byk/Code/getsentry/publish/docs/key_rotation_vault_changes.md
/home/byk/Code/getsentry/publish/docs/publish-issue-format.md
gateway · 2026-09-10 16:54:45
result
<path>/home/byk/Code/getsentry/publish/docs/publish-issue-format.md</path>
<type>file</type>
<content>
1: # Publish Issue Format
2: 
3: The Publish workflow treats a publish issue as a release request. The title identifies
4: the release; the body supplies the merge target and selected targets. Do not edit the
5: title by hand unless it remains valid under this format.
6: 
7: ## Title
8: 
9: Every title starts with `publish: `. The following canonical Peggy grammar is
10: generated from `src/modules/publish-issue-title.peggy`:
11: 
12: <!-- BEGIN GENERATED TITLE GRAMMAR -->
13: ```peggy
14: // Canonical grammar for publish issue titles. A path suffix is syntactic only:
15: // the controller resolves the complete suffix as a workspace after checking
16: // out the CI-approved revision.
17: PublishIssueTitle
18:   = "publish: " "getsentry/"? repo:Repository path:Path? "@" version:Version !. ...
gateway · 2026-09-10 16:54:58
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 16:55:11
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 16:55:28
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 16:55:49
result
Found 8 matches
/home/byk/Code/getsentry/publish/src/modules/approval-attestation.js:
  Line 44: function createAutoApprovalAttestation({ autoApprover, issue }) {

  Line 46:     autoApprover,

  Line 107:     typeof value?.autoApprover !== "string" ||

  Line 209: function hasAutoApprovalAttestation({

  Line 210:   autoApprover,

  Line 225:       attestation?.autoApprover === autoApprover &&

  Line 261:   createAutoApprovalAttestation,

  Line 267:   hasAutoApprovalAttestation,
gateway · 2026-09-10 16:56:08
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 16:56:35
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 16:57:54

Distillations (5)

Gen 0 2026-09-10 16:47:54 · 1757 tokens

🔴 (16:14) `/home/byk/Code/getsentry/publish/auto-approve-repos.txt` contains exactly 46 entries, in order: 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. `gets…

Gen 0 2026-09-10 16:26:48 · 6911 tokens

🔴 (16:05) `/home/byk/Code/getsentry/publish/src/publish/validate-approval-attestation.js` is a 158-line CommonJS executable module importing `currentAcceptedEvent`, `currentCiReadyEvent`, `hasApprovalAttestation`, `hasCiReadyAttestation`, and `requestDigest` from `../modules/approval-attestation.js`. 🔴 (16:05) `src/publish/validate-approval-attestation.js:9-17` defines `getGitHubResponse(path)`…

Gen 0 2026-09-10 16:21:09 · 3059 tokens

🔴 (16:02) `/home/byk/Code/getsentry/publish/src/modules/approval-attestation.js` is a 273-line CommonJS module using Node’s `crypto`. 🔴 (16:02) `src/modules/approval-attestation.js:3-6` defines attestation framing constants: `APPROVAL_ATTESTATION_PREFIX = "<!-- publish-approval "`, `AUTO_APPROVAL_ATTESTATION_PREFIX = "<!-- publish-auto-approval "`, `CI_READY_ATTESTATION_PREFIX = "<!-- publish-c…

Gen 0 2026-09-10 16:19:24 · 758 tokens

🔴 (16:02) User stated the CI poller “always adds ci-ready.” 🔴 (16:02) `.github/workflows/publish.yml:10` sets `cancel-in-progress: false`. 🔴 (16:02) `.github/workflows/publish.yml:200` uses the `production` environment for the shown job. 🔴 (16:02) `.github/workflows/publish.yml:204` documents coordination with `waiting-for-ci` on the same event and states that the poller always adds `ci-ready…

Gen 0 2026-09-10 16:12:18 · 3717 tokens

🔴 (15:56) [requested-review] User requested a substantive, independent adversarial correctness and security review of the exact current worktree at `/home/byk/Code/getsentry/publish`, covering the full integrated diff against `origin/main`, including every changed/untracked source, test, workflow, and relevant documentation file. 🔴 (15:56) User required the review to be READ ONLY: do not edit f…