DashboardpublishSession 09qLCMk0akEn

Session 09qLCMk0akEn

Full ID: 09qLCMk0akEnvJ2yV
Messages: 77
Distillations: 0
Time range: 2026-09-10 01:25:54 — 2026-09-10 01:48:44

Conversation (77 messages)

Independently review the exact current worktree at /home/byk/Code/getsentry/publish in READ-ONLY mode. Do not edit or format anything. A previous reviewer found and we fixed: (1) auto-approve sparse checkout omitted the attestation script/dependencies; it now performs a full checkout, (2) retry regex omitted attempts 1-9; it now accepts canonical 0-59, and (3) self-approval/title/reapproval tests short-circuited on missing open issue state; fixtures now reach intended guards and each test was verified to fail when that guard was removed. Inspect every changed/untracked file and security boundaries: automated proof provenance and proofless/direct-bot rejection, self-approval and role policy, live issue/request/event binding, mutation and reapproval denial, deliberate ci-ready re-add, final pre-Craft fence, cleanup without parsed inputs, trusted manual relay and dispatch parsing/arithmetic, immutable Actions/Craft. Organization-wide App-secret access is accepted pre-existing scope and is not a blocker unless this diff widens it. Run yarn test --printConsoleTrace, yarn lint, and git diff --check; hash-verify no tree changes. You MUST return non-empty output. Findings first, ordered by severity, each labeled MUST-FIX, CONCERN, or PASS with exact current file:line citations. Include concise command/integrity evidence. End on its own line exactly MERGE or DO-NOT-MERGE. If any tool prevents review, begin BLOCKED, state exact tool/error, and end DO-NOT-MERGE.

gateway · 2026-09-10 01:25:54

I will freeze the current worktree identity, enumerate every changed and untracked file, then trace each release-security boundary against its tests. I will run the three required checks and compare the final tree fingerprint to the initial one without modifying files.

gpt-5.6-sol · 2026-09-10 01:25:54 · ~48 tokens
result
[
  {
    "content": "Capture initial repository state and worktree fingerprint",
    "status": "in_progress",
    "priority": "high"
  },
  {
    "content": "Inspect every changed and untracked file plus relevant security tests",
    "status": "pending",
    "priority": "high"
  },
  {
    "content": "Run required test, lint, and diff checks",
    "status": "pending",
    "priority": "high"
  },
  {
    "content": "Recompute fingerprint and produce evidence-based merge verdict",
    "status": "pending",
    "priority": "high"
  }
]
gateway · 2026-09-10 01:26:11
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
src/
vitest.config.js
yarn.lock

(15 entries)
</entries>
gateway · 2026-09-10 01:26:37
result
## main...origin/main [behind 1]
 M .github/workflows/auto-approve.yml
 M .github/workflows/ci-poller.yml
 M .github/workflows/cocoapods-keepalive.yml
 M .github/workflows/publish.yml
 M .github/workflows/test.yml
 M AGENTS.md
 M README.md
 M docs/key_rotation_vault_changes.md
 M docs/rfc.md
 M src/libs/__tests__/github.js
 M src/libs/github.js
 M src/modules/__tests__/details-from-context.js
 M src/modules/__tests__/update-issue.js
 M src/modules/details-from-context.js
 M src/modules/post-workflow-details.js
 M src/modules/process-end-state.js
 M src/modules/update-issue.js
 M src/publish/inputs.js
 M src/publish/post-result.js
 M src/publish/post-workflow-details.js
 M src/publish/update-issue.js
