DashboardpublishSession 0CXICskeZwhc

Session 0CXICskeZwhc

Full ID: 0CXICskeZwhc2Ps88
Messages: 172
Distillations: 7
Time range: 2026-09-10 15:56:45 — 2026-09-10 17:22:59

Conversation (172 messages)

Perform a substantive independent workflow and release-lifecycle correctness 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, inspect every changed/untracked file relevant to behavior, and trace all paths end to end: issue opened/accepted, waiting-for-CI activation, manual relay and retries, per-issue poller processing, malformed/transient API failures, CI failure, revision changes, ci-ready handoff including add failure, publish setup failures, Craft success/failure/cancellation, issue close/deauthorization, target restoration, terminal reporting, and cleanup retries. Verify comments/telemetry/dependencies never gate critical state transitions; verify retries cannot strand or incorrectly advance releases; verify updateIssue handles already-absent labels without hiding target restoration errors; inspect tests for false positives and missing adversarial states. The exact tree has passed generated verification, 192 tests, lint, whitespace checks, and 10 focused stability runs, but review implementation independently. Return evidence with current file:line references and classify each point PASS, CONCERN, or MUST-FIX. If blocked, begin BLOCKED and state the exact blocker. End exactly MERGE or DO-NOT-MERGE. Never return empty output.

gateway · 2026-09-10 15:56:45

The required repo-setup skill is not available in this session, so I will apply the repository instructions directly. I’ll fingerprint the worktree, inspect the complete origin/main...worktree delta and all untracked files, trace the lifecycle and tests, run only read-only checks, then verify the fingerprint again.

