DashboardpublishSession 0geankYTI3pO

Session 0geankYTI3pO

Full ID: 0geankYTI3pOB2506
Messages: 90
Distillations: 12
Time range: 2026-09-10 03:20:45 — 2026-09-10 03:37:04

Conversation (90 messages)

Independently review the exact current uncommitted tree in /home/byk/Code/getsentry/publish. This is a read-only adversarial correctness and security review scoped to release input parsing, immutable request/revision identity, target checkout, path/workspace discovery, Craft arguments/environment, and state-file identity. Do not edit files. Inspect all relevant changed workflow, source, and test files. You MUST return a non-empty report. Classify each audited area PASS, CONCERN, or MUST-FIX with exact current file:line evidence. For every finding, give the concrete exploit/failure sequence and minimal fix. Verify that event snapshot input cannot diverge from a later live issue; checked-out code/revision/path/workspace and state identity match the approved canonical request; paths remain contained; shell and Actions expressions cannot inject commands; credentials do not reach mutable code; Actions and Craft references are immutable. Treat organization-wide SENTRY_INTERNAL_APP_PRIVATE_KEY exposure as an accepted separate Security workstream, not a blocker in this repository review. If blocked by a tool, begin BLOCKED and name its exact error. End exactly MERGE or DO-NOT-MERGE.

gateway · 2026-09-10 03:20:45

I will freeze the current worktree identity, enumerate every changed and untracked file, then trace the release request through parsing, checkout, workspace discovery, Craft invocation, and state persistence. I will keep the tree unchanged and validate the final tree identity after tests and inspection.

gpt-5.6-sol · 2026-09-10 03:20:45 · ~54 tokens
result
[
  {
    "content": "Freeze worktree identity and enumerate all changed and untracked files",
    "status": "in_progress",
    "priority": "high"
  },
  {
    "content": "Inspect changed workflows, source, and tests across the scoped release path",
    "status": "pending",
    "priority": "high"
  },
  {
    "content": "Run targeted and repository validation without modifying files",
    "status": "pending",
    "priority": "high"
  },
  {
    "content": "Reverify worktree identity and produce evidence-based verdict",
    "status": "pending",
    "priority": "high"
  }
]
gateway · 2026-09-10 03:21:01
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 03:21:16
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__/publish-location.js
 M src/modules/__tests__/publish-workflow.js
 M src/modules/__tests__/release-revision.js
M  src/modules/__tests__/update-issue.js
 M src/modules/ci-poller-input.js
M  src/modules/details-from-context.js
M  src/modules/post-workflow-details.js
M  src/modules/process-end-state.js
 M src/modules/publish-location.js
 M src/modules/release-revision.js
 M src/publish/__tests__/discover-location.js
 M src/publish/__tests__/resolve-release-revision.js
 M src/publish/discover-location.js
M  ...
gateway · 2026-09-10 03:21:19
result
7c60ddb7f43040fe8fbfea70efc833f689c04e75
gateway · 2026-09-10 03:21:24
result
4c954652949e34401f065f7f3a882df48215bbd48ea2b78bc89a4b17b46ff23d  -
gateway · 2026-09-10 03:21:29
result
M	.github/workflows/auto-approve.yml
M	.github/workflows/ci-poller.yml
M	.github/workflows/cocoapods-keepalive.yml
M	.github/workflows/publish.yml
M	.github/workflows/test.yml
M	AGENTS.md
M	README.md
M	docs/key_rotation_vault_changes.md
M	docs/rfc.md
M	src/libs/__tests__/github.js
M	src/libs/github.js
M	src/modules/__tests__/ci-poller-input.js
M	src/modules/__tests__/ci-poller-workflow.js
M	src/modules/__tests__/details-from-context.js
M	src/modules/__tests__/publish-location.js
M	src/modules/__tests__/publish-workflow.js
M	src/modules/__tests__/release-revision.js
M	src/modules/__tests__/update-issue.js
M	src/modules/ci-poller-input.js
M	src/modules/details-from-context.js
M	src/modules/post-workflow-details.js
M	src/modules/process-end-state.js
M	src/modules/publish-location.js
M	src/modules/release-revision.js
M	src/publish/__tests__/discover-location.js
M	src/publish/__tests__/resolve-release-revision.js
M	src/publish/discover-location.js
M	src/publish/inputs.js
M	src/publish/post-...
gateway · 2026-09-10 03:21:33
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__/request-digest-from-event.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/request-digest-from-event.js
src/publish/validate-approval-attestation.js
gateway · 2026-09-10 03:21:48

The frozen tree is based on 7c60ddb7f43040fe8fbfea70efc833f689c04e75; the tracked patch hash is 4c954652949e34401f065f7f3a882df48215bbd48ea2b78bc89a4b17b46ff23d. The patch spans both index and worktree changes plus 22 untracked files, so I am reviewing the combined HEAD-to-worktree state rather than either layer alone.