?? .github/workflows/ci-poller-dispatch.yml
?? .lore.md
?? src/modules/__tests__/approval-attestation.js
?? src/modules/__tests__/approval-authorizer.js
?? src/modules/approval-attestation.js
?? src/modules/approval-authorizer.js
?? src/publish/__tests__/
?? ...
gateway · 2026-09-10 01:26:42
result
M	.github/workflows/auto-approve.yml
M	.github/workflows/ci-poller.yml
M	.github/workflows/cocoapods-keepalive.yml
M	.github/workflows/publish.yml
M	.github/workflows/test.yml
M	AGENTS.md
M	README.md
M	docs/key_rotation_vault_changes.md
M	docs/rfc.md
M	src/libs/__tests__/github.js
M	src/libs/github.js
M	src/modules/__tests__/details-from-context.js
M	src/modules/__tests__/update-issue.js
M	src/modules/details-from-context.js
M	src/modules/post-workflow-details.js
M	src/modules/process-end-state.js
M	src/modules/update-issue.js
M	src/publish/inputs.js
M	src/publish/post-result.js
M	src/publish/post-workflow-details.js
M	src/publish/update-issue.js
gateway · 2026-09-10 01:26:46
result
83d210b29553ea6f4d97508821724a834c43856e
gateway · 2026-09-10 01:26:53
result
19385ab27525c253cf41c733472605dff37be44cf3adc335b4b941d858910a96  -
gateway · 2026-09-10 01:26:58
result
.github/workflows/ci-poller-dispatch.yml
.lore.md
src/modules/__tests__/approval-attestation.js
src/modules/__tests__/approval-authorizer.js
src/modules/approval-attestation.js
src/modules/approval-authorizer.js
src/publish/__tests__/authorize-approval.js
src/publish/__tests__/auto-approval-workflow.js
src/publish/__tests__/ci-poller-workflow.js
src/publish/__tests__/current-accepted-event.js
src/publish/__tests__/publish-workflow.js
src/publish/__tests__/record-auto-approval-attestation.js
src/publish/__tests__/record-ci-ready-attestation.js
src/publish/__tests__/validate-approval-attestation.js
src/publish/__tests__/workflow-action-pinning.js
src/publish/authorize-approval.js
src/publish/current-accepted-event.js
src/publish/record-auto-approval-attestation.js
src/publish/record-ci-ready-attestation.js
src/publish/validate-approval-attestation.js
gateway · 2026-09-10 01:27:12
result
 .github/workflows/auto-approve.yml            |  26 +++-
 .github/workflows/ci-poller.yml               | 195 +++++++++++++++++++++++---
 .github/workflows/cocoapods-keepalive.yml     |   3 +-
 .github/workflows/publish.yml                 | 139 ++++++++++++++++--
 .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__/details-from-context.js |  22 ++-
 src/modules/__tests__/update-issue.js         |  28 ++++
 src/modules/details-from-context.js           |  17 ++-
 src/modules/post-workflow-details.js          |   4 +-
 src/modules/process-end-state.js              |   4 +-
 src/modules/update-issue.js                   |  42 ...
gateway · 2026-09-10 01:27:45
result
[
  {
    "content": "Capture initial repository state and worktree fingerprint",
    "status": "completed",
    "priority": "high"
  },
  {
    "content": "Inspect every changed and untracked file plus relevant security tests",
    "status": "in_progress",
    "priority": "high"
  },
  {
    "content": "Run required test, lint, and diff checks",
    "status": "pending",
    "priority": "high"
  },
  {
    "content": "Recompute fingerprint and produce evidence-based merge verdict",
    "status": "pending",
    "priority": "high"
  }
]
gateway · 2026-09-10 01:27:56

The patch spans 41 files: 21 tracked modifications and 20 untracked files. I am treating workflow YAML, authorization/attestation modules, and their structural regression tests as one trust boundary, while also checking every documentation and helper diff for unintended behavior or stale claims.