gpt-5.6-sol · 2026-09-10 15:56:45 · ~65 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 15:57:14
result
7c60ddb7f43040fe8fbfea70efc833f689c04e75
7c60ddb7f43040fe8fbfea70efc833f689c04e75
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 ...
gateway · 2026-09-10 15:57:38
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:57: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 15:58:22
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 15:58:39
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 15:59:01
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 15:59:06
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 15:59:28
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 15:59:50
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:00:12
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:00:32
result
/home/byk/Code/getsentry/publish/src/publish/request-digest-from-event.js
/home/byk/Code/getsentry/publish/src/publish/update-issue.js
/home/byk/Code/getsentry/publish/src/publish/post-workflow-details.js
/home/byk/Code/getsentry/publish/src/publish/post-result.js
/home/byk/Code/getsentry/publish/src/publish/inputs.js
/home/byk/Code/getsentry/publish/src/publish/resolve-release-revision.js
/home/byk/Code/getsentry/publish/src/publish/resolve-location.js
/home/byk/Code/getsentry/publish/src/publish/resolve-ci-poller-input.js
/home/byk/Code/getsentry/publish/src/publish/discover-location.js
/home/byk/Code/getsentry/publish/src/publish/record-auto-approval-attestation.js
/home/byk/Code/getsentry/publish/src/publish/current-accepted-event.js
/home/byk/Code/getsentry/publish/src/publish/record-ci-ready-attestation.js
/home/byk/Code/getsentry/publish/src/publish/validate-approval-attestation.js
/home/byk/Code/getsentry/publish/src/publish/__tests__/post-result.js
/home/byk/Code/getsentry/pub...
gateway · 2026-09-10 16:00:37
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:00:51
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:01:11
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:01:33
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:01:38
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:01:46
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:02:04
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:02:20
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:02:34
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:02: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:03:07
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:03:20
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:03:33
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:03:41
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:04:00
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:04:19
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:04:32
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:04:47
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:04:54
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:05:10
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:05:29
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:05:54
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:06:04
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:06:24
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:06:43
result
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/approval-authorizer.js</path>
<type>file</type>
<content>
1: import { describe, expect, test, vi } from "vitest";
2: 
3: const { authorizeApproval } = require("../approval-authorizer.js");
4: 
5: const issueTitle = "publish: getsentry/sentry-javascript@10.0.0";
6: 
7: describe("authorizeApproval", () => {
8:   test.each(["getsantry[bot]", "sentry-release-bot[bot]"])(
9:     "rejects direct approval by %s without a target repository lookup",
10:     async (actor) => {
11:       const getPermission = vi.fn();
12: 
13:       await expect(
14:         authorizeApproval({
15:           actor,
16:           issueTitle,
17:           getPermission,
18:           autoApprovedRepositories: new Set(["getsentry/sentry-javascript"]),
19:         })
20:       ).resolves.toEqual({
21:         authorized: false,
22:         repository: "getsentry/sentry-javascript",
23:       });
24: 
25:       ...
gateway · 2026-09-10 16:06:48
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:07:00
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:07:24
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:07:38
result
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/process-end-state.js</path>
<type>file</type>
<content>
1: import { vi, describe, test, expect } from "vitest";
2: 
3: vi.mock("fs");
4: 
5: const processEndState = require("../process-end-state.js");
6: 
7: describe("publish failed", () => {
8:   const failureArgs = {
9:     status: "failure",
10:     inputs: { repo: "sentry", version: "21.3.1" },
11:     context: {
12:       runId: "1234",
13:       repo: { owner: "getsentry", repo: "publish" },
14:       payload: { issue: { number: "211" } },
15:     },
16:     octokit: {
17:       rest: {
18:         actions: {
19:           getWorkflowRun: async () => ({
20:             data: {
21:               html_url: "https://github.com/getsentry/sentry/actions/runs/1234",
22:             },
23:           }),
24:         },
25:         issues: {
26:           createComment: vi.fn(),
27:         },
28:       },
29:     },
30:     Sentry: {
31:       Scope: class Scope {
32:         ...
gateway · 2026-09-10 16:07:44
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:07:57
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:08:05
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:08:19
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:08:39
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:09:00
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:09:14
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:09:30
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:09:44
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:10:02
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:10:25
result
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/auto-approval-workflow.js</path>
<type>file</type>
<content>
1: import { readFileSync } from "fs";
2: import { describe, expect, test } from "vitest";
3: 
4: const workflow = readFileSync(".github/workflows/auto-approve.yml", "utf8");
5: 
6: describe("auto-approval workflow", () => {
7:   test("checks out the code used to record the attestation", () => {
8:     const checkout = workflow.slice(
9:       workflow.indexOf("uses: actions/checkout@"),
10:       workflow.indexOf("Record automated approval attestation")
11:     );
12: 
13:     expect(checkout).not.toContain("sparse-checkout:");
14:   });
15: 
16:   test("records a request-bound attestation before adding accepted", () => {
17:     expect(workflow).toContain("node src/publish/request-digest-from-event.js");
18:     expect(workflow).toContain(
19:       "EXPECTED_REQUEST_DIGEST: ${{ steps.request-digest.outputs.request_digest }}"
20:     );
21:     ...
gateway · 2026-09-10 16:10:52
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:11:11
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:11:37
result
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/post-result.js</path>
<type>file</type>
<content>
1: import { afterEach, describe, expect, test, vi } from "vitest";
2: 
3: const { main } = require("../post-result.js");
4: 
5: const originalEnvironment = { ...process.env };
6: 
7: afterEach(() => {
8:   process.env = { ...originalEnvironment };
9: });
10: 
11: describe("post-result entry point", () => {
12:   test.each([undefined, "not-json"])(
13:     "reports terminal state when publish inputs are %s",
14:     async (publishArgs) => {
15:       const report = vi.fn().mockResolvedValue(undefined);
16:       if (publishArgs === undefined) {
17:         delete process.env.PUBLISH_ARGS;
18:       } else {
19:         process.env.PUBLISH_ARGS = publishArgs;
20:       }
21: 
22:       await main({
23:         context: {},
24:         octokit: {},
25:         report,
26:         status: "failure",
27:       });
28: 
29:       expect(report).toHaveBeenCalledWith(
30:         ...
gateway · 2026-09-10 16:12:00
result
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/discover-location.js</path>
<type>file</type>
<content>
1: import { afterEach, expect, test, vi } from "vitest";
2: 
3: const {
4:   discoverLocation,
5:   getWorkspaceNames,
6: } = require("../discover-location.js");
7: 
8: afterEach(() => {
9:   vi.restoreAllMocks();
10: });
11: 
12: test("retains checkout-path behavior when the root config is absent", () => {
13:   expect(
14:     discoverLocation({
15:       input: { path: "./packages/cli" },
16:       repositoryDirectory: "__repo__",
17:       exists: () => false,
18:     })
19:   ).toEqual({ path: "./packages/cli" });
20: });
21: 
22: test("discovers exact workspace paths using the released Craft image", () => {
23:   const execFile = vi.fn(() => '["packages/CLI"]');
24: 
25:   expect(
26:     discoverLocation({
27:       input: { path: "./packages/CLI" },
28:       repositoryDirectory: "__repo__",
29:       exists: () => true,
30:       execFile,
31:     })
32:   ...
gateway · 2026-09-10 16:12:18
result
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/resolve-release-revision.js</path>
<type>file</type>
<content>
1: import { spawnSync } from "child_process";
2: import { expect, test } from "vitest";
3: 
4: const script = new URL("../resolve-release-revision.js", import.meta.url)
5:   .pathname;
6: 
7: test("rejects missing workflow input through release revision validation", () => {
8:   const result = spawnSync(process.execPath, [script], {
9:     encoding: "utf8",
10:     env: {
11:       ...process.env,
12:       PUBLISH_ARGS: "",
13:       PUBLISH_ISSUE_BODY: "",
14:     },
15:   });
16: 
17:   expect(result.status).not.toBe(0);
18:   expect(result.stderr).toContain("Publish input must define a repository.");
19:   expect(result.stderr).not.toContain("Unexpected end of JSON input");
20: });

(End of file - total 20 lines)
</content>
gateway · 2026-09-10 16:12:42
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:13:01
result
Found 5 matches
/home/byk/Code/getsentry/publish/src/publish/__tests__/post-result.js:
  Line 3: const { main } = require("../post-result.js");

  Line 11: describe("post-result entry point", () => {

  Line 13:     "reports terminal state when publish inputs are %s",

  Line 30:         expect.objectContaining({ inputs: {}, status: "failure" })


/home/byk/Code/getsentry/publish/src/publish/__tests__/publish-workflow.js:
  Line 121:       'node .__publish__/src/publish/post-result.js "$status"'
gateway · 2026-09-10 16:13:17
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:13:28
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:13:43
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:14:08
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:15
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:14:25
result
<path>/home/byk/Code/getsentry/publish/src/modules/publish-issue-title.js</path>
<type>file</type>
<content>
1: /* eslint-disable */
2: // @generated by Peggy 5.1.0.
3: //
4: // https://peggyjs.org/
5: 
6: "use strict";
7: 
8: class peg$SyntaxError extends SyntaxError {
9:   constructor(message, expected, found, location) {
10:     super(message);
11:     this.expected = expected;
12:     this.found = found;
13:     this.location = location;
14:     this.name = "SyntaxError";
15:   }
16: 
17:   format(sources) {
18:     let str = "Error: " + this.message;
19:     if (this.location) {
20:       let src = null;
21:       const st = sources.find((s) => s.source === this.location.source);
22:       if (st) {
23:         src = st.text.split(/\r\n|\n|\r/g);
24:       }
25:       const s = this.location.start;
26:       const offset_s =
27:         this.location.source &&
28:         typeof this.location.source.offset === "function"
29:           ? this.location.source.offset(s)
30:          ...
gateway · 2026-09-10 16:14:34
result
/home/byk/Code/getsentry/publish/scripts/generate-publish-issue-title-parser.js
gateway · 2026-09-10 16:14:45
result
<path>/home/byk/Code/getsentry/publish/scripts/generate-publish-issue-title-parser.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const path = require("path");
3: const peggy = require("peggy");
4: const prettier = require("prettier");
5: 
6: const grammarPath = path.join(
7:   __dirname,
8:   "..",
9:   "src",
10:   "modules",
11:   "publish-issue-title.peggy"
12: );
13: const outputPath = path.join(
14:   __dirname,
15:   "..",
16:   "src",
17:   "modules",
18:   "publish-issue-title.js"
19: );
20: const documentationPath = path.join(
21:   __dirname,
22:   "..",
23:   "docs",
24:   "publish-issue-format.md"
25: );
26: function generateDocumentation({ documentation, titleGrammar }) {
27:   const markers = documentation.match(
28:     /<!--\s*(?:BEGIN|END)\s+GENERATED\s+TITLE\s+GRAMMAR\b/g
29:   );
30: 
31:   if (
32:     markers?.filter(marker => marker.includes("BEGIN")).length !== 1 ||
33:     markers?.filter(marker => marker.includes("END")).length !== ...
gateway · 2026-09-10 16:14:56
result
...output truncated...

Full output saved to: /home/byk/.local/share/opencode/tool-output/tool_08c19e463001lhiSLC9e0jveH9

+        run: gh issue comment "$ISSUE_URL" --body "$AUTO_APPROVAL_ATTESTATION"
       - name: Auto-approve if repo is in the list
+        if: steps.attestation.outcome == 'success'
         env:
           GH_TOKEN: ${{ steps.token.outputs.token }}
           ISSUE_TITLE: ${{ github.event.issue.title }}
           ISSUE_URL: ${{ github.event.issue.html_url }}
         run: |
           REPO=$(echo "$ISSUE_TITLE" | sed -n 's/^publish: \(.*\)@.*/\1/p')
           if [ -n "$REPO" ] && grep -qxF "$REPO" auto-approve-repos.txt; then
             gh issue edit "$ISSUE_URL" --add-label accepted
           fi
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,75 +1,97 @@
 name: CI Status Poller
 
 on:
   schedule:
     - cron: "*/5 ...
gateway · 2026-09-10 16:16:39
result
Found 2 matches
/home/byk/Code/getsentry/publish/src/publish/__tests__/authorize-approval.js:
  Line 270:     const error = vi.spyOn(console, "error").mockImplementation(() => {});


/home/byk/Code/getsentry/publish/src/modules/process-end-state.js:
  Line 103:         severity: "error",
gateway · 2026-09-10 16:20:04
result
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/post-workflow-details.js</path>
<type>file</type>
<content>
1: import { vi, describe, test, expect } from "vitest";
2: 
3: vi.mock("fs");
4: 
5: const postWorkflowDetails = require("../post-workflow-details.js");
6: 
7: describe("postWorkflowDetails", () => {
8:   test("create comment with workflow details", async () => {
9:     const args = {
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:
21:                   "https://github.com/getsentry/sentry/actions/runs/1234",
22:               },
23:             }),
24:           },
25:           issues: {
26:             createComment: vi.fn(),
27:           },
28:         },
29:       },
30:    ...
gateway · 2026-09-10 16:20:29
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:20:45
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:03
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:46
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:22:05
result
Found 13 matches
/home/byk/Code/getsentry/publish/src/publish/__tests__/publish-workflow.js:
  Line 107:   test("reports exactly one terminal status from the Craft outcome", () => {


/home/byk/Code/getsentry/publish/src/modules/release-revision.js:
  Line 8:   if (parse(issueBody, { startRule: "CheckRunsLinkCount" }) !== 1) {

  Line 10:       `Expected exactly one View check runs link in Quick links for getsentry/${repo}.`


/home/byk/Code/getsentry/publish/src/modules/publish-issue-title.js:
  Line 200:     CheckRunsLinkCount: peg$parseCheckRunsLinkCount,

  Line 922:                                   s23 = peg$parseCheckRunsRevision();

  Line 1046:   function peg$parseCheckRunsRevision() {

  Line 1105:   function peg$parseCheckRunsLinkCount() {

  Line 1110:     s2 = peg$parseCheckRunsLink();

  Line 1130:       s2 = peg$parseCheckRunsLink();

  Line 1178:   function peg$parseCheckRunsLink() {

  Line 1470:   StartRules: ["PublishIssueTitle", "ReleaseRevision", ...
gateway · 2026-09-10 16:22:31
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:22:49
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:23:12
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:24:13

I’m resuming the read-only review from the exact worktree. I’ll re-fingerprint it, inspect the remaining implementation and workflow paths, run the required checks, then verify that the tree did not change. The required repo-setup skill is not available in this environment, so I’m applying the checked-in AGENTS.md and package.json rules directly.

gpt-5.6-sol · 2026-09-10 16:24:13 · ~77 tokens
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:24:51
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:25:11
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:25:58
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:27:42
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:27:48
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:27:55
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:28:08
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:28:24
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:28:32
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:28: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:28:46
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:29:10
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:29:26
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:29:39
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:29:57
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:30:23
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:30:49
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:31:12
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:31:35
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:31:42
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:32:04
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:32:28
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:32:53
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:36:58
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:37:12
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:37:24
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:39: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:39:25
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:39:31
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:39:39
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:42:29
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:42:52
result
Found 24 matches
/home/byk/Code/getsentry/publish/src/modules/__tests__/update-issue.js:
  Line 8: const updateTargetsArgs = {

  Line 27:         removeLabel: vi.fn(),

  Line 45:   process.env.CRAFT_STATE_FILE_PATH =

  Line 52:   updateTargetsArgs.octokit.rest.issues.get.mockReturnValue({

  Line 78:     await updateIssue(updateTargetsArgs);

  Line 88:       expect(updateTargetsArgs.octokit.rest.issues.get).toHaveBeenCalledTimes(

  Line 91:       expect(updateTargetsArgs.octokit.rest.issues.get.mock.calls[0]).toEqual([

  Line 100:         updateTargetsArgs.octokit.rest.issues.update

  Line 104:         updateTargetsArgs.octokit.rest.issues.update.mock.calls[0][0];

  Line 117:         updateTargetsArgs.octokit.rest.issues.update

  Line 123:     const removeLabel = updateTargetsArgs.octokit.rest.issues.removeLabel;

  Line 124:     expect(removeLabel).toHaveBeenCalledTimes(1);

  Line 125:     expect(removeLabel).toHaveBeenCalledWith({

  Line 135:   delete ...
gateway · 2026-09-10 16:43:08
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:43:19
result
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/process-end-state.js</path>
<type>file</type>
<content>
1: import { vi, describe, test, expect } from "vitest";
2: 
3: vi.mock("fs");
4: 
5: const processEndState = require("../process-end-state.js");
6: 
7: describe("publish failed", () => {
8:   const failureArgs = {
9:     status: "failure",
10:     inputs: { repo: "sentry", version: "21.3.1" },
11:     context: {
12:       runId: "1234",
13:       repo: { owner: "getsentry", repo: "publish" },
14:       payload: { issue: { number: "211" } },
15:     },
16:     octokit: {
17:       rest: {
18:         actions: {
19:           getWorkflowRun: async () => ({
20:             data: {
21:               html_url: "https://github.com/getsentry/sentry/actions/runs/1234",
22:             },
23:           }),
24:         },
25:         issues: {
26:           createComment: vi.fn(),
27:         },
28:       },
29:     },
30:     Sentry: {
31:       Scope: class Scope {
32:         ...
gateway · 2026-09-10 16:43:26
result
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/post-result.js</path>
<type>file</type>
<content>
1: import { afterEach, describe, expect, test, vi } from "vitest";
2: 
3: const { main } = require("../post-result.js");
4: 
5: const originalEnvironment = { ...process.env };
6: 
7: afterEach(() => {
8:   process.env = { ...originalEnvironment };
9: });
10: 
11: describe("post-result entry point", () => {
12:   test.each([undefined, "not-json"])(
13:     "reports terminal state when publish inputs are %s",
14:     async (publishArgs) => {
15:       const report = vi.fn().mockResolvedValue(undefined);
16:       if (publishArgs === undefined) {
17:         delete process.env.PUBLISH_ARGS;
18:       } else {
19:         process.env.PUBLISH_ARGS = publishArgs;
20:       }
21: 
22:       await main({
23:         context: {},
24:         octokit: {},
25:         report,
26:         status: "failure",
27:       });
28: 
29:       expect(report).toHaveBeenCalledWith(
30:         ...
gateway · 2026-09-10 16:43:44
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:44:09
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:44:35
result
 .github/workflows/auto-approve.yml                |  30 ++-
 .github/workflows/ci-poller.yml                   | 267 +++++++++++++++++----
 .github/workflows/cocoapods-keepalive.yml         |   1 -
 .github/workflows/publish.yml                     | 272 +++++++++++++++++++---
 .github/workflows/test.yml                        |   6 +-
 AGENTS.md                                         |   6 +
 README.md                                         |   4 +-
 docs/key_rotation_vault_changes.md                |  40 ++--
 docs/rfc.md                                       |   9 +-
 src/libs/__tests__/github.js                      |   2 +-
 src/libs/github.js                                |   6 +-
 src/modules/__tests__/ci-poller-input.js          |  22 +-
 src/modules/__tests__/ci-poller-workflow.js       | 252 +++++++++++++++++---
 src/modules/__tests__/details-from-context.js     |  22 +-
 src/modules/__tests__/process-end-state.js        |  27 +++
 ...
gateway · 2026-09-10 16:45:00
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:45:19
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:45:41
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:46:07
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:46:27
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:46:43
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:47:00
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:47:18
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:51:16

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 integrated diff against origin/main. Trace and cite current file:line evidence for every lifecycle: accepted/waiting-for-CI activation; manual recovery and retries; malformed/transient poller inputs; branch/check-suite lookup failures; revision movement; every approval rejection exit; stale ci-ready removal, final revalidation, unconditional add, and failed-add recovery; publish setup/Craft success/failure/cancellation; dependency-free deauthorization and issue closure; already-absent accepted-label handling with target restoration; informational/terminal reporting isolation; and workspace/state restoration behavior. Identify false-positive tests or missing adversarial states. Classify every point PASS, CONCERN, or MUST-FIX. Do not edit, format, generate, stage, or otherwise mutate files. End exactly MERGE or DO-NOT-MERGE. Empty output is forbidden.

gateway · 2026-09-10 16:53:03

I’m restarting the review from a fresh worktree fingerprint. I will keep the tree unchanged, inspect every changed and untracked file, run only read-only checks, then verify the fingerprint again. The required repo-setup skill is not available in this session, so I’m applying the checked-in repository instructions directly.

gpt-5.6-sol · 2026-09-10 16:53:03 · ~64 tokens
result
7c60ddb7f43040fe8fbfea70efc833f689c04e75
7c60ddb7f43040fe8fbfea70efc833f689c04e75
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 ...
gateway · 2026-09-10 16:53:32
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:53:57
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:54:38
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:55:05
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:21
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:55:43
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:56:11
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:56:27
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:56:39
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:56:56
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:59:26
result
gh version 2.100.0 (2026-09-03)
https://github.com/cli/cli/releases/tag/v2.100.0
/usr/bin/gh
gateway · 2026-09-10 16:59: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 17:00:19
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 17:00:41
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 17:01:11

The server restarted while your read-only review was active. Resume from your existing context and complete the exact-tree lifecycle review now. Do not edit or mutate files. Return the required non-empty, path-by-path PASS/CONCERN/MUST-FIX report with current file:line evidence, then end exactly MERGE or DO-NOT-MERGE. If blocked, begin BLOCKED and quote the exact blocker.

gateway · 2026-09-10 17:03:28

I’m resuming against a new read-only fingerprint because the restart may have changed the worktree snapshot. I will reuse the prior inspection, verify every current path that changed, run only read-only validation, and compare the final fingerprint.

gpt-5.6-sol · 2026-09-10 17:03:28 · ~47 tokens
result
7c60ddb7f43040fe8fbfea70efc833f689c04e75
7c60ddb7f43040fe8fbfea70efc833f689c04e75
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 ...
gateway · 2026-09-10 17:05:38
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 17:05:55
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 17:08:37
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 17:08:52
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 17:09:07
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 17:09:22
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 17:12:24
result
Found 1 matches
/home/byk/Code/getsentry/publish/src/modules/__tests__/publish-workflow.js:
  Line 120:     "!contains(github.event.issue.labels.*.name, 'ci-failed')"
gateway · 2026-09-10 17:14:54
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 17:14:58
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 17:15:08
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 17:15:25
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 17:15:42
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 17:16:57
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 17:17:13
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 17:20:38
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 17:20:55
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 17:21:01
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 17:21:14
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 17:21:30
result
Found 100 matches (more matches available)
/home/byk/Code/getsentry/publish/src/publish/__tests__/post-result.js:
  Line 13:     "reports terminal state when publish inputs are %s",


/home/byk/Code/getsentry/publish/src/publish/__tests__/request-digest-from-event.js:
  Line 3: const { requestDigest } = require("../../modules/approval-attestation.js");

  Line 4: const { requestDigestFromEvent } = require("../request-digest-from-event.js");

  Line 6: describe("requestDigestFromEvent", () => {

  Line 7:   test("digests the exact issue event snapshot", () => {

  Line 9:       body: "Merge target: main",

  Line 10:       labels: [{ name: "accepted" }, { name: "dry-run" }],

  Line 11:       title: "publish: getsentry/relay@1.2.3",

  Line 14:     expect(requestDigestFromEvent({ issue })).toBe(requestDigest(issue));

  Line 17:   test("rejects events without a complete issue snapshot", () => {

  Line 18:     expect(() => requestDigestFromEvent({})).toThrow(

  Line 19:       "The ...
gateway · 2026-09-10 17:21:36
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 17:21:45
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 17:22:08

I’m continuing the read-only review from the frozen worktree. I’ll finish the end-to-end trace, run the read-only checks, then compare the final fingerprint with the initial one.

gpt-5.6-sol · 2026-09-10 17:22:08 · ~38 tokens
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 17:22:33
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 17:22:59

Distillations (7)

Gen 0 2026-09-10 17:16:02 · 460 tokens

* 🔴 (16:16) User directed that `workflow_dispatch` must always be allowed for manual recovery. * 🔴 (16:16) User directed that manually dispatched workflows must always run trusted code because `workflow_dispatch` can target any ref. * 🔴 (16:16) User stated the CI poller always adds the `"ci-ready"` label. * 🔴 (16:16) User directed that a release must never be moved to `"ci-ready"` after its r…

Gen 0 2026-09-10 17:06:02 · 3888 tokens

Date: Sep 10, 2026 * 🔴 (15:59) User provided `/home/byk/Code/getsentry/publish/.github/workflows`, containing exactly 6 workflows: `auto-approve.yml`, `ci-poller-dispatch.yml`, `ci-poller.yml`, `cocoapods-keepalive.yml`, `publish.yml`, and `test.yml`. * 🔴 (16:00) User provided `/home/byk/Code/getsentry/publish/.github/workflows/test.yml`; it runs on pushes to `main` and pull requests with `cont…

Gen 0 2026-09-10 16:53:13 · 2923 tokens

Date: Sep 10, 2026 * 🔴 (16:11) User provided `/home/byk/Code/getsentry/publish/src/publish/__tests__/publish-workflow.js`; it requires `node src/publish/request-digest-from-event.js` and exactly 4 occurrences of `EXPECTED_REQUEST_DIGEST: ${{ steps.request-digest.outputs.request_digest }}` to bind approval/publication to the triggering issue snapshot. * 🔴 (16:11) Publish workflow tests require `…

Gen 0 2026-09-10 16:51:10 · 2806 tokens

Date: Sep 10, 2026 * 🔴 (16:08) User provided `/home/byk/Code/getsentry/publish/src/modules/__tests__/update-issue.js`, which tests `updateIssue()` and `transformIssueBody()`. * 🔴 (16:08) `updateIssue()` tests use `GITHUB_WORKSPACE="."` and secure workflow output `CRAFT_STATE_FILE_PATH=".craft-state/craft/publish-state-getsentry-sentry-c232c383e26f-21.3.1.json"`; when that exact file exists, its…

Gen 0 2026-09-10 16:37:06 · 2606 tokens

Date: Sep 10, 2026 * 🔴 (16:05) User provided `/home/byk/Code/getsentry/publish/src/publish/post-result.js`: `main()` injects `context = github.context`, `octokit = github.getOctokit(getGitHubToken())`, `report = processEndState`, and `status = process.argv.slice(2)[0]`; parses `process.env.PUBLISH_ARGS` as JSON, warns `"Could not parse publish inputs; reporting without them"` on failure, calls `…

Gen 0 2026-09-10 16:18:57 · 24 tokens

* 🔴 (15:59) User stated that the CI poller always adds the `ci-ready` label.

Gen 0 2026-09-10 16:11:02 · 2803 tokens

Date: Sep 10, 2026 * 🔴 [requested-review] (15:56) User requested a substantive, independent workflow and release-lifecycle correctness review of the exact current worktree at `/home/byk/Code/getsentry/publish`. * 🔴 [enforced-read-only] (15:56) User required the review to be READ ONLY: do not edit files, run formatters, or mutate the tree. * 🔴 (15:56) User required review of the full integrated…