Dashboard › publish › Session 1XzeAkJpapzX
1XzeAkJpapzX380rcREAD-ONLY audit in /home/byk/Code/getsentry/publish. Do not modify any repository state. Compare the exact current worktree with HEAD/base 7c60ddb7f43040fe8fbfea70efc833f689c04e75, including staged, unstaged, and untracked files. Independently verify these five reported defects with current file:line evidence and executable reasoning: newer unlabeled events may fail to revoke accepted/ci-ready attestations; branch-head/check-suite lookup errors may fall back to a stale SHA and advance to ci-ready; final ci-ready add failure may strand an accepted issue after ci-pending removal; duplicate accepted-label removal may reject before target restoration completes; Inform start may block publication on a comment/metadata failure. Then inspect approval provenance/request digest binding, direct-bot rejection, trusted manual poller relay, exact revision checkout, path/state containment, action/Craft pins, pre-Craft validation, and dependency-free terminal cleanup for any additional security defect. Return findings first, ordered by severity and labeled MUST-FIX, CONCERN, or PASS, with current file:line references. State which files and read-only tests you inspected. If any required command fails, report the exact command/error. End with exactly MERGE or DO-NOT-MERGE. Empty output is forbidden.
<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 scripts/ src/ vitest.config.js yarn.lock (16 entries) </entries>
MM .github/workflows/auto-approve.yml MM .github/workflows/ci-poller.yml M .github/workflows/cocoapods-keepalive.yml MM .github/workflows/publish.yml M .github/workflows/test.yml M AGENTS.md M README.md M docs/key_rotation_vault_changes.md M docs/rfc.md M src/libs/__tests__/github.js M src/libs/github.js M src/modules/__tests__/ci-poller-input.js M src/modules/__tests__/ci-poller-workflow.js MM src/modules/__tests__/details-from-context.js M src/modules/__tests__/process-end-state.js M src/modules/__tests__/publish-location.js M src/modules/__tests__/publish-workflow.js M src/modules/__tests__/release-revision.js M src/modules/__tests__/update-issue.js M src/modules/ci-poller-input.js M src/modules/details-from-context.js M src/modules/post-workflow-details.js MM src/modules/process-end-state.js M src/modules/publish-location.js M src/modules/release-revision.js M src/publish/__tests__/discover-location.js M src/publish/__tests__/resolve-release-revision.js M ...
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 docs/key_rotation_vault_changes.md M docs/rfc.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/publish/inputs.js M src/publish/post-result.js M src/publish/post-workflow-details.js M src/publish/update-issue.js 8e4530c4a3b36b00445028365ab293870bc3d19520ef829cb92b6c60eefe187d -
M .github/workflows/auto-approve.yml M .github/workflows/ci-poller.yml M .github/workflows/publish.yml M src/modules/__tests__/ci-poller-input.js M src/modules/__tests__/ci-poller-workflow.js M src/modules/__tests__/details-from-context.js M src/modules/__tests__/process-end-state.js M src/modules/__tests__/publish-location.js M src/modules/__tests__/publish-workflow.js M src/modules/__tests__/release-revision.js M src/modules/ci-poller-input.js M src/modules/process-end-state.js M src/modules/publish-location.js M src/modules/release-revision.js M src/publish/__tests__/discover-location.js M src/publish/__tests__/resolve-release-revision.js M src/publish/discover-location.js M src/publish/post-result.js M src/publish/resolve-ci-poller-input.js 16c8e3d564c98865ee0d3c1412f21e156a09d8790b579ba8b8092d5b246a21f4 -
.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__/authorize-approval.js src/publish/__tests__/auto-approval-workflow.js src/publish/__tests__/ci-poller-workflow.js src/publish/__tests__/current-accepted-event.js src/publish/__tests__/post-result.js src/publish/__tests__/publish-workflow.js src/publish/__tests__/record-auto-approval-attestation.js src/publish/__tests__/record-ci-ready-attestation.js src/publish/__tests__/request-digest-from-event.js src/publish/__tests__/validate-approval-attestation.js src/publish/__tests__/workflow-action-pinning.js 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 src/publish/request-digest-from-event.js src/publish/validate-approval-attestation.js 570dcb023a...
...output truncated...
Full output saved to: /home/byk/.local/share/opencode/tool-output/tool_08b7375cc0015IV7DKh65lHFZQ
+ # resulting ci-ready event to have the same actor.
+ attestation_output=$(mktemp)
+ if ! GITHUB_OUTPUT="$attestation_output" \
+ APPROVAL_TOKEN="$GH_TOKEN" \
+ APPROVAL_ISSUE_NUMBER="$number" \
+ APPROVAL_ISSUE_REPOSITORY="$GITHUB_REPOSITORY" \
+ APPROVAL_ISSUE_TITLE="$title" \
+ EXPECTED_ACCEPTED_ACTOR="$accepted_actor" \
+ EXPECTED_ACCEPTED_EVENT_ID="$accepted_event_id" \
+ EXPECTED_REQUEST_DIGEST="$request_digest" \
+ node .__publish__/src/publish/record-ci-ready-attestation.js; then
+ rm -f "$attestation_output"
+ echo "::warning::Approval for issue #${number} changed before CI-ready proof could be recorded. Removing accepted."
+ gh issue edit "$number" -R ...
<path>/home/byk/.local/share/opencode/tool-output/tool_08b7375cc0015IV7DKh65lHFZQ</path> <type>file</type> <content> 1: diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml 2: index d9763c7..5f86245 100644 3: --- a/.github/workflows/auto-approve.yml 4: +++ b/.github/workflows/auto-approve.yml 5: @@ -5,25 +5,47 @@ on: 6: 7: permissions: 8: contents: read 9: + issues: write 10: 11: jobs: 12: auto-approve: 13: runs-on: ubuntu-latest 14: + environment: production 15: if: | 16: (github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') && 17: startsWith(github.event.issue.title, 'publish: ') 18: steps: 19: - - uses: actions/checkout@v4 20: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 21: with: 22: - sparse-checkout: auto-approve-repos.txt 23: - sparse-checkout-cone-mode: false 24: + persist-credentials: false 25: - ...
<path>/home/byk/.local/share/opencode/tool-output/tool_08b7375cc0015IV7DKh65lHFZQ</path>
<type>file</type>
<content>
1078: + writeFileSync(eventLogFile, "");
1079:
1080: writeExecutable(
1081: join(binDirectory, "node"),
1082: `#!/usr/bin/env bash
1083: set -eu
1084: -if [[ -n "\${PUBLISH_REVISION:-}" ]]; then
1085: - printf '%s' "\${NODE_UPDATE_OUTPUT:-}"
1086: -elif [[ "\${NODE_INITIAL_EXIT:-0}" != "0" ]]; then
1087: - exit "\${NODE_INITIAL_EXIT}"
1088: -else
1089: - printf '%s' "\${NODE_INITIAL_OUTPUT}"
1090: -fi
1091: +printf '%s\n' "$1" >> "$NODE_LOG"
1092: +printf 'node %s\n' "$1" >> "$EVENT_LOG"
1093: +case "$1" in
1094: + *current-accepted-event.js)
1095: + if [[ "\${CURRENT_ACCEPTED_EXIT:-0}" != "0" ]]; then
1096: + exit "$CURRENT_ACCEPTED_EXIT"
1097: + fi
1098: + printf '%s' '{"actor":"approver","eventId":"1"}'
1099: + ;;
1100: + *validate-approval-attestation.js)
1101: + validation_count=0
1102: + [[ ! ...
<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: persist-credentials: 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: Bind request snapshot
28: ...
<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/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/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/.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: ...
<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 (
16: typeof body !== "string" ||
17: !Array.isArray(labels) ||
18: typeof title !== "string"
19: ) {
20: throw new Error("Invalid publish request");
21: }
22:
23: return crypto
24: .createHash("sha256")
25: .update(
26: JSON.stringify({
27: body,
28: dryRun: labels.some((label) => label.name === ...
<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({ actor, issueTitle, getPermission }) {
28: const title = parsePublishTitle(issueTitle);
29:
30: if (!title) {
31: return { authorized: ...
<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: requestDigest,
7: } = require("../modules/approval-attestation.js");
8:
9: async function getGitHubResponse(path) {
10: return fetch(`https://api.github.com/${path}`, {
11: headers: {
12: Accept: "application/vnd.github+json",
13: Authorization: `Bearer ${process.env.APPROVAL_TOKEN}`,
14: "X-GitHub-Api-Version": "2026-03-10",
15: },
16: });
17: }
18:
19: async function getIssue({ repository, issueNumber }) {
20: const response = await getGitHubResponse(
21: `repos/${repository}/issues/${issueNumber}`
22: );
23:
24: if (!response.ok) {
25: throw new Error(
26: `Could not retrieve issue #${issueNumber}: GitHub returned ${response.status}`
27: );
28: }
29:
30: return ...
<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: requestDigest,
7: } = require("../modules/approval-attestation.js");
8: const {
9: AUTO_APPROVAL_LABELER,
10: AUTO_APPROVERS,
11: authorizeApproval,
12: isAutoApprovedRepository,
13: } = require("../modules/approval-authorizer.js");
14:
15: function getAutoApprovedRepositories() {
16: return new Set(
17: fs
18: .readFileSync("auto-approve-repos.txt", "utf8")
19: .split(/\r?\n/)
20: .filter(Boolean)
21: );
22: }
23:
24: async function getPermission({ owner, repository, username }) {
25: const response = await getGitHubResponse(
26: `repos/${encodeURIComponent(owner)}/${encodeURIComponent(
27: repository
28: )}/collaborators/${encodeURIComponent(username)}/permission`
29: );
30:
31: if ...
<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: requestDigest,
5: } = require("../modules/approval-attestation.js");
6: const { getIssue } = require("./validate-approval-attestation.js");
7:
8: async function recordAutoApprovalAttestation({
9: autoApprover,
10: expectedRequestDigest,
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: requestDigest(issue) !== expectedRequestDigest ||
22: issue.user?.login?.toLowerCase() !== autoApprover.toLowerCase()
23: ) {
24: throw new Error("The automated approval request changed before approval");
25: }
26:
27: return createAutoApprovalAttestation({ autoApprover, issue });
28: }
29:
30: ...
<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: requestDigest,
6: } = require("../modules/approval-attestation.js");
7: const {
8: getAllPages,
9: getGitHubResponse,
10: getIssue,
11: } = require("./validate-approval-attestation.js");
12:
13: async function getAuthenticatedLogin() {
14: const response = await getGitHubResponse("user");
15:
16: if (!response.ok) {
17: throw new Error(
18: `Could not retrieve the authenticated user: GitHub returned ${response.status}`
19: );
20: }
21:
22: const { login } = await response.json();
23:
24: if (typeof login !== "string") {
25: throw new Error("GitHub returned no authenticated user login");
26: }
27:
28: return login;
29: }
30:
31: async function recordCiReadyAttestation({
32: expectedAcceptedEvent,
33: ...
<path>/home/byk/Code/getsentry/publish/src/publish/request-digest-from-event.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const { requestDigest } = require("../modules/approval-attestation.js");
3:
4: function requestDigestFromEvent(event) {
5: if (!event || typeof event.issue !== "object" || event.issue === null) {
6: throw new Error("The GitHub event has no issue snapshot");
7: }
8:
9: return requestDigest(event.issue);
10: }
11:
12: function main() {
13: if (!process.env.GITHUB_EVENT_PATH) {
14: throw new Error('No "GITHUB_EVENT_PATH" environment variable found');
15: }
16:
17: if (!process.env.GITHUB_OUTPUT) {
18: throw new Error('No "GITHUB_OUTPUT" environment variable found');
19: }
20:
21: const event = JSON.parse(
22: fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8")
23: );
24: fs.appendFileSync(
25: process.env.GITHUB_OUTPUT,
26: `request_digest=${requestDigestFromEvent(event)}\n`
27: );
28: ...
<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:
8: // Validate the status before applying any terminal state transition.
9: sentryInfoFromDetails({ status, repo });
10:
11: if (status === "success") {
12: await octokit.rest.issues.update({
13: ...publishRepo,
14: issue_number,
15: state: "closed",
16: });
17: }
18:
19: const workflowInfo = (
20: await octokit.rest.actions.getWorkflowRun({
21: ...publishRepo,
22: run_id,
23: })
24: ).data;
25:
26: const details = {
27: repo,
28: version,
29: publishRepo,
30: run_id,
31: issue_number,
32: workflowInfo,
33: ...
<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({
8: octokit,
9: stateFilePath,
10: publishRepo,
11: issue_number,
12: }) {
13: if (!stateFilePath || !fs.existsSync(stateFilePath)) {
14: return;
15: }
16:
17: const issueRequest = octokit.rest.issues.get({
18: ...publishRepo,
19: issue_number,
20: });
21:
22: const craftStateRequest = fs.promises
23: .readFile(stateFilePath, { encoding: "utf-8" })
24: .then((data) => JSON.parse(data));
25:
26: const [{ data: issue }, craftState] = await Promise.all([
27: issueRequest,
28: craftStateRequest,
29: ]);
30:
31: const newIssueBody = transformIssueBody(craftState, issue.body);
32:
33: await octokit.rest.issues.update({
34: ...publishRepo,
35: ...
<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/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/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>
<path>/home/byk/Code/getsentry/publish/src/modules/details-from-context.js</path>
<type>file</type>
<content>
1: const { parse: parsePublishIssueTitle } = require("./publish-issue-title");
2: const { getReleaseRevisionDetails } = require("./release-revision");
3: const { isPublishPath } = require("./publish-location");
4: const {
5: isPublishRepository,
6: isReleaseVersion,
7: } = require("./publish-issue-validation");
8:
9: /**
10: * Matches the entire "Targets" section of a github publish issue body.
11: */
12: const TARGETS_SECTION_PARSER_REGEX =
13: /^(?!### Targets$\s)(?: *- \[[ xX]\] \S+\s*$(?:\r?\n)?)+/m;
14:
15: /**
16: * Matches all targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`.
17: * The "id" of the targets is captured within a capture group. ...
<path>/home/byk/Code/getsentry/publish/src/modules/ci-poller-input.js</path>
<type>file</type>
<content>
1: const { parse: parsePublishIssueTitle } = require("./publish-issue-title");
2: const {
3: getReleaseRevision,
4: updateReleaseRevision,
5: } = require("./release-revision");
6: const {
7: isPublishRepository,
8: isReleaseVersion,
9: } = require("./publish-issue-validation");
10: const { isPublishPath } = require("./publish-location");
11: const { requestDigest } = require("./approval-attestation");
12:
13: function getCiPollerInput({ title, issueBody, labels, revision }) {
14: let parsedTitle;
15: try {
16: parsedTitle = parsePublishIssueTitle(title);
17: } catch {
18: throw new Error(`Invalid publish issue title: '${title}'`);
19: }
20: const { repo, version } = parsedTitle;
21: if (!isPublishRepository(repo)) {
22: throw new Error(`Invalid publish issue repository: '${repo}'`);
23: }
24: if (!isReleaseVersion(version)) {
25: throw new ...
<path>/home/byk/Code/getsentry/publish/src/modules/release-revision.js</path>
<type>file</type>
<content>
1: const { parse } = require("./publish-issue-title");
2:
3: function isRevision(revision) {
4: return /^[0-9a-f]{40}$/.test(revision);
5: }
6:
7: function getReleaseRevisionDetails({ issueBody, repo }) {
8: if (parse(issueBody, { startRule: "CheckRunsLinkCount" }) !== 1) {
9: throw new Error(
10: `Expected exactly one View check runs link in Quick links for getsentry/${repo}.`
11: );
12: }
13:
14: let details;
15: try {
16: details = parse(issueBody, { startRule: "ReleaseRevision" });
17: } catch {
18: throw new Error(
19: `Expected a View check runs link for getsentry/${repo} in the publish issue body.`
20: );
21: }
22:
23: if (details.repo !== repo) {
24: throw new Error(
25: `Expected a View check runs link for getsentry/${repo} in the publish issue body.`
26: );
27: }
28:
29: return details;
30: }
31:
32: ...
<path>/home/byk/Code/getsentry/publish/src/publish/resolve-release-revision.js</path>
<type>file</type>
<content>
1: const core = require("@actions/core");
2: const { getReleaseRevision } = require("../modules/release-revision");
3:
4: function resolveReleaseRevision() {
5: const { repo } = JSON.parse(process.env.PUBLISH_ARGS || "{}");
6: if (!repo) {
7: throw new Error("Publish input must define a repository.");
8: }
9:
10: core.setOutput(
11: "revision",
12: getReleaseRevision({
13: issueBody: process.env.PUBLISH_ISSUE_BODY || "",
14: repo,
15: })
16: );
17: }
18:
19: resolveReleaseRevision();
(End of file - total 19 lines)
</content>
<path>/home/byk/Code/getsentry/publish/src/modules/publish-location.js</path>
<type>file</type>
<content>
1: function resolvePublishLocation({ path, workspaceNames }) {
2: if (!isPublishPath(path)) {
3: throw new Error("Invalid publish path.");
4: }
5:
6: if (!workspaceNames.every(isWorkspaceName)) {
7: throw new Error(
8: "Craft workspace discovery returned an invalid workspace list."
9: );
10: }
11:
12: if (path === ".") {
13: return { path };
14: }
15:
16: const workspace = path.slice(2);
17: if (workspaceNames.includes(workspace)) {
18: return { path: ".", workspace };
19: }
20:
21: return { path };
22: }
23:
24: function isPublishPath(path) {
25: return (
26: typeof path === "string" &&
27: (path === "." ||
28: (path.startsWith("./") &&
29: path.slice(2).split("/").every(isSafeWorkspaceSegment)))
30: );
31: }
32:
33: function isWorkspaceName(name) {
34: return (
35: typeof name === "string" && ...
<path>/home/byk/Code/getsentry/publish/src/publish/discover-location.js</path>
<type>file</type>
<content>
1: const { execFileSync } = require("child_process");
2: const { existsSync } = require("fs");
3: const path = require("path");
4:
5: const core = require("@actions/core");
6:
7: const { resolvePublishLocation } = require("../modules/publish-location");
8:
9: const CRAFT_IMAGE =
10: "getsentry/craft@sha256:9a4a5d5efa44a00c2215078ead39800d4aaa5a97908b94f45a64d7d506d6e14b";
11:
12: function getWorkspaceNames({
13: repositoryDirectory,
14: exists = existsSync,
15: execFile = execFileSync,
16: }) {
17: if (!exists(path.join(repositoryDirectory, ".craft.yml"))) {
18: return [];
19: }
20:
21: const output = execFile(
22: "docker",
23: [
24: "run",
25: "--rm",
26: "--volume",
27: `${path.resolve(repositoryDirectory)}:/github/workspace/__repo__`,
28: "--workdir",
29: "/github/workspace/__repo__",
30: ...
<path>/home/byk/Code/getsentry/publish/src/modules/publish-issue-validation.js</path>
<type>file</type>
<content>
1: function isPublishRepository(name) {
2: return (
3: /^[A-Za-z0-9_.-]+$/.test(name) &&
4: name !== "." &&
5: name !== ".." &&
6: name !== "__proto__" &&
7: !name.startsWith("-")
8: );
9: }
10:
11: function isReleaseVersion(version) {
12: return /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-?([0-9a-z-]+(?:\.[0-9a-z-]+)*))?(?:\+([0-9a-z-]+(?:\.[0-9a-z-]+)*))?$/i.test(
13: version
14: );
15: }
16:
17: module.exports = { isPublishRepository, isReleaseVersion };
(End of file - total 17 lines)
</content>
# REST API endpoints for issue comments
Use the REST API to manage comments on issues and pull requests.
## About issue and pull request comments
You can use the REST API to create and manage comments on issues and pull requests. Every pull request is an issue, but not every issue is a pull request. For this reason, "shared" actions for both features, like managing assignees, labels, and milestones, are provided within the Issues endpoints. To manage pull request review comments, see [REST API endpoints for pull request review comments](/en/rest/pulls/comments).
> \[!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 issue comments for a repository
```
GET /repos/{owner}/{repo}/issues/comments
```
You can use the REST API to list comments on issues and pull requests for a repository. ...
<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: ...
<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: requestDigest,
6: } = require("../../modules/approval-attestation.js");
7: const {
8: getAutoApprovedRepositories,
9: main,
10: } = require("../authorize-approval.js");
11:
12: const originalEnvironment = { ...process.env };
13:
14: afterEach(() => {
15: process.env = { ...originalEnvironment };
16: vi.restoreAllMocks();
17: vi.resetModules();
18: });
19:
20: function jsonResponse(json) {
21: return { ok: true, json: vi.fn().mockResolvedValue(json) };
22: }
23:
24: async function runAuthorization({ actor, issueTitle, responses }) {
25: process.env.GITHUB_OUTPUT = "/tmp/github-output";
26: process.env.APPROVAL_TOKEN = "release-bot-token";
27: process.env.APPROVAL_ACTOR = actor;
28: ...
<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: requestDigest,
10: } = require("../../modules/approval-attestation.js");
11:
12: afterEach(() => {
13: vi.restoreAllMocks();
14: });
15:
16: function jsonResponse(json) {
17: return { ok: true, json: vi.fn().mockResolvedValue(json) };
18: }
19:
20: function issue(
21: title,
22: { body = "", labels = [{ name: "accepted" }], state = "open" } = {}
23: ) {
24: return { body, labels, state, title };
25: }
26:
27: describe("validateApprovalAttestation", () => {
28: test("accepts the current accepted event and its trusted attestation", async () => {
29: const title = "publish: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/current-accepted-event.js</path>
<type>file</type>
<content>
1: import { describe, expect, test, vi } from "vitest";
2:
3: const { getCurrentAcceptedEvent } = require("../current-accepted-event.js");
4:
5: describe("getCurrentAcceptedEvent", () => {
6: test("returns the latest accepted-label event", async () => {
7: await expect(
8: getCurrentAcceptedEvent({
9: getIssueEvents: vi.fn().mockResolvedValue([
10: {
11: actor: { login: "contractor" },
12: event: "labeled",
13: id: "100",
14: label: { name: "accepted" },
15: },
16: {
17: actor: { login: "contractor" },
18: event: "labeled",
19: id: "200",
20: label: { name: "accepted" },
21: },
22: ]),
23: issueNumber: "123",
24: repository: "getsentry/publish",
25: })
26: ).resolves.toEqual({ ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/record-ci-ready-attestation.js</path>
<type>file</type>
<content>
1: import { describe, expect, test, vi } from "vitest";
2:
3: const {
4: parseCiReadyAttestation,
5: requestDigest,
6: } = require("../../modules/approval-attestation.js");
7: const {
8: recordCiReadyAttestation,
9: } = require("../record-ci-ready-attestation.js");
10:
11: describe("recordCiReadyAttestation", () => {
12: test("binds the current approval to the app that will add ci-ready", async () => {
13: const title = "publish: getsentry/sentry-javascript@10.0.0";
14:
15: await expect(
16: recordCiReadyAttestation({
17: expectedRequestDigest: requestDigest({
18: body: "Merge target: main",
19: labels: [{ name: "accepted" }],
20: title,
21: }),
22: getAuthenticatedLogin: vi
23: .fn()
24: .mockResolvedValue("sentry-internal-app[bot]"),
25: getIssue: ...
<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: requestDigest,
6: } = require("../../modules/approval-attestation.js");
7: const {
8: recordAutoApprovalAttestation,
9: } = require("../record-auto-approval-attestation.js");
10:
11: describe("recordAutoApprovalAttestation", () => {
12: test("binds an automated requester to the live publish request", async () => {
13: const title = "publish: getsentry/sentry-javascript@10.0.0";
14:
15: await expect(
16: recordAutoApprovalAttestation({
17: autoApprover: "getsantry[bot]",
18: expectedRequestDigest: requestDigest({
19: body: "Merge target: main",
20: labels: [],
21: title,
22: }),
23: getIssue: vi.fn().mockResolvedValue({
24: body: "Merge target: main",
25: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/request-digest-from-event.js</path>
<type>file</type>
<content>
1: import { describe, expect, test } from "vitest";
2:
3: const { requestDigest } = require("../../modules/approval-attestation.js");
4: const { requestDigestFromEvent } = require("../request-digest-from-event.js");
5:
6: describe("requestDigestFromEvent", () => {
7: test("digests the exact issue event snapshot", () => {
8: const issue = {
9: body: "Merge target: main",
10: labels: [{ name: "accepted" }, { name: "dry-run" }],
11: title: "publish: getsentry/relay@1.2.3",
12: };
13:
14: expect(requestDigestFromEvent({ issue })).toBe(requestDigest(issue));
15: });
16:
17: test("rejects events without a complete issue snapshot", () => {
18: expect(() => requestDigestFromEvent({})).toThrow(
19: "The GitHub event has no issue snapshot"
20: );
21: expect(() =>
22: requestDigestFromEvent({ issue: { body: ...
<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/modules/__tests__/ci-poller-workflow.js</path>
<type>file</type>
<content>
1: import {
2: mkdtempSync,
3: mkdirSync,
4: readFileSync,
5: readdirSync,
6: rmSync,
7: writeFileSync,
8: } from "fs";
9: import { tmpdir } from "os";
10: import { join } from "path";
11: import { spawnSync } from "child_process";
12:
13: import { afterEach, expect, test } from "vitest";
14:
15: const temporaryDirectories = [];
16: const INITIAL_REVISION = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
17: const UPDATED_REVISION = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
18:
19: function getPollerScript() {
20: const workflow = readFileSync(
21: join(__dirname, "../../../.github/workflows/ci-poller.yml"),
22: "utf8"
23: );
24: const section = workflow.match(
25: / {6}- name: Check CI status for ci-pending issues[\s\S]*? {8}run: \|\n(?<script>[\s\S]*?)\n {6}- name: Check for remaining pending issues/
26: );
27:
28: if ...
<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("binds approval and publication to the triggering issue snapshot", () => {
8: expect(workflow).toContain("node src/publish/request-digest-from-event.js");
9: expect(
10: workflow.match(
11: /EXPECTED_REQUEST_DIGEST: \$\{\{ steps\.request-digest\.outputs\.request_digest \}\}/g
12: )
13: ).toHaveLength(4);
14: });
15:
16: test("revalidates approval immediately before Craft", () => {
17: const fence = workflow.indexOf("Revalidate approval attestation");
18:
19: expect(fence).toBeGreaterThan(workflow.indexOf("Set targets"));
20: expect(fence).toBeLessThan(workflow.indexOf("Publish using Craft"));
21: ...
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/publish-workflow.js</path>
<type>file</type>
<content>
1: import { spawnSync } from "child_process";
2: import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
3: import { tmpdir } from "os";
4: import { join } from "path";
5:
6: import { afterEach, expect, test } from "vitest";
7:
8: const { resolvePublishLocation } = require("../publish-location.js");
9:
10: const temporaryDirectories = [];
11:
12: function getWorkflow() {
13: return readFileSync(
14: join(__dirname, "../../../.github/workflows/publish.yml"),
15: "utf8"
16: );
17: }
18:
19: function getSetTargetsScript() {
20: const workflow = getWorkflow();
21: const section = workflow.match(
22: / {6}- name: Set targets[\s\S]*? {8}run: \|\n(?<script>[\s\S]*?)\n {6}- name: Revalidate approval attestation/
23: );
24: if (!section?.groups?.script) {
25: throw new Error("Missing Set targets workflow script");
26: }
27: ...
<path>/home/byk/Code/getsentry/publish/src/publish/post-result.js</path>
<type>file</type>
<content>
1: const processEndState = require("../modules/process-end-state.js");
2: const { getGitHubToken } = require("../libs/github.js");
3: const github = require("@actions/github");
4:
5: async function main({
6: context = github.context,
7: octokit = github.getOctokit(getGitHubToken()),
8: report = processEndState,
9: status = process.argv.slice(2)[0],
10: } = {}) {
11: let inputs = {};
12:
13: if (process.env.PUBLISH_ARGS) {
14: try {
15: inputs = JSON.parse(process.env.PUBLISH_ARGS);
16: } catch {
17: console.warn("Could not parse publish inputs; reporting without them");
18: }
19: }
20:
21: await report({ context, octokit, inputs, status });
22: }
23:
24: if (require.main === module) {
25: main().catch((error) => {
26: console.error(error);
27: process.exitCode = 1;
28: });
29: }
30:
31: module.exports = { main };
(End of file - ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/post-result.js</path>
<type>file</type>
<content>
1: import { afterEach, describe, expect, test, vi } from "vitest";
2:
3: const { main } = require("../post-result.js");
4:
5: const originalEnvironment = { ...process.env };
6:
7: afterEach(() => {
8: process.env = { ...originalEnvironment };
9: });
10:
11: describe("post-result entry point", () => {
12: test.each([undefined, "not-json"])(
13: "reports terminal state when publish inputs are %s",
14: async (publishArgs) => {
15: const report = vi.fn().mockResolvedValue(undefined);
16: if (publishArgs === undefined) {
17: delete process.env.PUBLISH_ARGS;
18: } else {
19: process.env.PUBLISH_ARGS = publishArgs;
20: }
21:
22: await main({
23: context: {},
24: octokit: {},
25: report,
26: status: "failure",
27: });
28:
29: expect(report).toHaveBeenCalledWith(
30: ...
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/update-issue.js</path>
<type>file</type>
<content>
1: import { vi, describe, test, expect, beforeAll, beforeEach, it } from "vitest";
2: import fs from "fs";
3:
4: const { updateIssue, transformIssueBody } = require("../update-issue.js");
5:
6: let mockExistsSync;
7:
8: const updateTargetsArgs = {
9: inputs: { repo: "sentry", version: "21.3.1" },
10: context: {
11: runId: "1234",
12: repo: { owner: "getsentry", repo: "publish" },
13: payload: { issue: { number: "211" } },
14: },
15: octokit: {
16: rest: {
17: actions: {
18: getWorkflowRun: async () => ({
19: data: {
20: html_url: "https://github.com/getsentry/sentry/actions/runs/1234",
21: },
22: }),
23: },
24: issues: {
25: get: vi.fn(),
26: update: vi.fn(),
27: removeLabel: vi.fn(),
28: },
29: },
30: },
31: Sentry: {
32: Scope: class Scope ...
<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", JSON.stringify(result));
10: }
11: inputs();
(End of file - total 11 lines)
</content>
<path>/home/byk/Code/getsentry/publish/src/publish/resolve-ci-poller-input.js</path>
<type>file</type>
<content>
1: const { getCiPollerInput } = require("../modules/ci-poller-input");
2: const { readFileSync } = require("fs");
3:
4: const issueBody = process.env.PUBLISH_ISSUE_BODY_FILE
5: ? readFileSync(process.env.PUBLISH_ISSUE_BODY_FILE, "utf8")
6: : process.env.PUBLISH_ISSUE_BODY || "";
7:
8: process.stdout.write(
9: JSON.stringify(
10: getCiPollerInput({
11: issueBody,
12: labels: JSON.parse(process.env.PUBLISH_ISSUE_LABELS || ""),
13: title: process.env.PUBLISH_TITLE || "",
14: revision: process.env.PUBLISH_REVISION || "",
15: })
16: )
17: );
(End of file - total 17 lines)
</content>
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/discover-location.js</path>
<type>file</type>
<content>
1: import { afterEach, expect, test, vi } from "vitest";
2:
3: const {
4: discoverLocation,
5: getWorkspaceNames,
6: } = require("../discover-location.js");
7:
8: afterEach(() => {
9: vi.restoreAllMocks();
10: });
11:
12: test("retains checkout-path behavior when the root config is absent", () => {
13: expect(
14: discoverLocation({
15: input: { path: "./packages/cli" },
16: repositoryDirectory: "__repo__",
17: exists: () => false,
18: })
19: ).toEqual({ path: "./packages/cli" });
20: });
21:
22: test("discovers exact workspace paths using the released Craft image", () => {
23: const execFile = vi.fn(() => '["packages/CLI"]');
24:
25: expect(
26: discoverLocation({
27: input: { path: "./packages/CLI" },
28: repositoryDirectory: "__repo__",
29: exists: () => true,
30: execFile,
31: })
32: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/resolve-release-revision.js</path>
<type>file</type>
<content>
1: import { spawnSync } from "child_process";
2: import { expect, test } from "vitest";
3:
4: const script = new URL("../resolve-release-revision.js", import.meta.url)
5: .pathname;
6:
7: test("rejects missing workflow input through release revision validation", () => {
8: const result = spawnSync(process.execPath, [script], {
9: encoding: "utf8",
10: env: {
11: ...process.env,
12: PUBLISH_ARGS: "",
13: PUBLISH_ISSUE_BODY: "",
14: },
15: });
16:
17: expect(result.status).not.toBe(0);
18: expect(result.stderr).toContain("Publish input must define a repository.");
19: expect(result.stderr).not.toContain("Unexpected end of JSON input");
20: });
(End of file - total 20 lines)
</content>
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/release-revision.js</path>
<type>file</type>
<content>
1: import { expect, test } from "vitest";
2:
3: const {
4: getReleaseRevision,
5: updateReleaseRevision,
6: } = require("../release-revision.js");
7:
8: const REVISION = "7e5ca7ed5581552de066e2a8bc295b8306be38ac";
9:
10: function requestBody(quickLinks) {
11: return `Requested by: @byk
12:
13: Merge target: (default)
14:
15: Quick links:
16: ${quickLinks}`;
17: }
18:
19: function canonicalQuickLinks(revision = REVISION) {
20: return `- [View changes](https://github.com/getsentry/toolkit/compare/1.2.2...release/1.2.3)
21: - [View check runs](https://github.com/getsentry/toolkit/commit/${revision}/checks/)`;
22: }
23:
24: test("gets the CI-approved revision from the request header", () => {
25: expect(
26: getReleaseRevision({
27: repo: "toolkit",
28: issueBody: requestBody(canonicalQuickLinks()),
29: })
30: ).toBe(REVISION);
31: ...
<path>/home/byk/Code/getsentry/publish/src/modules/publish-issue-title.js</path>
<type>file</type>
<content>
1: /* eslint-disable */
2: // @generated by Peggy 5.1.0.
3: //
4: // https://peggyjs.org/
5:
6: "use strict";
7:
8: class peg$SyntaxError extends SyntaxError {
9: constructor(message, expected, found, location) {
10: super(message);
11: this.expected = expected;
12: this.found = found;
13: this.location = location;
14: this.name = "SyntaxError";
15: }
16:
17: format(sources) {
18: let str = "Error: " + this.message;
19: if (this.location) {
20: let src = null;
21: const st = sources.find((s) => s.source === this.location.source);
22: if (st) {
23: src = st.text.split(/\r\n|\n|\r/g);
24: }
25: const s = this.location.start;
26: const offset_s =
27: this.location.source &&
28: typeof this.location.source.offset === "function"
29: ? this.location.source.offset(s)
30: ...
/home/byk/Code/getsentry/publish/src/modules/publish-issue-title.peggy /home/byk/Code/getsentry/publish/src/modules/publish-issue-title.js
<path>/home/byk/Code/getsentry/publish/src/modules/publish-issue-title.peggy</path>
<type>file</type>
<content>
1: {
2: function join(characters) {
3: return characters.join("");
4: }
5: }
6:
7: // BEGIN TITLE GRAMMAR
8: // Canonical grammar for publish issue titles. A path suffix is syntactic only:
9: // the controller resolves the complete suffix as a workspace after checking
10: // out the CI-approved revision.
11: PublishIssueTitle
12: = "publish: " "getsentry/"? repo:Repository path:Path? "@" version:Version !. {
13: return {
14: repo,
15: path: path || "",
16: version,
17: };
18: }
19:
20: Repository
21: = characters:RepositoryCharacter+ { return join(characters); }
22:
23: RepositoryCharacter
24: = [A-Za-z0-9_.-]
25:
26: Path
27: = segments:("/" segment:PathSegment { return `/${segment}`; })+ { return join(segments); }
28:
29: PathSegment
30: = characters:RepositoryCharacter+ { return join(characters); }
31:
32: ...
Found 18 matches /home/byk/Code/getsentry/publish/.github/workflows/test.yml: Line 19: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 Line 20: - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 Line 23: - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 /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 56: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 Line 167: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 Line 220: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 Line 256: uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 Line 284: uses: ...
<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/.lore.md</path> <type>file</type> <content> 1: <!-- Managed by lore (https://github.com/BYK/loreai) — manual edits are imported on next session. --> 2: 3: ## Long-term Knowledge 4: 5: ### Architecture 6: 7: <!-- lore:01a043d7-502d-7de9-9416-66f3050c12b7 --> 8: * **approval-authorizer GitHub roles**: Chose live GitHub collaborator permission checks over private \`security-as-code\` because Publish is public and GitHub reflects effective target-repository access without exposing RBAC. Humans authorize only with \`write\`, \`maintain\`, or \`admin\`; \`triage\` never authorizes. \`getsantry\[bot]\` must never fall through to collaborator lookup: it authorizes only when its exact parsed repository/release path is in \`auto-approve-repos.txt\`, because bot repository access does not prove the event followed an allowlisted auto-approval path. 9: 10: <!-- lore:01a082db-d8b9-7438-927a-07bf7aa5e369 --> 11: * **production environment secret boundary**: ...
<path>/home/byk/Code/getsentry/publish/.lore.md</path> <type>file</type> <content> 241: <!-- lore:01a08b2d-aae8-771e-8c37-4b6a283ff28c --> 242: * **Use \`ci-ready\` as the sole publish trigger**: Assume the CI poller always adds \`ci-ready\`, and gate the publish job on \`ci-ready\` label events rather than \`accepted\`. Keep approval and CI-waiting flows separate so \`publish\` cannot race \`waiting-for-ci\`. Bind transitions to the current request and approval event, revalidate before adding or consuming \`ci-ready\`, and ensure renamed, changed, or re-approved releases cannot publish using stale authorization. On terminal outcomes, reliably remove active authorization labels (\`accepted\`, \`ci-pending\`, and \`ci-ready\`), closing the issue only on success. Protect these lifecycle rules with focused fail-first regression tests and mutation checks. 243: 244: <!-- lore:01a08722-a96f-7f8c-b212-adbcce39fc05 --> 245: * **Use precise GitHub REST API documentation and preserve access ...
<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("checks out the code used to record the attestation", () => {
8: const checkout = workflow.slice(
9: workflow.indexOf("uses: actions/checkout@"),
10: workflow.indexOf("Record automated approval attestation")
11: );
12:
13: expect(checkout).not.toContain("sparse-checkout:");
14: });
15:
16: test("records a request-bound attestation before adding accepted", () => {
17: expect(workflow).toContain("node src/publish/request-digest-from-event.js");
18: expect(workflow).toContain(
19: "EXPECTED_REQUEST_DIGEST: ${{ steps.request-digest.outputs.request_digest }}"
20: );
21: ...
/home/byk/Code/getsentry/publish/src/modules/__tests__/post-workflow-details.js
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/post-workflow-details.js</path>
<type>file</type>
<content>
1: import { vi, describe, test, expect } from "vitest";
2:
3: vi.mock("fs");
4:
5: const postWorkflowDetails = require("../post-workflow-details.js");
6:
7: describe("postWorkflowDetails", () => {
8: test("create comment with workflow details", async () => {
9: const args = {
10: context: {
11: runId: "1234",
12: repo: { owner: "getsentry", repo: "publish" },
13: payload: { issue: { number: "211" } },
14: },
15: octokit: {
16: rest: {
17: actions: {
18: getWorkflowRun: async () => ({
19: data: {
20: html_url:
21: "https://github.com/getsentry/sentry/actions/runs/1234",
22: },
23: }),
24: },
25: issues: {
26: createComment: vi.fn(),
27: },
28: },
29: },
30: ...
.github/workflows/auto-approve.yml | 30 ++- .github/workflows/ci-poller.yml | 232 +++++++++++++++--- .github/workflows/cocoapods-keepalive.yml | 1 - .github/workflows/publish.yml | 271 +++++++++++++++++++--- .github/workflows/test.yml | 6 +- AGENTS.md | 6 + README.md | 4 +- docs/key_rotation_vault_changes.md | 40 ++-- docs/rfc.md | 9 +- src/libs/__tests__/github.js | 2 +- src/libs/github.js | 6 +- src/modules/__tests__/ci-poller-input.js | 22 +- src/modules/__tests__/ci-poller-workflow.js | 207 ++++++++++++++--- src/modules/__tests__/details-from-context.js | 22 +- src/modules/__tests__/process-end-state.js | 27 +++ src/modules/__tests__/publish-location.js ...
<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", JSON.stringify(result));
10: }
11: inputs();
(End of file - total 11 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/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/modules/__tests__/details-from-context.js</path>
<type>file</type>
<content>
1: import { test, expect } from "vitest";
2:
3: const {
4: detailsFromContext,
5: parsePublishTitle,
6: } = require("../details-from-context.js");
7:
8: const inputsArgs = {
9: context: {
10: repo: { owner: "getsentry", repo: "publish" },
11: payload: {
12: issue: {
13: number: "223",
14: title: "publish: getsentry/sentry@21.3.1",
15: body: `Requested by: @BYK
16:
17: Merge target: custom-branch
18:
19: Quick links:
20: - [View changes](https://github.com/getsentry/sentry/compare/21.3.0...refs/heads/releases/21.3.1)
21: - [View check runs](https://github.com/getsentry/sentry/commit/7e5ca7ed5581552de066e2a8bc295b8306be38ac/checks/)
22:
23: Assign the **accepted** label to this issue to approve the release. ...
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/process-end-state.js</path>
<type>file</type>
<content>
1: import { vi, describe, test, expect } from "vitest";
2:
3: vi.mock("fs");
4:
5: const processEndState = require("../process-end-state.js");
6:
7: describe("publish failed", () => {
8: const failureArgs = {
9: status: "failure",
10: inputs: { repo: "sentry", version: "21.3.1" },
11: context: {
12: runId: "1234",
13: repo: { owner: "getsentry", repo: "publish" },
14: payload: { issue: { number: "211" } },
15: },
16: octokit: {
17: rest: {
18: actions: {
19: getWorkflowRun: async () => ({
20: data: {
21: html_url: "https://github.com/getsentry/sentry/actions/runs/1234",
22: },
23: }),
24: },
25: issues: {
26: createComment: vi.fn(),
27: },
28: },
29: },
30: Sentry: {
31: Scope: class Scope {
32: ...
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/publish-location.js</path>
<type>file</type>
<content>
1: import { expect, test } from "vitest";
2:
3: const { resolvePublishLocation } = require("../publish-location.js");
4:
5: test("classifies an exact full workspace path without normalizing it", () => {
6: expect(
7: resolvePublishLocation({
8: path: "./packages/CLI",
9: workspaceNames: ["packages/cli", "packages/CLI"],
10: })
11: ).toStrictEqual({ path: ".", workspace: "packages/CLI" });
12: });
13:
14: test("keeps a non-workspace suffix as a checkout path", () => {
15: expect(
16: resolvePublishLocation({
17: path: "./packages",
18: workspaceNames: ["cli"],
19: })
20: ).toStrictEqual({ path: "./packages" });
21: });
22:
23: test("keeps a multi-segment suffix that is not an exact workspace path", () => {
24: expect(
25: resolvePublishLocation({
26: path: "./packages/cli",
27: workspaceNames: ...
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/ci-poller-input.js</path>
<type>file</type>
<content>
1: import { readFileSync } from "fs";
2: import { join } from "path";
3:
4: import { expect, test } from "vitest";
5:
6: const { getCiPollerInput } = require("../ci-poller-input.js");
7: const { requestDigest } = require("../approval-attestation.js");
8:
9: const REVISION = "7e5ca7ed5581552de066e2a8bc295b8306be38ac";
10: const labels = [{ name: "accepted" }, { name: "dry-run" }];
11: const issueBody = `Requested by: @byk
12:
13: Merge target: (default)
14:
15: Quick links:
16: - [View changes](https://github.com/getsentry/toolkit/compare/1.2.2...release/1.2.3)
17: - [View check runs](https://github.com/getsentry/toolkit/commit/${REVISION}/checks/)`;
18:
19: test("parses root and full-path publish titles", () => {
20: const rootTitle = "publish: getsentry/toolkit/cli@1.2.3";
21: expect(
22: getCiPollerInput({
23: issueBody,
24: labels,
25: ...
<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: "generate": "node scripts/generate-publish-issue-title-parser.js",
19: "check:generated": "node scripts/generate-publish-issue-title-parser.js --check",
20: "test": "yarn check:generated && vitest run",
21: "test:watch": "vitest",
22: "lint": "eslint src .github --ignore-pattern '!.github'",
23: "prettier": "prettier --write src"
24: },
25: "devDependencies": {
26: "eslint": "^8.9.0",
27: ...
<path>/home/byk/Code/getsentry/publish/scripts/generate-publish-issue-title-parser.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const path = require("path");
3: const peggy = require("peggy");
4: const prettier = require("prettier");
5:
6: const grammarPath = path.join(
7: __dirname,
8: "..",
9: "src",
10: "modules",
11: "publish-issue-title.peggy"
12: );
13: const outputPath = path.join(
14: __dirname,
15: "..",
16: "src",
17: "modules",
18: "publish-issue-title.js"
19: );
20: const documentationPath = path.join(
21: __dirname,
22: "..",
23: "docs",
24: "publish-issue-format.md"
25: );
26: function generateDocumentation({ documentation, titleGrammar }) {
27: const markers = documentation.match(
28: /<!--\s*(?:BEGIN|END)\s+GENERATED\s+TITLE\s+GRAMMAR\b/g
29: );
30:
31: if (
32: markers?.filter(marker => marker.includes("BEGIN")).length !== 1 ||
33: markers?.filter(marker => marker.includes("END")).length !== ...
Found 2 matches
/home/byk/Code/getsentry/publish/src/publish/__tests__/publish-workflow.js:
Line 41: const guard = waitingForCi.indexOf("name: Revoke stranded approval");
/home/byk/Code/getsentry/publish/src/modules/__tests__/publish-workflow.js:
Line 68: const informStart = workflow.indexOf("name: Inform start");
gh version 2.94.0 (2026-06-10) https://github.com/cli/cli/releases/tag/v2.94.0
package edit
import (
"fmt"
"net/http"
"sort"
"strings"
"sync"
"time"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/api"
fd "github.com/cli/cli/v2/internal/featuredetection"
"github.com/cli/cli/v2/internal/gh"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/cli/cli/v2/internal/text"
issueShared "github.com/cli/cli/v2/pkg/cmd/issue/shared"
prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/spf13/cobra"
)
type EditOptions struct {
HttpClient func() (*http.Client, error)
IO *iostreams.IOStreams
BaseRepo func() (ghrepo.Interface, error)
Prompter prShared.EditPrompter
Detector fd.Detector
DetermineEditor func() (string, error)
FieldsToEditSurvey func(prShared.EditPrompter, *prShared.Editable) error
EditFieldsSurvey func(prShared.EditPrompter, *prShared.Editable, string) error
FetchOptions func(*api.Client, ghrepo.Interface, ...
package shared
import (
"net/http"
"github.com/cli/cli/v2/api"
"github.com/cli/cli/v2/internal/ghrepo"
"github.com/shurcooL/githubv4"
"golang.org/x/sync/errgroup"
)
func UpdateIssue(httpClient *http.Client, repo ghrepo.Interface, id string, isPR bool, options Editable) error {
var wg errgroup.Group
// Labels are updated through discrete mutations to avoid having to replace the entire list of labels
// and risking race conditions.
if options.Labels.Edited {
if len(options.Labels.Add) > 0 {
wg.Go(func() error {
addedLabelIds, err := options.Metadata.LabelsToIDs(options.Labels.Add)
if err != nil {
return err
}
return addLabels(httpClient, id, repo, addedLabelIds)
})
}
if len(options.Labels.Remove) > 0 {
wg.Go(func() error {
removeLabelIds, err := options.Metadata.LabelsToIDs(options.Labels.Remove)
if err != nil {
return err
}
return removeLabels(httpClient, id, repo, removeLabelIds)
})
}
}
// updateIssue ...
yarn run v1.22.22 $ yarn check:generated && vitest run --printConsoleTrace $ node scripts/generate-publish-issue-title-parser.js --check [1m[46m RUN [49m[22m [36mv4.1.0 [39m[90m/home/byk/Code/getsentry/publish[39m [2m Test Files [22m [1m[32m27 passed[39m[22m[90m (27)[39m [2m Tests [22m [1m[32m185 passed[39m[22m[90m (185)[39m [2m Start at [22m 13:31:09 [2m Duration [22m 7.73s[2m (transform 595ms, setup 0ms, import 4.09s, tests 6.34s, environment 7ms)[22m Done in 10.52s.
yarn run v1.22.22 $ eslint src .github --ignore-pattern '!.github' Done in 3.42s.
(no output)
{"acceptedUnlabeled":true,"readyUnlabeled":true}
name: Publish
on:
issues:
types: [labeled]
concurrency:
# Use the issue title (e.g. "publish: getsentry/foo@1.2.3") so duplicate
# issues for the same repo@version share a concurrency group.
group: ${{ github.event.issue.title }}
cancel-in-progress: false
permissions:
contents: read
issues: write
packages: write
jobs:
# When accepted is added to a publish issue:
# - Add ci-pending (and remove ci-failed if retrying)
# - Enable the poller via CI_POLLER_HAS_PENDING=true
# - Comment on the issue
# - Trigger the poller immediately so we don't wait for the next cron tick
# The publish job below requires ci-ready, so it will not fire until the
# poller flips ci-pending → ci-ready (which also prevents publishing without
# CI verification in the auto-approve race).
waiting-for-ci:
runs-on: ubuntu-latest
name: Waiting for CI
environment: production
if: >-
github.event.label.name == 'accepted'
&& ...
{"id":9871907870,"node_id":"EN_kwDOEgGvjc8AAAACTGlcHg","name":"production","url":"https://api.github.com/repos/getsentry/publish/environments/production","html_url":"https://github.com/getsentry/publish/deployments/activity_log?environments_filter=production","created_at":"2025-11-10T21:33:16Z","updated_at":"2026-09-08T20:55:15Z","can_admins_bypass":false,"protection_rules":[{"id":42720759,"node_id":"GA_kwDOEgGvjc4Ci933","type":"branch_policy"}],"deployment_branch_policy":{"protected_branches":false,"custom_branch_policies":true}}