Dashboard › publish › Session 1Qc6uYMGjA8J
1Qc6uYMGjA8JPChkRPerform a read-only, substantive security review of the current working tree's release approval gate. Focus on privilege boundaries, GitHub token identities, workflow expression/shell hazards, TOCTOU races, forged labels/comments/events, API failure paths, cleanup, and auto-approve provenance. Review .github/workflows/publish.yml, .github/workflows/ci-poller.yml, src/modules/approval-attestation.js, src/modules/approval-authorizer.js, src/publish/{authorize-approval,current-accepted-event,record-ci-ready-attestation,validate-approval-attestation}.js, and tests. Do not edit. Return a substantive report with file:line evidence. Classify each finding PASS, CONCERN, or MUST-FIX and end exactly MERGE or DO-NOT-MERGE. Do not return an empty response.
/home/byk/Code/getsentry/publish/src/publish/__tests__/validate-approval-attestation.js /home/byk/Code/getsentry/publish/src/publish/__tests__/authorize-approval.js /home/byk/Code/getsentry/publish/src/modules/__tests__/approval-attestation.js /home/byk/Code/getsentry/publish/src/modules/__tests__/approval-authorizer.js
/home/byk/Code/getsentry/publish/src/publish/__tests__/current-accepted-event.js /home/byk/Code/getsentry/publish/src/publish/__tests__/record-ci-ready-attestation.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> 241: # Setting the target repo branch will cause the craft config (.craft.yml) to be taken from the checked out branch 242: # By default, we check out the default branch of the repo. 243: # If you need to maintain diverging craft configs on different branches, add your repo and the merge target branch 244: # (i.e. the branch craft will merge the release branch into) into the if condition below. 245: - name: Set target repo checkout branch 246: # Note: Branches registered here MUST BE protected in the target repo! 247: if: | 248: fromJSON(steps.inputs.outputs.result).repo == 'sentry-migr8' && fromJSON(steps.inputs.outputs.result).merge_target == 'tmp-merge-target' || 249: fromJSON(steps.inputs.outputs.result).repo == 'sentry-javascript' && fromJSON(steps.inputs.outputs.result).merge_target == 'v10' || 250: ...
<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: write 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/.github/workflows/ci-poller.yml</path>
<type>file</type>
<content>
361: echo " CI failed (commit status)! Swapping ci-pending → ci-failed and removing accepted."
362: gh issue edit "$number" -R "$GITHUB_REPOSITORY" \
363: --remove-label "ci-pending" \
364: --remove-label "accepted" \
365: --add-label "ci-failed"
366:
367: failed_contexts=$(echo "$status_json" \
368: | jq -r '[.statuses[] | select(.state == "failure" or .state == "error")] | map(if .target_url and .target_url != "" and .target_url != "null" then "- [" + .context + "](" + .target_url + ") (" + .state + ")" else "- " + .context + " (" + .state + ")" end) | join("\n")')
369: comment_body="CI **commit status** checks failed for ${repo}@${version} (\`${sha:0:8}\`). Publishing is blocked."$'\n\n'"Failed status checks:"$'\n'"${failed_contexts}"$'\n\n'"[View check ...
<path>/home/byk/Code/getsentry/publish/src/modules/approval-attestation.js</path>
<type>file</type>
<content>
1: const APPROVAL_ATTESTATION_PREFIX = "<!-- publish-approval ";
2: const CI_READY_ATTESTATION_PREFIX = "<!-- publish-ci-ready ";
3: const ATTESTATION_SUFFIX = " -->";
4:
5: function createAttestation(prefix, value) {
6: return `${prefix}${Buffer.from(JSON.stringify(value)).toString(
7: "base64url"
8: )}${ATTESTATION_SUFFIX}`;
9: }
10:
11: function createApprovalAttestation({ actor, eventId, title }) {
12: return createAttestation(APPROVAL_ATTESTATION_PREFIX, {
13: actor,
14: eventId: String(eventId),
15: title,
16: });
17: }
18:
19: function createCiReadyAttestation({ acceptedEvent, ciReadyActor, title }) {
20: return createAttestation(CI_READY_ATTESTATION_PREFIX, {
21: acceptedActor: acceptedEvent.actor,
22: acceptedEventId: String(acceptedEvent.eventId),
23: ciReadyActor,
24: title,
25: });
26: }
27:
28: function ...
<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/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: ({ ...
<path>/home/byk/Code/getsentry/publish/src/publish/record-ci-ready-attestation.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3: createCiReadyAttestation,
4: currentAcceptedEvent,
5: } = require("../modules/approval-attestation.js");
6: const {
7: getAllPages,
8: getGitHubResponse,
9: getIssue,
10: } = require("./validate-approval-attestation.js");
11:
12: async function getAuthenticatedLogin() {
13: const response = await getGitHubResponse("user");
14:
15: if (!response.ok) {
16: throw new Error(
17: `Could not retrieve the authenticated user: GitHub returned ${response.status}`
18: );
19: }
20:
21: const { login } = await response.json();
22:
23: if (typeof login !== "string") {
24: throw new Error("GitHub returned no authenticated user login");
25: }
26:
27: return login;
28: }
29:
30: async function recordCiReadyAttestation({
31: expectedAcceptedEvent,
32: getAuthenticatedLogin,
33: getIssue,
34: ...
<path>/home/byk/Code/getsentry/publish/src/publish/validate-approval-attestation.js</path>
<type>file</type>
<content>
1: const {
2: currentAcceptedEvent,
3: currentCiReadyEvent,
4: hasApprovalAttestation,
5: hasCiReadyAttestation,
6: } = require("../modules/approval-attestation.js");
7:
8: async function getGitHubResponse(path) {
9: return fetch(`https://api.github.com/${path}`, {
10: headers: {
11: Accept: "application/vnd.github+json",
12: Authorization: `Bearer ${process.env.APPROVAL_TOKEN}`,
13: "X-GitHub-Api-Version": "2026-03-10",
14: },
15: });
16: }
17:
18: async function getIssue({ repository, issueNumber }) {
19: const response = await getGitHubResponse(
20: `repos/${repository}/issues/${issueNumber}`
21: );
22:
23: if (!response.ok) {
24: throw new Error(
25: `Could not retrieve issue #${issueNumber}: GitHub returned ${response.status}`
26: );
27: }
28:
29: return response.json();
30: }
31:
32: async ...
<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: } = require("../approval-attestation.js");
11:
12: describe("approval attestations", () => {
13: test("matches the latest accepted event to a trusted attestation", () => {
14: const attestation = createApprovalAttestation({
15: actor: "contractor",
16: eventId: "200",
17: title: "publish: getsentry/relay/py@1.2.3",
18: });
19: const event = currentAcceptedEvent([
20: {
21: actor: { login: "contractor" },
22: event: "labeled",
23: id: "100",
24: label: { name: "accepted" },
25: },
26: {
27: actor: { login: "contractor" },
28: ...
<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__/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__/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({ ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/record-ci-ready-attestation.js</path>
<type>file</type>
<content>
1: import { describe, expect, test, vi } from "vitest";
2:
3: const {
4: parseCiReadyAttestation,
5: } = require("../../modules/approval-attestation.js");
6: const {
7: recordCiReadyAttestation,
8: } = require("../record-ci-ready-attestation.js");
9:
10: describe("recordCiReadyAttestation", () => {
11: test("binds the current approval to the app that will add ci-ready", async () => {
12: const title = "publish: getsentry/sentry-javascript@10.0.0";
13:
14: await expect(
15: recordCiReadyAttestation({
16: getAuthenticatedLogin: vi
17: .fn()
18: .mockResolvedValue("sentry-internal-app[bot]"),
19: getIssue: vi.fn().mockResolvedValue({
20: labels: [{ name: "accepted" }],
21: title,
22: }),
23: getIssueEvents: vi.fn().mockResolvedValue([
24: {
25: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/validate-approval-attestation.js</path>
<type>file</type>
<content>
1: import { afterEach, describe, expect, test, vi } from "vitest";
2:
3: const {
4: validateApprovalAttestation,
5: } = require("../validate-approval-attestation.js");
6: const {
7: createApprovalAttestation,
8: createCiReadyAttestation,
9: } = require("../../modules/approval-attestation.js");
10:
11: afterEach(() => {
12: vi.restoreAllMocks();
13: });
14:
15: function jsonResponse(json) {
16: return { ok: true, json: vi.fn().mockResolvedValue(json) };
17: }
18:
19: describe("validateApprovalAttestation", () => {
20: test("accepts the current accepted event and its trusted attestation", async () => {
21: const title = "publish: getsentry/sentry-javascript@10.0.0";
22: const attestation = createApprovalAttestation({
23: actor: "contractor",
24: eventId: "100",
25: title,
26: });
27: global.fetch = vi
28: ...
File not found: /home/byk/Code/getsentry/publish/.jj
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/current-accepted-event.js ?? src/publish/record-ci-ready-attestation.js ?? src/publish/validate-approval-attestation.js diff --git a/.github/workflows/ci-poller.yml b/.github/workflows/ci-poller.yml index a3db430..a557c2d 100644 --- a/.github/workflows/ci-poller.yml +++ ...
No files found
No files found
<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/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/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: ...
<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/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>
Found 7 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) /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. For that process we need to ensure that craft doesn't have to run on employees' machines but somewhere, where access tokens cannot be accessed but approved engineers can click publish. ...
<path>/home/byk/Code/getsentry/publish/docs/rfc.md</path> <type>file</type> <content> 1: # [RFC] Central Publish Repository 2: 3: This design document is written to address the concerns mentioned in [Key Rotation & Vault Changes](key_rotation_vault_changes.md). Implementation pending feedback from engineering team members, specifically the SDK team. 4: 5: # Problem Statement 6: 7: 1. We need to be able to quickly recycle all publishing tokens (NPM, PyPI, Cargo, Docker, etc.) at will without disrupting our releases or our developer flow 8: 2. We need to limit the publishing tokens to authorized personnel only and not leak them in any way 9: 1. GitHub secrets is not enough as it lets anyone with write access to the repo to access the secrets and scuttle them out, even in a brand new PR. Example: [https://github.com/getsentry/sentry/pull/21930](https://github.com/getsentry/sentry/pull/21930) 10: 3. ...
yarn run v1.22.22 $ vitest run src/modules/__tests__/approval-attestation.js src/modules/__tests__/approval-authorizer.js src/publish/__tests__/authorize-approval.js src/publish/__tests__/current-accepted-event.js src/publish/__tests__/record-ci-ready-attestation.js src/publish/__tests__/validate-approval-attestation.js [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[2m7 tests[22m[2m)[22m[32m 51[2mms[22m[39m [32m✓[39m src/modules/__tests__/approval-authorizer.js [2m([22m[2m14 tests[22m[2m)[22m[32m 26[2mms[22m[39m [32m✓[39m src/publish/__tests__/current-accepted-event.js [2m([22m[2m2 tests[22m[2m)[22m[32m 37[2mms[22m[39m [32m✓[39m src/modules/__tests__/approval-attestation.js [2m([22m[2m4 tests[22m[2m)[22m[32m 36[2mms[22m[39m [32m✓[39m src/publish/__tests__/authorize-approval.js [2m([22m[2m3 tests[22m[2m)[22m[32m ...
<path>/home/byk/Code/getsentry/publish/src/modules/update-issue.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3: TARGETS_SECTION_PARSER_REGEX,
4: TARGETS_PARSER_REGEX,
5: } = require("./details-from-context");
6:
7: async function updateTargets({ octokit, version, publishRepo, issue_number }) {
8: const CRAFT_STATE_FILE_PATH = `${process.env.GITHUB_WORKSPACE}/__repo__/.craft-publish-${version}.json`;
9:
10: if (!fs.existsSync(CRAFT_STATE_FILE_PATH)) {
11: return;
12: }
13:
14: const issueRequest = octokit.rest.issues.get({
15: ...publishRepo,
16: issue_number,
17: });
18:
19: const craftStateRequest = fs.promises
20: .readFile(CRAFT_STATE_FILE_PATH, { encoding: "utf-8" })
21: .then((data) => JSON.parse(data));
22:
23: const [{ data: issue }, craftState] = await Promise.all([
24: issueRequest,
25: craftStateRequest,
26: ]);
27:
28: const newIssueBody = transformIssueBody(craftState, issue.body);
29: ...
Targeted tests pass: 33/33 across the six approval-attestation/authorization test files.
src/modules/approval-attestation.js:11-25 signs only actor, accepted-event ID, and title. src/publish/authorize-approval.js:111-136 reads only the issue title; src/publish/validate-approval-attestation.js:63-99 validates only title, labels, events, and comments.
This leaves the issue body and dry-run label mutable after approval:
ci-poller.yml:149-156 extracts the CI SHA from the mutable body.src/modules/details-from-context.js:36-64 derives dry-run, merge target, and targets from mutable issue state.src/publish/inputs.js:6-9 later uses the ci-ready event snapshot, which can contain attacker-modified body/labels.A Publish-repo issue editor can change the checked SHA to a known-green commit, change targets or merge target, or remove dry-run after a valid approval. The poller then validates CI for the altered SHA and Craft publishes the altered request. Bind a canonical request digest—including relevant body-derived fields, dry-run state, and checked SHA—to both attestations and validate it at every transition. Tests cover changed titles (validate-approval-attestation.js:58-95) but not this mutation.
src/modules/approval-authorizer.js:1,28-34 authorizes any target-repository write, maintain, or admin collaborator. src/publish/authorize-approval.js:111-120 fetches no issue author, so it cannot reject an actor approving their own request.
A user with Publish issue-label rights and target-repository write access can create a release issue and apply accepted themselves. This violates the required second-person approval boundary. Fetch and bind the requester identity, reject self-approval outside the explicit automatic path, and use a designated approver policy rather than generic write. ...
Date: Aug 29, 2026 * 🟡 (13:30) User provided `src/modules/update-issue.js` (77 lines) from `/home/byk/Code/getsentry/publish`. `updateTargets({ octokit, version, publishRepo, issue_number })` reads `${process.env.GITHUB_WORKSPACE}/__repo__/.craft-publish-${version}.json` only if it exists; concurrently fetches the issue and parses the Craft state JSON; then calls `transformIssueBody(craftState, …
Date: Aug 29, 2026 * 🟡 (13:24) Targeted Vitest command `yarn run v1.22.22` / `vitest run src/modules/__tests__/approval-attestation.js src/modules/__tests__/approval-authorizer.js src/publish/__tests__/authorize-approval.js src/publish/__tests__/current-accepted-event.js src/publish/__tests__/record-ci-ready-attestation.js src/publish/__tests__/validate-approval-attestation.js` passed in `/home/…
Date: Aug 29, 2026 * 🟡 (13:13) `src/publish/validate-approval-attestation.js:8-52` defines authenticated GitHub REST helpers using `APPROVAL_TOKEN`, `Accept: application/vnd.github+json`, and `X-GitHub-Api-Version: 2026-03-10`; `getIssue()` fetches `repos/${repository}/issues/${issueNumber}`, and `getAllPages()` fetches `events` or `comments` with `per_page=100&page=${page}` until a page has few…
Date: Aug 29, 2026 * 🟡 (13:12) `.github/workflows/ci-poller.yml:361-370` handles failed commit-status checks by swapping `ci-pending` to `ci-failed`, removing `accepted`, and posting a comment identifying failed contexts, target URLs when available, `${repo}@${version}`, short SHA `${sha:0:8}`, check-runs URL, and retry instruction to re-add `accepted`. * 🟡 (13:12) `.github/workflows/ci-poller.…
Date: Aug 29, 2026 * 🟡 [requested-security-review] (13:09) User requested a read-only, substantive security review of the current working tree’s release approval gate; explicitly requested no edits. Scope: privilege boundaries, GitHub token identities, workflow expression/shell hazards, TOCTOU races, forged labels/comments/events, API failure paths, cleanup, and auto-approve provenance. * 🟡 (13…