Dashboard › publish › Session 1ePxfti6wPsv
1ePxfti6wPsvdBc9VPerform 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.
<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>
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 ...
<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 ...
<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: ${{ ...
<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: ...
<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: ...
<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: ...
<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 ...
<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 ...
<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: { ...
<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 ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__</path> <type>directory</type> <entries> authorize-approval.js validate-approval-attestation.js (2 entries) </entries>
<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 = ...
<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: ...
/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
<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: ${{ ...
<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>
<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: ...
yarn run v1.22.22 $ vitest run [1m[46m RUN [49m[22m [36mv4.0.17 [39m[90m/home/byk/Code/getsentry/publish[39m [32m✓[39m src/publish/__tests__/validate-approval-attestation.js [2m([22m[2m4 tests[22m[2m)[22m[32m 41[2mms[22m[39m [32m✓[39m src/modules/__tests__/approval-authorizer.js [2m([22m[2m14 tests[22m[2m)[22m[32m 33[2mms[22m[39m [32m✓[39m src/modules/__tests__/post-workflow-details.js [2m([22m[2m1 test[22m[2m)[22m[32m 24[2mms[22m[39m [32m✓[39m src/publish/__tests__/authorize-approval.js [2m([22m[2m3 tests[22m[2m)[22m[32m 32[2mms[22m[39m [32m✓[39m src/modules/__tests__/process-end-state.js [2m([22m[2m4 tests[22m[2m)[22m[32m 71[2mms[22m[39m [32m✓[39m src/modules/__tests__/update-issue.js [2m([22m[2m5 tests[22m[2m)[22m[32m 12[2mms[22m[39m [32m✓[39m src/modules/__tests__/details-from-context.js [2m([22m[2m5 tests[22m[2m)[22m[32m 16[2mms[22m[39m [32m✓[39m ...
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. ...
<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: ...
<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>
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.
File not found: /home/byk/Code/getsentry/publish/.jj
yarn run v1.22.22 $ vitest run [1m[46m RUN [49m[22m [36mv4.0.17 [39m[90m/home/byk/Code/getsentry/publish[39m [32m✓[39m src/modules/__tests__/approval-attestation.js [2m([22m[2m3 tests[22m[2m)[22m[32m 17[2mms[22m[39m [32m✓[39m src/publish/__tests__/validate-approval-attestation.js [2m([22m[2m4 tests[22m[2m)[22m[32m 32[2mms[22m[39m [32m✓[39m src/modules/__tests__/approval-authorizer.js [2m([22m[2m14 tests[22m[2m)[22m[32m 15[2mms[22m[39m [32m✓[39m src/publish/__tests__/authorize-approval.js [2m([22m[2m3 tests[22m[2m)[22m[32m 21[2mms[22m[39m [32m✓[39m src/modules/__tests__/post-workflow-details.js [2m([22m[2m1 test[22m[2m)[22m[32m 22[2mms[22m[39m [32m✓[39m src/modules/__tests__/details-from-context.js [2m([22m[2m5 tests[22m[2m)[22m[32m 8[2mms[22m[39m [32m✓[39m src/modules/__tests__/process-end-state.js [2m([22m[2m4 tests[22m[2m)[22m[32m 52[2mms[22m[39m [32m✓[39m ...
<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: ${{ ...
File not found: /home/byk/Code/getsentry/publish/.github/workflows/ci-pending.yml
<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>
<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: ...
<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 ...
<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 ...
<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 ...
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 ...
<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: ...
<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: { ...
<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>
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
...
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,…
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…
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…
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) …
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…