Dashboard › publish › Session 0HhxYmKC9XzC
0HhxYmKC9XzCymoiOReview the FINAL current uncommitted implementation for the release-approval authorization gate in /home/byk/Code/getsentry/publish. Do not edit. Focus on correctness, GitHub Actions event semantics, workflow failure/cleanup paths, supported title parsing, and tests. The intended policy is: human actor must currently have write/maintain/admin effective access to the target getsentry repository; getsantry[bot] auto-approval is permitted only for exact entries in auto-approve-repos.txt; failed checks fail closed and remove accepted. Report findings with severity and exact file:line. Classify each PASS/CONCERN/MUST-FIX and end exactly MERGE or DO-NOT-MERGE.
<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/publish.yml
M AGENTS.md
M src/libs/__tests__/github.js
M src/libs/github.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-authorizer.js
?? src/modules/approval-authorizer.js
?? src/publish/__tests__/
?? src/publish/authorize-approval.js
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index cff6bc3..081a17b 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -32,6 +32,11 @@ jobs:
&& github.event.issue.state == 'open'
&& startsWith(github.event.issue.title, 'publish: ')
steps:
+ - name: Get publish code
+ uses: actions/checkout@v6
+ with:
+ persist-credentials: false
+
- name: Get auth token
id: token
uses: ...
/home/byk/Code/getsentry/publish/src/publish/__tests__/authorize-approval.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/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 PUBLISH_TITLE_REGEX =
4: /^publish: (?:getsentry\/)?(?<repository>[^/@]+)(?<path>\/[^@]+)?@[^@]+$/;
5:
6: async function authorizeApproval({
7: actor,
8: issueTitle,
9: getPermission,
10: autoApprovedRepositories = new Set(),
11: }) {
12: const title = issueTitle.match(PUBLISH_TITLE_REGEX);
13:
14: if (!title) {
15: return { authorized: false, repository: null };
16: }
17:
18: const repository = `getsentry/${title.groups.repository}`;
19: const releasePath = `${repository}${title.groups.path || ""}`;
20:
21: if (actor === AUTO_APPROVER && autoApprovedRepositories.has(releasePath)) {
22: return { authorized: true, repository };
23: }
24:
25: const { role_name } = await getPermission({
26: owner: "getsentry",
27: ...
<path>/home/byk/Code/getsentry/publish/src/publish/authorize-approval.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const { authorizeApproval } = require("../modules/approval-authorizer.js");
3:
4: function getAutoApprovedRepositories() {
5: return new Set(
6: fs
7: .readFileSync("auto-approve-repos.txt", "utf8")
8: .split(/\r?\n/)
9: .filter(Boolean)
10: );
11: }
12:
13: async function getPermission({ owner, repository, username }) {
14: const response = await fetch(
15: `https://api.github.com/repos/${encodeURIComponent(
16: owner
17: )}/${encodeURIComponent(repository)}/collaborators/${encodeURIComponent(
18: username
19: )}/permission`,
20: {
21: headers: {
22: Accept: "application/vnd.github+json",
23: Authorization: `Bearer ${process.env.APPROVAL_TOKEN}`,
24: "X-GitHub-Api-Version": "2026-03-10",
25: },
26: }
27: );
28:
29: if (!response.ok) {
30: ...
<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/__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: async function runAuthorization({ actor, issueTitle, response }) {
17: process.env.GITHUB_OUTPUT = "/tmp/github-output";
18: process.env.APPROVAL_TOKEN = "release-bot-token";
19: process.env.APPROVAL_ACTOR = actor;
20: process.env.APPROVAL_ISSUE_TITLE = issueTitle;
21:
22: const appendFileSync = vi
23: .spyOn(fs, "appendFileSync")
24: .mockImplementation(() => {});
25: global.fetch = vi.fn().mockResolvedValue(response);
26:
27: await main();
28: await ...
<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:
19: async function detailsFromContext({ context }) {
20: if (!context || !context.payload || ...
Found 55 matches /home/byk/Code/getsentry/publish/.lore.md: Line 13: * **approval-authorizer legacy titles**: Trap: requiring only \`publish: getsentry/repo@version\` looks safer, but Publish accepts legacy unqualified titles such as \`publish: sentry@version\`, including optional release paths. Fix: match Publish’s title grammar and map both qualified and unqualified forms explicitly to the \`getsentry\` owner before checking permissions. Reject malformed or non-getsentry titles without querying GitHub. Line 16: * **publish.yml approval cleanup**: Trap: letting the permission lookup fail normally looks fail-closed because the job fails, but it leaves the unsafe \`accepted\` label attached and allows CI polling to proceed. Fix: create the internal-app token before authorization, run the authorizer with \`continue-on-error\`, and remove \`accepted\` whenever its outcome fails or \`authorized\` is not true; only then fail the job. ...
<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/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: # sentry-internal-app token for label changes on ...
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__/update-issue.js [2m([22m[2m5 tests[22m[2m)[22m[32m 23[2mms[22m[39m [32m✓[39m src/modules/__tests__/approval-authorizer.js [2m([22m[2m14 tests[22m[2m)[22m[32m 34[2mms[22m[39m [32m✓[39m src/publish/__tests__/authorize-approval.js [2m([22m[2m3 tests[22m[2m)[22m[32m 24[2mms[22m[39m [32m✓[39m src/modules/__tests__/process-end-state.js [2m([22m[2m4 tests[22m[2m)[22m[32m 61[2mms[22m[39m [32m✓[39m src/modules/__tests__/post-workflow-details.js [2m([22m[2m1 test[22m[2m)[22m[32m 25[2mms[22m[39m [32m✓[39m src/modules/__tests__/details-from-context.js [2m([22m[2m4 tests[22m[2m)[22m[32m 8[2mms[22m[39m [32m✓[39m src/libs/__tests__/github.js [2m([22m[2m2 tests[22m[2m)[22m[32m 5[2mms[22m[39m [2m Test Files [22m [1m[32m7 passed[39m[22m[90m (7)[39m [2m ...
# Contexts reference Find information about contexts available in GitHub Actions workflows, including available properties, access methods, and usage examples. ## Available contexts | Context name | Type | Description | | ------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `github` | `object` | Information about the workflow run. For more information, see [`github` context](#github-context). | | `env` | `object` | Contains variables set in a workflow, job, or step. For more information, see [`env` context](#env-context). ...
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/details-from-context.js</path>
<type>file</type>
<content>
1: import { test, expect } from "vitest";
2:
3: const { detailsFromContext } = require("../details-from-context.js");
4:
5: const inputsArgs = {
6: context: {
7: repo: { owner: "getsentry", repo: "publish" },
8: payload: {
9: issue: {
10: number: "223",
11: title: "publish: getsentry/sentry@21.3.1",
12: body: `
13: Requested by: @BYK
14:
15: Merge target: custom-branch
16:
17: Quick links:
18: - [View changes](https://github.com/getsentry/sentry/compare/21.3.0...refs/heads/releases/21.3.1)
19: - [View check runs](https://github.com/getsentry/sentry/commit/7e5ca7ed5581552de066e2a8bc295b8306be38ac/checks/)
20:
21: Assign the **accepted** label to this issue to approve the release. ...
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. info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command. error Command failed with exit code 1.
{ authorized: true, repository: 'getsentry/relay' }
TypeError
{ authorized: true, repository: 'getsentry/sentry-javascript' }
Findings:
MUST-FIX — Critical — .github/workflows/publish.yml:64, .github/workflows/ci-poller.yml:92 — Authorization uses the title snapshot from the accepted event, while the poller later reads the current issue title. An approver for repo A can add accepted, then change the title to repo B before polling. Repo B releases without an access check for it.
MUST-FIX — High — .github/workflows/publish.yml:63, .github/workflows/auto-approve.yml:28 — Auto-approve adds accepted with the internal-app token, so the labeled event actor is that app, not getsantry[bot]. The gate rejects valid allowlisted bot approvals or authorizes them based on the internal app’s target-repo role.
MUST-FIX — High — src/modules/approval-authorizer.js:21-31 — A non-allowlisted getsantry[bot] falls through to the permission lookup and is authorized when GitHub returns write, maintain, or admin. The bot must be rejected unless its release path exactly matches the allowlist.
MUST-FIX — High — src/modules/approval-authorizer.js:4, src/modules/details-from-context.js:25 — The authorization parser accepts titles Publish cannot parse. For example, the allowlisted publish: getsentry/relay/py@bad version authorizes, but the Publish parser throws. The regex must share Publish’s title grammar and fail closed for unsupported titles.
MUST-FIX — Medium — .github/workflows/publish.yml:35-45, .github/workflows/publish.yml:67 — Checkout or the internal-app token can fail before the only rejection step. GitHub skips that step after an earlier failure, leaving accepted attached. This violates the required cleanup on failed checks.
CONCERN — src/modules/__tests__/approval-authorizer.js:26-45 — Tests cover a non-allowlisted bot only with role_name: "none"; they miss the bypass with write. They also miss parser divergence, title mutation after approval, auto-approve’s app-label actor, and pre-authorization cleanup failures. ...
Date: Aug 27, 2026 * 🟡 (15:45) `yarn test` / `vitest run` completed successfully in `/home/byk/Code/getsentry/publish`: 7 test files and 33 tests passed. Suites: `src/modules/__tests__/update-issue.js` (5), `approval-authorizer.js` (14), `src/publish/__tests__/authorize-approval.js` (3), `process-end-state.js` (4), `post-workflow-details.js` (1), `details-from-context.js` (4), and `src/libs/__te…
Date: Aug 27, 2026 * 🔴 (15:44) User stated that `workflow_dispatch` must always be allowed for manual recovery. * 🟡 (15:44) Shared CI poller workflow excerpt shows a cron trigger of `*/5 * * * *`, a `check-ci` job, and concurrency group `ci-status-poller`. * 🟡 (15:44) CI poller workflow comments indicate `sentry-release-bot` token is used for cross-repository API access, while `sentry-internal…
Date: Aug 27, 2026 * 🔴 [requested-review] (15:39) User requested a final review only (no edits) of the current uncommitted release-approval authorization gate implementation in `/home/byk/Code/getsentry/publish`, focused on correctness, GitHub Actions event semantics, workflow failure/cleanup paths, supported title parsing, and tests. Requested severity plus exact `file:line` for every finding; …