Dashboard › publish › Session 0Y8WEQRI0cAu
0Y8WEQRI0cAu0hbSGConduct an independent, read-only exact-head review of the current worktree. Do not edit. Review every modified relevant file, especially the manual CI-poller dispatch architecture. Verify that arbitrary workflow_dispatch refs never access secrets or can influence privileged behavior; manual recovery remains always available; the default-branch trusted poller executes the privileged path; repository dispatch attempts are strict base-10 bounded before arithmetic; self-dispatch works; actions and Craft are immutable; approval attestations remain secure (automation proof, direct bot rejection, human self-approval separation, request/state/event binding, ci-ready and pre-Craft fences, cleanup). Cite current file:line. Respond non-empty in the exact format: findings high-to-low marked MUST-FIX/CONCERN/PASS, compact test evidence, and end exactly MERGE or DO-NOT-MERGE. If blocked, state exact cause and end DO-NOT-MERGE.
<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/publish.yml M .github/workflows/test.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/__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 ?? src/publish/record-auto-approval-attestation.js ?? src/publish/record-ci-ready-attestation.js ?? ...
diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml
index d9763c7..15e819a 100644
--- a/.github/workflows/auto-approve.yml
+++ b/.github/workflows/auto-approve.yml
@@ -1,35 +1,53 @@
name: auto-approve non-sdks
on:
issues:
types: [opened]
permissions:
contents: read
+ issues: write
jobs:
auto-approve:
runs-on: ubuntu-latest
if: |
(github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') &&
startsWith(github.event.issue.title, 'publish: ')
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
with:
sparse-checkout: auto-approve-repos.txt
sparse-checkout-cone-mode: false
- name: Get auth token
id: token
- uses: actions/create-github-app-token@v3
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
with:
client-id: ...
diff --git a/src/libs/__tests__/github.js b/src/libs/__tests__/github.js
index 710091c..ea7761b 100644
--- a/src/libs/__tests__/github.js
+++ b/src/libs/__tests__/github.js
@@ -1,19 +1,19 @@
import { vi, describe, test, expect } from "vitest";
vi.mock("fs");
const { getGitHubToken } = require("../github.js");
describe("getGitHubToken", () => {
test("throw if no token is defined", async () => {
delete process.env.GITHUB_TOKEN;
expect(() => getGitHubToken()).toThrow(
- 'No "GITHUB_TOKEN" environment variable found.',
+ 'No "GITHUB_TOKEN" environment variable found.'
);
});
test("return token if defined", async () => {
process.env.GITHUB_TOKEN = "Example Token";
expect(getGitHubToken()).toEqual("Example Token");
});
});
diff --git a/src/libs/github.js b/src/libs/github.js
index e4d506f..f1188c1 100644
--- a/src/libs/github.js
+++ b/src/libs/github.js
@@ -1,17 +1,19 @@
/**
* This is a helper function that throws a useful error ...
<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: runs-on: ubuntu-latest
11: steps:
12: - run: true
(End of file - total 12 lines)
</content>
<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/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/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/.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: # Always allow the workflow_dispatch relay for manual recovery. 23: if: vars.CI_POLLER_HAS_PENDING == 'true' || github.event_name == 'workflow_run' || github.event_name == 'repository_dispatch' 24: concurrency: 25: group: ci-status-poller 26: cancel-in-progress: false 27: steps: 28: - name: Validate dispatch attempt 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/src/publish/__tests__</path> <type>directory</type> <entries> authorize-approval.js auto-approval-workflow.js ci-poller-workflow.js current-accepted-event.js publish-workflow.js record-auto-approval-attestation.js record-ci-ready-attestation.js validate-approval-attestation.js workflow-action-pinning.js (9 entries) </entries>
<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 no-secret workflow dispatch relay for manual recovery", () => {
22: ...
<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/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/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__/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__/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__/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__/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__/workflow-action-pinning.js</path>
<type>file</type>
<content>
1: import { readFileSync } from "fs";
2: import { describe, expect, test } from "vitest";
3:
4: const workflows = [
5: ".github/workflows/auto-approve.yml",
6: ".github/workflows/ci-poller.yml",
7: ".github/workflows/publish.yml",
8: ".github/workflows/test.yml",
9: ].map((path) => ({ path, contents: readFileSync(path, "utf8") }));
10:
11: describe("workflow action pinning", () => {
12: test.each(workflows)("pins actions in $path", ({ contents }) => {
13: const actionReferences = contents.matchAll(
14: /^\s+(?:- )?uses: actions\/[^\s@]+@([^\s]+)$/gm
15: );
16:
17: for (const [, revision] of actionReferences) {
18: expect(revision).toMatch(/^[a-f0-9]{40}$/);
19: }
20: });
21: });
(End of file - total 21 lines)
</content>
<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: if: |
14: (github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') &&
15: startsWith(github.event.issue.title, 'publish: ')
16: steps:
17: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
18: with:
19: sparse-checkout: auto-approve-repos.txt
20: sparse-checkout-cone-mode: false
21: - name: Get auth token
22: id: token
23: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
24: with:
25: client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
26: private-key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }}
27: - name: ...
<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__/approval-attestation.js [2m([22m[2m5 tests[22m[2m)[22m[32m 31[2mms[22m[39m [32m✓[39m src/publish/__tests__/validate-approval-attestation.js [2m([22m[2m9 tests[22m[2m)[22m[32m 32[2mms[22m[39m [32m✓[39m src/publish/__tests__/current-accepted-event.js [2m([22m[2m2 tests[22m[2m)[22m[32m 38[2mms[22m[39m [32m✓[39m src/publish/__tests__/authorize-approval.js [2m([22m[2m7 tests[22m[2m)[22m[32m 23[2mms[22m[39m [32m✓[39m src/modules/__tests__/process-end-state.js [2m([22m[2m4 tests[22m[2m)[22m[32m 102[2mms[22m[39m [32m✓[39m src/modules/__tests__/approval-authorizer.js [2m([22m[2m15 tests[22m[2m)[22m[32m 36[2mms[22m[39m [32m✓[39m src/modules/__tests__/post-workflow-details.js [2m([22m[2m1 test[22m[2m)[22m[32m 19[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.
# Workflow syntax for GitHub Actions A workflow is a configurable automated process made up of one or more jobs. You must create a YAML file to define your workflow configuration. ## About YAML syntax for workflows Workflow files use YAML syntax, and must have either a `.yml` or `.yaml` file extension. If you're new to YAML and want to learn more, see [Learn YAML in Y minutes](https://learnxinyminutes.com/docs/yaml/). You must store workflow files in the `.github/workflows` directory of your repository. > \[!TIP] > Unlike traditional GitHub Actions workflows that require you to script every decision as YAML job steps, GitHub Agentic Workflows use YAML frontmatter for triggers and configuration, but let you describe what you want in natural-language Markdown—so you don't need to anticipate and encode every scenario in advance. For more information, see [Creating GitHub Agentic Workflows](/en/copilot/how-tos/github-agentic-workflows/creating-github-agentic-workflows). ...
# Events that trigger workflows You can configure your workflows to run when specific activity on GitHub happens, at a scheduled time, or when an event outside of GitHub occurs. ## About events that trigger workflows Workflow triggers are events that cause a workflow to run. For more information about how to use workflow triggers, see [Triggering a workflow](/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow). Some events have multiple activity types. For these events, you can specify which activity types will trigger a workflow run. For more information about what each activity type means, see [Webhook events and payloads](/en/webhooks/webhook-events-and-payloads). > \[!NOTE] > Not all webhook events trigger workflows. Like GitHub Actions workflows, agentic workflows can be triggered by repository events and schedules. ...
<path>/home/byk/Code/getsentry/publish/.github/workflows</path> <type>directory</type> <entries> auto-approve.yml ci-poller-dispatch.yml ci-poller.yml cocoapods-keepalive.yml publish.yml test.yml (6 entries) </entries>
<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: workflow_dispatch:
10:
11: permissions:
12: contents: read
13:
14: jobs:
15: keepalive:
16: name: Refresh CocoaPods Session
17: runs-on: macos-15
18: environment: production
19: env:
20: COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
21:
22: steps:
23: - name: Install CocoaPods
24: run: gem install cocoapods
25: - name: Print CocoaPods Version
26: run: pod --version
27: ...
# Manually running a workflow When a workflow is configured to run on the workflow_dispatch event, you can run the workflow using the Actions tab on GitHub, GitHub CLI, or the REST API. ## Configuring a workflow to run manually To run a workflow manually, the workflow must be configured to run on the `workflow_dispatch` event. To trigger the `workflow_dispatch` event, your workflow must be in the default branch. For more information about configuring the `workflow_dispatch` event, see [Events that trigger workflows](/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#workflow_dispatch). Write access to the repository is required to perform these steps. ## Running a workflow <div class="ghd-tool webui"> 1. On GitHub, navigate to the main page of the repository. 2. Under your repository name, click **<svg version="1.1" width="16" height="16" viewBox="0 0 16 16" class="octicon octicon-play" aria-label="play" role="img"><path d="M8 0a8 8 0 1 1 0 16A8 8 0 0 1 ...
# REST API endpoints for workflows
Use the REST API to interact with workflows in GitHub Actions.
## About workflows in GitHub Actions
You can use the REST API to view workflows for a repository in GitHub Actions. Workflows automate your software development life cycle with a wide range of tools and services. For more information, see [Workflows](/en/actions/concepts/workflows-and-actions/workflows) in the GitHub Actions documentation.
> \[!NOTE]
> Most endpoints use `Authorization: Bearer <YOUR-TOKEN>` and `Accept: application/vnd.github+json` headers, plus `X-GitHub-Api-Version: 2026-03-10`. Curl examples below omit these standard headers for brevity.
## List repository workflows
```
GET /repos/{owner}/{repo}/actions/workflows
```
Lists the workflows in a repository.
Anyone with read access to the repository can use this endpoint.
OAuth app tokens and personal access tokens (classic) need the repo scope to use this endpoint with a private repository. ...
MUST-FIX — Arbitrary workflow_dispatch refs can still run secret-bearing YAML. The CI relay’s permissions: {} only removes GITHUB_TOKEN; a selected ref supplies workflow config and can add ${{ secrets.* }} (.github/workflows/ci-poller-dispatch.yml:3-12). Worse, CocoaPods dispatch directly exposes a production secret (.github/workflows/cocoapods-keepalive.yml:9,18-20). This violates the arbitrary-ref secret boundary.
CONCERN — yarn lint fails at .github/workflows/cocoapods-keepalive.yml:1 (yml/plain-scalar). It is pre-existing but leaves the exact head lint-red.
PASS — The privileged poller checks out the default branch before requesting either app token (.github/workflows/ci-poller.yml:40-67); the relay has no declared credentials (.github/workflows/ci-poller-dispatch.yml:3-12).
PASS — Manual recovery remains unconditional through the relay’s workflow_dispatch and the poller’s workflow_run gate (.github/workflows/ci-poller-dispatch.yml:3-12, .github/workflows/ci-poller.yml:8-10,23).
PASS — Repository-dispatch attempts are strictly bounded before arithmetic and self-dispatch uses validated base-10 input (.github/workflows/ci-poller.yml:28-38,445-455).
PASS — Approval protections hold: request-bound automated proof and direct-bot rejection (src/publish/authorize-approval.js:135-169), human requester/approver separation (src/publish/authorize-approval.js:200-210), event/digest binding (src/publish/validate-approval-attestation.js:71-100), CI-ready revalidation (.github/workflows/ci-poller.yml:130-145,266-346), and pre-Craft fencing (.github/workflows/publish.yml:197-220,328-353).
PASS — Changed workflows pin actions and Craft to full immutable revisions (.github/workflows/auto-approve.yml:17-23, .github/workflows/ci-poller.yml:41-52, .github/workflows/publish.yml:192-223,353).
Test evidence — yarn test --printConsoleTrace: 16 files, 72 tests passed. git diff --check: passed. yarn lint: failed as noted above. ...
Date: Sep 8, 2026 * 🔴 [workflow-id-file-name-convention] (20:47) User specified that REST API `workflow_id` parameters can be replaced with the workflow file name (for example, `main.yaml`). * 🟡 (20:47) GitHub Actions REST API documentation: `GET /repos/{owner}/{repo}/actions/workflows` lists repository workflows; read access is sufficient, and private repositories require `repo` scope for OAut…
Date: Sep 8, 2026 * 🔴 [workflow-command-convention] (20:45) User specified that the `workflow` parameter in `gh workflow run WORKFLOW` should be replaced with the workflow’s name, ID, or file name; examples: `"Link Checker"`, `1234567`, or `"link-check-test.yml"`. * 🟡 (20:45) GitHub Actions documentation states manual runs require a `workflow_dispatch` trigger and the workflow file to exist on …
Date: Sep 8, 2026 * 🟡 (20:44) `yarn lint` (`eslint src .github --ignore-pattern '!.github'`) failed with exit code `1`: `.github/workflows/cocoapods-keepalive.yml:1:7` violates `yml/plain-scalar` (“Must use plain style scalar”). ESLint reported `1` error, `0` warnings; the error is potentially auto-fixable via `--fix`. * 🟡 (20:44) GitHub Actions workflow-syntax documentation was retrieved succe…
Date: Sep 8, 2026 * 🟡 (20:42) `src/publish/__tests__/authorize-approval.js` has 7 Vitest tests. `getAutoApprovedRepositories()` reads exact newline-delimited paths from `auto-approve-repos.txt`, including `getsentry/sentry-javascript` and nested path `getsentry/objectstore/clients`; it must return a `Set` of those exact paths. * 🟡 (20:42) `authorize-approval.js` test harness sets `GITHUB_OUTPUT…
Date: Sep 8, 2026 * 🔴 (20:42) User stated that the CI poller always adds `ci-ready` after checking CI; `waiting-for-ci` must first remove an existing `ci-ready` label so this creates a fresh `labeled` event and guarantees the `publish` job triggers on the happy path. * 🟡 (20:42) `.github/workflows/publish.yml` defines `Publish`, triggered by `issues` `labeled` events, with concurrency group `${…
Date: Sep 8, 2026 * 🟡 (20:40) `.github/workflows/ci-poller-dispatch.yml` defines `Run CI Status Poller`, triggered only by `workflow_dispatch`, with empty `permissions: {}` and one `dispatch` job on `ubuntu-latest` whose sole step is `run: true`; this workflow serves as a relay for the main poller’s `workflow_run` trigger. * 🟡 (20:41) `src/modules/approval-attestation.js` implements base64url-e…
Date: Sep 8, 2026 * 🟡 (20:40) `src/libs/github.js` reformatted `getGitHubToken()`’s missing-token error construction without changing its full runtime message (`No "GITHUB_TOKEN" environment variable found. Please ensure the workflow is configured correctly`), while `src/libs/__tests__/github.js` was changed to expect only `No "GITHUB_TOKEN" environment variable found.`—creating a test/implement…
Date: Sep 8, 2026 * 🔴 [requested-review] (20:39) User requested an independent, read-only exact-HEAD review of the current worktree; explicitly instructed not to edit and to review every modified relevant file, especially the manual CI-poller dispatch architecture. * 🔴 [requested-security-review] (20:39) User required verification that arbitrary `workflow_dispatch` refs never access secrets or …