DashboardpublishSession 1ePxfti6wPsv

Session 1ePxfti6wPsv

Full ID: 1ePxfti6wPsvdBc9V
Messages: 38
Distillations: 5
Time range: 2026-08-28 19:05:39 — 2026-08-28 19:48:35

Conversation (38 messages)

Perform a security review of the current uncommitted approval-gate changes in /home/byk/Code/getsentry/publish. Do not modify files. Examine authorization bypasses, mutable issue title/repository binding, acceptance label event binding, bot provenance, token privilege boundaries, workflow step failure/cleanup behavior, label-event races, and external input injection. Return a substantive report with file:line citations. Classify every observation as PASS, CONCERN, or MUST-FIX, and conclude exactly with MERGE or DO-NOT-MERGE. Never return an empty report.

gateway · 2026-08-28 19:05:39
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-08-28 19:07:02
result
 M .github/workflows/ci-poller.yml
 M .github/workflows/publish.yml
 M AGENTS.md
 M src/libs/__tests__/github.js
 M src/libs/github.js
 M src/modules/__tests__/details-from-context.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
?? .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__/
?? src/publish/authorize-approval.js
?? src/publish/validate-approval-attestation.js
 .github/workflows/ci-poller.yml               | 54 +++++++++++++--
 .github/workflows/publish.yml                 | 96 +++++++++++++++++++++++++++
 AGENTS.md                                     |  6 ++
 src/libs/__tests__/github.js               ...