gpt-5.6-sol · 2026-09-10 03:21:48 · ~110 tokens
result
 .github/workflows/auto-approve.yml                |  30 +++-
 .github/workflows/ci-poller.yml                   | 202 +++++++++++++++++++---
 .github/workflows/cocoapods-keepalive.yml         |   1 -
 .github/workflows/publish.yml                     | 152 +++++++++++++++-
 .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       |  93 ++++++----
 src/modules/__tests__/details-from-context.js     |  22 ++-
 src/modules/__tests__/publish-location.js         |  19 +-
 src/modules/__tests__/publish-workflow.js         | ...
gateway · 2026-09-10 03:22:05
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 03:22:10
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 03:22:14
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 03:22:19
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 03:22:23
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 03:22:37
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 03:22:41
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 03:22:47
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 03:22: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 03:22:58
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 03:23:04
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 03:23:08
result
/home/byk/Code/getsentry/publish/src/modules/publish-issue-validation.js
/home/byk/Code/getsentry/publish/src/modules/publish-issue-title.peggy
/home/byk/Code/getsentry/publish/src/modules/publish-issue-title.js
gateway · 2026-09-10 03:23:15
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 03:23:21
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 03:23: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 03:23:31
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 03:23:35
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 03:23:41
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 03:23:46
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 03:23:50
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 03:23:56
result
<path>/home/byk/Code/getsentry/publish/src/publish/validate-approval-attestation.js</path>
<type>file</type>
<content>
1: const {
2:   currentAcceptedEvent,
3:   currentCiReadyEvent,
4:   hasApprovalAttestation,
5:   hasCiReadyAttestation,
6:   requestDigest,
7: } = require("../modules/approval-attestation.js");
8: 
9: async function getGitHubResponse(path) {
10:   return fetch(`https://api.github.com/${path}`, {
11:     headers: {
12:       Accept: "application/vnd.github+json",
13:       Authorization: `Bearer ${process.env.APPROVAL_TOKEN}`,
14:       "X-GitHub-Api-Version": "2026-03-10",
15:     },
16:   });
17: }
18: 
19: async function getIssue({ repository, issueNumber }) {
20:   const response = await getGitHubResponse(
21:     `repos/${repository}/issues/${issueNumber}`
22:   );
23: 
24:   if (!response.ok) {
25:     throw new Error(
26:       `Could not retrieve issue #${issueNumber}: GitHub returned ${response.status}`
27:     );
28:   }
29: 
30:   return ...
gateway · 2026-09-10 03:24:09
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 03:24:18
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 03:24:22
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 03:24:26
result
<path>/home/byk/Code/getsentry/publish/src/publish/record-ci-ready-attestation.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3:   createCiReadyAttestation,
4:   currentAcceptedEvent,
5:   requestDigest,
6: } = require("../modules/approval-attestation.js");
7: const {
8:   getAllPages,
9:   getGitHubResponse,
10:   getIssue,
11: } = require("./validate-approval-attestation.js");
12: 
13: async function getAuthenticatedLogin() {
14:   const response = await getGitHubResponse("user");
15: 
16:   if (!response.ok) {
17:     throw new Error(
18:       `Could not retrieve the authenticated user: GitHub returned ${response.status}`
19:     );
20:   }
21: 
22:   const { login } = await response.json();
23: 
24:   if (typeof login !== "string") {
25:     throw new Error("GitHub returned no authenticated user login");
26:   }
27: 
28:   return login;
29: }
30: 
31: async function recordCiReadyAttestation({
32:   expectedAcceptedEvent,
33:   ...
gateway · 2026-09-10 03:24:31
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 03:24:35
result
<path>/home/byk/Code/getsentry/publish/src/modules/update-issue.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3:   TARGETS_SECTION_PARSER_REGEX,
4:   TARGETS_PARSER_REGEX,
5: } = require("./details-from-context");
6: 
7: async function updateTargets({
8:   octokit,
9:   stateFilePath,
10:   publishRepo,
11:   issue_number,
12: }) {
13:   if (!stateFilePath || !fs.existsSync(stateFilePath)) {
14:     return;
15:   }
16: 
17:   const issueRequest = octokit.rest.issues.get({
18:     ...publishRepo,
19:     issue_number,
20:   });
21: 
22:   const craftStateRequest = fs.promises
23:     .readFile(stateFilePath, { encoding: "utf-8" })
24:     .then((data) => JSON.parse(data));
25: 
26:   const [{ data: issue }, craftState] = await Promise.all([
27:     issueRequest,
28:     craftStateRequest,
29:   ]);
30: 
31:   const newIssueBody = transformIssueBody(craftState, issue.body);
32: 
33:   await octokit.rest.issues.update({
34:     ...publishRepo,
35:     ...
gateway · 2026-09-10 03:24:42
result
Found 22 matches
/home/byk/Code/getsentry/publish/src/publish/resolve-location.js:
  Line 6:   const workspaceNames = JSON.parse(process.env.CRAFT_WORKSPACE_NAMES || "[]");

  Line 10:       "Craft workspace discovery returned an invalid workspace list."


/home/byk/Code/getsentry/publish/src/publish/discover-location.js:
  Line 41:       "Craft workspace discovery returned an invalid workspace list."

  Line 46:       "Craft workspace discovery returned an invalid workspace list."


/home/byk/Code/getsentry/publish/src/publish/__tests__/resolve-location.js:
  Line 12:       CRAFT_WORKSPACE_NAMES: "[]",


/home/byk/Code/getsentry/publish/src/publish/__tests__/discover-location.js:
  Line 44: test("fails closed when Craft returns an invalid workspace list", () => {

  Line 53:   ).toThrow("Craft workspace discovery returned an invalid workspace list.");

  Line 65:   ).toThrow("Craft workspace discovery returned an invalid workspace ...
gateway · 2026-09-10 03:24:46
result
<path>/home/byk/Code/getsentry/publish/src/publish/resolve-location.js</path>
<type>file</type>
<content>
1: const core = require("@actions/core");
2: const { resolvePublishLocation } = require("../modules/publish-location");
3: 
4: function resolveLocation() {
5:   const input = JSON.parse(process.env.PUBLISH_ARGS || "{}");
6:   const workspaceNames = JSON.parse(process.env.CRAFT_WORKSPACE_NAMES || "[]");
7: 
8:   if (!Array.isArray(workspaceNames)) {
9:     throw new Error(
10:       "Craft workspace discovery returned an invalid workspace list."
11:     );
12:   }
13: 
14:   core.setOutput(
15:     "result",
16:     JSON.stringify(
17:       resolvePublishLocation({
18:         path: input.path,
19:         workspaceNames,
20:       })
21:     )
22:   );
23: }
24: 
25: resolveLocation();

(End of file - total 25 lines)
</content>
gateway · 2026-09-10 03:24:50
result
<path>/home/byk/Code/getsentry/publish/docs/publish-issue-format.md</path>
<type>file</type>
<content>
1: # Publish Issue Format
2: 
3: The Publish workflow treats a publish issue as a release request. The title identifies
4: the release; the body supplies the merge target and selected targets. Do not edit the
5: title by hand unless it remains valid under this format.
6: 
7: ## Title
8: 
9: Every title starts with `publish: `. The following canonical Peggy grammar is
10: generated from `src/modules/publish-issue-title.peggy`:
11: 
12: <!-- BEGIN GENERATED TITLE GRAMMAR -->
13: ```peggy
14: // Canonical grammar for publish issue titles. A path suffix is syntactic only:
15: // the controller resolves the complete suffix as a workspace after checking
16: // out the CI-approved revision.
17: PublishIssueTitle
18:   = "publish: " "getsentry/"? repo:Repository path:Path? "@" version:Version !. ...
gateway · 2026-09-10 03:24:58
result
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 2635ea6..69210d1 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -32,13 +32,90 @@ jobs:
       && github.event.issue.state == 'open'
       && startsWith(github.event.issue.title, 'publish: ')
     steps:
+      - name: Get publish code
+        uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
+        with:
+          persist-credentials: false
+
       - name: Get auth token
         id: token
-        uses: actions/create-github-app-token@v3
+        uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
         with:
           client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
           private-key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }}
 
+      - name: Bind request snapshot
+        id: request-digest
+        run: node src/publish/request-digest-from-event.js
+
+      # The release bot sees all target repositories. ...
gateway · 2026-09-10 03:25:02
result
diff --git a/.github/workflows/ci-poller.yml b/.github/workflows/ci-poller.yml
index 487e9da..e81af3f 100644
--- a/.github/workflows/ci-poller.yml
+++ b/.github/workflows/ci-poller.yml
@@ -3,16 +3,15 @@ name: CI Status Poller
 on:
   schedule:
     - cron: "*/5 * * * *"
-  workflow_dispatch:
-    inputs:
-      attempt:
-        description: Self-dispatch attempt counter (internal use)
-        required: false
-        default: "0"
+  repository_dispatch:
+    types: [ci-poller]
+  workflow_run:
+    workflows: [Run CI Status Poller]
+    types: [completed]
 
 permissions:
   contents: read
-  issues: read
+  issues: write
 
 jobs:
   check-ci:
@@ -20,18 +19,46 @@ jobs:
     environment: production
     # Skip entirely (no runner provisioned) when there's nothing to check.
     # Set to "true" by ci-pending.yml, reset to "false" here when done.
-    # Always allow workflow_dispatch for manual recovery.
-    if: vars.CI_POLLER_HAS_PENDING == 'true' || github.event_name == ...
gateway · 2026-09-10 03:25:11
result
diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml
index d9763c7..5f86245 100644
--- a/.github/workflows/auto-approve.yml
+++ b/.github/workflows/auto-approve.yml
@@ -5,25 +5,47 @@ on:
 
 permissions:
   contents: read
+  issues: write
 
 jobs:
   auto-approve:
     runs-on: ubuntu-latest
+    environment: production
     if: |
       (github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') &&
       startsWith(github.event.issue.title, 'publish: ')
     steps:
-      - uses: actions/checkout@v4
+      - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
         with:
-          sparse-checkout: auto-approve-repos.txt
-          sparse-checkout-cone-mode: false
+          persist-credentials: false
       - name: Get auth token
         id: token
-        uses: actions/create-github-app-token@v3
+        uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
         with:
           ...
gateway · 2026-09-10 03:25:19
result
Found 12 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 56:         uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1

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

  Line 198:         uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1

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

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


/home/byk/Code/getsentry/publish/.github/workflows/ci-poller.yml:
  Line 49:         uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803

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

  Line 72:     ...
gateway · 2026-09-10 03:25:24
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 155:         uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1

  Line 198:         uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1

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

  Line 262:         uses: ...
gateway · 2026-09-10 03:26:01

The request snapshot is not used as authority by itself: current validation recomputes the digest from the live issue and compares it to the event digest before parsing the event payload, then repeats that check immediately before Craft. I am now testing whether the approved SHA, location, and Craft state naming stay identical through every transformation, especially across workspace mode and shell interpolation.

gpt-5.6-sol · 2026-09-10 03:26:01 · ~72 tokens
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 03:26:16
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 03:26:21
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 03:26:31
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 03:26:37
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 03:26:42
result
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/ci-poller-input.js</path>
<type>file</type>
<content>
1: import { readFileSync } from "fs";
2: import { join } from "path";
3: 
4: import { expect, test } from "vitest";
5: 
6: const { getCiPollerInput } = require("../ci-poller-input.js");
7: const { requestDigest } = require("../approval-attestation.js");
8: 
9: const REVISION = "7e5ca7ed5581552de066e2a8bc295b8306be38ac";
10: const labels = [{ name: "accepted" }, { name: "dry-run" }];
11: const issueBody = `Requested by: @byk
12: 
13: Merge target: (default)
14: 
15: Quick links:
16: - [View changes](https://github.com/getsentry/toolkit/compare/1.2.2...release/1.2.3)
17: - [View check runs](https://github.com/getsentry/toolkit/commit/${REVISION}/checks/)`;
18: 
19: test("parses root and full-path publish titles", () => {
20:   const rootTitle = "publish: getsentry/toolkit/cli@1.2.3";
21:   expect(
22:     getCiPollerInput({
23:       issueBody,
24:       labels,
25:       ...
gateway · 2026-09-10 03:26:47
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 03:26:53
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 03:26:59
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 03:27:05
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 03:27:12
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 03:27:17
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 03:27:24
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 03:27:31
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 03:28:16
result
29.1.3
gateway · 2026-09-10 03:28:31
result
[
    {
        "Id": "sha256:fc314146c06b14791ab860e80e73043c7c5fa06f61bce3081a34a8a227305a5a",
        "RepoTags": [
            "getsentry/craft:2.30.1"
        ],
        "RepoDigests": [
            "getsentry/craft@sha256:9a4a5d5efa44a00c2215078ead39800d4aaa5a97908b94f45a64d7d506d6e14b"
        ],
        "Comment": "buildkit.dockerfile.v0",
        "Created": "2026-08-08T00:37:30.542283842Z",
        "Config": {
            "Env": [
                "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/root/.cargo/bin:/opt/flutter/bin:/venv/bin",
                "NODE_VERSION=24.18.0",
                "YARN_VERSION=1.22.22",
                "DEBIAN_FRONTEND=noninteractive",
                "DOTNET_CLI_TELEMETRY_OPTOUT=1",
                "COCOAPODS_ALLOW_ROOT=1",
                "CARGO_HOME=/root/.cargo",
                "RUSTUP_HOME=/root/.rustup",
                "CRAFT_BUILD_SHA=cd1e8294061fd970b40d98b77aaa109cb1e00e78"
            ],
            "Entrypoint": ...
gateway · 2026-09-10 03:28:46
result
{
  "sha": "cd1e8294061fd970b40d98b77aaa109cb1e00e78",
  "url": "https://api.github.com/repos/getsentry/craft/git/trees/cd1e8294061fd970b40d98b77aaa109cb1e00e78",
  "tree": [
    {
      "path": ".craft.yml",
      "mode": "100644",
      "type": "blob",
      "sha": "6b57065f12f49609f7e3cf37560020526c174ed6",
      "size": 1534,
      "url": "https://api.github.com/repos/getsentry/craft/git/blobs/6b57065f12f49609f7e3cf37560020526c174ed6"
    },
    {
      "path": ".dockerignore",
      "mode": "100644",
      "type": "blob",
      "sha": "139298ee3cf2f055cc968fdb1f0a9fa0ef69506a",
      "size": 130,
      "url": "https://api.github.com/repos/getsentry/craft/git/blobs/139298ee3cf2f055cc968fdb1f0a9fa0ef69506a"
    },
    {
      "path": ".github",
      "mode": "040000",
      "type": "tree",
      "sha": "de42c2404a556912debcffbc4c0e49f9467c90dc",
      "url": "https://api.github.com/repos/getsentry/craft/git/trees/de42c2404a556912debcffbc4c0e49f9467c90dc"
    },
    {
      "path": ...
gateway · 2026-09-10 03:28:55
result
Found 20 matches
/home/byk/.local/share/opencode/tool-output/tool_0895c5cbc001miG2Hcai9Zw57R:
  Line 1447:       "path": "src/utils/__fixtures__/workspaces",

  Line 1454:       "path": "src/utils/__fixtures__/workspaces/no-workspace",

  Line 1461:       "path": "src/utils/__fixtures__/workspaces/no-workspace/package.json",

  Line 1469:       "path": "src/utils/__fixtures__/workspaces/npm-workspace",

  Line 1476:       "path": "src/utils/__fixtures__/workspaces/npm-workspace/package.json",

  Line 1484:       "path": "src/utils/__fixtures__/workspaces/npm-workspace/packages",

  Line 1491:       "path": "src/utils/__fixtures__/workspaces/npm-workspace/packages/pkg-a",

  Line 1498:       "path": "src/utils/__fixtures__/workspaces/npm-workspace/packages/pkg-a/package.json",

  Line 1506:       "path": "src/utils/__fixtures__/workspaces/npm-workspace/packages/pkg-b",

  Line 1513:       "path": "src/utils/__fixtures__/workspaces/npm-workspace/packages/pkg-b/package.json",

  Line ...
gateway · 2026-09-10 03:29:02
result
Found 2 matches
/home/byk/.local/share/opencode/tool-output/tool_0895c5cbc001miG2Hcai9Zw57R:
  Line 1850:       "path": "src/utils/__tests__/workspaces.test.ts",

  Line 2098:       "path": "src/utils/workspaces.ts",
gateway · 2026-09-10 03:29:08
result
import { readFileSync } from 'fs';
import * as path from 'path';
import { load } from 'js-yaml';
import { glob } from 'glob';

import { logger } from '../logger';

/**
 * Check if an error is a "file not found" error
 */
function isNotFoundError(err: unknown): boolean {
  return err instanceof Error && 'code' in err && err.code === 'ENOENT';
}

/** Information about a workspace package */
export interface WorkspacePackage {
  /** The package name from package.json */
  name: string;
  /** Absolute path to the package directory */
  location: string;
  /** Whether the package is private */
  private: boolean;
  /** Whether the package has publishConfig.access set to 'public' */
  hasPublicAccess: boolean;
  /** Dependencies that are also workspace packages */
  workspaceDependencies: string[];
}

/** Result of workspace discovery */
export interface WorkspaceDiscoveryResult {
  /** The type of workspace manager detected */
  type: 'npm' | 'yarn' | 'pnpm' | 'none';
  /** List of ...
gateway · 2026-09-10 03:29:14
result
#!/usr/bin/env node
// Import Sentry instrumentation first, before any other code
import './instrument';

import isCI from 'is-ci';
import yargs from 'yargs';

import { logger, LogLevel } from './logger';
import {
  sanitizeDynamicLinkerEnv,
  warnIfCraftEnvFileExists,
} from './utils/env';
import { envToBool, setGlobals } from './utils/helpers';
import { getPackageVersion } from './utils/version';
import { withTracing } from './utils/tracing';

// Commands
import * as prepare from './commands/prepare';
import * as publish from './commands/publish';
import * as targets from './commands/targets';
import * as config from './commands/config';
import * as artifacts from './commands/artifacts';
import * as changelog from './commands/changelog';

function printVersion(): void {
  if (!process.argv.includes('-v') && !process.argv.includes('--version')) {
    // Print the current version
    logger.debug(`craft ${getPackageVersion()}`);
  }
}

const GLOBAL_BOOLEAN_FLAGS = {
  'no-input': {
   ...
gateway · 2026-09-10 03:29:24
result
craft <command>

Commands:
  craft prepare [NEW-VERSION]  🚢 Prepare a new release branch
                          [aliases: p, prerelease, prepublish, prepare, release]
  craft publish NEW-VERSION    🛫 Publish artifacts       [aliases: pp, publish]
  craft targets                List defined targets as JSON array
  craft config                 Print the parsed, processed, and validated Craft
                               config for the current project in pretty-JSON.
  craft artifacts <command>    📦 Manage artifacts        [aliases: a, artifact]
  craft changelog              Generate changelog from git history

Options:
      --no-input   Suppresses all user prompts                  [default: false]
      --dry-run    Dry run mode: no file writes, commits, pushes, or API
                   mutations
      --log-level  Logging level
          [choices: "Fatal", "Error", "Warn", "Log", "Info", "Success", "Debug",
                                 "Trace", "Silent", "Verbose"] ...
gateway · 2026-09-10 03:29:35
result
craft <command>

Commands:
  craft prepare [NEW-VERSION]  🚢 Prepare a new release branch
                          [aliases: p, prerelease, prepublish, prepare, release]
  craft publish NEW-VERSION    🛫 Publish artifacts       [aliases: pp, publish]
  craft targets                List defined targets as JSON array
  craft config                 Print the parsed, processed, and validated Craft
                               config for the current project in pretty-JSON.
  craft artifacts <command>    📦 Manage artifacts        [aliases: a, artifact]
  craft changelog              Generate changelog from git history

Options:
      --no-input   Suppresses all user prompts                  [default: false]
      --dry-run    Dry run mode: no file writes, commits, pushes, or API
                   mutations
      --log-level  Logging level
          [choices: "Fatal", "Error", "Warn", "Log", "Info", "Success", "Debug",
                                 "Trace", "Silent", "Verbose"] ...
gateway · 2026-09-10 03:29:49

The pinned image is immutable, but it does not implement the command the new discovery path invokes. The exact digest resolves locally to Craft 2.30.1 (CRAFT_BUILD_SHA=cd1e8294061fd970b40d98b77aaa109cb1e00e78), and craft workspace list exits with “Unknown commands: workspace, list”; every target checkout with a root .craft.yml will fail before publication.

gpt-5.6-sol · 2026-09-10 03:29:50 · ~94 tokens
result
import { Arguments, Argv, CommandBuilder } from 'yargs';
import chalk from 'chalk';
import { existsSync, mkdirSync, readFileSync } from 'fs';

import { safeFs } from '../utils/dryRun';
import { dirname, join } from 'path';
import * as shellQuote from 'shell-quote';
import stringLength from 'string-length';

import {
  getConfiguration,
  getStatusProviderFromConfig,
  getArtifactProviderFromConfig,
  DEFAULT_RELEASE_BRANCH_NAME,
  getGlobalGitHubConfig,
  expandWorkspaceTargets,
  getNoMergeConfig,
} from '../config';
import { formatTable, logger } from '../logger';
import { TargetConfig } from '../schemas/project_config';
import { getAllTargetNames, getTargetByName, SpecialTarget } from '../targets';
import { BaseTarget } from '../targets/base';
import {
  ConfigurationError,
  handleGlobalError,
  reportError,
} from '../utils/errors';
import { withTempDir } from '../utils/files';
import { stringToRegexp } from '../utils/filters';
import { promptConfirmation } from ...
gateway · 2026-09-10 03:29:59
result
/**
 * Helpers for locating Craft's publish-state file in a path that is NOT
 * writable by the repository being published.
 *
 * Background: `craft publish` writes a small JSON file listing targets
 * that have completed so a resumed run can skip them. Before this module
 * existed, the file lived at `.craft-publish-<version>.json` in the
 * project's cwd. That path is inside the repository checkout, so any
 * committed file at the same path (or any earlier CI step) could
 * pre-populate the "published" set and trick Craft into silently
 * skipping targets.
 *
 * The file now lives under `$XDG_STATE_HOME/craft/` (falling back to
 * `$HOME/.local/state/craft/`). The filename is keyed on
 * owner, repo, a hash of cwd (to disambiguate monorepo subpaths), and
 * the version being published. `getsentry/publish` runs inside a Docker
 * image with `HOME=/root`, so the XDG state dir is a clean,
 * workflow-writable location that committed repo contents cannot reach. ...
gateway · 2026-09-10 03:30:15
result
/usr/local/bin/craft
/usr/local/bin/craft
gateway · 2026-09-10 03:30:24
result
-rwxr-xr-x 1 root root 9689940 Aug  8 00:33 /usr/local/bin/craft
gateway · 2026-09-10 03:30:37
result
import { existsSync, lstatSync, readFileSync } from 'fs';
import path from 'path';

import { load } from 'js-yaml';
import GitUrlParse from 'git-url-parse';
import { createGitClient } from './utils/git';
import { ZodError } from 'zod';

import { logger } from './logger';
import {
  CraftProjectConfig,
  CraftProjectConfigSchema,
  GitHubGlobalConfig,
  ArtifactProviderName,
  StatusProviderName,
  TargetConfig,
  ChangelogPolicy,
  VersioningPolicy,
} from './schemas/project_config';
import { ConfigurationError } from './utils/errors';
import { isCompiledGitHubAction } from './utils/detection';
import {
  getPackageVersion,
  parseVersion,
  versionGreaterOrEqualThan,
} from './utils/version';
// Note: We import getTargetByName lazily in expandWorkspaceTargets to avoid
// circular dependency: config -> targets -> registry -> utils/registry -> symlink -> version -> config
import { BaseArtifactProvider } from './artifact_providers/base';
import { GitHubArtifactProvider } from ...
gateway · 2026-09-10 03:31:39
result
diff --git a/src/modules/ci-poller-input.js b/src/modules/ci-poller-input.js
index ca08a72..bf2d23d 100644
--- a/src/modules/ci-poller-input.js
+++ b/src/modules/ci-poller-input.js
@@ -8,8 +8,9 @@ const {
   isReleaseVersion,
 } = require("./publish-issue-validation");
 const { isPublishPath } = require("./publish-location");
+const { requestDigest } = require("./approval-attestation");
 
-function getCiPollerInput({ title, issueBody, revision }) {
+function getCiPollerInput({ title, issueBody, labels, revision }) {
   let parsedTitle;
   try {
     parsedTitle = parsePublishIssueTitle(title);
@@ -28,12 +29,14 @@ function getCiPollerInput({ title, issueBody, revision }) {
     throw new Error(`Invalid publish issue path: '${path}'`);
   }
   const currentRevision = getReleaseRevision({ issueBody, repo });
+  const resolvedIssueBody = revision
+    ? updateReleaseRevision({ issueBody, repo, revision })
+    : issueBody;
 
   return {
-    ...(revision
-      ? ...
gateway · 2026-09-10 03:32:08
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 03:32:14
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 03:32:18
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 03:32:23
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 03:32:31
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 03:32:36
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 03:32:41
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 03:32:45
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 03:33:45
result
{
  "sha": "d23441a48e516b6c34aea4fa41551a30e30af803",
  "node_id": "C_kwDOC8ppZdoAKGQyMzQ0MWE0OGU1MTZiNmMzNGFlYTRmYTQxNTUxYTMwZTMwYWY4MDM",
  "commit": {
    "author": {
      "name": "Aiqiao Yan",
      "email": "55104035+aiqiaoy@users.noreply.github.com",
      "date": "2026-07-16T19:43:33Z"
    },
    "committer": {
      "name": "GitHub",
      "email": "noreply@github.com",
      "date": "2026-07-16T19:43:33Z"
    },
    "message": "backport fixes to releases-v6 (#2527)",
    "tree": {
      "sha": "f8632066738fbaeb50e02febf46852ea4f048e40",
      "url": "https://api.github.com/repos/actions/checkout/git/trees/f8632066738fbaeb50e02febf46852ea4f048e40"
    },
    "url": "https://api.github.com/repos/actions/checkout/git/commits/d23441a48e516b6c34aea4fa41551a30e30af803",
    "comment_count": 0,
    "verification": {
      "verified": true,
      "reason": "valid",
      "signature": "-----BEGIN PGP ...
gateway · 2026-09-10 03:33:52
result
{
  "sha": "3d3c42e5aac5ba805825da76410c181273ba90b1",
  "node_id": "C_kwDOC8ppZdoAKDNkM2M0MmU1YWFjNWJhODA1ODI1ZGE3NjQxMGMxODEyNzNiYTkwYjE",
  "commit": {
    "author": {
      "name": "Aiqiao Yan",
      "email": "55104035+aiqiaoy@users.noreply.github.com",
      "date": "2026-07-17T18:45:11Z"
    },
    "committer": {
      "name": "GitHub",
      "email": "noreply@github.com",
      "date": "2026-07-17T18:45:11Z"
    },
    "message": "prep v7.0.1 release (#2531)\n\n* prep v7.0.1 release\n\n* Potential fix for pull request finding\n\nCo-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>\n\n---------\n\nCo-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>",
    "tree": {
      "sha": "150e70ecad5ebf06a559a01ea3a56d9a3236d2b1",
      "url": "https://api.github.com/repos/actions/checkout/git/trees/150e70ecad5ebf06a559a01ea3a56d9a3236d2b1"
    },
    "url": ...
gateway · 2026-09-10 03:33:59
result
{
  "sha": "11d5960a326750d5838078e36cf38b85af677262",
  "node_id": "C_kwDOC8ppZdoAKDExZDU5NjBhMzI2NzUwZDU4MzgwNzhlMzZjZjM4Yjg1YWY2NzcyNjI",
  "commit": {
    "author": {
      "name": "Aiqiao Yan",
      "email": "55104035+aiqiaoy@users.noreply.github.com",
      "date": "2026-07-16T19:43:47Z"
    },
    "committer": {
      "name": "GitHub",
      "email": "noreply@github.com",
      "date": "2026-07-16T19:43:47Z"
    },
    "message": "backport fixes to releases-v4 (#2524)",
    "tree": {
      "sha": "f8a7b72dc00648d050099727d25ca92a43ad1162",
      "url": "https://api.github.com/repos/actions/checkout/git/trees/f8a7b72dc00648d050099727d25ca92a43ad1162"
    },
    "url": "https://api.github.com/repos/actions/checkout/git/commits/11d5960a326750d5838078e36cf38b85af677262",
    "comment_count": 0,
    "verification": {
      "verified": true,
      "reason": "valid",
      "signature": "-----BEGIN PGP ...
gateway · 2026-09-10 03:36:47
result
No files found
gateway · 2026-09-10 03:36:55
result
diff --git a/AGENTS.md b/AGENTS.md
index cf473df..51bf3a8 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -273,3 +273,9 @@ This codebase interacts heavily with GitHub Actions. Entry points in `src/publis
 ## Code Owners
 
 All code is owned by `@getsentry/releng` (Release Engineering team).
+
+<!-- This section is maintained by the coding agent via lore (https://github.com/BYK/loreai) -->
+## Long-term Knowledge
+
+For long-term knowledge entries managed by [lore](https://github.com/BYK/loreai) (gotchas, patterns, decisions, architecture), see [`.lore.md`](.lore.md) in the project root.
+<!-- End lore-managed section -->
diff --git a/README.md b/README.md
index 4cbc9b9..82bc3f5 100644
--- a/README.md
+++ b/README.md
@@ -99,6 +99,8 @@ Automatic approvals are managed in the [`auto-approve.yml`](https://github.com/g
 
 The system uses [Craft](https://github.com/getsentry/craft) under the hood to prepare and publish releases. It uses tokens from [Sentry Release ...
gateway · 2026-09-10 03:37:04

Distillations (12)

Gen 0 2026-09-10 06:40:31 · 991 tokens

Date: Sep 10, 2026 * 🔴 (03:36) An `actions/checkout` commit titled `backport fixes to releases-v4 (#2524)`, authored by Aiqiao Yan (`aiqiaoy`) and committed by GitHub (`web-flow`), is verified and reports 163 total changes with 19 deletions; its author timestamp is `2026-07-16T19:43:47Z` and parent is `c915c33a16f01166c17c4e35fe1d4085a2d71adb`. (meaning Jul 16, 2026) * 🔴 (03:36) The `releases-v…

Gen 0 2026-09-10 06:37:11 · 920 tokens

* 🔴 (03:33) `src/publish/post-result.js` imports `processEndState` from `../modules/process-end-state`, obtains `context` from `github.context`, creates `octokit` with `github.getOctokit(getGitHubToken())`, parses required `process.env.PUBLISH_ARGS` with `JSON.parse`, takes `status` from `process.argv.slice(2)[0]`, and calls `processEndState({ context, octokit, inputs, status })` without awaitin…

Gen 0 2026-09-10 06:33:02 · 2127 tokens

Date: September 10, 2026 * 🔴 (03:31) `src/config.ts` defines `CONFIG_FILE_NAME = '.craft.yml'` and `DEFAULT_RELEASE_BRANCH_NAME = 'release'`; `getConfigFilePath()` throws `ConfigurationError` with `Cannot find Craft configuration file. Have you added ".craft.yml" to your project?` when discovery fails. * 🔴 (03:31) `src/config.ts` caches parsed configuration in `_configCache`; `getConfiguration(…

Gen 0 2026-09-10 06:26:59 · 1130 tokens

Date: September 10, 2026 * 🔴 (03:29) User directed the workspace template-to-regex logic to “Replace template markers with placeholders.” * 🔴 (03:29) User directed the workspace template-to-regex logic to “Replace placeholders with escaped values (or regex pattern for version).” * 🔴 (03:29) Workspace regex-template handling uses `NAME_PLACEHOLDER = '\x00NAME\x00'`, `SIMPLE_PLACEHOLDER = '\x00S…

Gen 0 2026-09-10 06:19:59 · 75 tokens

Date: September 10, 2026 * 🔴 (03:28) Repository tree includes `build.mjs`, `img/logo.svg`, `src/logger.ts`, and the `src/targets` directory. * 🔴 (03:28) Repository target implementations include `src/targets/nuget.ts` and `src/targets/upm.ts`.

Gen 0 2026-09-10 06:16:01 · 1061 tokens

🔴 (03:28) `/home/byk/Code/getsentry/publish/src/modules/__tests__/ci-poller-workflow.js` extracts the shell script from `.github/workflows/ci-poller.yml` between workflow steps `Check CI status for ci-pending issues` and `Check for remaining pending issues`, then executes it with `bash -e -o pipefail -c`. 🔴 (03:28) `ci-poller-workflow.js` defines `INITIAL_REVISION = "aaaaaaaaaaaaaaaaaaaaaaaaaaa…

Gen 0 2026-09-10 06:15:17 · 746 tokens

🔴 (03:27) `/home/byk/Code/getsentry/publish/src/publish/__tests__/workflow-action-pinning.js` enumerates every `.yml` file in `.github/workflows` and requires every `uses: actions/...@revision` reference to use an exact lowercase 40-character hexadecimal revision matching `/^[a-f0-9]{40}$/`. 🔴 (03:27) `workflow-action-pinning.js` prohibits `workflow_dispatch:` in every workflow except `.github/…

Gen 0 2026-09-10 06:14:49 · 2382 tokens

🔴 (03:26) An expanded action-pin search found 18 uses across `.github/workflows/test.yml`, `.github/workflows/publish.yml`, `.github/workflows/auto-approve.yml`, and `.github/workflows/ci-poller.yml`; newly surfaced entries include `.github/workflows/publish.yml` line 268 using `actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1` and line 367 using `docker://getsentry/craft@sha256:9a4a5d5…

Gen 0 2026-09-10 06:09:34 · 1537 tokens

🔴 (03:25) User directed: Always allow `workflow_dispatch` for manual recovery. 🔴 (03:25) User directed: Always run trusted code; because `workflow_dispatch` can target any ref, the CI poller’s protected manual-recovery path must execute code checked out from a trusted ref rather than arbitrary dispatched-ref code. 🔴 (03:25) User directed: Never move a release to `ci-ready` after it changes. 🔴…

Gen 0 2026-09-10 06:02:25 · 2661 tokens

🔴 (03:22) `src/modules/release-revision.js` defines `isRevision(revision)` as `/^[0-9a-f]{40}$/`; `updateReleaseRevision({ issueBody, repo, revision })` rejects anything other than a lowercase 40-character SHA with `Release revision must be a lowercase 40-character SHA.` and replaces only the parsed revision range in the issue body. 🔴 (03:22) `src/modules/release-revision.js` requires exactly o…

Gen 0 2026-09-10 06:00:52 · 1589 tokens

🔴 (03:22) The combined tracked diff covered exactly 32 files with 662 insertions and 183 deletions. 🔴 (03:22) User stated the publish workflow’s poller always adds `ci-ready`; the publish job fires only on `ci-ready` label events, not `accepted`, to avoid racing with `waiting-for-ci`. 🔴 (03:22) User stated `workflow_dispatch` can target any ref and directed the CI poller to “Always run trusted…

Gen 0 2026-09-10 05:54:52 · 1655 tokens

🟡 (03:20) [requested-review] User requested an independent, read-only adversarial correctness and security review of the exact current uncommitted tree at `/home/byk/Code/getsentry/publish`; instructed not to edit files and to inspect all relevant changed workflow, source, and test files. 🟡 (03:20) User scoped the review to: release input parsing; immutable request/revision identity; target che…