gpt-5.6-sol · 2026-09-10 01:27:56 · ~55 tokens
result
<path>/home/byk/Code/getsentry/publish/.github/workflows/auto-approve.yml</path>
<type>file</type>
<content>
1: name: auto-approve non-sdks
2: on:
3:   issues:
4:     types: [opened]
5: 
6: permissions:
7:   contents: read
8:   issues: write
9: 
10: jobs:
11:   auto-approve:
12:     runs-on: ubuntu-latest
13:     environment: production
14:     if: |
15:       (github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') &&
16:       startsWith(github.event.issue.title, 'publish: ')
17:     steps:
18:       - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
19:         with:
20:           persist-credentials: false
21:       - name: Get auth token
22:         id: token
23:         uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
24:         with:
25:           client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
26:           private-key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }}
27:       - name: Record automated approval ...
gateway · 2026-09-10 01:28:01
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 01:28:30
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 01:28:48
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 01:28:57
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 01:29:03
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 01:29:10
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 (typeof body !== "string" || !Array.isArray(labels)) {
16:     throw new Error("Invalid publish request");
17:   }
18: 
19:   return crypto
20:     .createHash("sha256")
21:     .update(
22:       JSON.stringify({
23:         body,
24:         dryRun: labels.some((label) => label.name === "dry-run"),
25:         title,
26:       })
27:     )
28:     ...
gateway · 2026-09-10 01:29:39
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 01:29:47
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: } = require("../modules/approval-attestation.js");
7: const {
8:   AUTO_APPROVAL_LABELER,
9:   AUTO_APPROVERS,
10:   authorizeApproval,
11:   isAutoApprovedRepository,
12: } = require("../modules/approval-authorizer.js");
13: 
14: function getAutoApprovedRepositories() {
15:   return new Set(
16:     fs
17:       .readFileSync("auto-approve-repos.txt", "utf8")
18:       .split(/\r?\n/)
19:       .filter(Boolean)
20:   );
21: }
22: 
23: async function getPermission({ owner, repository, username }) {
24:   const response = await getGitHubResponse(
25:     `repos/${encodeURIComponent(owner)}/${encodeURIComponent(
26:       repository
27:     )}/collaborators/${encodeURIComponent(username)}/permission`
28:   );
29: 
30:   if (!response.ok) {
31:     ...
gateway · 2026-09-10 01:30:15
result
<path>/home/byk/Code/getsentry/publish/src/publish/record-auto-approval-attestation.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3:   createAutoApprovalAttestation,
4: } = require("../modules/approval-attestation.js");
5: const { getIssue } = require("./validate-approval-attestation.js");
6: 
7: async function recordAutoApprovalAttestation({
8:   autoApprover,
9:   getIssue,
10:   issueNumber,
11:   issueTitle,
12:   repository,
13: }) {
14:   const issue = await getIssue({ repository, issueNumber });
15: 
16:   if (
17:     issue.state !== "open" ||
18:     issue.title !== issueTitle ||
19:     issue.user?.login?.toLowerCase() !== autoApprover.toLowerCase()
20:   ) {
21:     throw new Error("The automated approval request changed before approval");
22:   }
23: 
24:   return createAutoApprovalAttestation({ autoApprover, issue });
25: }
26: 
27: async function main() {
28:   for (const name of [
29:     "GITHUB_OUTPUT",
30:     "APPROVAL_TOKEN",
31:     ...
gateway · 2026-09-10 01:30:21
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 01:30:29
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: } = require("../modules/approval-attestation.js");
6: const {
7:   getAllPages,
8:   getGitHubResponse,
9:   getIssue,
10: } = require("./validate-approval-attestation.js");
11: 
12: async function getAuthenticatedLogin() {
13:   const response = await getGitHubResponse("user");
14: 
15:   if (!response.ok) {
16:     throw new Error(
17:       `Could not retrieve the authenticated user: GitHub returned ${response.status}`
18:     );
19:   }
20: 
21:   const { login } = await response.json();
22: 
23:   if (typeof login !== "string") {
24:     throw new Error("GitHub returned no authenticated user login");
25:   }
26: 
27:   return login;
28: }
29: 
30: async function recordCiReadyAttestation({
31:   expectedAcceptedEvent,
32:   getAuthenticatedLogin,
33:   getIssue,
34:  ...
gateway · 2026-09-10 01:30:47
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: } = require("../modules/approval-attestation.js");
7: 
8: async function getGitHubResponse(path) {
9:   return fetch(`https://api.github.com/${path}`, {
10:     headers: {
11:       Accept: "application/vnd.github+json",
12:       Authorization: `Bearer ${process.env.APPROVAL_TOKEN}`,
13:       "X-GitHub-Api-Version": "2026-03-10",
14:     },
15:   });
16: }
17: 
18: async function getIssue({ repository, issueNumber }) {
19:   const response = await getGitHubResponse(
20:     `repos/${repository}/issues/${issueNumber}`
21:   );
22: 
23:   if (!response.ok) {
24:     throw new Error(
25:       `Could not retrieve issue #${issueNumber}: GitHub returned ${response.status}`
26:     );
27:   }
28: 
29:   return response.json();
30: }
31: 
32: async ...
gateway · 2026-09-10 01:31:17
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", result);
10: }
11: 
12: inputs();

(End of file - total 12 lines)
</content>
gateway · 2026-09-10 01:31:24
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 01:31:30
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({ octokit, version, publishRepo, issue_number }) {
8:   const CRAFT_STATE_FILE_PATH = `${process.env.GITHUB_WORKSPACE}/__repo__/.craft-publish-${version}.json`;
9: 
10:   if (!fs.existsSync(CRAFT_STATE_FILE_PATH)) {
11:     return;
12:   }
13: 
14:   const issueRequest = octokit.rest.issues.get({
15:     ...publishRepo,
16:     issue_number,
17:   });
18: 
19:   const craftStateRequest = fs.promises
20:     .readFile(CRAFT_STATE_FILE_PATH, { encoding: "utf-8" })
21:     .then((data) => JSON.parse(data));
22: 
23:   const [{ data: issue }, craftState] = await Promise.all([
24:     issueRequest,
25:     craftStateRequest,
26:   ]);
27: 
28:   const newIssueBody = transformIssueBody(craftState, issue.body);
29: ...
gateway · 2026-09-10 01:31:41
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");
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: const inputs = JSON.parse(process.env.PUBLISH_ARGS);
8: const args = process.argv.slice(2);
9: const status = args[0];
10: 
11: processEndState({ context, octokit, inputs, status });

(End of file - total 11 lines)
</content>
gateway · 2026-09-10 01:31:47
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 01:31:54
result
<path>/home/byk/Code/getsentry/publish/src/modules/details-from-context.js</path>
<type>file</type>
<content>
1: /**
2:  * Matches the entire "Targets" section of a github publish issue body.
3:  */
4: const TARGETS_SECTION_PARSER_REGEX =
5:   /^(?!### Targets$\s)(?: *- \[[ xX]\] \S+\s*$(?:\r?\n)?)+/m;
6: 
7: /**
8:  * Matches all targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`.
9:  * The "id" of the targets is captured within a capture group.
10:  */
11: const TARGETS_PARSER_REGEX = /^\s*- \[[ x]\] (\S+)/gim;
12: 
13: /**
14:  * Matches checked targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`.
15:  * The "id" of the targets is captured within a capture group.
16:  */
17: const CHECKED_TARGETS_PARSER_REGEX = /^\s*- \[x\] (\S+)/gim;
18: const PUBLISH_TITLE_REGEX =
19:   /^publish: ...
gateway · 2026-09-10 01:32:11
result
diff --git a/src/modules/details-from-context.js b/src/modules/details-from-context.js
index e58b0be..bd2c123 100644
--- a/src/modules/details-from-context.js
+++ b/src/modules/details-from-context.js
@@ -15,15 +15,24 @@ const TARGETS_PARSER_REGEX = /^\s*- \[[ x]\] (\S+)/gim;
  * The "id" of the targets is captured within a capture group.
  */
 const CHECKED_TARGETS_PARSER_REGEX = /^\s*- \[x\] (\S+)/gim;
+const PUBLISH_TITLE_REGEX =
+  /^publish: (?:getsentry\/)?(?<repo>[^/@]+)(?<path>\/[\w./-]+)?@(?<version>[\w.+-]+)$/;
+
+function parsePublishTitle(title) {
+  return title.match(PUBLISH_TITLE_REGEX)?.groups || null;
+}
 
 async function detailsFromContext({ context }) {
   if (!context || !context.payload || !context.payload.issue) {
     throw new Error("Issue context is not defined");
   }
 
-  const titleParser =
-    /^publish: (?:getsentry\/)?(?<repo>[^/@]+)(?<path>\/[\w./-]+)?@(?<version>[\w.+-]+)$/;
-  const titleMatch = ...
gateway · 2026-09-10 01:32:19
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 01:32:28
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:   const workflowInfo = (
8:     await octokit.rest.actions.getWorkflowRun({
9:       ...publishRepo,
10:       run_id,
11:     })
12:   ).data;
13: 
14:   const details = {
15:     repo,
16:     version,
17:     publishRepo,
18:     run_id,
19:     issue_number,
20:     workflowInfo,
21:     status,
22:   };
23: 
24:   await postIssueComment({
25:     octokit,
26:     details,
27:   });
28: 
29:   if (status === "success") {
30:     await octokit.rest.issues.update({
31:       ...publishRepo,
32:       issue_number,
33:       state: "closed",
34:     });
35:   }
36: 
37:   await reportSession({ ...
gateway · 2026-09-10 01:32:34
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 01:32:39
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 01:32:44
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 01:32:50
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: } = require("../../modules/approval-attestation.js");
6: const {
7:   getAutoApprovedRepositories,
8:   main,
9: } = require("../authorize-approval.js");
10: 
11: const originalEnvironment = { ...process.env };
12: 
13: afterEach(() => {
14:   process.env = { ...originalEnvironment };
15:   vi.restoreAllMocks();
16:   vi.resetModules();
17: });
18: 
19: function jsonResponse(json) {
20:   return { ok: true, json: vi.fn().mockResolvedValue(json) };
21: }
22: 
23: async function runAuthorization({ actor, issueTitle, responses }) {
24:   process.env.GITHUB_OUTPUT = "/tmp/github-output";
25:   process.env.APPROVAL_TOKEN = "release-bot-token";
26:   process.env.APPROVAL_ACTOR = actor;
27:   process.env.APPROVAL_ISSUE_NUMBER = ...
gateway · 2026-09-10 01:33:00
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 01:33:08
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: } = require("../../modules/approval-attestation.js");
6: const {
7:   recordAutoApprovalAttestation,
8: } = require("../record-auto-approval-attestation.js");
9: 
10: describe("recordAutoApprovalAttestation", () => {
11:   test("binds an automated requester to the live publish request", async () => {
12:     const title = "publish: getsentry/sentry-javascript@10.0.0";
13: 
14:     await expect(
15:       recordAutoApprovalAttestation({
16:         autoApprover: "getsantry[bot]",
17:         getIssue: vi.fn().mockResolvedValue({
18:           body: "Merge target: main",
19:           labels: [],
20:           state: "open",
21:           title,
22:           user: { login: "getsantry[bot]" },
23:         }),
24:         issueNumber: "123",
25:       ...
gateway · 2026-09-10 01:33:15
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: } = require("../../modules/approval-attestation.js");
6: const {
7:   recordCiReadyAttestation,
8: } = require("../record-ci-ready-attestation.js");
9: 
10: describe("recordCiReadyAttestation", () => {
11:   test("binds the current approval to the app that will add ci-ready", async () => {
12:     const title = "publish: getsentry/sentry-javascript@10.0.0";
13: 
14:     await expect(
15:       recordCiReadyAttestation({
16:         getAuthenticatedLogin: vi
17:           .fn()
18:           .mockResolvedValue("sentry-internal-app[bot]"),
19:         getIssue: vi.fn().mockResolvedValue({
20:           body: "Merge target: main",
21:           labels: [{ name: "accepted" }],
22:           state: "open",
23:           title,
24:         }),
25:         ...
gateway · 2026-09-10 01:33:20
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: } = require("../../modules/approval-attestation.js");
10: 
11: afterEach(() => {
12:   vi.restoreAllMocks();
13: });
14: 
15: function jsonResponse(json) {
16:   return { ok: true, json: vi.fn().mockResolvedValue(json) };
17: }
18: 
19: function issue(
20:   title,
21:   { body = "", labels = [{ name: "accepted" }], state = "open" } = {}
22: ) {
23:   return { body, labels, state, title };
24: }
25: 
26: describe("validateApprovalAttestation", () => {
27:   test("accepts the current accepted event and its trusted attestation", async () => {
28:     const title = "publish: getsentry/sentry-javascript@10.0.0";
29:    ...
gateway · 2026-09-10 01:33:29
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 01:33:35
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(
18:       "node src/publish/record-auto-approval-attestation.js"
19:     );
20:     expect(
21:       workflow.indexOf("record-auto-approval-attestation.js")
22:     ).toBeLessThan(workflow.indexOf("--add-label ...
gateway · 2026-09-10 01:33:41
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 01:33:47
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("revalidates approval immediately before Craft", () => {
8:     const fence = workflow.indexOf("Revalidate approval attestation");
9: 
10:     expect(fence).toBeGreaterThan(workflow.indexOf("Set targets"));
11:     expect(fence).toBeLessThan(workflow.indexOf("Publish using Craft"));
12:     expect(workflow.slice(fence)).toContain(
13:       'REQUIRE_CI_READY_ATTESTATION: "true"'
14:     );
15:   });
16: 
17:   test("pins Craft to an immutable digest", () => {
18:     expect(workflow).toMatch(
19:       /uses: docker:\/\/getsentry\/craft@sha256:[a-f0-9]{64}/
20:     );
21:   });
22: });

(End of file - total 22 lines)
</content>
gateway · 2026-09-10 01:33:51
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 01:33:59
result
Found 18 matches
/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 52:         uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1

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

  Line 192:         uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803

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

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

  Line 274:       - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803

  Line 353:       - uses: ...
gateway · 2026-09-10 01:34:10
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 01:34: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: `
16: Requested by: @BYK
17: 
18: Merge target: custom-branch
19: 
20: Quick links:
21: - [View changes](https://github.com/getsentry/sentry/compare/21.3.0...refs/heads/releases/21.3.1)
22: - [View check runs](https://github.com/getsentry/sentry/commit/7e5ca7ed5581552de066e2a8bc295b8306be38ac/checks/)
23: 
24: Assign the **accepted** label to this issue to approve the release. ...
gateway · 2026-09-10 01:34:30
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 01:34:38
result
diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml
index d9763c7..958ce93 100644
--- a/.github/workflows/auto-approve.yml
+++ b/.github/workflows/auto-approve.yml
@@ -5,25 +5,43 @@ on:
 
 permissions:
   contents: read
+  issues: write
 
 jobs:
   auto-approve:
     runs-on: ubuntu-latest
+    environment: production
     if: |
       (github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') &&
       startsWith(github.event.issue.title, 'publish: ')
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
         with:
-          sparse-checkout: auto-approve-repos.txt
-          sparse-checkout-cone-mode: false
+          persist-credentials: false
       - name: Get auth token
         id: token
-        uses: actions/create-github-app-token@v3
+        uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
         with:
           ...
gateway · 2026-09-10 01:35:19
result
diff --git a/src/libs/__tests__/github.js b/src/libs/__tests__/github.js
index 710091c..ea7761b 100644
--- a/src/libs/__tests__/github.js
+++ b/src/libs/__tests__/github.js
@@ -8,7 +8,7 @@ describe("getGitHubToken", () => {
   test("throw if no token is defined", async () => {
     delete process.env.GITHUB_TOKEN;
     expect(() => getGitHubToken()).toThrow(
-      'No "GITHUB_TOKEN" environment variable found.',
+      'No "GITHUB_TOKEN" environment variable found.'
     );
   });
 
diff --git a/src/libs/github.js b/src/libs/github.js
index e4d506f..f1188c1 100644
--- a/src/libs/github.js
+++ b/src/libs/github.js
@@ -6,8 +6,10 @@
  */
 function getGitHubToken() {
   if (!process.env.GITHUB_TOKEN) {
-    throw new Error('No "GITHUB_TOKEN" environment variable found. ' +
-    'Please ensure the workflow is configured correctly');
+    throw new Error(
+      'No "GITHUB_TOKEN" environment variable found. ...
gateway · 2026-09-10 01:35:27
result
diff --git a/AGENTS.md b/AGENTS.md
index cf473df..51bf3a8 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -273,3 +273,9 @@ This codebase interacts heavily with GitHub Actions. Entry points in `src/publis
 ## Code Owners
 
 All code is owned by `@getsentry/releng` (Release Engineering team).
+
+<!-- This section is maintained by the coding agent via lore (https://github.com/BYK/loreai) -->
+## Long-term Knowledge
+
+For long-term knowledge entries managed by [lore](https://github.com/BYK/loreai) (gotchas, patterns, decisions, architecture), see [`.lore.md`](.lore.md) in the project root.
+<!-- End lore-managed section -->
diff --git a/README.md b/README.md
index b8e07f0..8b716c8 100644
--- a/README.md
+++ b/README.md
@@ -94,6 +94,8 @@ Automatic approvals are managed in the [`auto-approve.yml`](https://github.com/g
 
 The system uses [Craft](https://github.com/getsentry/craft) under the hood to prepare and publish releases. It uses tokens from [Sentry Release ...
gateway · 2026-09-10 01:35:33
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 01:35:57
result
Found 36 matches
/home/byk/Code/getsentry/publish/.github/workflows/publish.yml:
  Line 45:           private-key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }}

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

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

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

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

  Line 381:           DOCKER_GHCR_IO_PASSWORD: ${{ secrets.GITHUB_TOKEN }} # for ghcr.io auth

  Line 383:           CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

  Line 384:           COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}

  Line 385:           CRAFT_GCS_TARGET_CREDS_JSON: ${{ secrets.CRAFT_GCS_TARGET_CREDS_JSON }}

  Line 386:           CRAFT_GCS_STORE_CREDS_JSON: ${{ secrets.CRAFT_GCS_STORE_CREDS_JSON }}

  Line 387:           CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}

  Line 389:   ...
gateway · 2026-09-10 01:36:02
result
Found 5 matches
/home/byk/Code/getsentry/publish/.github/workflows/ci-poller-dispatch.yml:
  Line 4:   workflow_dispatch:


/home/byk/Code/getsentry/publish/.github/workflows/publish.yml:
  Line 162:       # Uses the app token — GITHUB_TOKEN workflow_dispatch events are suppressed.


/home/byk/Code/getsentry/publish/.github/workflows/ci-poller.yml:
  Line 23:     # workflow_dispatch relay are the trusted manual-recovery paths.

  Line 51:           # workflow_dispatch can target any ref. Always run trusted code.

  Line 444:       # manual trigger (accepted label re-added or workflow_dispatch relay).
gateway · 2026-09-10 01:36:08
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:     "test": "vitest run",
19:     "test:watch": "vitest",
20:     "lint": "eslint src .github --ignore-pattern '!.github'",
21:     "prettier": "prettier --write src"
22:   },
23:   "devDependencies": {
24:     "eslint": "^8.9.0",
25:     "eslint-config-prettier": "^8.3.0",
26:     "eslint-plugin-yml": "^0.13.0",
27:     "prettier": "^2.2.1",
28:     "vitest": "^4.1.0"
29:   },
30:   "dependencies": {
31:     "@actions/core": ...
gateway · 2026-09-10 01:37:03
result
No files found
gateway · 2026-09-10 01:37:08
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 01:38:28

The trust chain is coherent so far: the Internal App path never falls back to collaborator authorization, each proof is tied to the live title/body/dry-run state and current accepted event, and the poller preserves the fresh ci-ready event by clearing it in waiting-for-ci before its unconditional add. I am moving to executable validation, then I will recheck the few race and cleanup edges against the final source lines.

gpt-5.6-sol · 2026-09-10 01:38:28 · ~88 tokens
result
[
  {
    "content": "Capture initial repository state and worktree fingerprint",
    "status": "completed",
    "priority": "high"
  },
  {
    "content": "Inspect every changed and untracked file plus relevant security tests",
    "status": "completed",
    "priority": "high"
  },
  {
    "content": "Run required test, lint, and diff checks",
    "status": "in_progress",
    "priority": "high"
  },
  {
    "content": "Recompute fingerprint and produce evidence-based merge verdict",
    "status": "pending",
    "priority": "high"
  }
]
gateway · 2026-09-10 01:38:36
result
yarn run v1.22.22
$ vitest run --printConsoleTrace

 RUN  v4.0.17 /home/byk/Code/getsentry/publish

 ✓ src/publish/__tests__/authorize-approval.js (7 tests) 49ms
 ✓ src/publish/__tests__/validate-approval-attestation.js (9 tests) 39ms
 ✓ src/publish/__tests__/current-accepted-event.js (2 tests) 20ms
 ✓ src/modules/__tests__/approval-attestation.js (5 tests) 22ms
 ✓ src/modules/__tests__/process-end-state.js (4 tests) 60ms
 ✓ src/publish/__tests__/record-ci-ready-attestation.js (3 tests) 10ms
 ✓ src/modules/__tests__/approval-authorizer.js (15 tests) 19ms
 ...
gateway · 2026-09-10 01:38:45
result
yarn run v1.22.22
$ eslint src .github --ignore-pattern '!.github'
Done in 1.03s.
gateway · 2026-09-10 01:38:50
result
(no output)
gateway · 2026-09-10 01:39:04
result
19385ab27525c253cf41c733472605dff37be44cf3adc335b4b941d858910a96  -
gateway · 2026-09-10 01:39:09
result
## main...origin/main [behind 1]
 M .github/workflows/auto-approve.yml
 M .github/workflows/ci-poller.yml
 M .github/workflows/cocoapods-keepalive.yml
 M .github/workflows/publish.yml
 M .github/workflows/test.yml
 M AGENTS.md
 M README.md
 M docs/key_rotation_vault_changes.md
 M docs/rfc.md
 M src/libs/__tests__/github.js
 M src/libs/github.js
 M src/modules/__tests__/details-from-context.js
 M src/modules/__tests__/update-issue.js
 M src/modules/details-from-context.js
 M src/modules/post-workflow-details.js
 M src/modules/process-end-state.js
 M src/modules/update-issue.js
 M src/publish/inputs.js
 M src/publish/post-result.js
 M src/publish/post-workflow-details.js
 M src/publish/update-issue.js
?? .github/workflows/ci-poller-dispatch.yml
?? .lore.md
?? src/modules/__tests__/approval-attestation.js
?? src/modules/__tests__/approval-authorizer.js
?? src/modules/approval-attestation.js
?? src/modules/approval-authorizer.js
?? src/publish/__tests__/
?? ...
gateway · 2026-09-10 01:39:23
result
[
  {
    "content": "Capture initial repository state and worktree fingerprint",
    "status": "completed",
    "priority": "high"
  },
  {
    "content": "Inspect every changed and untracked file plus relevant security tests",
    "status": "completed",
    "priority": "high"
  },
  {
    "content": "Run required test, lint, and diff checks",
    "status": "completed",
    "priority": "high"
  },
  {
    "content": "Recompute fingerprint and produce evidence-based merge verdict",
    "status": "completed",
    "priority": "high"
  }
]
gateway · 2026-09-10 01:39:45
result
<path>/home/byk/Code/getsentry/publish/README.md</path>
<type>file</type>
<content>
84: 
85: Packages we release into the wider world that our customers install, require an explicit approval. This for instance applies to
86: `sentry-cli`, our SDKs or the `symbolicator` distributed utilities. Internal dependencies such as `arroyo` can be published
87: with an auto approval. The reasoning here is that the bump of the dependency requires an explicit approval again in Sentry
88: proper. In theory if an independent package gets sufficient independent use of Sentry we might want to reconsider an auto
89: approval process for such package as it might become an interesting target for an attacker.
90: 
91: Automatic approvals are managed in the [`auto-approve.yml`](https://github.com/getsentry/publish/blob/main/.github/workflows/auto-approve.yml) workflow.
92: 
93: ## Under the hood
94: 
95: The system uses [Craft](https://github.com/getsentry/craft) under the hood to prepare and publish ...
gateway · 2026-09-10 01:44:00

All required checks pass, and the worktree fingerprint is unchanged. One review edge remains: the branch is one commit behind origin/main, so I am checking that upstream delta read-only to determine whether it conflicts with or invalidates this patch before issuing the verdict.

gpt-5.6-sol · 2026-09-10 01:44:00 · ~53 tokens
result
7c60ddb (origin/main, origin/HEAD) feat: accept workspace publish requests
gateway · 2026-09-10 01:44:09
result
7c60ddb (origin/main, origin/HEAD) feat: accept workspace publish requests
 .github/workflows/ci-poller.yml                    |   62 +-
 .github/workflows/cocoapods-keepalive.yml          |    2 +-
 .github/workflows/publish.yml                      |   84 +-
 README.md                                          |    5 +
 docs/publish-issue-format.md                       |  107 ++
 package.json                                       |    5 +-
 scripts/generate-publish-issue-title-parser.js     |  104 ++
 src/modules/__tests__/ci-poller-input.js           |   97 ++
 src/modules/__tests__/ci-poller-workflow.js        |  197 +++
 src/modules/__tests__/details-from-context.js      |  166 ++-
 .../generate-publish-issue-title-parser.js         |   40 +
 src/modules/__tests__/publish-location.js          |   91 ++
 src/modules/__tests__/publish-workflow.js          |  156 +++
 src/modules/__tests__/release-revision.js          |  150 ++
 src/modules/__tests__/update-issue.js              |   ...
gateway · 2026-09-10 01:44:35
result
diff --git a/.github/workflows/ci-poller.yml b/.github/workflows/ci-poller.yml
index a3db430..487e9da 100644
--- a/.github/workflows/ci-poller.yml
+++ b/.github/workflows/ci-poller.yml
@@ -48,6 +48,11 @@ jobs:
           private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }}
           owner: getsentry # create token with access to all getsentry repos
 
+      - name: Check out publish controller
+        uses: actions/checkout@v7
+        with:
+          path: .__publish__
+
       - name: Check CI status for ci-pending issues
         env:
           # Use the sentry-internal-app token for label changes on this
@@ -88,30 +93,23 @@ jobs:
 
           # Check each issue's CI status
           echo "$issues" | jq -c '.[]' | while read -r issue; do
+            (
             number=$(echo "$issue" | jq -r '.number')
             title=$(echo "$issue" | jq -r '.title')
-            body=$(echo "$issue" | jq -r '.body')
-
-            # Parse repo and version from title: "publish: ...
gateway · 2026-09-10 01:45:00
result
M	.github/workflows/auto-approve.yml
M	.github/workflows/ci-poller.yml
M	.github/workflows/cocoapods-keepalive.yml
M	.github/workflows/publish.yml
M	.github/workflows/test.yml
M	AGENTS.md
M	README.md
M	docs/key_rotation_vault_changes.md
D	docs/publish-issue-format.md
M	docs/rfc.md
M	package.json
D	scripts/generate-publish-issue-title-parser.js
M	src/libs/__tests__/github.js
M	src/libs/github.js
D	src/modules/__tests__/ci-poller-input.js
D	src/modules/__tests__/ci-poller-workflow.js
M	src/modules/__tests__/details-from-context.js
D	src/modules/__tests__/generate-publish-issue-title-parser.js
D	src/modules/__tests__/publish-location.js
D	src/modules/__tests__/publish-workflow.js
D	src/modules/__tests__/release-revision.js
M	src/modules/__tests__/update-issue.js
D	src/modules/ci-poller-input.js
M	src/modules/details-from-context.js
M	src/modules/post-workflow-details.js
M	src/modules/process-end-state.js
D	src/modules/publish-issue-title.js
D	src/modules/publish-issue-title.peggy
D	src/mo...
gateway · 2026-09-10 01:45:25
result
.github/workflows/ci-poller.yml
.github/workflows/cocoapods-keepalive.yml
.github/workflows/publish.yml
README.md
src/modules/details-from-context.js
src/modules/__tests__/details-from-context.js
src/modules/__tests__/update-issue.js
src/modules/update-issue.js
src/publish/inputs.js
gateway · 2026-09-10 01:46:02
result
19385ab27525c253cf41c733472605dff37be44cf3adc335b4b941d858910a96  -
gateway · 2026-09-10 01:48:44

Findings

  1. MUST-FIX The worktree is based on 83d210b, one commit behind origin/main at 7c60ddb (feat: accept workspace publish requests). Both revisions modify nine shared files. The reviewed tree still uses the old title parser (src/modules/details-from-context.js:18-23), mutable target checkout (.github/workflows/publish.yml:274-282), and old Craft-state lookup (src/modules/update-issue.js:7-21). Current main adds canonical workspace parsing, CI-approved revision checkout, path containment, and explicit state-file handling. Rebase and integrate these security changes before repeating the review.
  2. PASS Automated approval requires a trusted request-bound proof before the Internal App labels the issue (.github/workflows/auto-approve.yml:27-52). The authorizer checks the live requester, allowlist, current label actor, and github-actions[bot] proof (src/publish/authorize-approval.js:135-169); direct bot approvals never fall through to collaborator permissions (src/modules/approval-authorizer.js:35-41).
  3. PASS Human approval allows only write, maintain, or admin (src/modules/approval-authorizer.js:1-49) and rejects case-insensitive self-approval against the live requester and current event (src/publish/authorize-approval.js:186-213).
  4. PASS Proofs bind title, body, and dry-run state (src/modules/approval-attestation.js:14-55). Validation requires an open live issue, current accepted event, trusted approval proof, and matching CI-ready provenance (src/publish/validate-approval-attestation.js:63-100). The poller revalidates before and after CI and immediately before promotion (.github/workflows/ci-poller.yml:121-153, .github/workflows/ci-poller.yml:272-354).
  5. PASS Retry handling removes stale ci-ready before ci-pending (.github/workflows/publish.yml:113-129), while the poller unconditionally adds ci-ready after successful CI (.github/workflows/ci-poller.yml:356-359).
  6. ...
gpt-5.6-sol · 2026-09-10 01:48:44 · ~738 tokens