gateway · 2026-08-28 19:07:08
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-08-28 19:07:10
result
<path>/home/byk/Code/getsentry/publish/.github/workflows/publish.yml</path>
<type>file</type>
<content>
351:           # cannot do other cross-repo operations like our Release Bot App
352:           # Thanks GitHub
353:           DOCKER_GHCR_IO_USERNAME: x-access-token # for ghcr.io auth
354:           DOCKER_GHCR_IO_PASSWORD: ${{ secrets.GITHUB_TOKEN }} # for ghcr.io auth
355:           GITHUB_API_TOKEN: ${{ steps.token.outputs.token }} # for commit-on-repo token
356:           CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
357:           COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
358:           CRAFT_GCS_TARGET_CREDS_JSON: ${{ secrets.CRAFT_GCS_TARGET_CREDS_JSON }}
359:           CRAFT_GCS_STORE_CREDS_JSON: ${{ secrets.CRAFT_GCS_STORE_CREDS_JSON }}
360:           CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
361:           DOCKER_USERNAME: sentrybuilder
362:           DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
363:           HEX_API_KEY: ${{ ...
gateway · 2026-08-28 19:07:42
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:   workflow_dispatch:
7:     inputs:
8:       attempt:
9:         description: Self-dispatch attempt counter (internal use)
10:         required: false
11:         default: "0"
12: 
13: permissions:
14:   contents: read
15:   issues: read
16: 
17: jobs:
18:   check-ci:
19:     runs-on: ubuntu-latest
20:     environment: production
21:     # Skip entirely (no runner provisioned) when there's nothing to check.
22:     # Set to "true" by ci-pending.yml, reset to "false" here when done.
23:     # Always allow workflow_dispatch for manual recovery.
24:     if: vars.CI_POLLER_HAS_PENDING == 'true' || github.event_name == 'workflow_dispatch'
25:     concurrency:
26:       group: ci-status-poller
27:       cancel-in-progress: false
28:     steps:
29:       - name: Get publish code
30:         uses: ...
gateway · 2026-08-28 19:08:21
result
<path>/home/byk/Code/getsentry/publish/src/modules/approval-attestation.js</path>
<type>file</type>
<content>
1: const ATTESTATION_PREFIX = "<!-- publish-approval ";
2: const ATTESTATION_SUFFIX = " -->";
3: 
4: function createApprovalAttestation({ actor, eventId, title }) {
5:   const value = Buffer.from(
6:     JSON.stringify({ actor, eventId: String(eventId), title })
7:   ).toString("base64url");
8: 
9:   return `${ATTESTATION_PREFIX}${value}${ATTESTATION_SUFFIX}`;
10: }
11: 
12: function parseApprovalAttestation(body) {
13:   const start = body.indexOf(ATTESTATION_PREFIX);
14: 
15:   if (start === -1) {
16:     return null;
17:   }
18: 
19:   const end = body.indexOf(ATTESTATION_SUFFIX, start);
20: 
21:   if (end === -1) {
22:     return null;
23:   }
24: 
25:   try {
26:     const value = JSON.parse(
27:       Buffer.from(
28:         body.slice(start + ATTESTATION_PREFIX.length, end),
29:         "base64url"
30:       ).toString("utf8")
31:     );
32: 
33:     if (
34:       ...
gateway · 2026-08-28 19:08:54
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 AUTO_APPROVER = "getsantry[bot]";
3: const { parsePublishTitle } = require("./details-from-context.js");
4: 
5: async function authorizeApproval({
6:   actor,
7:   issueTitle,
8:   getPermission,
9:   autoApprovedRepositories = new Set(),
10: }) {
11:   const title = parsePublishTitle(issueTitle);
12: 
13:   if (!title) {
14:     return { authorized: false, repository: null };
15:   }
16: 
17:   const repository = `getsentry/${title.repo}`;
18:   const releasePath = `${repository}${title.path || ""}`;
19: 
20:   if (actor === AUTO_APPROVER && autoApprovedRepositories.has(releasePath)) {
21:     return { authorized: true, repository };
22:   }
23: 
24:   if (actor === AUTO_APPROVER) {
25:     return { authorized: false, repository };
26:   }
27: 
28:   const { role_name } = await getPermission({
29:  ...
gateway · 2026-08-28 19:09:34
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: } = require("../modules/approval-attestation.js");
6: const { authorizeApproval } = require("../modules/approval-authorizer.js");
7: 
8: function getAutoApprovedRepositories() {
9:   return new Set(
10:     fs
11:       .readFileSync("auto-approve-repos.txt", "utf8")
12:       .split(/\r?\n/)
13:       .filter(Boolean)
14:   );
15: }
16: 
17: async function getPermission({ owner, repository, username }) {
18:   const response = await getGitHubResponse(
19:     `repos/${encodeURIComponent(owner)}/${encodeURIComponent(
20:       repository
21:     )}/collaborators/${encodeURIComponent(username)}/permission`
22:   );
23: 
24:   if (!response.ok) {
25:     throw new Error(
26:       `Could not retrieve ${username}'s permission for ${owner}/${repository}: GitHub returned ...
gateway · 2026-08-28 19:09:37
result
<path>/home/byk/Code/getsentry/publish/src/publish/validate-approval-attestation.js</path>
<type>file</type>
<content>
1: const {
2:   currentAcceptedEvent,
3:   hasApprovalAttestation,
4: } = require("../modules/approval-attestation.js");
5: 
6: async function getGitHubResponse(path) {
7:   return fetch(`https://api.github.com/${path}`, {
8:     headers: {
9:       Accept: "application/vnd.github+json",
10:       Authorization: `Bearer ${process.env.APPROVAL_TOKEN}`,
11:       "X-GitHub-Api-Version": "2026-03-10",
12:     },
13:   });
14: }
15: 
16: async function getIssue({ repository, issueNumber }) {
17:   const response = await getGitHubResponse(
18:     `repos/${repository}/issues/${issueNumber}`
19:   );
20: 
21:   if (!response.ok) {
22:     throw new Error(
23:       `Could not retrieve issue #${issueNumber}: GitHub returned ${response.status}`
24:     );
25:   }
26: 
27:   return response.json();
28: }
29: 
30: async function getAllPages({ repository, issueNumber, resource ...
gateway · 2026-08-28 19:10:08
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:   currentAcceptedEvent,
6:   hasApprovalAttestation,
7:   parseApprovalAttestation,
8: } = require("../approval-attestation.js");
9: 
10: describe("approval attestations", () => {
11:   test("matches the latest accepted event to a trusted attestation", () => {
12:     const attestation = createApprovalAttestation({
13:       actor: "contractor",
14:       eventId: "200",
15:       title: "publish: getsentry/relay/py@1.2.3",
16:     });
17:     const event = currentAcceptedEvent([
18:       {
19:         actor: { login: "contractor" },
20:         event: "labeled",
21:         id: "100",
22:         label: { name: "accepted" },
23:       },
24:       {
25:         actor: { login: "contractor" },
26:         event: "labeled",
27:         id: "200",
28:         label: { ...
gateway · 2026-08-28 19:10:48
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("allows the auto-approval bot for an allowlisted release without a target repository lookup", async () => {
9:     const getPermission = vi.fn();
10: 
11:     await expect(
12:       authorizeApproval({
13:         actor: "getsantry[bot]",
14:         issueTitle,
15:         getPermission,
16:         autoApprovedRepositories: new Set(["getsentry/sentry-javascript"]),
17:       })
18:     ).resolves.toEqual({
19:       authorized: true,
20:       repository: "getsentry/sentry-javascript",
21:     });
22: 
23:     expect(getPermission).not.toHaveBeenCalled();
24:   });
25: 
26:   test("rejects the auto-approval bot ...
gateway · 2026-08-28 19:11:26
result
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__</path>
<type>directory</type>
<entries>
authorize-approval.js
validate-approval-attestation.js

(2 entries)
</entries>
gateway · 2026-08-28 19:12:04
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:   getAutoApprovedRepositories,
5:   main,
6: } = require("../authorize-approval.js");
7: 
8: const originalEnvironment = { ...process.env };
9: 
10: afterEach(() => {
11:   process.env = { ...originalEnvironment };
12:   vi.restoreAllMocks();
13:   vi.resetModules();
14: });
15: 
16: function jsonResponse(json) {
17:   return { ok: true, json: vi.fn().mockResolvedValue(json) };
18: }
19: 
20: async function runAuthorization({ actor, issueTitle, responses }) {
21:   process.env.GITHUB_OUTPUT = "/tmp/github-output";
22:   process.env.APPROVAL_TOKEN = "release-bot-token";
23:   process.env.APPROVAL_ACTOR = actor;
24:   process.env.APPROVAL_ISSUE_NUMBER = "123";
25:   process.env.APPROVAL_ISSUE_REPOSITORY = "getsentry/publish";
26:   process.env.APPROVAL_ISSUE_TITLE = ...
gateway · 2026-08-28 19:12:11
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: } = require("../../modules/approval-attestation.js");
9: 
10: afterEach(() => {
11:   vi.restoreAllMocks();
12: });
13: 
14: function jsonResponse(json) {
15:   return { ok: true, json: vi.fn().mockResolvedValue(json) };
16: }
17: 
18: describe("validateApprovalAttestation", () => {
19:   test("accepts the current accepted event and its trusted attestation", async () => {
20:     const title = "publish: getsentry/sentry-javascript@10.0.0";
21:     const attestation = createApprovalAttestation({
22:       actor: "contractor",
23:       eventId: "100",
24:       title,
25:     });
26:     global.fetch = vi
27:       .fn()
28:       ...
gateway · 2026-08-28 19:12:14
result
/home/byk/Code/getsentry/publish/.github/workflows/publish.yml
/home/byk/Code/getsentry/publish/.github/workflows/ci-poller.yml
/home/byk/Code/getsentry/publish/.github/workflows/auto-approve.yml
/home/byk/Code/getsentry/publish/.github/workflows/test.yml
/home/byk/Code/getsentry/publish/.github/workflows/cocoapods-keepalive.yml
gateway · 2026-08-28 19:12:19
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: 
9: jobs:
10:   auto-approve:
11:     runs-on: ubuntu-latest
12:     if: |
13:       (github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') &&
14:       startsWith(github.event.issue.title, 'publish: ')
15:     steps:
16:       - uses: actions/checkout@v4
17:         with:
18:           sparse-checkout: auto-approve-repos.txt
19:           sparse-checkout-cone-mode: false
20:       - name: Get auth token
21:         id: token
22:         uses: actions/create-github-app-token@v3
23:         with:
24:           client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
25:           private-key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }}
26:       - name: Auto-approve if repo is in the list
27:         env:
28:           GH_TOKEN: ${{ ...
gateway · 2026-08-28 19:12:55
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@v6
20:       - uses: actions/setup-node@v6
21:         with:
22:           node-version: 24
23:       - uses: actions/cache@v4
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:         run: yarn test

(End of file - total 34 lines)
</content>
gateway · 2026-08-28 19:14:43
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-08-28 19:17:36
result
yarn run v1.22.22
$ vitest run

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

 ✓ src/publish/__tests__/validate-approval-attestation.js (4 tests) 41ms
 ✓ src/modules/__tests__/approval-authorizer.js (14 tests) 33ms
 ✓ src/modules/__tests__/post-workflow-details.js (1 test) 24ms
 ✓ src/publish/__tests__/authorize-approval.js (3 tests) 32ms
 ✓ src/modules/__tests__/process-end-state.js (4 tests) 71ms
 ✓ src/modules/__tests__/update-issue.js (5 tests) 12ms
 ✓ src/modules/__tests__/details-from-context.js (5 tests) 16ms
 ✓ ...
gateway · 2026-08-28 19:27:37
result
Found 22 matches
/home/byk/Code/getsentry/publish/docs/rfc.md:
  Line 10: 3. We need SDK releases to be approved by a manager or senior engineer per our SOC/ISO compliance policy

  Line 23: 6. The owners of the release repo adds an "accepted" tag to trigger the actual release

  Line 25: 8. Special case: OSS CalVer releases are done automatically and get automatically approved unless there is a release blocker.

  Line 30: 2. Should release repo owners still need to create an issue to trigger a release, even if they mark it as approved themselves? (or should we always require a second person to approve, like a code review)

  Line 32: 4. Should we restrict who can approve what release? How?


/home/byk/Code/getsentry/publish/docs/key_rotation_vault_changes.md:
  Line 13: The actual publish via Craft however still requires access to the credentials. ...
gateway · 2026-08-28 19:28:44
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-08-28 19:34:22
result
<path>/home/byk/Code/getsentry/publish/src/modules/update-issue.js</path>
<type>file</type>
<content>
55:         )
56:         .join("\n") + "\n";
57:     targetsText += `\n${unlistedTargets}\n`;
58:     return targetsText;
59:   });
60: }
61: 
62: async function updateIssue({ context, octokit, inputs }) {
63:   const { version } = inputs;
64:   const { repo: publishRepo } = context;
65:   const { number: issue_number } = context.payload.issue;
66: 
67:   await Promise.all([
68:     updateTargets({ octokit, version, publishRepo, issue_number }),
69:     octokit.rest.issues.removeLabel({
70:       ...publishRepo,
71:       issue_number,
72:       name: "accepted",
73:     }),
74:   ]);
75: }
76: 
77: module.exports = { updateIssue, transformIssueBody };

(End of file - total 77 lines)
</content>
gateway · 2026-08-28 19:41:05
result
yarn run v1.22.22
$ eslint src .github --ignore-pattern '!.github'

/home/byk/Code/getsentry/publish/.github/workflows/cocoapods-keepalive.yml
  1:7  error  Must use plain style scalar  yml/plain-scalar

✖ 1 problem (1 error, 0 warnings)
  1 error and 0 warnings potentially fixable with the `--fix` option.

error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
gateway · 2026-08-28 19:41:19
result
File not found: /home/byk/Code/getsentry/publish/.jj
gateway · 2026-08-28 19:41:24
result
yarn run v1.22.22
$ vitest run

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

 ✓ src/modules/__tests__/approval-attestation.js (3 tests) 17ms
 ✓ src/publish/__tests__/validate-approval-attestation.js (4 tests) 32ms
 ✓ src/modules/__tests__/approval-authorizer.js (14 tests) 15ms
 ✓ src/publish/__tests__/authorize-approval.js (3 tests) 21ms
 ✓ src/modules/__tests__/post-workflow-details.js (1 test) 22ms
 ✓ src/modules/__tests__/details-from-context.js (5 tests) 8ms
 ✓ src/modules/__tests__/process-end-state.js (4 tests) 52ms
 ✓ ...
gateway · 2026-08-28 19:42:16
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: 
9: jobs:
10:   auto-approve:
11:     runs-on: ubuntu-latest
12:     if: |
13:       (github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') &&
14:       startsWith(github.event.issue.title, 'publish: ')
15:     steps:
16:       - uses: actions/checkout@v4
17:         with:
18:           sparse-checkout: auto-approve-repos.txt
19:           sparse-checkout-cone-mode: false
20:       - name: Get auth token
21:         id: token
22:         uses: actions/create-github-app-token@v3
23:         with:
24:           client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
25:           private-key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }}
26:       - name: Auto-approve if repo is in the list
27:         env:
28:           GH_TOKEN: ${{ ...
gateway · 2026-08-28 19:42:21
result
File not found: /home/byk/Code/getsentry/publish/.github/workflows/ci-pending.yml
gateway · 2026-08-28 19:42:25
result
<path>/home/byk/Code/getsentry/publish/.github/workflows</path>
<type>directory</type>
<entries>
auto-approve.yml
ci-poller.yml
cocoapods-keepalive.yml
publish.yml
test.yml

(5 entries)
</entries>
gateway · 2026-08-28 19:42:30
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:   workflow_dispatch:
7:     inputs:
8:       attempt:
9:         description: Self-dispatch attempt counter (internal use)
10:         required: false
11:         default: "0"
12: 
13: permissions:
14:   contents: read
15:   issues: read
16: 
17: jobs:
18:   check-ci:
19:     runs-on: ubuntu-latest
20:     environment: production
21:     # Skip entirely (no runner provisioned) when there's nothing to check.
22:     # Set to "true" by ci-pending.yml, reset to "false" here when done.
23:     # Always allow workflow_dispatch for manual recovery.
24:     if: vars.CI_POLLER_HAS_PENDING == 'true' || github.event_name == 'workflow_dispatch'
25:     concurrency:
26:       group: ci-status-poller
27:       cancel-in-progress: false
28:     steps:
29:       - name: Get publish code
30:         uses: ...
gateway · 2026-08-28 19:42:36
result
<path>/home/byk/Code/getsentry/publish/src/publish/validate-approval-attestation.js</path>
<type>file</type>
<content>
1: const {
2:   currentAcceptedEvent,
3:   hasApprovalAttestation,
4: } = require("../modules/approval-attestation.js");
5: 
6: async function getGitHubResponse(path) {
7:   return fetch(`https://api.github.com/${path}`, {
8:     headers: {
9:       Accept: "application/vnd.github+json",
10:       Authorization: `Bearer ${process.env.APPROVAL_TOKEN}`,
11:       "X-GitHub-Api-Version": "2026-03-10",
12:     },
13:   });
14: }
15: 
16: async function getIssue({ repository, issueNumber }) {
17:   const response = await getGitHubResponse(
18:     `repos/${repository}/issues/${issueNumber}`
19:   );
20: 
21:   if (!response.ok) {
22:     throw new Error(
23:       `Could not retrieve issue #${issueNumber}: GitHub returned ${response.status}`
24:     );
25:   }
26: 
27:   return response.json();
28: }
29: 
30: async function getAllPages({ repository, issueNumber, resource ...
gateway · 2026-08-28 19:42:40
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: } = require("../modules/approval-attestation.js");
6: const { authorizeApproval } = require("../modules/approval-authorizer.js");
7: 
8: function getAutoApprovedRepositories() {
9:   return new Set(
10:     fs
11:       .readFileSync("auto-approve-repos.txt", "utf8")
12:       .split(/\r?\n/)
13:       .filter(Boolean)
14:   );
15: }
16: 
17: async function getPermission({ owner, repository, username }) {
18:   const response = await getGitHubResponse(
19:     `repos/${encodeURIComponent(owner)}/${encodeURIComponent(
20:       repository
21:     )}/collaborators/${encodeURIComponent(username)}/permission`
22:   );
23: 
24:   if (!response.ok) {
25:     throw new Error(
26:       `Could not retrieve ${username}'s permission for ${owner}/${repository}: GitHub returned ...
gateway · 2026-08-28 19:42:44
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-08-28 19:42:55
result
 M .github/workflows/ci-poller.yml
 M .github/workflows/publish.yml
 M AGENTS.md
 M src/libs/__tests__/github.js
 M src/libs/github.js
 M src/modules/__tests__/details-from-context.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
?? .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__/
?? src/publish/authorize-approval.js
?? src/publish/validate-approval-attestation.js
 .github/workflows/ci-poller.yml               | 54 +++++++++++++--
 .github/workflows/publish.yml                 | 96 +++++++++++++++++++++++++++
 AGENTS.md                                     |  6 ++
 src/libs/__tests__/github.js               ...
gateway · 2026-08-28 19:43:06
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 AUTO_APPROVER = "getsantry[bot]";
3: const { parsePublishTitle } = require("./details-from-context.js");
4: 
5: async function authorizeApproval({
6:   actor,
7:   issueTitle,
8:   getPermission,
9:   autoApprovedRepositories = new Set(),
10: }) {
11:   const title = parsePublishTitle(issueTitle);
12: 
13:   if (!title) {
14:     return { authorized: false, repository: null };
15:   }
16: 
17:   const repository = `getsentry/${title.repo}`;
18:   const releasePath = `${repository}${title.path || ""}`;
19: 
20:   if (actor === AUTO_APPROVER && autoApprovedRepositories.has(releasePath)) {
21:     return { authorized: true, repository };
22:   }
23: 
24:   if (actor === AUTO_APPROVER) {
25:     return { authorized: false, repository };
26:   }
27: 
28:   const { role_name } = await getPermission({
29:  ...
gateway · 2026-08-28 19:43:11
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:   currentAcceptedEvent,
6:   hasApprovalAttestation,
7:   parseApprovalAttestation,
8: } = require("../approval-attestation.js");
9: 
10: describe("approval attestations", () => {
11:   test("matches the latest accepted event to a trusted attestation", () => {
12:     const attestation = createApprovalAttestation({
13:       actor: "contractor",
14:       eventId: "200",
15:       title: "publish: getsentry/relay/py@1.2.3",
16:     });
17:     const event = currentAcceptedEvent([
18:       {
19:         actor: { login: "contractor" },
20:         event: "labeled",
21:         id: "100",
22:         label: { name: "accepted" },
23:       },
24:       {
25:         actor: { login: "contractor" },
26:         event: "labeled",
27:         id: "200",
28:         label: { ...
gateway · 2026-08-28 19:45:39
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-08-28 19:48:28

Security review

  • MUST-FIX — Any actor able to manage labels can bypass CI by manually adding ci-ready. The publish job checks only label state, not the actor or provenance of the ci-ready event, and its validator only rechecks approval. .github/workflows/publish.yml:169-185,196-218

  • MUST-FIX — Re-approval can race the poller’s final validation. The poller validates approval, then separately adds ci-ready; a new accepted-label event can receive its attestation before the new waiting job resets labels. The publish job then accepts the new approval without proof that CI passed for it. Bind a CI-ready attestation to the exact accepted-label event and require it before publishing. .github/workflows/ci-poller.yml:234-254, .github/workflows/publish.yml:69-129,196-218

  • CONCERN — Title validation only compares the current title string. A title changed and restored before validation is indistinguishable from an untouched title. If any title mutation must invalidate approval, bind title-change event history or a revision marker. src/publish/validate-approval-attestation.js:59-76

  • PASS — Approval authorization checks the actor’s current target-repository role and only allows write, maintain, or admin; automatic approval is restricted to explicit repository/path entries. src/modules/approval-authorizer.js:1-35

  • PASS — The attestation binds the latest accepted event’s actor and ID, the title, and a comment from github-actions[bot]. src/modules/approval-attestation.js, src/publish/validate-approval-attestation.js:64-76

  • PASS — Authorization and validation fail closed before the credentialed Craft step. .github/workflows/publish.yml:93-111,196-218,326-380

  • PASS — User-controlled title and body values are passed through environment variables or quoted shell expansions; parsed versions exclude shell metacharacters. .github/workflows/ci-poller.yml:119-129, src/modules/details-from-context.js:18-22

  • ...

gpt-5.6-terra · 2026-08-28 19:48:35 · ~523 tokens

Distillations (5)

Gen 0 2026-08-28 19:54:14 · 2081 tokens

Date: Aug 28, 2026 * 🟡 (19:42) `src/publish/validate-approval-attestation.js` defines GitHub REST helpers authenticated with `APPROVAL_TOKEN`, `Accept: application/vnd.github+json`, and `X-GitHub-Api-Version: 2026-03-10`; `getAllPages()` paginates issue `events` or `comments` at `per_page=100` until a page has fewer than 100 records. * 🟡 (19:42) `validateApprovalAttestation({ attestationAuthor,…

Gen 0 2026-08-28 19:45:39 · 1998 tokens

Date: Aug 28, 2026 * 🟡 (19:27) `yarn test` (`vitest run`, Vitest `v4.0.17`) passed: 9 test files and 41 tests passed; duration 1.51s (transform 212ms, import 929ms, tests 269ms). * 🟡 (19:27) Working tree had modified files: `.github/workflows/ci-poller.yml`, `.github/workflows/publish.yml`, `AGENTS.md`, `src/libs/__tests__/github.js`, `src/libs/github.js`, `src/modules/__tests__/details-from-co…

Gen 0 2026-08-28 19:43:29 · 2729 tokens

Date: Aug 28, 2026 * 🟡 (19:09) `src/modules/approval-authorizer.js` defines `ALLOWED_ROLE_NAMES = new Set(["write", "maintain", "admin"])` and `AUTO_APPROVER = "getsantry[bot]"`. * 🟡 (19:09) `authorizeApproval({ actor, issueTitle, getPermission, autoApprovedRepositories = new Set() })` parses `issueTitle` with `parsePublishTitle()`; malformed titles return `{ authorized: false, repository: null…

Gen 0 2026-08-28 19:12:04 · 451 tokens

Date: Aug 28, 2026 * 🟡 (19:08) `src/modules/approval-attestation.js` defines `ATTESTATION_PREFIX = "<!-- publish-approval "` and `ATTESTATION_SUFFIX = " -->"`. * 🟡 (19:08) `createApprovalAttestation({ actor, eventId, title })` serializes `{ actor, eventId: String(eventId), title }` as base64url JSON and returns it wrapped in the `<!-- publish-approval ... -->` HTML-comment format. * 🟡 (19:08) …

Gen 0 2026-08-28 19:11:28 · 1549 tokens

Date: Aug 28, 2026 * 🔴 [requested-security-review] (19:05) User requested a security review of current uncommitted approval-gate changes in `/home/byk/Code/getsentry/publish`; instructed not to modify files. Requested examination of authorization bypasses, mutable issue title/repository binding, acceptance-label event binding, bot provenance, token privilege boundaries, workflow step failure/cle…