Dashboard › publish › Session 0oiYcLCLuyO5
0oiYcLCLuyO5KcWJlPerform an independent, read-only security and correctness review of the exact current worktree. Focus on the approval pipeline, protected production environment secret boundary, manual CI-poller dispatch relay, workflow_run semantics, action/image pinning, request-bound attestations, and regressions. Inspect every relevant changed file and verify tests/static contracts. Do not edit files. Return evidence with file:line references. Classify each point PASS, CONCERN, or MUST-FIX. End exactly MERGE or DO-NOT-MERGE. If blocked, start BLOCKED and name the exact tool/error; never return empty.
<skill_content name="jj-guide"> # Skill: jj-guide # jj Guide for AI Agents Jujutsu (jj) is a Git-compatible VCS with mutable commits, automatic snapshotting, no staging area, and first-class conflicts. This skill teaches you how to use it safely from a non-interactive agent environment. If `.jj/` exists in the repo root, this is a jj repo. **Use `jj` commands, not `git`.** In a colocated repo (`.jj/` *and* `.git/`), git tools can read the state, but mutations should go through `jj` so the operation log stays consistent. ## Critical Rules - **NEVER** use interactive flags (`-i`, `--interactive`). TUI prompts hang in agent environments. This applies to `jj split -i`, `jj squash -i`, `jj commit -i`, `jj resolve`, `jj diffedit`, etc. - **ALWAYS** pass `-m "msg"` when describing/committing. Without `-m`, an editor opens and hangs. - **VERIFY** mutations with `jj st` and `jj log` after `squash`, `abandon`, `rebase`, `restore`, `commit`. ...
Error: There is no jj repo in "." Hint: It looks like this is a git repo. You can create a jj repo backed by it by running this: jj git init
M .github/workflows/auto-approve.yml M .github/workflows/ci-poller.yml M .github/workflows/cocoapods-keepalive.yml M .github/workflows/publish.yml M .github/workflows/test.yml M AGENTS.md M README.md M src/libs/__tests__/github.js M src/libs/github.js M src/modules/__tests__/details-from-context.js M src/modules/__tests__/update-issue.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 ?? .github/workflows/ci-poller-dispatch.yml ?? .lore.md ?? src/modules/__tests__/approval-attestation.js ?? src/modules/__tests__/approval-authorizer.js ?? src/modules/approval-attestation.js ?? src/modules/approval-authorizer.js ?? src/publish/__tests__/ ?? src/publish/authorize-approval.js ?? src/publish/current-accepted-event.js ?? ...
.github/workflows/auto-approve.yml | 23 ++- .github/workflows/ci-poller.yml | 195 +++++++++++++++++++++++--- .github/workflows/cocoapods-keepalive.yml | 1 - .github/workflows/publish.yml | 139 ++++++++++++++++-- .github/workflows/test.yml | 6 +- AGENTS.md | 6 + README.md | 33 ++++- src/libs/__tests__/github.js | 2 +- src/libs/github.js | 6 +- src/modules/__tests__/details-from-context.js | 22 ++- src/modules/__tests__/update-issue.js | 28 ++++ src/modules/details-from-context.js | 17 ++- src/modules/post-workflow-details.js | 4 +- src/modules/process-end-state.js | 4 +- src/modules/update-issue.js | 42 +++--- src/publish/inputs.js | 17 ++- src/publish/post-result.js | 8 ...
<path>/home/byk/Code/getsentry/publish/.github/workflows/ci-poller-dispatch.yml</path>
<type>file</type>
<content>
1: name: Run CI Status Poller
2:
3: on:
4: workflow_dispatch:
5:
6: permissions: {}
7:
8: jobs:
9: dispatch:
10: # production only permits deployments from main. Repository secrets must
11: # never be used by this workflow; protected environment secrets enforce
12: # that an arbitrary dispatched ref cannot add them.
13: environment: production
14: runs-on: ubuntu-latest
15: steps:
16: - run: ":"
(End of file - total 16 lines)
</content>
<path>/home/byk/Code/getsentry/publish/.github/workflows/ci-poller.yml</path> <type>file</type> <content> 1: name: CI Status Poller 2: 3: on: 4: schedule: 5: - cron: "*/5 * * * *" 6: repository_dispatch: 7: types: [ci-poller] 8: workflow_run: 9: workflows: [Run CI Status Poller] 10: types: [completed] 11: 12: permissions: 13: contents: read 14: issues: write 15: 16: jobs: 17: check-ci: 18: runs-on: ubuntu-latest 19: environment: production 20: # Skip entirely (no runner provisioned) when there's nothing to check. 21: # Set to "true" by ci-pending.yml, reset to "false" here when done. 22: # repository_dispatch and a successful main-branch run of the protected 23: # workflow_dispatch relay are the trusted manual-recovery paths. 24: if: >- 25: vars.CI_POLLER_HAS_PENDING == 'true' 26: || github.event_name == 'repository_dispatch' 27: || ( 28: github.event_name == 'workflow_run' 29: && ...
<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/auto-approve.yml</path>
<type>file</type>
<content>
1: name: auto-approve non-sdks
2: on:
3: issues:
4: types: [opened]
5:
6: permissions:
7: contents: read
8: issues: write
9:
10: jobs:
11: auto-approve:
12: runs-on: ubuntu-latest
13: environment: production
14: if: |
15: (github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') &&
16: startsWith(github.event.issue.title, 'publish: ')
17: steps:
18: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
19: with:
20: sparse-checkout: auto-approve-repos.txt
21: sparse-checkout-cone-mode: false
22: - name: Get auth token
23: id: token
24: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
25: with:
26: client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
27: private-key: ${{ ...
<path>/home/byk/Code/getsentry/publish/src/modules/approval-attestation.js</path>
<type>file</type>
<content>
1: const crypto = require("crypto");
2:
3: const APPROVAL_ATTESTATION_PREFIX = "<!-- publish-approval ";
4: const AUTO_APPROVAL_ATTESTATION_PREFIX = "<!-- publish-auto-approval ";
5: const CI_READY_ATTESTATION_PREFIX = "<!-- publish-ci-ready ";
6: const ATTESTATION_SUFFIX = " -->";
7:
8: function createAttestation(prefix, value) {
9: return `${prefix}${Buffer.from(JSON.stringify(value)).toString(
10: "base64url"
11: )}${ATTESTATION_SUFFIX}`;
12: }
13:
14: function requestDigest({ body, labels, title }) {
15: if (typeof body !== "string" || !Array.isArray(labels)) {
16: throw new Error("Invalid publish request");
17: }
18:
19: return crypto
20: .createHash("sha256")
21: .update(
22: JSON.stringify({
23: body,
24: dryRun: labels.some((label) => label.name === "dry-run"),
25: title,
26: })
27: )
28: ...
<path>/home/byk/Code/getsentry/publish/src/modules/approval-authorizer.js</path>
<type>file</type>
<content>
1: const ALLOWED_ROLE_NAMES = new Set(["write", "maintain", "admin"]);
2: const { parsePublishTitle } = require("./details-from-context.js");
3:
4: const AUTO_APPROVAL_LABELER = "sentry-internal-app[bot]";
5: const AUTO_APPROVERS = new Set(["getsantry[bot]", "sentry-release-bot[bot]"]);
6:
7: function isAutoApprovedRepository({
8: actor,
9: autoApprovedRepositories,
10: issueTitle,
11: }) {
12: if (!AUTO_APPROVERS.has(actor)) {
13: return false;
14: }
15:
16: const title = parsePublishTitle(issueTitle);
17:
18: if (!title) {
19: return false;
20: }
21:
22: return autoApprovedRepositories.has(
23: `getsentry/${title.repo}${title.path || ""}`
24: );
25: }
26:
27: async function authorizeApproval({
28: actor,
29: issueTitle,
30: getPermission,
31: }) {
32: const title = parsePublishTitle(issueTitle);
33:
34: if (!title) {
35: ...
<path>/home/byk/Code/getsentry/publish/src/publish/authorize-approval.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3: createApprovalAttestation,
4: currentAcceptedEvent,
5: hasAutoApprovalAttestation,
6: } = require("../modules/approval-attestation.js");
7: const {
8: AUTO_APPROVAL_LABELER,
9: AUTO_APPROVERS,
10: authorizeApproval,
11: isAutoApprovedRepository,
12: } = require("../modules/approval-authorizer.js");
13:
14: function getAutoApprovedRepositories() {
15: return new Set(
16: fs
17: .readFileSync("auto-approve-repos.txt", "utf8")
18: .split(/\r?\n/)
19: .filter(Boolean)
20: );
21: }
22:
23: async function getPermission({ owner, repository, username }) {
24: const response = await getGitHubResponse(
25: `repos/${encodeURIComponent(owner)}/${encodeURIComponent(
26: repository
27: )}/collaborators/${encodeURIComponent(username)}/permission`
28: );
29:
30: if (!response.ok) {
31: ...
<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/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/publish/record-auto-approval-attestation.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3: createAutoApprovalAttestation,
4: } = require("../modules/approval-attestation.js");
5: const {
6: getIssue,
7: } = require("./validate-approval-attestation.js");
8:
9: async function recordAutoApprovalAttestation({
10: autoApprover,
11: getIssue,
12: issueNumber,
13: issueTitle,
14: repository,
15: }) {
16: const issue = await getIssue({ repository, issueNumber });
17:
18: if (
19: issue.state !== "open" ||
20: issue.title !== issueTitle ||
21: issue.user?.login?.toLowerCase() !== autoApprover.toLowerCase()
22: ) {
23: throw new Error("The automated approval request changed before approval");
24: }
25:
26: return createAutoApprovalAttestation({ autoApprover, issue });
27: }
28:
29: async function main() {
30: for (const name of [
31: "GITHUB_OUTPUT",
32: ...
<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/modules/__tests__/approval-attestation.js</path>
<type>file</type>
<content>
1: import { describe, expect, test } from "vitest";
2:
3: const {
4: createApprovalAttestation,
5: createCiReadyAttestation,
6: currentAcceptedEvent,
7: hasApprovalAttestation,
8: hasCiReadyAttestation,
9: parseApprovalAttestation,
10: requestDigest,
11: } = require("../approval-attestation.js");
12:
13: function issue(title, { body = "", dryRun = false } = {}) {
14: return {
15: body,
16: labels: dryRun ? [{ name: "dry-run" }] : [],
17: title,
18: };
19: }
20:
21: describe("approval attestations", () => {
22: test("matches the latest accepted event to a trusted attestation", () => {
23: const title = "publish: getsentry/relay/py@1.2.3";
24: const publishIssue = issue(title);
25: const attestation = createApprovalAttestation({
26: actor: "contractor",
27: eventId: "200",
28: issue: publishIssue,
29: ...
<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.each(["getsantry[bot]", "sentry-release-bot[bot]"])(
9: "rejects direct approval by %s without a target repository lookup",
10: async (actor) => {
11: const getPermission = vi.fn();
12:
13: await expect(
14: authorizeApproval({
15: actor,
16: issueTitle,
17: getPermission,
18: autoApprovedRepositories: new Set(["getsentry/sentry-javascript"]),
19: })
20: ).resolves.toEqual({
21: authorized: false,
22: repository: "getsentry/sentry-javascript",
23: });
24:
25: expect(getPermission).not.toHaveBeenCalled();
26: }
27: );
28: ...
/home/byk/Code/getsentry/publish/src/publish/__tests__/workflow-action-pinning.js /home/byk/Code/getsentry/publish/src/publish/__tests__/record-auto-approval-attestation.js /home/byk/Code/getsentry/publish/src/publish/__tests__/publish-workflow.js /home/byk/Code/getsentry/publish/src/publish/__tests__/auto-approval-workflow.js /home/byk/Code/getsentry/publish/src/publish/__tests__/ci-poller-workflow.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 /home/byk/Code/getsentry/publish/src/publish/__tests__/validate-approval-attestation.js /home/byk/Code/getsentry/publish/src/publish/__tests__/authorize-approval.js
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/workflow-action-pinning.js</path>
<type>file</type>
<content>
1: import { readFileSync, readdirSync } from "fs";
2: import { describe, expect, test } from "vitest";
3:
4: const workflowPaths = readdirSync(".github/workflows")
5: .filter((path) => path.endsWith(".yml"))
6: .map((path) => `.github/workflows/${path}`);
7: const workflows = workflowPaths.map((path) => ({
8: path,
9: contents: readFileSync(path, "utf8"),
10: }));
11: const SECRET_BEARING_JOBS = {
12: ".github/workflows/auto-approve.yml": ["auto-approve"],
13: ".github/workflows/ci-poller.yml": ["check-ci"],
14: ".github/workflows/cocoapods-keepalive.yml": ["keepalive"],
15: ".github/workflows/publish.yml": ["waiting-for-ci", "publish"],
16: };
17:
18: function jobContents(workflow, jobName) {
19: const header = ` ${jobName}:`;
20: const jobStart = workflow.indexOf(header);
21: const nextJobOffset = workflow
22: .slice(jobStart + ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/publish-workflow.js</path>
<type>file</type>
<content>
1: import { readFileSync } from "fs";
2: import { describe, expect, test } from "vitest";
3:
4: const workflow = readFileSync(".github/workflows/publish.yml", "utf8");
5:
6: describe("publish workflow", () => {
7: test("revalidates approval immediately before Craft", () => {
8: const fence = workflow.indexOf("Revalidate approval attestation");
9:
10: expect(fence).toBeGreaterThan(workflow.indexOf("Set targets"));
11: expect(fence).toBeLessThan(workflow.indexOf("Publish using Craft"));
12: expect(workflow.slice(fence)).toContain(
13: 'REQUIRE_CI_READY_ATTESTATION: "true"'
14: );
15: });
16:
17: test("pins Craft to an immutable digest", () => {
18: expect(workflow).toMatch(
19: /uses: docker:\/\/getsentry\/craft@sha256:[a-f0-9]{64}/
20: );
21: });
22: });
(End of file - total 22 lines)
</content>
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/ci-poller-workflow.js</path>
<type>file</type>
<content>
1: import { readFileSync } from "fs";
2: import { describe, expect, test } from "vitest";
3:
4: const workflow = readFileSync(".github/workflows/ci-poller.yml", "utf8");
5: const manualDispatchWorkflow = readFileSync(
6: ".github/workflows/ci-poller-dispatch.yml",
7: "utf8"
8: );
9:
10: describe("CI poller workflow", () => {
11: test("checks out trusted code before requesting app tokens", () => {
12: expect(workflow).toMatch(
13: /- name: Get publish code\n\s+uses: actions\/checkout@[a-f0-9]{40}\n\s+with:\n(?:\s+#.*\n)*\s+ref: \$\{\{ github\.event\.repository\.default_branch \}\}/
14: );
15:
16: expect(workflow.indexOf("Get publish code")).toBeLessThan(
17: workflow.indexOf("Get auth token")
18: );
19: });
20:
21: test("uses a protected workflow dispatch relay for manual recovery", () => {
22: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/auto-approval-workflow.js</path>
<type>file</type>
<content>
1: import { readFileSync } from "fs";
2: import { describe, expect, test } from "vitest";
3:
4: const workflow = readFileSync(".github/workflows/auto-approve.yml", "utf8");
5:
6: describe("auto-approval workflow", () => {
7: test("records a request-bound attestation before adding accepted", () => {
8: expect(workflow).toContain(
9: "node src/publish/record-auto-approval-attestation.js"
10: );
11: expect(
12: workflow.indexOf("record-auto-approval-attestation.js")
13: ).toBeLessThan(workflow.indexOf("--add-label accepted"));
14: });
15:
16: test("posts the proof as github-actions before the internal app labels", () => {
17: const proof = workflow.indexOf("Post automated approval attestation");
18: const label = workflow.indexOf("--add-label accepted");
19:
20: expect(workflow.slice(proof, label)).toContain(
21: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/authorize-approval.js</path>
<type>file</type>
<content>
1: import { afterEach, describe, expect, test, vi } from "vitest";
2: import fs from "fs";
3: const {
4: createAutoApprovalAttestation,
5: } = require("../../modules/approval-attestation.js");
6: const {
7: getAutoApprovedRepositories,
8: main,
9: } = require("../authorize-approval.js");
10:
11: const originalEnvironment = { ...process.env };
12:
13: afterEach(() => {
14: process.env = { ...originalEnvironment };
15: vi.restoreAllMocks();
16: vi.resetModules();
17: });
18:
19: function jsonResponse(json) {
20: return { ok: true, json: vi.fn().mockResolvedValue(json) };
21: }
22:
23: async function runAuthorization({ actor, issueTitle, responses }) {
24: process.env.GITHUB_OUTPUT = "/tmp/github-output";
25: process.env.APPROVAL_TOKEN = "release-bot-token";
26: process.env.APPROVAL_ACTOR = actor;
27: process.env.APPROVAL_ISSUE_NUMBER = ...
<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: function issue(
20: title,
21: { body = "", labels = [{ name: "accepted" }], state = "open" } = {}
22: ) {
23: return { body, labels, state, title };
24: }
25:
26: describe("validateApprovalAttestation", () => {
27: test("accepts the current accepted event and its trusted attestation", async () => {
28: const title = "publish: getsentry/sentry-javascript@10.0.0";
29: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/record-auto-approval-attestation.js</path>
<type>file</type>
<content>
1: import { describe, expect, test, vi } from "vitest";
2:
3: const {
4: parseAutoApprovalAttestation,
5: } = require("../../modules/approval-attestation.js");
6: const {
7: recordAutoApprovalAttestation,
8: } = require("../record-auto-approval-attestation.js");
9:
10: describe("recordAutoApprovalAttestation", () => {
11: test("binds an automated requester to the live publish request", async () => {
12: const title = "publish: getsentry/sentry-javascript@10.0.0";
13:
14: await expect(
15: recordAutoApprovalAttestation({
16: autoApprover: "getsantry[bot]",
17: getIssue: vi.fn().mockResolvedValue({
18: body: "Merge target: main",
19: labels: [],
20: state: "open",
21: title,
22: user: { login: "getsantry[bot]" },
23: }),
24: issueNumber: "123",
25: ...
<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: body: "Merge target: main",
21: labels: [{ name: "accepted" }],
22: state: "open",
23: title,
24: }),
25: ...
<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/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/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>
<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: ...
<path>/home/byk/Code/getsentry/publish/src/modules/process-end-state.js</path>
<type>file</type>
<content>
1: const Sentry = require("@sentry/node");
2:
3: async function processEndState({ context, octokit, inputs, status }) {
4: const { repo, version } = inputs;
5: const { repo: publishRepo, runId: run_id } = context;
6: const { number: issue_number } = context.payload.issue;
7: const workflowInfo = (
8: await octokit.rest.actions.getWorkflowRun({
9: ...publishRepo,
10: run_id,
11: })
12: ).data;
13:
14: const details = {
15: repo,
16: version,
17: publishRepo,
18: run_id,
19: issue_number,
20: workflowInfo,
21: status,
22: };
23:
24: await postIssueComment({
25: octokit,
26: details,
27: });
28:
29: if (status === "success") {
30: await octokit.rest.issues.update({
31: ...publishRepo,
32: issue_number,
33: state: "closed",
34: });
35: }
36:
37: await reportSession({ ...
<path>/home/byk/Code/getsentry/publish/src/modules/post-workflow-details.js</path>
<type>file</type>
<content>
1: async function postWorkflowDetails({ context, octokit }) {
2: const { repo: publishRepo, runId: run_id } = context;
3: const { number: issue_number } = context.payload.issue;
4:
5: const workflowInfo = (
6: await octokit.rest.actions.getWorkflowRun({
7: ...publishRepo,
8: run_id,
9: })
10: ).data;
11:
12: return octokit.rest.issues.createComment({
13: ...publishRepo,
14: issue_number,
15: body: `Publishing: [run#${run_id}](${workflowInfo.html_url})`,
16: });
17: }
18:
19: module.exports = postWorkflowDetails;
(End of file - total 19 lines)
</content>
<path>/home/byk/Code/getsentry/publish/src/publish/update-issue.js</path>
<type>file</type>
<content>
1: const { updateIssue } = require("../modules/update-issue.js");
2: const { getGitHubToken } = require("../libs/github");
3: const github = require("@actions/github");
4:
5: async function main() {
6: const context = github.context;
7: const octokit = github.getOctokit(getGitHubToken());
8: let inputs;
9:
10: if (process.env.PUBLISH_ARGS) {
11: try {
12: inputs = JSON.parse(process.env.PUBLISH_ARGS);
13: } catch {
14: console.warn("Could not parse publish inputs; skipping target update");
15: }
16: }
17:
18: await updateIssue({ context, octokit, inputs });
19: }
20:
21: if (require.main === module) {
22: main().catch((error) => {
23: console.error(error);
24: process.exitCode = 1;
25: });
26: }
27:
28: module.exports = { main };
(End of file - total 28 lines)
</content>
<path>/home/byk/Code/getsentry/publish/src/libs/github.js</path>
<type>file</type>
<content>
1: /**
2: * This is a helper function that throws a useful error message if the
3: * workflow environment is not configured correctly.
4: *
5: * @returns string
6: */
7: function getGitHubToken() {
8: if (!process.env.GITHUB_TOKEN) {
9: throw new Error(
10: 'No "GITHUB_TOKEN" environment variable found. ' +
11: "Please ensure the workflow is configured correctly"
12: );
13: }
14: return process.env.GITHUB_TOKEN;
15: }
16:
17: module.exports = {
18: getGitHubToken,
19: };
(End of file - total 19 lines)
</content>
<path>/home/byk/Code/getsentry/publish/src/publish/post-result.js</path>
<type>file</type>
<content>
1: const processEndState = require("../modules/process-end-state");
2: const { getGitHubToken } = require("../libs/github");
3: const github = require("@actions/github");
4:
5: const context = github.context;
6: const octokit = github.getOctokit(getGitHubToken());
7: const inputs = JSON.parse(process.env.PUBLISH_ARGS);
8: const args = process.argv.slice(2);
9: const status = args[0];
10:
11: processEndState({ context, octokit, inputs, status });
(End of file - total 11 lines)
</content>
<path>/home/byk/Code/getsentry/publish/src/publish/post-workflow-details.js</path>
<type>file</type>
<content>
1: const postWorkflowDetails = require("../modules/post-workflow-details.js");
2: const { getGitHubToken } = require("../libs/github");
3: const github = require("@actions/github");
4:
5: const context = github.context;
6: const octokit = github.getOctokit(getGitHubToken());
7:
8: postWorkflowDetails({ context, octokit });
(End of file - total 8 lines)
</content>
diff --git a/AGENTS.md b/AGENTS.md
index cf473df..51bf3a8 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -176,100 +176,106 @@ Before reading files:
```javascript
if (!fs.existsSync(CRAFT_STATE_FILE_PATH)) {
return;
}
```
## Testing Guidelines
### Framework
- **Vitest v4.0.0** with globals enabled
- No need to import `describe`, `test`, `expect` in test files (they're global)
- Mocks are auto-cleared between tests
### Test File Structure
```javascript
import { vi, describe, test, expect } from "vitest";
// Mock external dependencies
vi.mock("@actions/github");
vi.mock("@actions/core");
// Import module under test AFTER mocks
const { myFunction } = require("../my-module.js");
describe("myFunction", () => {
test("does something", () => {
expect(myFunction()).toBe(expected);
});
});
```
### Mocking Patterns
```javascript
// Mock a module
vi.mock("@actions/github");
// Mock with implementation
vi.mock("fs", () => ({
existsSync: ...
<path>/home/byk/Code/getsentry/publish/.github/workflows/cocoapods-keepalive.yml</path>
<type>file</type>
<content>
1: name: "CocoaPods Token Keep-Alive"
2:
3: on:
4: schedule:
5: # CocoaPods sessions currently expire after 3 days of inactivity (VALIDITY_LENGTH)
6: # This runs every day at midnight UTC to keep the current token alive
7: # See: https://github.com/CocoaPods/trunk.cocoapods.org/blob/a1869790e9ae2229b6985b6af4532da814202558/app/models/session.rb#L9
8: - cron: "0 0 * * *" # Runs every day at midnight UTC
9:
10: permissions:
11: contents: read
12:
13: jobs:
14: keepalive:
15: name: Refresh CocoaPods Session
16: runs-on: macos-15
17: environment: production
18: env:
19: COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
20:
21: steps:
22: - name: Install CocoaPods
23: run: gem install cocoapods
24: - name: Print CocoaPods Version
25: run: pod --version
26: - name: Refresh CocoaPods ...
<path>/home/byk/Code/getsentry/publish/package.json</path>
<type>file</type>
<content>
1: {
2: "name": "publish",
3: "private": true,
4: "version": "0.0.1",
5: "description": "Approval-based publishing system for Sentry",
6: "main": "index.js",
7: "repository": "git@github.com:getsentry/publish.git",
8: "author": "Sentry Open Source <oss@sentry.io>",
9: "license": "Apache-2.0",
10: "volta": {
11: "node": "24.0.0",
12: "yarn": "1.22.22"
13: },
14: "resolutions": {
15: "undici": "^6.23.0"
16: },
17: "scripts": {
18: "test": "vitest run",
19: "test:watch": "vitest",
20: "lint": "eslint src .github --ignore-pattern '!.github'",
21: "prettier": "prettier --write src"
22: },
23: "devDependencies": {
24: "eslint": "^8.9.0",
25: "eslint-config-prettier": "^8.3.0",
26: "eslint-plugin-yml": "^0.13.0",
27: "prettier": "^2.2.1",
28: "vitest": "^4.1.0"
29: },
30: "dependencies": {
31: "@actions/core": ...
yarn run v1.22.22 $ vitest run --printConsoleTrace [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[2m6 tests[22m[2m)[22m[32m 18[2mms[22m[39m [32m✓[39m src/publish/__tests__/validate-approval-attestation.js [2m([22m[2m9 tests[22m[2m)[22m[32m 56[2mms[22m[39m [32m✓[39m src/publish/__tests__/authorize-approval.js [2m([22m[2m7 tests[22m[2m)[22m[32m 19[2mms[22m[39m [32m✓[39m src/modules/__tests__/approval-authorizer.js [2m([22m[2m15 tests[22m[2m)[22m[32m 15[2mms[22m[39m [32m✓[39m src/publish/__tests__/current-accepted-event.js [2m([22m[2m2 tests[22m[2m)[22m[32m 19[2mms[22m[39m [32m✓[39m src/modules/__tests__/process-end-state.js [2m([22m[2m4 tests[22m[2m)[22m[32m 59[2mms[22m[39m [32m✓[39m src/modules/__tests__/approval-attestation.js [2m([22m[2m5 tests[22m[2m)[22m[32m 41[2mms[22m[39m [32m✓[39m ...
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.
Checking formatting... [warn] src/modules/__tests__/approval-authorizer.js [warn] src/modules/approval-authorizer.js [warn] src/publish/__tests__/validate-approval-attestation.js [warn] src/publish/__tests__/workflow-action-pinning.js [warn] src/publish/record-auto-approval-attestation.js [warn] Code style issues found in 5 files. Forgot to run Prettier?
<path>/home/byk/Code/getsentry/publish/.github/workflows/test.yml</path>
<type>file</type>
<content>
1: name: Test
2: on:
3: push:
4: branches:
5: - main
6: pull_request:
7:
8: permissions:
9: contents: read
10:
11: defaults:
12: run:
13: shell: bash
14: jobs:
15: unit-test:
16: runs-on: ubuntu-latest
17: name: unit tests
18: steps:
19: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
20: - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38
21: with:
22: node-version: 24
23: - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
24: id: cache
25: with:
26: path: node_modules
27: key: ${{ runner.os }}-node_modules-${{ hashFiles('package.json', 'yarn.lock') }}
28:
29: - name: Install Dependencies
30: if: steps.cache.outputs.cache-hit != 'true'
31: run: yarn install --frozen-lockfile
32:
33: - name: Test
34: ...
Found 18 matches /home/byk/Code/getsentry/publish/.github/workflows/publish.yml: Line 36: uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 Line 42: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 Line 52: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 Line 149: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 Line 192: uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 Line 223: uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 Line 268: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 Line 274: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 Line 353: - uses: ...
Found 36 matches
/home/byk/Code/getsentry/publish/.github/workflows/publish.yml:
Line 45: private-key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }}
Line 55: private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }}
Line 152: private-key: ${{ secrets.CI_POLLER_APP_PRIVATE_KEY }}
Line 239: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Line 271: private-key: ${{ secrets.SENTRY_RELEASE_BOT_PRIVATE_KEY }}
Line 381: DOCKER_GHCR_IO_PASSWORD: ${{ secrets.GITHUB_TOKEN }} # for ghcr.io auth
Line 383: CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
Line 384: COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
Line 385: CRAFT_GCS_TARGET_CREDS_JSON: ${{ secrets.CRAFT_GCS_TARGET_CREDS_JSON }}
Line 386: CRAFT_GCS_STORE_CREDS_JSON: ${{ secrets.CRAFT_GCS_STORE_CREDS_JSON }}
Line 387: CRATES_IO_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}
Line 389: ...
{"deployment_branch_policy":{"custom_branch_policies":true,"protected_branches":false},"protection_rules":[{"prevent_self_review":null,"reviewers":[],"type":"branch_policy"}]}
{"name":"main","type":"branch"}
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
CI_POLLER_APP_PRIVATE_KEY
CLOUDFLARE_API_TOKEN
COCOAPODS_TRUNK_TOKEN
CRAFT_GCS_STORE_CREDS_JSON
CRAFT_GCS_TARGET_CREDS_JSON
CRATES_IO_TOKEN
DOCKER_PASSWORD
GEM_HOST_API_KEY
GPG_PASSPHRASE
GPG_PRIVATE_KEY
HEX_API_KEY
NPM_TOKEN
NUGET_API_TOKEN
OSSRH_PASSWORD
OSSRH_USERNAME
POWERSHELL_API_KEY
PUBDEV_ACCESS_TOKEN
PUBDEV_REFRESH_TOKEN
SENTRY_RELEASE_BOT_PRIVATE_KEY
TWINE_PASSWORD
VERCEL_TOKEN
{"message":"Not Found","documentation_url":"https://docs.github.com/rest/actions/secrets#get-a-repository-secret","status":"404"}gh: Not Found (HTTP 404)
{"message":"You must be an org admin or have the actions secrets fine-grained permission.","documentation_url":"https://docs.github.com/rest/actions/secrets#get-an-organization-secret","status":"403"}gh: You must be an org admin or have the actions secrets fine-grained permission. (HTTP 403)
gh: This API operation needs the "admin:org" scope. To request it, run: gh auth refresh -h github.com -s admin:org
<path>/home/byk/Code/getsentry/publish/README.md</path>
<type>file</type>
<content>
76: version: ${{ inputs.version }}
77: merge_target: ${{ inputs.merge_target }}
78: secrets: inherit
79: ```
80:
81: The same `merge_target` input is also available when using the [Craft composite action](https://craft.sentry.dev/github-actions/#option-2-composite-action) directly.
82:
83: ## Approvals
84:
85: Packages we release into the wider world that our customers install, require an explicit approval. This for instance applies to
86: `sentry-cli`, our SDKs or the `symbolicator` distributed utilities. Internal dependencies such as `arroyo` can be published
87: with an auto approval. The reasoning here is that the bump of the dependency requires an explicit approval again in Sentry
88: proper. ...
{"can_admins_bypass":false,"deployment_branch_policy":{"custom_branch_policies":true,"protected_branches":false},"protection_rules":[{"type":"branch_policy"}]}
M .github/workflows/auto-approve.yml M .github/workflows/ci-poller.yml M .github/workflows/cocoapods-keepalive.yml M .github/workflows/publish.yml M .github/workflows/test.yml M AGENTS.md M README.md M src/libs/__tests__/github.js M src/libs/github.js M src/modules/__tests__/details-from-context.js M src/modules/__tests__/update-issue.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 ?? .github/workflows/ci-poller-dispatch.yml ?? .lore.md ?? src/modules/__tests__/approval-attestation.js ?? src/modules/__tests__/approval-authorizer.js ?? src/modules/approval-attestation.js ?? src/modules/approval-authorizer.js ?? src/publish/__tests__/ ?? src/publish/authorize-approval.js ?? src/publish/current-accepted-event.js ?? ...
MUST-FIX — The production secret migration is incomplete. SENTRY_INTERNAL_APP_PRIVATE_KEY is used by .github/workflows/auto-approve.yml:24-27, .github/workflows/ci-poller.yml:60-63, and .github/workflows/publish.yml:42-45, and README requires it in production at README.md:101-125. Read-only GitHub API checks show production allows only main and disables admin bypass, but its environment-secret list omits this key; the repository-secret endpoint returns 404. If inherited from the org, an arbitrary-ref dispatch can expose it; if absent, production approval and CI jobs fail. Add it to production and remove any org-level copy before merging.
PASS — The manual relay is isolated: it has only workflow_dispatch, permissions: {}, no secret references, and uses the protected environment (.github/workflows/ci-poller-dispatch.yml:3-16). The privileged poller requires a successful relay run from main (.github/workflows/ci-poller.yml:24-31) and checks out default-branch code without persisted credentials before minting app tokens (.github/workflows/ci-poller.yml:48-75).
PASS — Repository-dispatch input is constrained before secrets and uses base-10 arithmetic (.github/workflows/ci-poller.yml:36-46, 451-463).
PASS — Approval is bound to the live issue, latest accepted-label event, requester/approver separation, target-repository role, and trusted GitHub Actions comment (src/publish/authorize-approval.js:135-223, src/modules/approval-attestation.js:14-55, 176-241, src/publish/validate-approval-attestation.js:63-100).
PASS — CI-ready proof binds the accepted event and label actor, then validates again immediately before applying ci-ready (.github/workflows/ci-poller.yml:272-359). Publishing validates at entry and immediately before Craft (.github/workflows/publish.yml:197-220, 328-353).
PASS — Actions use immutable 40-character revisions and Craft uses a SHA-256 digest (.github/workflows/publish.yml:353, ...
Date: Sep 8, 2026 * 🔴 (21:11) User established publishing-security policy: workflows must never use repository or organization secrets for publishing; publishing secrets must live in protected `production`, which permits only `main` deployments and disables administrator bypass. * 🟡 (21:11) `README.md` documents a secret-migration prerequisite: add secrets to `production`, verify its deployment…
Date: Sep 8, 2026 * 🟡 (21:08) `src/modules/__tests__/approval-authorizer.js` verifies `authorizeApproval()` rejects direct approval by `getsantry[bot]` and `sentry-release-bot[bot]` without calling `getPermission`, even when `getsentry/sentry-javascript` is allowlisted; it also rejects `getsantry[bot]` when that target is absent from the allowlist. * 🟡 (21:08) `src/modules/__tests__/approval-au…
Date: Sep 8, 2026 * 🔴 (21:07) User stated that the CI poller always adds `ci-ready`. * 🟡 (21:07) `.github/workflows/publish.yml` triggers on issue `labeled` events, uses concurrency group `${{ github.event.issue.title }}` with `cancel-in-progress: false`, and grants `contents: read`, `issues: write`, and `packages: write`. * 🟡 (21:07) `.github/workflows/publish.yml:26-168` `waiting-for-ci` run…
Date: Sep 8, 2026 * 🔴 (21:07) User stated: “Always run trusted code.” * 🔴 (21:07) User stated: “Never move a release to ci-ready after it changes.” * 🔴 (21:07) User stated that a renamed or re-approved issue never reaches `ci-ready`. * 🟡 (21:07) `.github/workflows/ci-poller.yml:99-106` polls only open issues carrying both `ci-pending` and `accepted`, with `--limit 200` and JSON fields `number…
Date: Sep 8, 2026 * 🔴 [requested-security-review] (21:06) User requested an independent, read-only security and correctness review of the exact current worktree, focused on the approval pipeline, protected production-environment secret boundary, manual CI-poller dispatch relay, `workflow_run` semantics, action/image pinning, request-bound attestations, and regressions. * 🔴 [enforced-read-only] …