Dashboard › publish › Session 1BpBuVO0t33J
1BpBuVO0t33JPd6VYPerform an independent read-only correctness/security review of the exact current worktree. Do not edit. Scope decision: organization-wide availability of SENTRY_INTERNAL_APP_PRIVATE_KEY and SENTRY_RELEASE_BOT_PRIVATE_KEY is an explicitly accepted pre-existing organization Security issue and is NOT a merge blocker for this repository change; still note only if this change worsens it. Review all current changed files and tests, especially approval provenance, direct-bot denial, human self-approval denial, live request/event/digest binding, CI-ready transition (must always add ci-ready after CI and never after mutation/reapproval), pre-Craft fence, cleanup, manual workflow_dispatch relay and workflow_run main/success gate, repository dispatch input handling, protected production environment usage, immutable action and Craft pins. Verify test/lint/diff evidence yourself. Return non-empty findings first, classified MUST-FIX/CONCERN/PASS with exact current file:line evidence, then tests. End exactly MERGE or DO-NOT-MERGE. If blocked, begin BLOCKED with exact tool/error and end DO-NOT-MERGE.
<path>/home/byk/Code/getsentry/publish</path> <type>directory</type> <entries> .eslintrc.js .git/ .github/ .gitignore .lore.md AGENTS.md auto-approve-repos.txt docs/ LICENSE node_modules/ package.json README.md src/ vitest.config.js yarn.lock (15 entries) </entries>
## main...origin/main [behind 1] M .github/workflows/auto-approve.yml M .github/workflows/ci-poller.yml M .github/workflows/cocoapods-keepalive.yml M .github/workflows/publish.yml M .github/workflows/test.yml M AGENTS.md M README.md M src/libs/__tests__/github.js M src/libs/github.js M src/modules/__tests__/details-from-context.js M src/modules/__tests__/update-issue.js M src/modules/details-from-context.js M src/modules/post-workflow-details.js M src/modules/process-end-state.js M src/modules/update-issue.js M src/publish/inputs.js M src/publish/post-result.js M src/publish/post-workflow-details.js M src/publish/update-issue.js ?? .github/workflows/ci-poller-dispatch.yml ?? .lore.md ?? src/modules/__tests__/approval-attestation.js ?? src/modules/__tests__/approval-authorizer.js ?? src/modules/approval-attestation.js ?? src/modules/approval-authorizer.js ?? src/publish/__tests__/ ?? src/publish/authorize-approval.js ?? src/publish/current-accepted-event.js ?? ...
.github/workflows/auto-approve.yml | 23 ++- .github/workflows/ci-poller.yml | 195 +++++++++++++++++++++++--- .github/workflows/cocoapods-keepalive.yml | 3 +- .github/workflows/publish.yml | 139 ++++++++++++++++-- .github/workflows/test.yml | 6 +- AGENTS.md | 6 + README.md | 4 +- src/libs/__tests__/github.js | 2 +- src/libs/github.js | 6 +- src/modules/__tests__/details-from-context.js | 22 ++- src/modules/__tests__/update-issue.js | 28 ++++ src/modules/details-from-context.js | 17 ++- src/modules/post-workflow-details.js | 4 +- src/modules/process-end-state.js | 4 +- src/modules/update-issue.js | 42 +++--- src/publish/inputs.js | 17 ++- src/publish/post-result.js | 8 ...
<path>/home/byk/Code/getsentry/publish/.github/workflows/auto-approve.yml</path>
<type>file</type>
<content>
1: name: auto-approve non-sdks
2: on:
3: issues:
4: types: [opened]
5:
6: permissions:
7: contents: read
8: issues: write
9:
10: jobs:
11: auto-approve:
12: runs-on: ubuntu-latest
13: environment: production
14: if: |
15: (github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') &&
16: startsWith(github.event.issue.title, 'publish: ')
17: steps:
18: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
19: with:
20: sparse-checkout: auto-approve-repos.txt
21: sparse-checkout-cone-mode: false
22: - name: Get auth token
23: id: token
24: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
25: with:
26: client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
27: private-key: ${{ ...
<path>/home/byk/Code/getsentry/publish/.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/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/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/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: ...
/home/byk/Code/getsentry/publish/src/publish/__tests__/workflow-action-pinning.js /home/byk/Code/getsentry/publish/src/publish/__tests__/record-auto-approval-attestation.js /home/byk/Code/getsentry/publish/src/publish/__tests__/publish-workflow.js /home/byk/Code/getsentry/publish/src/publish/__tests__/auto-approval-workflow.js /home/byk/Code/getsentry/publish/src/publish/__tests__/ci-poller-workflow.js /home/byk/Code/getsentry/publish/src/publish/__tests__/current-accepted-event.js /home/byk/Code/getsentry/publish/src/publish/__tests__/record-ci-ready-attestation.js /home/byk/Code/getsentry/publish/src/publish/__tests__/validate-approval-attestation.js /home/byk/Code/getsentry/publish/src/publish/__tests__/authorize-approval.js
<path>/home/byk/Code/getsentry/publish/src/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({ actor, issueTitle, getPermission }) {
28: const title = parsePublishTitle(issueTitle);
29:
30: if (!title) {
31: return { authorized: ...
<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 { getIssue } = require("./validate-approval-attestation.js");
6:
7: async function recordAutoApprovalAttestation({
8: autoApprover,
9: getIssue,
10: issueNumber,
11: issueTitle,
12: repository,
13: }) {
14: const issue = await getIssue({ repository, issueNumber });
15:
16: if (
17: issue.state !== "open" ||
18: issue.title !== issueTitle ||
19: issue.user?.login?.toLowerCase() !== autoApprover.toLowerCase()
20: ) {
21: throw new Error("The automated approval request changed before approval");
22: }
23:
24: return createAutoApprovalAttestation({ autoApprover, issue });
25: }
26:
27: async function main() {
28: for (const name of [
29: "GITHUB_OUTPUT",
30: "APPROVAL_TOKEN",
31: ...
<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 ...
rg: regex parse error:
(?:\buses:\s*[^\s]+@(?![0-9a-f]{40}(?:\s|$)|sha256:[0-9a-f]{64}(?:\s|$)))
^^^
error: look-around, including look-ahead and look-behind, is not supported
Consider enabling PCRE2 with the --pcre2 flag, which can handle backreferences
and look-around.
Found 18 matches /home/byk/Code/getsentry/publish/.github/workflows/publish.yml: Line 36: uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 Line 42: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 Line 52: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 Line 149: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 Line 192: uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 Line 223: uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 Line 268: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 Line 274: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 Line 353: - uses: ...
<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/details-from-context.js</path> <type>file</type> <content> 1: /** 2: * Matches the entire "Targets" section of a github publish issue body. 3: */ 4: const TARGETS_SECTION_PARSER_REGEX = 5: /^(?!### Targets$\s)(?: *- \[[ xX]\] \S+\s*$(?:\r?\n)?)+/m; 6: 7: /** 8: * Matches all targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`. 9: * The "id" of the targets is captured within a capture group. 10: */ 11: const TARGETS_PARSER_REGEX = /^\s*- \[[ x]\] (\S+)/gim; 12: 13: /** 14: * Matches checked targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`. 15: * The "id" of the targets is captured within a capture group. 16: */ 17: const CHECKED_TARGETS_PARSER_REGEX = /^\s*- \[x\] (\S+)/gim; 18: const PUBLISH_TITLE_REGEX = 19: /^publish: ...
<path>/home/byk/Code/getsentry/publish/src/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/process-end-state.js</path>
<type>file</type>
<content>
1: const Sentry = require("@sentry/node");
2:
3: async function processEndState({ context, octokit, inputs, status }) {
4: const { repo, version } = inputs;
5: const { repo: publishRepo, runId: run_id } = context;
6: const { number: issue_number } = context.payload.issue;
7: const workflowInfo = (
8: await octokit.rest.actions.getWorkflowRun({
9: ...publishRepo,
10: run_id,
11: })
12: ).data;
13:
14: const details = {
15: repo,
16: version,
17: publishRepo,
18: run_id,
19: issue_number,
20: workflowInfo,
21: status,
22: };
23:
24: await postIssueComment({
25: octokit,
26: details,
27: });
28:
29: if (status === "success") {
30: await octokit.rest.issues.update({
31: ...publishRepo,
32: issue_number,
33: state: "closed",
34: });
35: }
36:
37: await reportSession({ ...
<path>/home/byk/Code/getsentry/publish/src/modules/update-issue.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3: TARGETS_SECTION_PARSER_REGEX,
4: TARGETS_PARSER_REGEX,
5: } = require("./details-from-context");
6:
7: async function updateTargets({ octokit, version, publishRepo, issue_number }) {
8: const CRAFT_STATE_FILE_PATH = `${process.env.GITHUB_WORKSPACE}/__repo__/.craft-publish-${version}.json`;
9:
10: if (!fs.existsSync(CRAFT_STATE_FILE_PATH)) {
11: return;
12: }
13:
14: const issueRequest = octokit.rest.issues.get({
15: ...publishRepo,
16: issue_number,
17: });
18:
19: const craftStateRequest = fs.promises
20: .readFile(CRAFT_STATE_FILE_PATH, { encoding: "utf-8" })
21: .then((data) => JSON.parse(data));
22:
23: const [{ data: issue }, craftState] = await Promise.all([
24: issueRequest,
25: craftStateRequest,
26: ]);
27:
28: const newIssueBody = transformIssueBody(craftState, issue.body);
29: ...
<path>/home/byk/Code/getsentry/publish/src/publish/inputs.js</path>
<type>file</type>
<content>
1: const github = require("@actions/github");
2: const core = require("@actions/core");
3: const { detailsFromContext } = require("../modules/details-from-context");
4:
5: async function inputs() {
6: const result = await detailsFromContext({
7: context: github.context,
8: });
9: core.setOutput("result", result);
10: }
11:
12: inputs();
(End of file - total 12 lines)
</content>
<path>/home/byk/Code/getsentry/publish/src/publish/post-result.js</path>
<type>file</type>
<content>
1: const processEndState = require("../modules/process-end-state");
2: const { getGitHubToken } = require("../libs/github");
3: const github = require("@actions/github");
4:
5: const context = github.context;
6: const octokit = github.getOctokit(getGitHubToken());
7: const inputs = JSON.parse(process.env.PUBLISH_ARGS);
8: const args = process.argv.slice(2);
9: const status = args[0];
10:
11: processEndState({ context, octokit, inputs, status });
(End of file - total 11 lines)
</content>
<path>/home/byk/Code/getsentry/publish/src/publish/post-workflow-details.js</path>
<type>file</type>
<content>
1: const postWorkflowDetails = require("../modules/post-workflow-details.js");
2: const { getGitHubToken } = require("../libs/github");
3: const github = require("@actions/github");
4:
5: const context = github.context;
6: const octokit = github.getOctokit(getGitHubToken());
7:
8: postWorkflowDetails({ context, octokit });
(End of file - total 8 lines)
</content>
<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>
diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml
index d9763c7..b2b489b 100644
--- a/.github/workflows/auto-approve.yml
+++ b/.github/workflows/auto-approve.yml
@@ -5,25 +5,44 @@ on:
permissions:
contents: read
+ issues: write
jobs:
auto-approve:
runs-on: ubuntu-latest
+ environment: production
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: ${{ vars.SENTRY_INTERNAL_APP_ID ...
<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: } = require("../../modules/approval-attestation.js");
6: const {
7: getAutoApprovedRepositories,
8: main,
9: } = require("../authorize-approval.js");
10:
11: const originalEnvironment = { ...process.env };
12:
13: afterEach(() => {
14: process.env = { ...originalEnvironment };
15: vi.restoreAllMocks();
16: vi.resetModules();
17: });
18:
19: function jsonResponse(json) {
20: return { ok: true, json: vi.fn().mockResolvedValue(json) };
21: }
22:
23: async function runAuthorization({ actor, issueTitle, responses }) {
24: process.env.GITHUB_OUTPUT = "/tmp/github-output";
25: process.env.APPROVAL_TOKEN = "release-bot-token";
26: process.env.APPROVAL_ACTOR = actor;
27: process.env.APPROVAL_ISSUE_NUMBER = ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/validate-approval-attestation.js</path>
<type>file</type>
<content>
1: import { afterEach, describe, expect, test, vi } from "vitest";
2:
3: const {
4: validateApprovalAttestation,
5: } = require("../validate-approval-attestation.js");
6: const {
7: createApprovalAttestation,
8: createCiReadyAttestation,
9: } = require("../../modules/approval-attestation.js");
10:
11: afterEach(() => {
12: vi.restoreAllMocks();
13: });
14:
15: function jsonResponse(json) {
16: return { ok: true, json: vi.fn().mockResolvedValue(json) };
17: }
18:
19: function issue(
20: title,
21: { body = "", labels = [{ name: "accepted" }], state = "open" } = {}
22: ) {
23: return { body, labels, state, title };
24: }
25:
26: describe("validateApprovalAttestation", () => {
27: test("accepts the current accepted event and its trusted attestation", async () => {
28: const title = "publish: getsentry/sentry-javascript@10.0.0";
29: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/record-ci-ready-attestation.js</path>
<type>file</type>
<content>
1: import { describe, expect, test, vi } from "vitest";
2:
3: const {
4: parseCiReadyAttestation,
5: } = require("../../modules/approval-attestation.js");
6: const {
7: recordCiReadyAttestation,
8: } = require("../record-ci-ready-attestation.js");
9:
10: describe("recordCiReadyAttestation", () => {
11: test("binds the current approval to the app that will add ci-ready", async () => {
12: const title = "publish: getsentry/sentry-javascript@10.0.0";
13:
14: await expect(
15: recordCiReadyAttestation({
16: getAuthenticatedLogin: vi
17: .fn()
18: .mockResolvedValue("sentry-internal-app[bot]"),
19: getIssue: vi.fn().mockResolvedValue({
20: body: "Merge target: main",
21: labels: [{ name: "accepted" }],
22: state: "open",
23: title,
24: }),
25: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/current-accepted-event.js</path>
<type>file</type>
<content>
1: import { describe, expect, test, vi } from "vitest";
2:
3: const { getCurrentAcceptedEvent } = require("../current-accepted-event.js");
4:
5: describe("getCurrentAcceptedEvent", () => {
6: test("returns the latest accepted-label event", async () => {
7: await expect(
8: getCurrentAcceptedEvent({
9: getIssueEvents: vi.fn().mockResolvedValue([
10: {
11: actor: { login: "contractor" },
12: event: "labeled",
13: id: "100",
14: label: { name: "accepted" },
15: },
16: {
17: actor: { login: "contractor" },
18: event: "labeled",
19: id: "200",
20: label: { name: "accepted" },
21: },
22: ]),
23: issueNumber: "123",
24: repository: "getsentry/publish",
25: })
26: ).resolves.toEqual({ ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/ci-poller-workflow.js</path>
<type>file</type>
<content>
1: import { readFileSync } from "fs";
2: import { describe, expect, test } from "vitest";
3:
4: const workflow = readFileSync(".github/workflows/ci-poller.yml", "utf8");
5: const manualDispatchWorkflow = readFileSync(
6: ".github/workflows/ci-poller-dispatch.yml",
7: "utf8"
8: );
9:
10: describe("CI poller workflow", () => {
11: test("checks out trusted code before requesting app tokens", () => {
12: expect(workflow).toMatch(
13: /- name: Get publish code\n\s+uses: actions\/checkout@[a-f0-9]{40}\n\s+with:\n(?:\s+#.*\n)*\s+ref: \$\{\{ github\.event\.repository\.default_branch \}\}/
14: );
15:
16: expect(workflow.indexOf("Get publish code")).toBeLessThan(
17: workflow.indexOf("Get auth token")
18: );
19: });
20:
21: test("uses a protected workflow dispatch relay for manual recovery", () => {
22: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/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__/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__/workflow-action-pinning.js</path>
<type>file</type>
<content>
1: import { readFileSync, readdirSync } from "fs";
2: import { describe, expect, test } from "vitest";
3:
4: const workflowPaths = readdirSync(".github/workflows")
5: .filter((path) => path.endsWith(".yml"))
6: .map((path) => `.github/workflows/${path}`);
7: const workflows = workflowPaths.map((path) => ({
8: path,
9: contents: readFileSync(path, "utf8"),
10: }));
11: const SECRET_BEARING_JOBS = {
12: ".github/workflows/auto-approve.yml": ["auto-approve"],
13: ".github/workflows/ci-poller.yml": ["check-ci"],
14: ".github/workflows/cocoapods-keepalive.yml": ["keepalive"],
15: ".github/workflows/publish.yml": ["waiting-for-ci", "publish"],
16: };
17:
18: function jobContents(workflow, jobName) {
19: const header = ` ${jobName}:`;
20: const jobStart = workflow.indexOf(header);
21: const nextJobOffset = workflow
22: .slice(jobStart + ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/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/libs/__tests__/github.js</path>
<type>file</type>
<content>
1: import { vi, describe, test, expect } from "vitest";
2:
3: vi.mock("fs");
4:
5: const { getGitHubToken } = require("../github.js");
6:
7: describe("getGitHubToken", () => {
8: test("throw if no token is defined", async () => {
9: delete process.env.GITHUB_TOKEN;
10: expect(() => getGitHubToken()).toThrow(
11: 'No "GITHUB_TOKEN" environment variable found.'
12: );
13: });
14:
15: test("return token if defined", async () => {
16: process.env.GITHUB_TOKEN = "Example Token";
17: expect(getGitHubToken()).toEqual("Example Token");
18: });
19: });
(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: `
16: Requested by: @BYK
17:
18: Merge target: custom-branch
19:
20: Quick links:
21: - [View changes](https://github.com/getsentry/sentry/compare/21.3.0...refs/heads/releases/21.3.1)
22: - [View check runs](https://github.com/getsentry/sentry/commit/7e5ca7ed5581552de066e2a8bc295b8306be38ac/checks/)
23:
24: Assign the **accepted** label to this issue to approve the release. ...
<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 ...
diff --git a/.github/workflows/cocoapods-keepalive.yml b/.github/workflows/cocoapods-keepalive.yml
index 21c0696..ffa5982 100644
--- a/.github/workflows/cocoapods-keepalive.yml
+++ b/.github/workflows/cocoapods-keepalive.yml
@@ -1,4 +1,4 @@
-name: "CocoaPods Token Keep-Alive"
+name: CocoaPods Token Keep-Alive
on:
schedule:
@@ -6,7 +6,6 @@ on:
# This runs every day at midnight UTC to keep the current token alive
# See: https://github.com/CocoaPods/trunk.cocoapods.org/blob/a1869790e9ae2229b6985b6af4532da814202558/app/models/session.rb#L9
- cron: "0 0 * * *" # Runs every day at midnight UTC
- workflow_dispatch:
permissions:
contents: read
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 44692d0..da7f78e 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -16,11 +16,11 @@ jobs:
runs-on: ubuntu-latest
name: unit tests
steps:
- - uses: actions/checkout@v6
- - uses: actions/setup-node@v6
+ ...
<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**: ...
7c60ddb feat: accept workspace publish requests .github/workflows/ci-poller.yml | 62 +- .github/workflows/cocoapods-keepalive.yml | 2 +- .github/workflows/publish.yml | 84 +- README.md | 5 + docs/publish-issue-format.md | 107 ++ package.json | 5 +- scripts/generate-publish-issue-title-parser.js | 104 ++ src/modules/__tests__/ci-poller-input.js | 97 ++ src/modules/__tests__/ci-poller-workflow.js | 197 +++ src/modules/__tests__/details-from-context.js | 166 ++- .../generate-publish-issue-title-parser.js | 40 + src/modules/__tests__/publish-location.js | 91 ++ src/modules/__tests__/publish-workflow.js | 156 +++ src/modules/__tests__/release-revision.js | 150 ++ src/modules/__tests__/update-issue.js | 15 +- ...
yarn run v1.22.22 $ vitest run --run --printConsoleTrace [1m[46m RUN [49m[22m [36mv4.0.17 [39m[90m/home/byk/Code/getsentry/publish[39m [32m✓[39m src/publish/__tests__/validate-approval-attestation.js [2m([22m[2m9 tests[22m[2m)[22m[32m 62[2mms[22m[39m [32m✓[39m src/modules/__tests__/approval-attestation.js [2m([22m[2m5 tests[22m[2m)[22m[32m 45[2mms[22m[39m [32m✓[39m src/modules/__tests__/update-issue.js [2m([22m[2m6 tests[22m[2m)[22m[32m 23[2mms[22m[39m [32m✓[39m src/modules/__tests__/post-workflow-details.js [2m([22m[2m1 test[22m[2m)[22m[32m 26[2mms[22m[39m [32m✓[39m src/modules/__tests__/process-end-state.js [2m([22m[2m4 tests[22m[2m)[22m[32m 60[2mms[22m[39m [32m✓[39m src/publish/__tests__/current-accepted-event.js [2m([22m[2m2 tests[22m[2m)[22m[32m 37[2mms[22m[39m [32m✓[39m src/publish/__tests__/authorize-approval.js [2m([22m[2m7 tests[22m[2m)[22m[32m 37[2mms[22m[39m [32m✓[39m ...
yarn run v1.22.22 $ eslint src .github --ignore-pattern '!.github' Done in 1.24s.
Found 2 matches /home/byk/Code/getsentry/publish/src/publish/validate-approval-attestation.js: Line 13: "X-GitHub-Api-Version": "2026-03-10", /home/byk/Code/getsentry/publish/src/publish/authorize-approval.js: Line 44: "X-GitHub-Api-Version": "2026-03-10",
# API Versions Learn how to specify which REST API version to use whenever you make a request to the REST API. ## About API versioning The GitHub REST API is versioned. The API version name is based on the date when the API version was released. For example, the API version `2026-03-10` was released on Tue, 10 Mar 2026. Breaking changes are changes that can potentially break an integration. Breaking changes will be released in a new API version. We will provide advance notice before releasing breaking changes. ...
<path>/home/byk/Code/getsentry/publish/auto-approve-repos.txt</path> <type>file</type> <content> 1: getsentry/arroyo 2: getsentry/auto-type-annotate 3: getsentry/devenv 4: getsentry/infra-event-notifier 5: getsentry/jest-sentry-environment 6: getsentry/json-schema-diff 7: getsentry/js-source-scopes 8: getsentry/objectstore/clients 9: getsentry/ophio 10: getsentry/pdb 11: getsentry/pyo3-python-tracing-subscriber 12: getsentry/pytest-sentry 13: getsentry/relay/py 14: getsentry/responses 15: getsentry/rust-proguard 16: getsentry/rust-sourcemap 17: getsentry/rust-usage-accountant 18: getsentry/script-runner 19: getsentry/sentry-api-schema 20: getsentry/sentry-forked-djangorestframework-stubs 21: getsentry/sentry-forked-django-stubs 22: getsentry/sentry-forked-jsonnet 23: getsentry/sentry-infra-tools 24: getsentry/sentry-kafka-management 25: getsentry/sentry-kafka-schemas 26: getsentry/sentry-protos 27: getsentry/sentry-redis-tools 28: getsentry/service-registry 29: getsentry/skrooge 30: ...
Found 25 matches /home/byk/Code/getsentry/publish/.github/workflows/ci-poller-dispatch.yml: Line 3: on: Line 4: workflow_dispatch: Line 14: runs-on: ubuntu-latest /home/byk/Code/getsentry/publish/.github/workflows/publish.yml: Line 2: on: Line 3: issues: Line 14: issues: write Line 27: runs-on: ubuntu-latest Line 171: runs-on: ubuntu-latest Line 225: node-version: 24 /home/byk/Code/getsentry/publish/.github/workflows/ci-poller.yml: Line 3: on: Line 5: - cron: "*/5 * * * *" Line 6: repository_dispatch: Line 8: workflow_run: Line 14: issues: write Line 18: runs-on: ubuntu-latest /home/byk/Code/getsentry/publish/.github/workflows/auto-approve.yml: Line 2: on: Line 3: issues: Line 8: issues: write Line 12: runs-on: ubuntu-latest /home/byk/Code/getsentry/publish/.github/workflows/test.yml: Line 2: on: Line 16: runs-on: ubuntu-latest Line 22: node-version: ...
## main...origin/main [behind 1] M .github/workflows/auto-approve.yml M .github/workflows/ci-poller.yml M .github/workflows/cocoapods-keepalive.yml M .github/workflows/publish.yml M .github/workflows/test.yml M AGENTS.md M README.md M src/libs/__tests__/github.js M src/libs/github.js M src/modules/__tests__/details-from-context.js M src/modules/__tests__/update-issue.js M src/modules/details-from-context.js M src/modules/post-workflow-details.js M src/modules/process-end-state.js M src/modules/update-issue.js M src/publish/inputs.js M src/publish/post-result.js M src/publish/post-workflow-details.js M src/publish/update-issue.js ?? .github/workflows/ci-poller-dispatch.yml ?? .lore.md ?? src/modules/__tests__/approval-attestation.js ?? src/modules/__tests__/approval-authorizer.js ?? src/modules/approval-attestation.js ?? src/modules/approval-authorizer.js ?? src/publish/__tests__/ ?? src/publish/authorize-approval.js ?? src/publish/current-accepted-event.js ?? ...
bbff32997b690408c3d70e8da6ecbc182a48f3d5a0d95386ec308d20d0ec65e6 -
d4b4de3ef92b0802c3ecd99bf327f8e46a3e51e213cbd8620fbca112d709d2fb -
publish: getsentry/toolkit/../.__publish__@1.2.3 {"repo":"toolkit","path":"/../.__publish__","version":"1.2.3"}
publish: getsentryreit/space name@1.2.3 null
publish: getsentry/toolkit@--dry-run {"repo":"toolkit","version":"--dry-run"}
diff --git a/.github/workflows/ci-poller-dispatch.yml b/.github/workflows/ci-poller-dispatch.yml
new file mode 100644
index 0000000..781bd08
--- /dev/null
+++ b/.github/workflows/ci-poller-dispatch.yml
@@ -0,0 +1,16 @@
+name: Run CI Status Poller
+
+on:
+ workflow_dispatch:
+
+permissions: {}
+
+jobs:
+ dispatch:
+ # production only permits deployments from main. Repository secrets must
+ # never be used by this workflow; protected environment secrets enforce
+ # that an arbitrary dispatched ref cannot add them.
+ environment: production
+ runs-on: ubuntu-latest
+ steps:
+ - run: ":"
diff --git a/src/modules/approval-attestation.js b/src/modules/approval-attestation.js
new file mode 100644
index 0000000..096024a
--- /dev/null
+++ b/src/modules/approval-attestation.js
@@ -0,0 +1,259 @@
+const crypto = require("crypto");
+
+const APPROVAL_ATTESTATION_PREFIX = "<!-- publish-approval ";
+const AUTO_APPROVAL_ATTESTATION_PREFIX = "<!-- publish-auto-approval ...
# Webhook events and payloads Learn about when each webhook event occurs and what the payload contains. ## About webhook events and payloads You can create webhooks that subscribe to the events listed on this page. To limit the number of HTTP requests to your server, you should only subscribe to the specific events that you plan on handling. For more information, see [Creating webhooks](/en/webhooks/using-webhooks/creating-webhooks). Each webhook event on this page includes a description of the webhook properties for that event. If the event has multiple actions, the properties corresponding to each action are included. Each event is only available to specific types of webhooks. For example, an organization webhook can subscribe to the `team` event, but a repository webhook cannot. The description of each webhook event lists the availability for that event. For more information, see [Types of webhooks](/en/webhooks/types-of-webhooks). ...
Found 3 matches /home/byk/.local/share/opencode/tool-output/tool_0859490b7001INPpc8Fu06bmdP: Line 406: | `workflow_run` | `object or null` | **Required.** | Line 459: | `workflow_run` | `object or null` | **Required.** | Line 1859: | `workflow_run` | `object` | **Required.** |
<path>/home/byk/.local/share/opencode/tool-output/tool_0859490b7001INPpc8Fu06bmdP</path> <type>file</type> <content> 1810: 1811: This event occurs when there is activity relating to a job in a GitHub Actions workflow. For more information, see "Using jobs in a workflow." For information about the API to manage workflow jobs, see "Workflow jobs" in the REST API documentation. 1812: For activity relating to a workflow run instead of a job in a workflow run, use the workflow\_run event. 1813: To subscribe to this event, a GitHub App must have at least read-level access for the "Actions" repository permission. 1814: 1815: ### Availability 1816: 1817: * `business` 1818: * `repository` 1819: * `organization` 1820: * `app` 1821: 1822: ### Webhook payload object 1823: 1824: **Action type:** `completed`, `in_progress`, `queued`, `waiting` 1825: 1826: A job in a workflow run finished. ...
# 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. ...
Found 9 matches /home/byk/.local/share/opencode/tool-output/tool_08594e0e7001zM8uzsnfG42Ys5: Line 455: Runs your workflow when activity on a pull request in the workflow's repository occurs. For example, if no activity types are specified, the workflow runs when a pull request is opened or reopened or when the head branch of the pull request is updated. For activity related to pull request reviews, pull request review comments, or pull request comments, use the [`pull_request_review`](#pull_request_review), [`pull_request_review_comment`](#pull_request_review_comment), or [`issue_comment`](#issue_comment) events instead. For information about the pull request APIs, see [Pull requests](/en/graphql/reference/pulls#object-pullrequest) in the GraphQL API documentation or [REST API endpoints for pull requests](/en/rest/pulls). Line 457: Note that `GITHUB_SHA` for this event is the last merge commit of the pull request merge branch. ...
<path>/home/byk/.local/share/opencode/tool-output/tool_08594e0e7001zM8uzsnfG42Ys5</path> <type>file</type> <content> 1225: 1226: ```shell 1227: gh workflow run run-tests.yml -f logLevel=warning -f tags=false -f environment=staging 1228: ``` 1229: 1230: For more information, see the GitHub CLI information in [Manually running a workflow](/en/actions/how-tos/manage-workflow-runs/manually-run-a-workflow). 1231: 1232: ## `workflow_run` 1233: 1234: | Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF` | 1235: | ----------------------------------------------------------------------- | --------------------------------------------------- | ----------------------------- | -------------- | 1236: | [`workflow_run`](/en/webhooks/webhook-events-and-payloads#workflow_run) | - `completed`<br/>- `requested`<br/>- `in_progress` | Last commit on default branch | Default branch ...
<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.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/auto-approve.yml</path>
<type>file</type>
<content>
1: name: auto-approve non-sdks
2: on:
3: issues:
4: types: [opened]
5:
6: permissions:
7: contents: read
8: issues: write
9:
10: jobs:
11: auto-approve:
12: runs-on: ubuntu-latest
13: environment: production
14: if: |
15: (github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') &&
16: startsWith(github.event.issue.title, 'publish: ')
17: steps:
18: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
19: with:
20: sparse-checkout: auto-approve-repos.txt
21: sparse-checkout-cone-mode: false
22: - name: Get auth token
23: id: token
24: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
25: with:
26: client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
27: private-key: ${{ ...
name: CI Status Poller
on:
schedule:
- cron: "*/5 * * * *"
workflow_dispatch:
inputs:
attempt:
description: Self-dispatch attempt counter (internal use)
required: false
default: "0"
permissions:
contents: read
issues: read
jobs:
check-ci:
runs-on: ubuntu-latest
environment: production
# Skip entirely (no runner provisioned) when there's nothing to check.
# Set to "true" by ci-pending.yml, reset to "false" here when done.
# Always allow workflow_dispatch for manual recovery.
if: vars.CI_POLLER_HAS_PENDING == 'true' || github.event_name == 'workflow_dispatch'
concurrency:
group: ci-status-poller
cancel-in-progress: false
steps:
# sentry-internal-app token for label changes on this repo.
# This token is what triggers publish.yml downstream — see
# https://github.com/getsentry/publish for why GITHUB_TOKEN can't. ...
# REST API endpoints for users Use the REST API to get public and private information about authenticated users. > [!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. ## Get the authenticated user ``` GET /user ``` OAuth app tokens and personal access tokens (classic) need the read:user scope, or the broader user scope, for this endpoint to return the private user response. The private user response includes additional fields such as private_gists, total_private_repos, owned_private_repos, disk_usage, collaborators, and two_factor_authentication. Tokens without these scopes receive the public user response. The private and public user response types are unrelated to the private profile setting. ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/publish-workflow.js</path>
<type>file</type>
<content>
1: import { readFileSync } from "fs";
2: import { describe, expect, test } from "vitest";
3:
4: const workflow = readFileSync(".github/workflows/publish.yml", "utf8");
5:
6: describe("publish workflow", () => {
7: test("revalidates approval immediately before Craft", () => {
8: const fence = workflow.indexOf("Revalidate approval attestation");
9:
10: expect(fence).toBeGreaterThan(workflow.indexOf("Set targets"));
11: expect(fence).toBeLessThan(workflow.indexOf("Publish using Craft"));
12: expect(workflow.slice(fence)).toContain(
13: 'REQUIRE_CI_READY_ATTESTATION: "true"'
14: );
15: });
16:
17: test("pins Craft to an immutable digest", () => {
18: expect(workflow).toMatch(
19: /uses: docker:\/\/getsentry\/craft@sha256:[a-f0-9]{64}/
20: );
21: });
22: });
(End of file - total 22 lines)
</content>
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/ci-poller-workflow.js</path>
<type>file</type>
<content>
1: import { readFileSync } from "fs";
2: import { describe, expect, test } from "vitest";
3:
4: const workflow = readFileSync(".github/workflows/ci-poller.yml", "utf8");
5: const manualDispatchWorkflow = readFileSync(
6: ".github/workflows/ci-poller-dispatch.yml",
7: "utf8"
8: );
9:
10: describe("CI poller workflow", () => {
11: test("checks out trusted code before requesting app tokens", () => {
12: expect(workflow).toMatch(
13: /- name: Get publish code\n\s+uses: actions\/checkout@[a-f0-9]{40}\n\s+with:\n(?:\s+#.*\n)*\s+ref: \$\{\{ github\.event\.repository\.default_branch \}\}/
14: );
15:
16: expect(workflow.indexOf("Get publish code")).toBeLessThan(
17: workflow.indexOf("Get auth token")
18: );
19: });
20:
21: test("uses a protected workflow dispatch relay for manual recovery", () => {
22: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/authorize-approval.js</path>
<type>file</type>
<content>
1: import { afterEach, describe, expect, test, vi } from "vitest";
2: import fs from "fs";
3: const {
4: createAutoApprovalAttestation,
5: } = require("../../modules/approval-attestation.js");
6: const {
7: getAutoApprovedRepositories,
8: main,
9: } = require("../authorize-approval.js");
10:
11: const originalEnvironment = { ...process.env };
12:
13: afterEach(() => {
14: process.env = { ...originalEnvironment };
15: vi.restoreAllMocks();
16: vi.resetModules();
17: });
18:
19: function jsonResponse(json) {
20: return { ok: true, json: vi.fn().mockResolvedValue(json) };
21: }
22:
23: async function runAuthorization({ actor, issueTitle, responses }) {
24: process.env.GITHUB_OUTPUT = "/tmp/github-output";
25: process.env.APPROVAL_TOKEN = "release-bot-token";
26: process.env.APPROVAL_ACTOR = actor;
27: process.env.APPROVAL_ISSUE_NUMBER = ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/validate-approval-attestation.js</path>
<type>file</type>
<content>
1: import { afterEach, describe, expect, test, vi } from "vitest";
2:
3: const {
4: validateApprovalAttestation,
5: } = require("../validate-approval-attestation.js");
6: const {
7: createApprovalAttestation,
8: createCiReadyAttestation,
9: } = require("../../modules/approval-attestation.js");
10:
11: afterEach(() => {
12: vi.restoreAllMocks();
13: });
14:
15: function jsonResponse(json) {
16: return { ok: true, json: vi.fn().mockResolvedValue(json) };
17: }
18:
19: function issue(
20: title,
21: { body = "", labels = [{ name: "accepted" }], state = "open" } = {}
22: ) {
23: return { body, labels, state, title };
24: }
25:
26: describe("validateApprovalAttestation", () => {
27: test("accepts the current accepted event and its trusted attestation", async () => {
28: const title = "publish: getsentry/sentry-javascript@10.0.0";
29: ...
<path>/home/byk/Code/getsentry/publish/src/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/update-issue.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3: TARGETS_SECTION_PARSER_REGEX,
4: TARGETS_PARSER_REGEX,
5: } = require("./details-from-context");
6:
7: async function updateTargets({ octokit, version, publishRepo, issue_number }) {
8: const CRAFT_STATE_FILE_PATH = `${process.env.GITHUB_WORKSPACE}/__repo__/.craft-publish-${version}.json`;
9:
10: if (!fs.existsSync(CRAFT_STATE_FILE_PATH)) {
11: return;
12: }
13:
14: const issueRequest = octokit.rest.issues.get({
15: ...publishRepo,
16: issue_number,
17: });
18:
19: const craftStateRequest = fs.promises
20: .readFile(CRAFT_STATE_FILE_PATH, { encoding: "utf-8" })
21: .then((data) => JSON.parse(data));
22:
23: const [{ data: issue }, craftState] = await Promise.all([
24: issueRequest,
25: craftStateRequest,
26: ]);
27:
28: const newIssueBody = transformIssueBody(craftState, issue.body);
29: ...
<path>/home/byk/Code/getsentry/publish/src/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>
diff --git a/src/modules/update-issue.js b/src/modules/update-issue.js
index 9b600cc..c5dfba3 100644
--- a/src/modules/update-issue.js
+++ b/src/modules/update-issue.js
@@ -4,7 +4,7 @@ const {
TARGETS_PARSER_REGEX,
} = require("./details-from-context");
-async function updateTargets({octokit, version, publishRepo, issue_number}) {
+async function updateTargets({ octokit, version, publishRepo, issue_number }) {
const CRAFT_STATE_FILE_PATH = `${process.env.GITHUB_WORKSPACE}/__repo__/.craft-publish-${version}.json`;
if (!fs.existsSync(CRAFT_STATE_FILE_PATH)) {
@@ -36,37 +36,41 @@ async function updateTargets({octokit, version, publishRepo, issue_number}) {
function transformIssueBody(craftState, issueBody) {
const declaredTargets = new Set();
- return issueBody.replace(
- TARGETS_SECTION_PARSER_REGEX,
- (targetsSection) => {
- let targetsText = targetsSection.trimEnd();
- targetsText = targetsText.replace(
- TARGETS_PARSER_REGEX,
- ...
name: auto-approve non-sdks
on:
issues:
types: [opened]
permissions:
contents: read
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
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
with:
client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
private-key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }}
- name: Auto-approve if repo is in the list
env:
GH_TOKEN: ${{ steps.token.outputs.token }}
ISSUE_TITLE: ${{ github.event.issue.title }}
ISSUE_URL: ${{ github.event.issue.html_url }}
run: |
REPO=$(echo "$ISSUE_TITLE" | sed -n 's/^publish: ...
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index cff6bc3..9ff9a9d 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -32,13 +32,84 @@ jobs:
&& github.event.issue.state == 'open'
&& startsWith(github.event.issue.title, 'publish: ')
steps:
+ - name: Get publish code
+ uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
+ with:
+ persist-credentials: false
+
- name: Get auth token
id: token
- uses: actions/create-github-app-token@v3
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
with:
client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
private-key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }}
+ # The release bot sees all target repositories. This checks the actor's
+ # current effective target-repository role without exposing RBAC data. ...
# after checking CI (even if ci-ready was already present, waiting-for-ci
# removes it first so a fresh labeled event fires), so this gate is
# guaranteed to trigger on the happy path.
if: >-
github.event.issue.state == 'open'
&& github.event.label.name == 'ci-ready'
&& contains(github.event.issue.labels.*.name, 'accepted')
&& contains(github.event.issue.labels.*.name, 'ci-ready')
&& !contains(github.event.issue.labels.*.name, 'ci-pending')
&& !contains(github.event.issue.labels.*.name, 'ci-failed')
timeout-minutes: 90
env:
SENTRY_DSN: "https://303a687befb64dc2b40ce4c96de507c5@o1.ingest.sentry.io/6183838"
steps:
- name: Get repo contents
uses: actions/checkout@v6
with:
path: .__publish__
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version: 24
cache: yarn
cache-dependency-path: .__publish__/yarn.lock
- name: ...
7c60ddb feat: accept workspace publish requests .github/workflows/ci-poller.yml | 62 +- .github/workflows/cocoapods-keepalive.yml | 2 +- .github/workflows/publish.yml | 84 +- README.md | 5 + docs/publish-issue-format.md | 107 ++ package.json | 5 +- scripts/generate-publish-issue-title-parser.js | 104 ++ src/modules/__tests__/ci-poller-input.js | 97 ++ src/modules/__tests__/ci-poller-workflow.js | 197 +++ src/modules/__tests__/details-from-context.js | 166 ++- .../generate-publish-issue-title-parser.js | 40 + src/modules/__tests__/publish-location.js | 91 ++ src/modules/__tests__/publish-workflow.js | 156 +++ src/modules/__tests__/release-revision.js | 150 ++ src/modules/__tests__/update-issue.js | 15 +- ...
<path>/home/byk/Code/getsentry/publish/src/modules/details-from-context.js</path> <type>file</type> <content> 1: /** 2: * Matches the entire "Targets" section of a github publish issue body. 3: */ 4: const TARGETS_SECTION_PARSER_REGEX = 5: /^(?!### Targets$\s)(?: *- \[[ xX]\] \S+\s*$(?:\r?\n)?)+/m; 6: 7: /** 8: * Matches all targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`. 9: * The "id" of the targets is captured within a capture group. 10: */ 11: const TARGETS_PARSER_REGEX = /^\s*- \[[ x]\] (\S+)/gim; 12: 13: /** 14: * Matches checked targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`. 15: * The "id" of the targets is captured within a capture group. 16: */ 17: const CHECKED_TARGETS_PARSER_REGEX = /^\s*- \[x\] (\S+)/gim; 18: const PUBLISH_TITLE_REGEX = 19: /^publish: ...
ok auto-approve.yml ok ci-poller-dispatch.yml ok ci-poller.yml ok cocoapods-keepalive.yml ok publish.yml ok test.yml
yarn run v1.22.22 $ vitest run --run --printConsoleTrace [1m[46m RUN [49m[22m [36mv4.0.17 [39m[90m/home/byk/Code/getsentry/publish[39m [32m✓[39m src/publish/__tests__/validate-approval-attestation.js [2m([22m[2m9 tests[22m[2m)[22m[32m 28[2mms[22m[39m [32m✓[39m src/modules/__tests__/approval-attestation.js [2m([22m[2m5 tests[22m[2m)[22m[32m 47[2mms[22m[39m [32m✓[39m src/publish/__tests__/authorize-approval.js [2m([22m[2m7 tests[22m[2m)[22m[32m 19[2mms[22m[39m [32m✓[39m src/publish/__tests__/current-accepted-event.js [2m([22m[2m2 tests[22m[2m)[22m[32m 20[2mms[22m[39m [32m✓[39m src/modules/__tests__/approval-authorizer.js [2m([22m[2m15 tests[22m[2m)[22m[32m 31[2mms[22m[39m [32m✓[39m src/modules/__tests__/process-end-state.js [2m([22m[2m4 tests[22m[2m)[22m[32m 58[2mms[22m[39m [32m✓[39m src/modules/__tests__/post-workflow-details.js [2m([22m[2m1 test[22m[2m)[22m[32m 24[2mms[22m[39m ...
yarn run v1.22.22 $ eslint src .github --ignore-pattern '!.github' Done in 1.49s.
yarn run v1.22.22 $ prettier --write src --check . Checking formatting... [warn] .lore.md [warn] AGENTS.md [warn] docs/key_rotation_vault_changes.md [warn] docs/rfc.md [warn] Code style issues found in 4 files. Done in 2.48s.
<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: 9: - **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. 10: 11: <!-- lore:01a082db-d8b9-7438-927a-07bf7aa5e369 --> 12: 13: - **production environment secret ...
# Authenticating to the REST API You can authenticate to the REST API to access more endpoints and have a higher rate limit. ## About authentication Many REST API endpoints require authentication or return additional information if you are authenticated. Additionally, you can make more requests per hour when you are authenticated. To authenticate your request, you will need to provide an authentication token with the required scopes or permissions. There a few different ways to get a token: You can create a personal access token, generate a token with a GitHub App, or use the built-in `GITHUB_TOKEN` in a GitHub Actions workflow. After creating a token, you can authenticate your request by sending the token in the `Authorization` header of your request. For example, in the following request, replace `YOUR-TOKEN` with a reference to your token: ```shell curl --request GET \ --url "https://api.github.com/octocat" \ --header "Authorization: Bearer YOUR-TOKEN" \ --header ...
<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": ...
{"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}}{"default_workflow_permissions":"write","can_approve_pull_request_reviews":true}
{"total_count":1,"branch_policies":[{"id":38207111,"node_id":"MDE2OkdhdGVCcmFuY2hQb2xpY3kzODIwNzExMQ==","name":"main","type":"branch"}]}
<path>/home/byk/Code/getsentry/publish/.github/workflows/auto-approve.yml</path>
<type>file</type>
<content>
1: name: auto-approve non-sdks
2: on:
3: issues:
4: types: [opened]
5:
6: permissions:
7: contents: read
8: issues: write
9:
10: jobs:
11: auto-approve:
12: runs-on: ubuntu-latest
13: environment: production
14: if: |
15: (github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') &&
16: startsWith(github.event.issue.title, 'publish: ')
17: steps:
18: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
19: with:
20: sparse-checkout: auto-approve-repos.txt
21: sparse-checkout-cone-mode: false
22: - name: Get auth token
23: id: token
24: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
25: with:
26: client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
27: private-key: ${{ ...
<path>/home/byk/Code/getsentry/publish/src/modules/approval-attestation.js</path>
<type>file</type>
<content>
1: const crypto = require("crypto");
2:
3: const APPROVAL_ATTESTATION_PREFIX = "<!-- publish-approval ";
4: const AUTO_APPROVAL_ATTESTATION_PREFIX = "<!-- publish-auto-approval ";
5: const CI_READY_ATTESTATION_PREFIX = "<!-- publish-ci-ready ";
6: const ATTESTATION_SUFFIX = " -->";
7:
8: function createAttestation(prefix, value) {
9: return `${prefix}${Buffer.from(JSON.stringify(value)).toString(
10: "base64url"
11: )}${ATTESTATION_SUFFIX}`;
12: }
13:
14: function requestDigest({ body, labels, title }) {
15: if (typeof body !== "string" || !Array.isArray(labels)) {
16: throw new Error("Invalid publish request");
17: }
18:
19: return crypto
20: .createHash("sha256")
21: .update(
22: JSON.stringify({
23: body,
24: dryRun: labels.some((label) => label.name === "dry-run"),
25: title,
26: })
27: )
28: ...
<path>/home/byk/Code/getsentry/publish/src/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: # 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: && ...
# Contexts reference Find information about contexts available in GitHub Actions workflows, including available properties, access methods, and usage examples. ## Available contexts | Context name | Type | Description | | ------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | `github` | `object` | Information about the workflow run. For more information, see [`github` context](#github-context). | | `env` | `object` | Contains variables set in a workflow, job, or step. For more information, see [`env` context](#env-context). ...
Response too large (exceeds 5MB limit)
2c4c77d1cafa8d792ab4a9d449799221baf95176a47692ad9a0b350b0a2618ed - bcd2ba49218906704ab6c1aa796996da409d3eb1 d23441a48e516b6c34aea4fa41551a30e30af803 11d5960a326750d5838078e36cf38b85af677262
gh: Not Found (HTTP 404) base64: invalid input
.github .github/CODEOWNERS .github/dependabot.yml .github/workflows .github/workflows/release.yml .github/workflows/stale.yml .github/workflows/test.yml .github/workflows/update-permission-inputs.yml .gitignore .release-please-manifest.json CHANGELOG.md CONTRIBUTING.md LICENSE README.md action.yml dist dist/main.cjs dist/post.cjs lib lib/get-permissions-from-inputs.js lib/main.js lib/post.js lib/request.js main.js package-lock.json package.json post.js release-please-config.beta.json release-please-config.json scripts scripts/generated scripts/generated/app-permissions.json scripts/update-permission-inputs.js tests tests/README.md tests/action-deprecated-inputs.test.js tests/index.js tests/index.js.snapshot tests/main-app-id-fallback.test.js tests/main-client-id-precedence.test.js tests/main-custom-github-api-url.test.js tests/main-enterprise-fail-response.test.js tests/main-enterprise-installation-not-found.test.js tests/main-enterprise-mutual-exclusivity-owner.test.js tests/main-ente...
import pRetry from "p-retry";
import isNetworkError from "is-network-error";
// @ts-check
/**
* @param {string} clientId
* @param {string} privateKey
* @param {string} enterprise
* @param {string} owner
* @param {string[]} repositories
* @param {undefined | Record<string, string>} permissions
* @param {import("@actions/core")} core
* @param {import("@octokit/auth-app").createAppAuth} createAppAuth
* @param {import("@octokit/request").request} request
* @param {boolean} skipTokenRevoke
*/
export async function main(
clientId,
privateKey,
enterprise,
owner,
repositories,
permissions,
core,
createAppAuth,
request,
skipTokenRevoke,
) {
// Validate mutual exclusivity of enterprise with owner/repositories
if (enterprise && (owner || repositories.length > 0)) {
throw new Error("Cannot use 'enterprise' input with 'owner' or 'repositories' inputs");
}
const target = resolveInstallationTarget(enterprise, owner, repositories, core);
const auth = ...
## main...origin/main [behind 1] 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/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__/ ?? ...
No files found
<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/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/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 { getIssue } = require("./validate-approval-attestation.js");
6:
7: async function recordAutoApprovalAttestation({
8: autoApprover,
9: getIssue,
10: issueNumber,
11: issueTitle,
12: repository,
13: }) {
14: const issue = await getIssue({ repository, issueNumber });
15:
16: if (
17: issue.state !== "open" ||
18: issue.title !== issueTitle ||
19: issue.user?.login?.toLowerCase() !== autoApprover.toLowerCase()
20: ) {
21: throw new Error("The automated approval request changed before approval");
22: }
23:
24: return createAutoApprovalAttestation({ autoApprover, issue });
25: }
26:
27: async function main() {
28: for (const name of [
29: "GITHUB_OUTPUT",
30: "APPROVAL_TOKEN",
31: ...
<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/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/__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: ...
AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY CI_POLLER_APP_PRIVATE_KEY CLOUDFLARE_API_TOKEN COCOAPODS_TRUNK_TOKEN CRAFT_GCS_STORE_CREDS_JSON CRAFT_GCS_TARGET_CREDS_JSON CRATES_IO_TOKEN DOCKER_PASSWORD GEM_HOST_API_KEY GPG_PASSPHRASE GPG_PRIVATE_KEY HEX_API_KEY NPM_TOKEN NUGET_API_TOKEN OSSRH_PASSWORD OSSRH_USERNAME POWERSHELL_API_KEY PUBDEV_ACCESS_TOKEN PUBDEV_REFRESH_TOKEN SENTRY_RELEASE_BOT_PRIVATE_KEY TWINE_PASSWORD VERCEL_TOKEN CI_POLLER_APP_CLIENT_ID Iv23ctZfkJQDhAmr8qnG CI_POLLER_HAS_PENDING false VERCEL_ORG_ID team_X2wL9ydOZqI86I3fHzJ6dmXs
<!DOCTYPE html><html data-color-mode="auto" data-light-theme="light" data-dark-theme="dark" lang="en"><head><meta charSet="utf-8" data-next-head=""/><meta name="viewport" content="width=device-width, initial-scale=1" data-next-head=""/><link rel="icon" type="image/png" href="/assets/cb-345/images/site/favicon.png" data-next-head=""/><link href="/manifest.json" rel="manifest" data-next-head=""/><meta name="google-site-verification" content="c1kuD-K2HIVF635lypcsWPoD4kilo5-jA_wBFyT4uMY" data-next-head=""/><title data-next-head="">REST API endpoints for users - GitHub Docs</title><meta name="description" content="Use the REST API to get public and private information about authenticated users." data-next-head=""/><link rel="alternate" hrefLang="es" href="https://docs.github.com/es/rest/users/users?apiVersion=2022-11-28" data-next-head=""/><link rel="alternate" hrefLang="ja" href="https://docs.github.com/ja/rest/users/users?apiVersion=2022-11-28" data-next-head=""/><link rel="alternate" ...
# REST API endpoints for users Use the REST API to get public and private information about authenticated users. > [!NOTE] > Most endpoints use `Authorization: Bearer <YOUR-TOKEN>` and `Accept: application/vnd.github+json` headers, plus `X-GitHub-Api-Version: 2022-11-28`. Curl examples below omit these standard headers for brevity. ## Get the authenticated user ``` GET /user ``` OAuth app tokens and personal access tokens (classic) need the read:user scope, or the broader user scope, for this endpoint to return the private user response. The private user response includes additional fields such as private_gists, total_private_repos, owned_private_repos, disk_usage, collaborators, and two_factor_authentication. Tokens without these scopes receive the public user response. The private and public user response types are unrelated to the private profile setting. ...
HTTP 422: ERROR_TYPE_QUERY_PARSING_FATAL unable to parse query! (https://api.github.com/search/code?page=1&per_page=20&q=gh+api+user+--jq+.login+language%3AYAML)
# Permissions required for GitHub Apps For each permission granted to a GitHub App, these are the REST API endpoints that the app can use. ## About GitHub App permissions GitHub Apps are created with a set of permissions. Permissions define what resources the GitHub App can access via the API. For more information, see [Choosing permissions for a GitHub App](/en/apps/creating-github-apps/registering-a-github-app/choosing-permissions-for-a-github-app). To help you choose the correct permissions, you will receive the `X-Accepted-GitHub-Permissions` header in the REST API response. The header will tell you what permissions are required in order to access the endpoint. For more information, see [Troubleshooting the REST API](/en/rest/using-the-rest-api/troubleshooting-the-rest-api#resource-not-accessible). These permissions are required to access private resources. Some endpoints can also be used to access public resources without these permissions. ...
Found 29 matches
/home/byk/.local/share/opencode/tool-output/tool_085aaff0f001frdlQk04l8pgZa:
Line 387: | `GET /user/memberships/orgs/{org}` | read | UAT | ✗ |
Line 775: | `GET /user/repository_invitations` | read | UAT | ✗ |
Line 874: | `GET /user/codespaces/{codespace_name}/exports/{export_id}` | read | UAT | ✗ |
Line 882: | `GET /user/codespaces/{codespace_name}/machines` | read | UAT | ✗ |
Line 910: | `GET /user/codespaces` | read | UAT | ✗ |
Line 911: | `GET /user/codespaces/{codespace_name}` | read | UAT | ✗ |
Line 1174: | `GET /user/installations/{installation_id}/repositories` | read | UAT | ✗ |
Line 1175: | `GET /user/repos` | read | UAT | ✗ |
Line 1358: | `GET /user/blocks` | read | UAT | ✗ |
Line 1359: | `GET /user/blocks/{username}` | read | UAT | ✗ |
Line 1370: | `GET /user/codespaces/secrets` | read | UAT | ✗ |
Line 1371: | `GET /user/codespaces/secrets/public-key` | read | UAT | ✗ |
Line 1372: | `GET ...
diff --git a/.github/workflows/ci-poller.yml b/.github/workflows/ci-poller.yml
index a3db430..17d5e91 100644
--- a/.github/workflows/ci-poller.yml
+++ b/.github/workflows/ci-poller.yml
@@ -1,53 +1,79 @@
name: CI Status Poller
on:
schedule:
- cron: "*/5 * * * *"
- workflow_dispatch:
- inputs:
- attempt:
- description: Self-dispatch attempt counter (internal use)
- required: false
- default: "0"
+ repository_dispatch:
+ types: [ci-poller]
+ workflow_run:
+ workflows: [Run CI Status Poller]
+ types: [completed]
permissions:
contents: read
- issues: read
+ issues: write
jobs:
check-ci:
runs-on: ubuntu-latest
environment: production
# Skip entirely (no runner provisioned) when there's nothing to check.
# Set to "true" by ci-pending.yml, reset to "false" here when done.
- # Always allow workflow_dispatch for manual recovery.
- if: vars.CI_POLLER_HAS_PENDING == 'true' || github.event_name == ...
No files found
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-authorizer.js [2m([22m[2m15 tests[22m[2m)[22m[32m 18[2mms[22m[39m [32m✓[39m src/modules/__tests__/approval-attestation.js [2m([22m[2m5 tests[22m[2m)[22m[32m 41[2mms[22m[39m [32m✓[39m src/publish/__tests__/validate-approval-attestation.js [2m([22m[2m9 tests[22m[2m)[22m[32m 25[2mms[22m[39m [32m✓[39m src/modules/__tests__/post-workflow-details.js [2m([22m[2m1 test[22m[2m)[22m[32m 22[2mms[22m[39m [32m✓[39m src/modules/__tests__/update-issue.js [2m([22m[2m6 tests[22m[2m)[22m[32m 25[2mms[22m[39m [32m✓[39m src/publish/__tests__/current-accepted-event.js [2m([22m[2m2 tests[22m[2m)[22m[32m 63[2mms[22m[39m [32m✓[39m src/modules/__tests__/process-end-state.js [2m([22m[2m4 tests[22m[2m)[22m[32m 86[2mms[22m[39m [32m✓[39m ...
yarn run v1.22.22 $ eslint src .github --ignore-pattern '!.github' Done in 1.02s.
yarn run v1.22.22 $ prettier --write src --check . Checking formatting... [warn] .lore.md [warn] AGENTS.md [warn] Code style issues found in 2 files. Done in 1.73s.
<path>/home/byk/Code/getsentry/publish/src/modules/details-from-context.js</path> <type>file</type> <content> 1: /** 2: * Matches the entire "Targets" section of a github publish issue body. 3: */ 4: const TARGETS_SECTION_PARSER_REGEX = 5: /^(?!### Targets$\s)(?: *- \[[ xX]\] \S+\s*$(?:\r?\n)?)+/m; 6: 7: /** 8: * Matches all targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`. 9: * The "id" of the targets is captured within a capture group. 10: */ 11: const TARGETS_PARSER_REGEX = /^\s*- \[[ x]\] (\S+)/gim; 12: 13: /** 14: * Matches checked targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`. 15: * The "id" of the targets is captured within a capture group. 16: */ 17: const CHECKED_TARGETS_PARSER_REGEX = /^\s*- \[x\] (\S+)/gim; 18: const PUBLISH_TITLE_REGEX = 19: /^publish: ...
File not found: /home/byk/Code/getsentry/publish/src/publish/__tests__/ACLE_PLACEHOLDER
File not found: /home/byk/Code/getsentry/publish/src/publish/__tests__/ciодаря
File not found: /home/byk/Code/getsentry/publish/src/publish/__tests__/ci-pollerlass
7c60ddb (origin/main, origin/HEAD) feat: accept workspace publish requests .github/workflows/ci-poller.yml | 62 +- .github/workflows/cocoapods-keepalive.yml | 2 +- .github/workflows/publish.yml | 84 +- README.md | 5 + docs/publish-issue-format.md | 107 ++ package.json | 5 +- scripts/generate-publish-issue-title-parser.js | 104 ++ src/modules/__tests__/ci-poller-input.js | 97 ++ src/modules/__tests__/ci-poller-workflow.js | 197 +++ src/modules/__tests__/details-from-context.js | 166 ++- .../generate-publish-issue-title-parser.js | 40 + src/modules/__tests__/publish-location.js | 91 ++ src/modules/__tests__/publish-workflow.js | 156 +++ src/modules/__tests__/release-revision.js | 150 ++ src/modules/__tests__/update-issue.js | ...
<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/.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>
.github/workflows/auto-approve.yml | 23 +- .github/workflows/ci-poller.yml | 245 +++- .github/workflows/cocoapods-keepalive.yml | 1 - .github/workflows/publish.yml | 219 ++- .github/workflows/test.yml | 6 +- AGENTS.md | 6 + README.md | 9 +- docs/key_rotation_vault_changes.md | 40 +- docs/publish-issue-format.md | 107 -- docs/rfc.md | 9 +- package.json | 5 +- scripts/generate-publish-issue-title-parser.js | 104 -- src/libs/__tests__/github.js | 2 +- src/libs/github.js | 6 +- src/modules/__tests__/ci-poller-input.js | 97 -- src/modules/__tests__/ci-poller-workflow.js | 197 --- ...
<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: ...
{"deployment_branch_policy":{"custom_branch_policies":true,"protected_branches":false},"protection_rules":[{"id":42720759,"node_id":"GA_kwDOEgGvjc4Ci933","type":"branch_policy"}]}
{"default_workflow_permissions":"write","can_approve_pull_request_reviews":true}{"message":"Conflict","errors":"All actions and workflows are allowed on this repository","documentation_url":"https://docs.github.com/rest/actions/permissions#get-allowed-actions-and-reusable-workflows-for-a-repository","status":"409"}gh: All actions and workflows are allowed on this repository (Conflict)
main branch
<path>/home/byk/Code/getsentry/publish/.github/workflows/auto-approve.yml</path>
<type>file</type>
<content>
1: name: auto-approve non-sdks
2: on:
3: issues:
4: types: [opened]
5:
6: permissions:
7: contents: read
8: issues: write
9:
10: jobs:
11: auto-approve:
12: runs-on: ubuntu-latest
13: environment: production
14: if: |
15: (github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') &&
16: startsWith(github.event.issue.title, 'publish: ')
17: steps:
18: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
19: with:
20: sparse-checkout: auto-approve-repos.txt
21: sparse-checkout-cone-mode: false
22: - name: Get auth token
23: id: token
24: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
25: with:
26: client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
27: private-key: ${{ ...
publish: getsentry/toolkit/../.__publish__@1.2.3 {"repo":"toolkit","path":"/../.__publish__","version":"1.2.3"}
publish: getsentryreit/space name@1.2.3 null
publish: getsentry/toolkit@--dry-run {"repo":"toolkit","version":"--dry-run"}
<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 ...
diff --git a/.github/workflows/ci-poller.yml b/.github/workflows/ci-poller.yml
index 487e9da..17d5e91 100644
--- a/.github/workflows/ci-poller.yml
+++ b/.github/workflows/ci-poller.yml
@@ -3,16 +3,15 @@ name: CI Status Poller
on:
schedule:
- cron: "*/5 * * * *"
- workflow_dispatch:
- inputs:
- attempt:
- description: Self-dispatch attempt counter (internal use)
- required: false
- default: "0"
+ repository_dispatch:
+ types: [ci-poller]
+ workflow_run:
+ workflows: [Run CI Status Poller]
+ types: [completed]
permissions:
contents: read
- issues: read
+ issues: write
jobs:
check-ci:
@@ -20,18 +19,45 @@ jobs:
environment: production
# Skip entirely (no runner provisioned) when there's nothing to check.
# Set to "true" by ci-pending.yml, reset to "false" here when done.
- # Always allow workflow_dispatch for manual recovery.
- if: vars.CI_POLLER_HAS_PENDING == 'true' || github.event_name == ...
<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/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/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: ...
## main...origin/main [behind 1] 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/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__/ ?? ...
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/publish/__tests__/current-accepted-event.js [2m([22m[2m2 tests[22m[2m)[22m[32m 39[2mms[22m[39m [32m✓[39m src/modules/__tests__/approval-attestation.js [2m([22m[2m5 tests[22m[2m)[22m[32m 34[2mms[22m[39m [32m✓[39m src/publish/__tests__/authorize-approval.js [2m([22m[2m7 tests[22m[2m)[22m[32m 38[2mms[22m[39m [32m✓[39m src/publish/__tests__/validate-approval-attestation.js [2m([22m[2m9 tests[22m[2m)[22m[32m 54[2mms[22m[39m [32m✓[39m src/modules/__tests__/update-issue.js [2m([22m[2m6 tests[22m[2m)[22m[32m 13[2mms[22m[39m [32m✓[39m src/modules/__tests__/process-end-state.js [2m([22m[2m4 tests[22m[2m)[22m[32m 97[2mms[22m[39m [32m✓[39m src/publish/__tests__/workflow-action-pinning.js [2m([22m[2m16 tests[22m[2m)[22m[32m 9[2mms[22m[39m [32m✓[39m ...
Resume and complete the independent read-only review requested previously. Return substantive file:line evidence and end exactly MERGE or DO-NOT-MERGE. Do not edit files.
# REST API endpoints for users Use the REST API to get public and private information about authenticated users. > [!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. ## Get the authenticated user ``` GET /user ``` OAuth app tokens and personal access tokens (classic) need the read:user scope, or the broader user scope, for this endpoint to return the private user response. The private user response includes additional fields such as private_gists, total_private_repos, owned_private_repos, disk_usage, collaborators, and two_factor_authentication. Tokens without these scopes receive the public user response. The private and public user response types are unrelated to the private profile setting. ...
<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 ...
# REST API endpoints for users Use the REST API to get public and private information about authenticated users. > [!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. ## Get the authenticated user ``` GET /user ``` OAuth app tokens and personal access tokens (classic) need the read:user scope, or the broader user scope, for this endpoint to return the private user response. The private user response includes additional fields such as private_gists, total_private_repos, owned_private_repos, disk_usage, collaborators, and two_factor_authentication. Tokens without these scopes receive the public user response. The private and public user response types are unrelated to the private profile setting. ...
<!DOCTYPE html><html data-color-mode="auto" data-light-theme="light" data-dark-theme="dark" lang="en"><head><meta charSet="utf-8" data-next-head=""/><meta name="viewport" content="width=device-width, initial-scale=1" data-next-head=""/><link rel="icon" type="image/png" href="/assets/cb-345/images/site/favicon.png" data-next-head=""/><link href="/manifest.json" rel="manifest" data-next-head=""/><meta name="google-site-verification" content="c1kuD-K2HIVF635lypcsWPoD4kilo5-jA_wBFyT4uMY" data-next-head=""/><title data-next-head="">REST API endpoints for users - GitHub Docs</title><meta name="description" content="Use the REST API to get public and private information about authenticated users." data-next-head=""/><link rel="alternate" hrefLang="es" href="https://docs.github.com/es/rest/users/users?apiVersion=2026-03-10" data-next-head=""/><link rel="alternate" hrefLang="ja" href="https://docs.github.com/ja/rest/users/users?apiVersion=2026-03-10" data-next-head=""/><link rel="alternate" ...
Ripgrep JSON record exceeded 65536 bytes
# REST API endpoints for users Use the REST API to get public and private information about authenticated users. > [!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. ## Get the authenticated user ``` GET /user ``` OAuth app tokens and personal access tokens (classic) need the read:user scope, or the broader user scope, for this endpoint to return the private user response. The private user response includes additional fields such as private_gists, total_private_repos, owned_private_repos, disk_usage, collaborators, and two_factor_authentication. Tokens without these scopes receive the public user response. The private and public user response types are unrelated to the private profile setting. ...
{"meta":{"title":"REST API endpoints for users","intro":"Use the REST API to get public and private information about authenticated users.","product":"REST API","breadcrumbs":[{"href":"/en/rest","title":"REST API"},{"href":"/en/rest/users","title":"Users"},{"href":"/en/rest/users/users","title":"Users"}],"documentType":"article"},"body":"# REST API endpoints for users\n\nUse the REST API to get public and private information about authenticated users.\n\n> [!NOTE]\n> 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.\n\n## Get the authenticated user\n\n```\nGET /user\n```\n\nOAuth app tokens and personal access tokens (classic) need the read:user scope, or the broader user scope, for this endpoint to return the private user response. ...
<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/src/publish/record-ci-ready-attestation.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3: createCiReadyAttestation,
4: currentAcceptedEvent,
5: } = require("../modules/approval-attestation.js");
6: const {
7: getAllPages,
8: getGitHubResponse,
9: getIssue,
10: } = require("./validate-approval-attestation.js");
11:
12: async function getAuthenticatedLogin() {
13: const response = await getGitHubResponse("user");
14:
15: if (!response.ok) {
16: throw new Error(
17: `Could not retrieve the authenticated user: GitHub returned ${response.status}`
18: );
19: }
20:
21: const { login } = await response.json();
22:
23: if (typeof login !== "string") {
24: throw new Error("GitHub returned no authenticated user login");
25: }
26:
27: return login;
28: }
29:
30: async function recordCiReadyAttestation({
31: expectedAcceptedEvent,
32: getAuthenticatedLogin,
33: getIssue,
34: ...
<path>/home/byk/Code/getsentry/publish/src/modules/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: ...
not-base64 ��~m��
!!!!
e30garbage {} j�ځ
YWJjZA==junk abcd
eyJhIjoxfQ {"a":1}
<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: ...
2c4c77d1cafa8d792ab4a9d449799221baf95176a47692ad9a0b350b0a2618ed -
{"date":"2026-05-12T23:31:18Z","message":"chore(main): release 3.2.0 (#370)\n\n:robot: I have created a release *beep* *boop*\n---\n\n\n##\n[3.2.0](https://github.com/actions/create-github-app-token/compare/v3.1.1...v3.2.0)\n(2026-05-12)\n\n\n### Features\n\n* add support for enterprise-level GitHub Apps\n([#263](https://github.com/actions/create-github-app-token/issues/263))\n([952a2a7](https://github.com/actions/create-github-app-token/commit/952a2a7073df6bfa5f49bc469ec895b6ec1acea4))\n* support full repository names in `repositories` input\n([#372](https://github.com/actions/create-github-app-token/issues/372))\n([85eb8dd](https://github.com/actions/create-github-app-token/commit/85eb8dd41472213aed25d1a126460e0069138ab6))\n\n\n### Bug Fixes\n\n* **deps:** bump @actions/core from 3.0.0 to 3.0.1 in the\nproduction-dependencies ...
action.yml 9f45ab3e2605ffeb987d3e029f27ee4d96bca6a0 dist/main.cjs 20b90dce8c14c24222d6552fac6a4d5e2866a7d6 lib/main.js 7108c3e584b8584b76d6dfa346b01706ca5fa5b4
{"date":"2026-07-16T19:43:33Z","message":"backport fixes to releases-v6 (#2527)","reason":"valid","sha":"d23441a48e516b6c34aea4fa41551a30e30af803","verified":true}
{"date":"2026-07-14T02:48:03Z","message":"Update @actions/cache to 5.1.0 and add security overrides for undici and fast-xml-parser (#1579)\n\n* Update @actions/cache to 5.1.0, add security overrides for undici and fast-xml-parser, update licenses\n\n* Fix debug message for unchanged cache validation in tests\n\n* Fix formatting in cache validation test for unchanged cache\n\n* Bump version to 6.5.0","reason":"valid","sha":"249970729cb0ef3589644e2896645e5dc5ba9c38","verified":true}
{"date":"2025-09-24T13:47:33Z","message":"Merge pull request #1655 from actions/Link-/prepare-4.3.0\n\nPrepare `v4.3.0` release","reason":"valid","sha":"0057852bfaa89a56745cba8c7296529d2fc39830","verified":true}
{"date":"2026-07-16T19:43:47Z","message":"backport fixes to releases-v4 (#2524)","reason":"valid","sha":"11d5960a326750d5838078e36cf38b85af677262","verified":true}
(no output)
<path>/home/byk/Code/getsentry/publish/.github/workflows/auto-approve.yml</path>
<type>file</type>
<content>
1: name: auto-approve non-sdks
2: on:
3: issues:
4: types: [opened]
5:
6: permissions:
7: contents: read
8: issues: write
9:
10: jobs:
11: auto-approve:
12: runs-on: ubuntu-latest
13: environment: production
14: if: |
15: (github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') &&
16: startsWith(github.event.issue.title, 'publish: ')
17: steps:
18: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
19: with:
20: sparse-checkout: auto-approve-repos.txt
21: sparse-checkout-cone-mode: false
22: - name: Get auth token
23: id: token
24: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
25: with:
26: client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
27: private-key: ${{ ...
<path>/home/byk/Code/getsentry/publish/src/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/.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>
## main...origin/main [behind 1] 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/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__/ ?? ...
.github/workflows/auto-approve.yml .github/workflows/ci-poller.yml .github/workflows/cocoapods-keepalive.yml .github/workflows/publish.yml .github/workflows/test.yml AGENTS.md README.md docs/key_rotation_vault_changes.md docs/rfc.md src/libs/__tests__/github.js src/libs/github.js src/modules/__tests__/details-from-context.js src/modules/__tests__/update-issue.js src/modules/details-from-context.js src/modules/post-workflow-details.js src/modules/process-end-state.js src/modules/update-issue.js src/publish/inputs.js src/publish/post-result.js src/publish/post-workflow-details.js src/publish/update-issue.js 21 2 .github/workflows/auto-approve.yml 172 23 .github/workflows/ci-poller.yml 1 2 .github/workflows/cocoapods-keepalive.yml 131 8 .github/workflows/publish.yml 3 3 .github/workflows/test.yml 6 0 AGENTS.md 3 1 README.md 20 20 docs/key_rotation_vault_changes.md 5 4 docs/rfc.md 1 1 src/libs/__tests__/github.js 4 2 src/libs/github.js 21 1 src/modules/__tests__/details-from-context.js 28 ...
CHANGELOG.md api.github.com.2022-11-28.json api.github.com.2022-11-28.yaml api.github.com.2026-03-10.json api.github.com.2026-03-10.yaml api.github.com.json api.github.com.yaml dereferenced
https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.2026-03-10.json
Response too large (exceeds 5MB limit)
api.github.com.2022-11-28.deref.json api.github.com.2022-11-28.deref.yaml api.github.com.2026-03-10.deref.json api.github.com.2026-03-10.deref.yaml api.github.com.deref.json api.github.com.deref.yaml
(no output)
diff --git a/src/modules/__tests__/details-from-context.js b/src/modules/__tests__/details-from-context.js
index e6411c5..c3a65b2 100644
--- a/src/modules/__tests__/details-from-context.js
+++ b/src/modules/__tests__/details-from-context.js
@@ -1,6 +1,9 @@
import { test, expect } from "vitest";
-const { detailsFromContext } = require("../details-from-context.js");
+const {
+ detailsFromContext,
+ parsePublishTitle,
+} = require("../details-from-context.js");
const inputsArgs = {
context: {
@@ -63,6 +66,23 @@ test("can parse version containing +", async () => {
expect(result.version).toEqual("4.2.6+sentry1");
});
+test("parses qualified and legacy publish titles", () => {
+ expect(
+ parsePublishTitle(
+ "publish: getsentry/sentry-javascript/packages/core@10.0.0"
+ )
+ ).toEqual({
+ path: "/packages/core",
+ repo: "sentry-javascript",
+ version: "10.0.0",
+ });
+ expect(parsePublishTitle("publish: sentry-javascript@10.0.0")).toEqual({
+ path: ...
(no output)
{
"summary": "Get the authenticated user",
"security": null,
"xGithub": {
"githubCloudOnly": false,
"enabledForGitHubApps": false,
"category": "users",
"subcategory": "users"
}
}
<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/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: ...
b2b489b4e6427c0dece46fd79a25de419ecdc868 17d5e91f956f3b5d8018d11d1a5f07499941b02d 781bd0850f4ea15ae89417acabdd353fc98b6bd0 ffa59824a15397dee70c1f85255d813d08c52c01 9ff9a9d55b07734dcbb33e7d3f4fa69178edb39c da7f78e280490ead1558d6db6d752677c643fd2f 096024af8c999595952a6f029cdb06794be23e11 d64534399a1a23a4d5578e2fc1c7d6f98c17c0f7 69f70a434528fa2adf1333d5045ad6b473304118 42bcf376d395dafbc410b704f0bed5544cf6d024 3ea8bb7bc2b76c5ae4ea2ba0d2bc9325b2bc8582 baa0ef86bfc0d7fc0db974197cc7a227833eb271 5c0baa5457a54f9c444de3cd1bf1b473c065f9b7 4989a1966b38b32111a8e9d1471caa1c81e5dff6 28ed176707f937156903fd82acbfa7d712efeba4 9009a04c9746fe800c3d7f490595d1c6dff3d69f d5c1f254e9610e0832a775dbea366939344beb0b
yarn run v1.22.22 $ vitest run --run --printConsoleTrace [1m[46m RUN [49m[22m [36mv4.0.17 [39m[90m/home/byk/Code/getsentry/publish[39m [32m✓[39m src/publish/__tests__/current-accepted-event.js [2m([22m[2m2 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 44[2mms[22m[39m [32m✓[39m src/modules/__tests__/approval-attestation.js [2m([22m[2m5 tests[22m[2m)[22m[32m 75[2mms[22m[39m [32m✓[39m src/publish/__tests__/authorize-approval.js [2m([22m[2m7 tests[22m[2m)[22m[32m 55[2mms[22m[39m [32m✓[39m src/modules/__tests__/approval-authorizer.js [2m([22m[2m15 tests[22m[2m)[22m[32m 49[2mms[22m[39m [32m✓[39m src/publish/__tests__/record-ci-ready-attestation.js [2m([22m[2m3 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 143[2mms[22m[39m ...
yarn run v1.22.22 $ eslint src .github --ignore-pattern '!.github' Done in 3.25s.
d0ec32e8a417ba16e7de5ad8f975ff6252ef751607ff7e214f0d24063b72f129 - d861d7b470bfeb8a0c3caf03d2d92ed670fe93381b87de1bb6961636e48d231a - d0ec32e8a417ba16e7de5ad8f975ff6252ef751607ff7e214f0d24063b72f129 - d861d7b470bfeb8a0c3caf03d2d92ed670fe93381b87de1bb6961636e48d231a -
Preparing worktree (detached HEAD 7c60ddb) HEAD is now at 7c60ddb feat: accept workspace publish requests error: patch failed: .github/workflows/ci-poller.yml:92 error: .github/workflows/ci-poller.yml: patch does not apply error: patch failed: .github/workflows/cocoapods-keepalive.yml:1 error: .github/workflows/cocoapods-keepalive.yml: patch does not apply error: patch failed: .github/workflows/publish.yml:117 error: .github/workflows/publish.yml: patch does not apply error: patch failed: src/modules/__tests__/update-issue.js:129 error: src/modules/__tests__/update-issue.js: patch does not apply error: patch failed: src/modules/details-from-context.js:15 error: src/modules/details-from-context.js: patch does not apply error: patch failed: src/modules/update-issue.js:4 error: src/modules/update-issue.js: patch does not apply error: patch failed: src/publish/inputs.js:1 error: src/publish/inputs.js: patch does not apply
(no output)
## main...origin/main [behind 1] 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/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__/ ?? ...
{
"summary": "Get the authenticated user",
"description": "OAuth app tokens and personal access tokens (classic) need the `read:user` scope, or the broader `user` scope, for this endpoint to return the private user response. The private user response includes additional fields such as `private_gists`, `total_private_repos`, `owned_private_repos`, `disk_usage`, `collaborators`, and `two_factor_authentication`. Tokens without these scopes receive the public user response.\n\nThe private and public user response types are unrelated to the [private profile](https://docs.github.com/account-and-profile/concepts/personal-profile#private-profiles) setting. A token without scopes still authenticates as the token's owner, so values subject to private profile visibility, such as `followers` and `following`, may differ from an unauthenticated response.",
"tags": [
"users"
],
"operationId": "users/get-authenticated",
"externalDocs": {
"description": "API method documentation",
...
<path>/home/byk/Code/getsentry/publish/.github/workflows/publish.yml</path> <type>file</type> <content> 170: publish: 171: runs-on: ubuntu-latest 172: environment: production 173: name: Publish a new version 174: # Publish when ci-ready is present (added by the poller after CI passes). 175: # Fires ONLY on ci-ready label events — not accepted — to avoid racing 176: # with waiting-for-ci on the same event. The poller always adds ci-ready 177: # after checking CI (even if ci-ready was already present, waiting-for-ci 178: # removes it first so a fresh labeled event fires), so this gate is 179: # guaranteed to trigger on the happy path. 180: if: >- 181: github.event.issue.state == 'open' 182: && github.event.label.name == 'ci-ready' 183: && contains(github.event.issue.labels.*.name, 'accepted') 184: && contains(github.event.issue.labels.*.name, 'ci-ready') 185: && !contains(github.event.issue.labels.*.name, ...
<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: && ...
getsentry/craft@sha256:9a4a5d5efa44a00c2215078ead39800d4aaa5a97908b94f45a64d7d506d6e14b OK ghcr.io/getsentry/craft@sha256:9a4a5d5efa44a00c2215078ead39800d4aaa5a97908b94f45a64d7d506d6e14b unavailable
:100644 100644 d9763c7 0000000 M .github/workflows/auto-approve.yml :100644 100644 a3db430 0000000 M .github/workflows/ci-poller.yml :100644 100644 21c0696 0000000 M .github/workflows/cocoapods-keepalive.yml :100644 100644 cff6bc3 0000000 M .github/workflows/publish.yml :100644 100644 44692d0 0000000 M .github/workflows/test.yml :100644 100644 cf473df 0000000 M AGENTS.md :100644 100644 b8e07f0 0000000 M README.md :100644 100644 b005518 0000000 M docs/key_rotation_vault_changes.md :100644 100644 abc3a55 0000000 M docs/rfc.md :100644 100644 710091c 0000000 M src/libs/__tests__/github.js :100644 100644 e4d506f 0000000 M src/libs/github.js :100644 100644 e6411c5 0000000 M src/modules/__tests__/details-from-context.js :100644 100644 954ef51 0000000 M src/modules/__tests__/update-issue.js :100644 100644 e58b0be 0000000 M src/modules/details-from-context.js :100644 100644 278d23e 0000000 M src/modules/post-workflow-details.js :100644 100644 d8af41c 0000000 ...
/home/byk/Code/getsentry/publish/src/publish/__tests__/workflow-action-pinning.js /home/byk/Code/getsentry/publish/src/publish/__tests__/record-auto-approval-attestation.js /home/byk/Code/getsentry/publish/src/publish/__tests__/publish-workflow.js /home/byk/Code/getsentry/publish/src/publish/__tests__/auto-approval-workflow.js /home/byk/Code/getsentry/publish/src/publish/__tests__/ci-poller-workflow.js /home/byk/Code/getsentry/publish/src/publish/__tests__/current-accepted-event.js /home/byk/Code/getsentry/publish/src/publish/__tests__/record-ci-ready-attestation.js /home/byk/Code/getsentry/publish/src/publish/__tests__/validate-approval-attestation.js /home/byk/Code/getsentry/publish/src/publish/__tests__/authorize-approval.js
<path>/home/byk/Code/getsentry/publish/src/modules/details-from-context.js</path> <type>file</type> <content> 1: /** 2: * Matches the entire "Targets" section of a github publish issue body. 3: */ 4: const TARGETS_SECTION_PARSER_REGEX = 5: /^(?!### Targets$\s)(?: *- \[[ xX]\] \S+\s*$(?:\r?\n)?)+/m; 6: 7: /** 8: * Matches all targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`. 9: * The "id" of the targets is captured within a capture group. 10: */ 11: const TARGETS_PARSER_REGEX = /^\s*- \[[ x]\] (\S+)/gim; 12: 13: /** 14: * Matches checked targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`. 15: * The "id" of the targets is captured within a capture group. 16: */ 17: const CHECKED_TARGETS_PARSER_REGEX = /^\s*- \[x\] (\S+)/gim; 18: const PUBLISH_TITLE_REGEX = 19: /^publish: ...
<path>/home/byk/Code/getsentry/publish/.github/workflows/auto-approve.yml</path>
<type>file</type>
<content>
1: name: auto-approve non-sdks
2: on:
3: issues:
4: types: [opened]
5:
6: permissions:
7: contents: read
8: issues: write
9:
10: jobs:
11: auto-approve:
12: runs-on: ubuntu-latest
13: environment: production
14: if: |
15: (github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') &&
16: startsWith(github.event.issue.title, 'publish: ')
17: steps:
18: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
19: with:
20: sparse-checkout: auto-approve-repos.txt
21: sparse-checkout-cone-mode: false
22: - name: Get auth token
23: id: token
24: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
25: with:
26: client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
27: private-key: ${{ ...
<path>/home/byk/Code/getsentry/publish/src/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: ...
Found 8 matches
/home/byk/Code/getsentry/publish/src/publish/__tests__/record-ci-ready-attestation.js:
Line 8: } = require("../record-ci-ready-attestation.js");
Line 11: test("binds the current approval to the app that will add ci-ready", async () => {
Line 48: test("rejects an approval that changes before ci-ready is recorded", async () => {
/home/byk/Code/getsentry/publish/src/publish/__tests__/validate-approval-attestation.js:
Line 307: labels: [{ name: "accepted" }, { name: "ci-ready" }],
Line 334: label: { name: "ci-ready" },
Line 362: test("rejects a manually added ci-ready label", async () => {
Line 366: labels: [{ name: "accepted" }, { name: "ci-ready" }],
Line 393: label: { name: "ci-ready" },
e6dcd42feff3f4d6d1dbfb05e7a9dc15c86a1dc370b4f257cf5257a611b9a071 - eac73c8e8d90eab94d9a3fd039cadc5195233546e8ec92cd4e10cab5786b1101 - ## main...origin/main [behind 1] 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/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 ?? ...
<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 ...
21 2 .github/workflows/auto-approve.yml 172 23 .github/workflows/ci-poller.yml 1 2 .github/workflows/cocoapods-keepalive.yml 131 8 .github/workflows/publish.yml 3 3 .github/workflows/test.yml 6 0 AGENTS.md 3 1 README.md 20 20 docs/key_rotation_vault_changes.md 5 4 docs/rfc.md 1 1 src/libs/__tests__/github.js 4 2 src/libs/github.js 21 1 src/modules/__tests__/details-from-context.js 28 0 src/modules/__tests__/update-issue.js 14 3 src/modules/details-from-context.js 2 2 src/modules/post-workflow-details.js 2 2 src/modules/process-end-state.js 23 19 src/modules/update-issue.js 8 9 src/publish/inputs.js 4 4 src/publish/post-result.js 4 4 src/publish/post-workflow-details.js 26 7 src/publish/update-issue.js 33
<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: ...
14720c4c499a2de70a06fffa6cfa3312ab6e3c16dbdcbf19fec1f1aca34c9897 - 570dcb023ae08f4fb8338923496a55bf1c2e012a944f9969650bc31feadd485c .github/workflows/ci-poller-dispatch.yml 51898ad7a79e99d1a22a116a40953e5b52030a8e90c9eda5c46eb3ed3998b701 src/modules/approval-attestation.js c9ca690c7ca173c6cdc973e511dadb5479ff10cd958f9b0c4f10f83017e6ec55 src/modules/approval-authorizer.js 7ced3d4b0d6137122942a8e3978a992ea20a30d0f12625ef9c04bf03d9bccb5a src/publish/authorize-approval.js 89327251626058674db523d8e541475bb9bf94cce2359adba06a85f99e359895 src/publish/current-accepted-event.js dc1dee4b851b54efec8ab987fbc6e27210c5ed099b7094112b48511a9bfd9382 src/publish/record-auto-approval-attestation.js ae06156794d4bb8b0559b1c5473d75b35bf1816960678c58e46f5f4c387dce3a src/publish/record-ci-ready-attestation.js 943aacefbec471c91f264dca602f0a37199f7a29875cc41adaa2429828693fa7 src/publish/validate-approval-attestation.js
No files found
83d210b29553ea6f4d97508821724a834c43856e 7c60ddb7f43040fe8fbfea70efc833f689c04e75 ## main...origin/main [behind 1] 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/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 ?? ...
<path>/home/byk/Code/getsentry/publish/.github/workflows/publish.yml</path> <type>file</type> <content> 170: publish: 171: runs-on: ubuntu-latest 172: environment: production 173: name: Publish a new version 174: # Publish when ci-ready is present (added by the poller after CI passes). 175: # Fires ONLY on ci-ready label events — not accepted — to avoid racing 176: # with waiting-for-ci on the same event. The poller always adds ci-ready 177: # after checking CI (even if ci-ready was already present, waiting-for-ci 178: # removes it first so a fresh labeled event fires), so this gate is 179: # guaranteed to trigger on the happy path. 180: if: >- 181: github.event.issue.state == 'open' 182: && github.event.label.name == 'ci-ready' 183: && contains(github.event.issue.labels.*.name, 'accepted') 184: && contains(github.event.issue.labels.*.name, 'ci-ready') 185: && !contains(github.event.issue.labels.*.name, ...
<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/src/modules/update-issue.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3: TARGETS_SECTION_PARSER_REGEX,
4: TARGETS_PARSER_REGEX,
5: } = require("./details-from-context");
6:
7: async function updateTargets({ octokit, version, publishRepo, issue_number }) {
8: const CRAFT_STATE_FILE_PATH = `${process.env.GITHUB_WORKSPACE}/__repo__/.craft-publish-${version}.json`;
9:
10: if (!fs.existsSync(CRAFT_STATE_FILE_PATH)) {
11: return;
12: }
13:
14: const issueRequest = octokit.rest.issues.get({
15: ...publishRepo,
16: issue_number,
17: });
18:
19: const craftStateRequest = fs.promises
20: .readFile(CRAFT_STATE_FILE_PATH, { encoding: "utf-8" })
21: .then((data) => JSON.parse(data));
22:
23: const [{ data: issue }, craftState] = await Promise.all([
24: issueRequest,
25: craftStateRequest,
26: ]);
27:
28: const newIssueBody = transformIssueBody(craftState, issue.body);
29: ...
<path>/home/byk/Code/getsentry/publish/src/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/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>
# REST API endpoints for users Use the REST API to get public and private information about authenticated users. > [!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. ## Get the authenticated user ``` GET /user ``` OAuth app tokens and personal access tokens (classic) need the read:user scope, or the broader user scope, for this endpoint to return the private user response. The private user response includes additional fields such as private_gists, total_private_repos, owned_private_repos, disk_usage, collaborators, and two_factor_authentication. Tokens without these scopes receive the public user response. The private and public user response types are unrelated to the private profile setting. ...
yarn run v1.22.22 $ vitest run --printConsoleTrace warning From Yarn 1.0 onwards, scripts don't require "--" for options to be forwarded. In a future version, any explicit "--" will be forwarded as-is to the scripts. [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 56[2mms[22m[39m [32m✓[39m src/publish/__tests__/authorize-approval.js [2m([22m[2m7 tests[22m[2m)[22m[32m 47[2mms[22m[39m [32m✓[39m src/modules/__tests__/approval-authorizer.js [2m([22m[2m15 tests[22m[2m)[22m[32m 16[2mms[22m[39m [32m✓[39m src/publish/__tests__/validate-approval-attestation.js [2m([22m[2m9 tests[22m[2m)[22m[32m 29[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 src/publish/__tests__/record-auto-approval-attestation.js [2m([22m[2m2 ...
<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/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": ...
<path>/home/byk/Code/getsentry/publish/.github/workflows/auto-approve.yml</path>
<type>file</type>
<content>
1: name: auto-approve non-sdks
2: on:
3: issues:
4: types: [opened]
5:
6: permissions:
7: contents: read
8: issues: write
9:
10: jobs:
11: auto-approve:
12: runs-on: ubuntu-latest
13: environment: production
14: if: |
15: (github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') &&
16: startsWith(github.event.issue.title, 'publish: ')
17: steps:
18: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
19: with:
20: sparse-checkout: auto-approve-repos.txt
21: sparse-checkout-cone-mode: false
22: - name: Get auth token
23: id: token
24: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
25: with:
26: client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
27: private-key: ${{ ...
Found 18 matches /home/byk/Code/getsentry/publish/.github/workflows/publish.yml: Line 36: uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 Line 42: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 Line 52: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 Line 149: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 Line 192: uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 Line 223: uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 Line 268: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 Line 274: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 Line 353: - uses: ...
<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 ...
## main...origin/main [behind 1] 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/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__/ ?? ...
<path>/home/byk/Code/getsentry/publish/src/publish/record-ci-ready-attestation.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3: createCiReadyAttestation,
4: currentAcceptedEvent,
5: } = require("../modules/approval-attestation.js");
6: const {
7: getAllPages,
8: getGitHubResponse,
9: getIssue,
10: } = require("./validate-approval-attestation.js");
11:
12: async function getAuthenticatedLogin() {
13: const response = await getGitHubResponse("user");
14:
15: if (!response.ok) {
16: throw new Error(
17: `Could not retrieve the authenticated user: GitHub returned ${response.status}`
18: );
19: }
20:
21: const { login } = await response.json();
22:
23: if (typeof login !== "string") {
24: throw new Error("GitHub returned no authenticated user login");
25: }
26:
27: return login;
28: }
29:
30: async function recordCiReadyAttestation({
31: expectedAcceptedEvent,
32: getAuthenticatedLogin,
33: getIssue,
34: ...
<path>/home/byk/Code/getsentry/publish/src/modules/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/details-from-context.js</path> <type>file</type> <content> 1: /** 2: * Matches the entire "Targets" section of a github publish issue body. 3: */ 4: const TARGETS_SECTION_PARSER_REGEX = 5: /^(?!### Targets$\s)(?: *- \[[ xX]\] \S+\s*$(?:\r?\n)?)+/m; 6: 7: /** 8: * Matches all targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`. 9: * The "id" of the targets is captured within a capture group. 10: */ 11: const TARGETS_PARSER_REGEX = /^\s*- \[[ x]\] (\S+)/gim; 12: 13: /** 14: * Matches checked targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`. 15: * The "id" of the targets is captured within a capture group. 16: */ 17: const CHECKED_TARGETS_PARSER_REGEX = /^\s*- \[x\] (\S+)/gim; 18: const PUBLISH_TITLE_REGEX = 19: /^publish: ...
<path>/home/byk/Code/getsentry/publish/src/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: `
16: Requested by: @BYK
17:
18: Merge target: custom-branch
19:
20: Quick links:
21: - [View changes](https://github.com/getsentry/sentry/compare/21.3.0...refs/heads/releases/21.3.1)
22: - [View check runs](https://github.com/getsentry/sentry/commit/7e5ca7ed5581552de066e2a8bc295b8306be38ac/checks/)
23:
24: Assign the **accepted** label to this issue to approve the release. ...
<path>/home/byk/Code/getsentry/publish/.github/workflows/publish.yml</path> <type>file</type> <content> 170: publish: 171: runs-on: ubuntu-latest 172: environment: production 173: name: Publish a new version 174: # Publish when ci-ready is present (added by the poller after CI passes). 175: # Fires ONLY on ci-ready label events — not accepted — to avoid racing 176: # with waiting-for-ci on the same event. The poller always adds ci-ready 177: # after checking CI (even if ci-ready was already present, waiting-for-ci 178: # removes it first so a fresh labeled event fires), so this gate is 179: # guaranteed to trigger on the happy path. 180: if: >- 181: github.event.issue.state == 'open' 182: && github.event.label.name == 'ci-ready' 183: && contains(github.event.issue.labels.*.name, 'accepted') 184: && contains(github.event.issue.labels.*.name, 'ci-ready') 185: && !contains(github.event.issue.labels.*.name, ...
<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/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/src/modules/update-issue.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3: TARGETS_SECTION_PARSER_REGEX,
4: TARGETS_PARSER_REGEX,
5: } = require("./details-from-context");
6:
7: async function updateTargets({ octokit, version, publishRepo, issue_number }) {
8: const CRAFT_STATE_FILE_PATH = `${process.env.GITHUB_WORKSPACE}/__repo__/.craft-publish-${version}.json`;
9:
10: if (!fs.existsSync(CRAFT_STATE_FILE_PATH)) {
11: return;
12: }
13:
14: const issueRequest = octokit.rest.issues.get({
15: ...publishRepo,
16: issue_number,
17: });
18:
19: const craftStateRequest = fs.promises
20: .readFile(CRAFT_STATE_FILE_PATH, { encoding: "utf-8" })
21: .then((data) => JSON.parse(data));
22:
23: const [{ data: issue }, craftState] = await Promise.all([
24: issueRequest,
25: craftStateRequest,
26: ]);
27:
28: const newIssueBody = transformIssueBody(craftState, issue.body);
29: ...
<path>/home/byk/Code/getsentry/publish/src/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/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/__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__/publish-workflow.js</path>
<type>file</type>
<content>
1: import { readFileSync } from "fs";
2: import { describe, expect, test } from "vitest";
3:
4: const workflow = readFileSync(".github/workflows/publish.yml", "utf8");
5:
6: describe("publish workflow", () => {
7: test("revalidates approval immediately before Craft", () => {
8: const fence = workflow.indexOf("Revalidate approval attestation");
9:
10: expect(fence).toBeGreaterThan(workflow.indexOf("Set targets"));
11: expect(fence).toBeLessThan(workflow.indexOf("Publish using Craft"));
12: expect(workflow.slice(fence)).toContain(
13: 'REQUIRE_CI_READY_ATTESTATION: "true"'
14: );
15: });
16:
17: test("pins Craft to an immutable digest", () => {
18: expect(workflow).toMatch(
19: /uses: docker:\/\/getsentry\/craft@sha256:[a-f0-9]{64}/
20: );
21: });
22: });
(End of file - total 22 lines)
</content>
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/ci-poller-workflow.js</path>
<type>file</type>
<content>
1: import { readFileSync } from "fs";
2: import { describe, expect, test } from "vitest";
3:
4: const workflow = readFileSync(".github/workflows/ci-poller.yml", "utf8");
5: const manualDispatchWorkflow = readFileSync(
6: ".github/workflows/ci-poller-dispatch.yml",
7: "utf8"
8: );
9:
10: describe("CI poller workflow", () => {
11: test("checks out trusted code before requesting app tokens", () => {
12: expect(workflow).toMatch(
13: /- name: Get publish code\n\s+uses: actions\/checkout@[a-f0-9]{40}\n\s+with:\n(?:\s+#.*\n)*\s+ref: \$\{\{ github\.event\.repository\.default_branch \}\}/
14: );
15:
16: expect(workflow.indexOf("Get publish code")).toBeLessThan(
17: workflow.indexOf("Get auth token")
18: );
19: });
20:
21: test("uses a protected workflow dispatch relay for manual recovery", () => {
22: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/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 + ...
# REST API endpoints for users Use the REST API to get public and private information about authenticated users. > [!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. ## Get the authenticated user ``` GET /user ``` OAuth app tokens and personal access tokens (classic) need the read:user scope, or the broader user scope, for this endpoint to return the private user response. The private user response includes additional fields such as private_gists, total_private_repos, owned_private_repos, disk_usage, collaborators, and two_factor_authentication. Tokens without these scopes receive the public user response. The private and public user response types are unrelated to the private profile setting. ...
# Authenticating as a GitHub App installation You can make your GitHub App authenticate as an installation in order to make API requests that affect resources owned by the account where the app is installed. ## About authentication as a GitHub App installation Once your GitHub App is installed on an account, you can make it authenticate as an app installation for API requests. This allows the app to access resources owned by that installation, as long as the app was granted the necessary repository access and permissions. API requests made by an app installation are attributed to the app. For more information about installing GitHub Apps, see [Installing your own GitHub App](/en/apps/using-github-apps/installing-your-own-github-app). For example, if you want your app to change the `Status` field of an issue on a project owned by an organization called "octo-org," then you would authenticate as the octo-org installation of your app. ...
<path>/home/byk/Code/getsentry/publish/.github/workflows/auto-approve.yml</path>
<type>file</type>
<content>
1: name: auto-approve non-sdks
2: on:
3: issues:
4: types: [opened]
5:
6: permissions:
7: contents: read
8: issues: write
9:
10: jobs:
11: auto-approve:
12: runs-on: ubuntu-latest
13: environment: production
14: if: |
15: (github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') &&
16: startsWith(github.event.issue.title, 'publish: ')
17: steps:
18: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
19: with:
20: sparse-checkout: auto-approve-repos.txt
21: sparse-checkout-cone-mode: false
22: - name: Get auth token
23: id: token
24: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
25: with:
26: client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
27: private-key: ${{ ...
<path>/home/byk/Code/getsentry/publish/src/modules/approval-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/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: ...
## main...origin/main [behind 1] 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/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__/ ?? ...
diff --git a/.github/workflows/ci-poller.yml b/.github/workflows/ci-poller.yml
index a3db430..17d5e91 100644
--- a/.github/workflows/ci-poller.yml
+++ b/.github/workflows/ci-poller.yml
@@ -3,16 +3,15 @@ name: CI Status Poller
on:
schedule:
- cron: "*/5 * * * *"
- workflow_dispatch:
- inputs:
- attempt:
- description: Self-dispatch attempt counter (internal use)
- required: false
- default: "0"
+ repository_dispatch:
+ types: [ci-poller]
+ workflow_run:
+ workflows: [Run CI Status Poller]
+ types: [completed]
permissions:
contents: read
- issues: read
+ issues: write
jobs:
check-ci:
@@ -20,18 +19,45 @@ jobs:
environment: production
# Skip entirely (no runner provisioned) when there's nothing to check.
# Set to "true" by ci-pending.yml, reset to "false" here when done.
- # Always allow workflow_dispatch for manual recovery.
- if: vars.CI_POLLER_HAS_PENDING == 'true' || github.event_name == ...
yarn run v1.22.22 $ vitest run --run --printConsoleTrace [1m[46m RUN [49m[22m [36mv4.0.17 [39m[90m/home/byk/Code/getsentry/publish[39m [32m✓[39m src/publish/__tests__/authorize-approval.js [2m([22m[2m7 tests[22m[2m)[22m[32m 39[2mms[22m[39m [32m✓[39m src/modules/__tests__/approval-attestation.js [2m([22m[2m5 tests[22m[2m)[22m[32m 80[2mms[22m[39m [32m✓[39m src/modules/__tests__/update-issue.js [2m([22m[2m6 tests[22m[2m)[22m[32m 33[2mms[22m[39m [32m✓[39m src/publish/__tests__/validate-approval-attestation.js [2m([22m[2m9 tests[22m[2m)[22m[32m 56[2mms[22m[39m [32m✓[39m src/publish/__tests__/current-accepted-event.js [2m([22m[2m2 tests[22m[2m)[22m[32m 49[2mms[22m[39m [32m✓[39m src/modules/__tests__/process-end-state.js [2m([22m[2m4 tests[22m[2m)[22m[32m 94[2mms[22m[39m [32m✓[39m src/publish/__tests__/record-ci-ready-attestation.js [2m([22m[2m3 tests[22m[2m)[22m[32m 19[2mms[22m[39m ...
yarn run v1.22.22 $ eslint src .github --ignore-pattern '!.github' Done in 1.26s.
58b43354ebad6c25f3a689f9e6672a60632c9461d537dd9229c85a03bfc44ef4 - 8ca4aa7afcb6a1c171f695d488a06b8cf4d08436a414a3223a18b33a08db4fc6 -
<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: ...
# 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. ...
Found 3 matches /home/byk/.local/share/opencode/tool-output/tool_086bff915001ubSkY0fDYYjGVE: Line 1232: ## `workflow_run` Line 1236: | [`workflow_run`](/en/webhooks/webhook-events-and-payloads#workflow_run) | - `completed`<br/>- `requested`<br/>- `in_progress` | Last commit on default branch | Default branch | Line 1294: You can use the `branches` or `branches-ignore` filter to specify what branches the triggering workflow must run on in order to trigger your workflow. For more information, see [Workflow syntax for GitHub Actions](/en/actions/reference/workflows-and-actions/workflow-syntax#onworkflow_runbranchesbranches-ignore). For example, a workflow with the following trigger will only run when the workflow named `Build` runs on a branch named `canary`.
<path>/home/byk/.local/share/opencode/tool-output/tool_086bff915001ubSkY0fDYYjGVE</path> <type>file</type> <content> 1232: ## `workflow_run` 1233: 1234: | Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF` | 1235: | ----------------------------------------------------------------------- | --------------------------------------------------- | ----------------------------- | -------------- | 1236: | [`workflow_run`](/en/webhooks/webhook-events-and-payloads#workflow_run) | - `completed`<br/>- `requested`<br/>- `in_progress` | Last commit on default branch | Default branch | 1237: 1238: > \[!NOTE] 1239: > 1240: > * More than one activity type triggers this event. The `requested` activity type does not occur when a workflow is re-run. For information about each activity type, see [Webhook events and payloads](/en/webhooks/webhook-events-and-payloads#workflow_run). ...
<path>/home/byk/Code/getsentry/publish/.github/workflows/publish.yml</path>
<type>file</type>
<content>
1: name: Publish
2: on:
3: issues:
4: types: [labeled]
5:
6: concurrency:
7: # Use the issue title (e.g. "publish: getsentry/foo@1.2.3") so duplicate
8: # issues for the same repo@version share a concurrency group.
9: group: ${{ github.event.issue.title }}
10: cancel-in-progress: false
11:
12: permissions:
13: contents: read
14: issues: write
15: packages: write
16:
17: jobs:
18: # When accepted is added to a publish issue:
19: # - Add ci-pending (and remove ci-failed if retrying)
20: # - Enable the poller via CI_POLLER_HAS_PENDING=true
21: # - Comment on the issue
22: # - Trigger the poller immediately so we don't wait for the next cron tick
23: # The publish job below requires ci-ready, so it will not fire until the
24: # poller flips ci-pending → ci-ready (which also prevents publishing without
25: # CI verification in the ...
<path>/home/byk/Code/getsentry/publish/.github/workflows/publish.yml</path> <type>file</type> <content> 170: publish: 171: runs-on: ubuntu-latest 172: environment: production 173: name: Publish a new version 174: # Publish when ci-ready is present (added by the poller after CI passes). 175: # Fires ONLY on ci-ready label events — not accepted — to avoid racing 176: # with waiting-for-ci on the same event. The poller always adds ci-ready 177: # after checking CI (even if ci-ready was already present, waiting-for-ci 178: # removes it first so a fresh labeled event fires), so this gate is 179: # guaranteed to trigger on the happy path. 180: if: >- 181: github.event.issue.state == 'open' 182: && github.event.label.name == 'ci-ready' 183: && contains(github.event.issue.labels.*.name, 'accepted') 184: && contains(github.event.issue.labels.*.name, 'ci-ready') 185: && !contains(github.event.issue.labels.*.name, ...
<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/ci-poller.yml</path>
<type>file</type>
<content>
115: # Check each issue's CI status
116: echo "$issues" | jq -c '.[]' | while read -r issue; do
117: number=$(echo "$issue" | jq -r '.number')
118: title=$(echo "$issue" | jq -r '.title')
119: body=$(echo "$issue" | jq -r '.body')
120:
121: # Bind this poll cycle to the current accepted-label event before
122: # inspecting CI. Never move a release to ci-ready after it changes.
123: if ! accepted_event=$(APPROVAL_TOKEN="$GH_TOKEN" \
124: APPROVAL_ISSUE_NUMBER="$number" \
125: APPROVAL_ISSUE_REPOSITORY="$GITHUB_REPOSITORY" \
126: node src/publish/current-accepted-event.js); then
127: echo "::warning::Could not bind approval event for issue #${number}. Removing accepted."
128: gh issue edit "$number" -R "$GITHUB_REPOSITORY" ...
<path>/home/byk/Code/getsentry/publish/.github/workflows/ci-poller.yml</path>
<type>file</type>
<content>
345: EXPECTED_ACCEPTED_EVENT_ID="$accepted_event_id" \
346: node src/publish/validate-approval-attestation.js; then
347: echo "::warning::Approval for issue #${number} changed before ci-ready. Removing accepted."
348: gh issue edit "$number" -R "$GITHUB_REPOSITORY" \
349: --remove-label "ci-pending" \
350: --remove-label "accepted"
351: gh issue comment "$number" -R "$GITHUB_REPOSITORY" \
352: --body "Approval is invalid or could not be verified. Re-add the accepted label to retry after resolving the issue."
353: continue
354: fi
355:
356: echo " CI passed! Adding ci-ready label."
357: gh issue edit "$number" -R "$GITHUB_REPOSITORY" \
358: --remove-label "ci-pending" \
359: ...
<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/.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>
{"name":"main","type":"branch"}
{"can_admins_bypass":false,"deployment_branch_policy":{"custom_branch_policies":true,"protected_branches":false},"protection_rules":[{"id":42720759,"node_id":"GA_kwDOEgGvjc4Ci933","type":"branch_policy"}]}
diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml
index d9763c7..b2b489b 100644
--- a/.github/workflows/auto-approve.yml
+++ b/.github/workflows/auto-approve.yml
@@ -5,25 +5,44 @@ on:
permissions:
contents: read
+ issues: write
jobs:
auto-approve:
runs-on: ubuntu-latest
+ environment: production
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: ${{ vars.SENTRY_INTERNAL_APP_ID ...
<path>/home/byk/Code/getsentry/publish/.github/workflows/auto-approve.yml</path>
<type>file</type>
<content>
1: name: auto-approve non-sdks
2: on:
3: issues:
4: types: [opened]
5:
6: permissions:
7: contents: read
8: issues: write
9:
10: jobs:
11: auto-approve:
12: runs-on: ubuntu-latest
13: environment: production
14: if: |
15: (github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') &&
16: startsWith(github.event.issue.title, 'publish: ')
17: steps:
18: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
19: with:
20: sparse-checkout: auto-approve-repos.txt
21: sparse-checkout-cone-mode: false
22: - name: Get auth token
23: id: token
24: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
25: with:
26: client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
27: private-key: ${{ ...
<path>/home/byk/Code/getsentry/publish/src/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/validate-approval-attestation.js</path>
<type>file</type>
<content>
1: const {
2: currentAcceptedEvent,
3: currentCiReadyEvent,
4: hasApprovalAttestation,
5: hasCiReadyAttestation,
6: } = require("../modules/approval-attestation.js");
7:
8: async function getGitHubResponse(path) {
9: return fetch(`https://api.github.com/${path}`, {
10: headers: {
11: Accept: "application/vnd.github+json",
12: Authorization: `Bearer ${process.env.APPROVAL_TOKEN}`,
13: "X-GitHub-Api-Version": "2026-03-10",
14: },
15: });
16: }
17:
18: async function getIssue({ repository, issueNumber }) {
19: const response = await getGitHubResponse(
20: `repos/${repository}/issues/${issueNumber}`
21: );
22:
23: if (!response.ok) {
24: throw new Error(
25: `Could not retrieve issue #${issueNumber}: GitHub returned ${response.status}`
26: );
27: }
28:
29: return response.json();
30: }
31:
32: async ...
<path>/home/byk/Code/getsentry/publish/src/publish/__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 = ...
{"can_approve_pull_request_reviews":true,"default_workflow_permissions":"write"}
AWS_ACCESS_KEY_ID
AWS_SECRET_ACCESS_KEY
CI_POLLER_APP_PRIVATE_KEY
CLOUDFLARE_API_TOKEN
COCOAPODS_TRUNK_TOKEN
CRAFT_GCS_STORE_CREDS_JSON
CRAFT_GCS_TARGET_CREDS_JSON
CRATES_IO_TOKEN
DOCKER_PASSWORD
GEM_HOST_API_KEY
GPG_PASSPHRASE
GPG_PRIVATE_KEY
HEX_API_KEY
NPM_TOKEN
NUGET_API_TOKEN
OSSRH_PASSWORD
OSSRH_USERNAME
POWERSHELL_API_KEY
PUBDEV_ACCESS_TOKEN
PUBDEV_REFRESH_TOKEN
SENTRY_RELEASE_BOT_PRIVATE_KEY
TWINE_PASSWORD
VERCEL_TOKEN
{"message":"You must be an org admin or have the actions secrets fine-grained permission.","documentation_url":"https://docs.github.com/rest/actions/secrets#list-selected-repositories-for-an-organization-secret","status":"403"}gh: You must be an org admin or have the actions secrets fine-grained permission. (HTTP 403)
gh: This API operation needs the "admin:org" scope. To request it, run: gh auth refresh -h github.com -s admin:org
<path>/home/byk/Code/getsentry/publish/src/modules/update-issue.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3: TARGETS_SECTION_PARSER_REGEX,
4: TARGETS_PARSER_REGEX,
5: } = require("./details-from-context");
6:
7: async function updateTargets({ octokit, version, publishRepo, issue_number }) {
8: const CRAFT_STATE_FILE_PATH = `${process.env.GITHUB_WORKSPACE}/__repo__/.craft-publish-${version}.json`;
9:
10: if (!fs.existsSync(CRAFT_STATE_FILE_PATH)) {
11: return;
12: }
13:
14: const issueRequest = octokit.rest.issues.get({
15: ...publishRepo,
16: issue_number,
17: });
18:
19: const craftStateRequest = fs.promises
20: .readFile(CRAFT_STATE_FILE_PATH, { encoding: "utf-8" })
21: .then((data) => JSON.parse(data));
22:
23: const [{ data: issue }, craftState] = await Promise.all([
24: issueRequest,
25: craftStateRequest,
26: ]);
27:
28: const newIssueBody = transformIssueBody(craftState, issue.body);
29: ...
<path>/home/byk/Code/getsentry/publish/src/modules/__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 ...
# REST API endpoints for users Use the REST API to get public and private information about authenticated users. > [!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. ## Get the authenticated user ``` GET /user ``` OAuth app tokens and personal access tokens (classic) need the read:user scope, or the broader user scope, for this endpoint to return the private user response. The private user response includes additional fields such as private_gists, total_private_repos, owned_private_repos, disk_usage, collaborators, and two_factor_authentication. Tokens without these scopes receive the public user response. The private and public user response types are unrelated to the private profile setting. ...
<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 { getIssue } = require("./validate-approval-attestation.js");
6:
7: async function recordAutoApprovalAttestation({
8: autoApprover,
9: getIssue,
10: issueNumber,
11: issueTitle,
12: repository,
13: }) {
14: const issue = await getIssue({ repository, issueNumber });
15:
16: if (
17: issue.state !== "open" ||
18: issue.title !== issueTitle ||
19: issue.user?.login?.toLowerCase() !== autoApprover.toLowerCase()
20: ) {
21: throw new Error("The automated approval request changed before approval");
22: }
23:
24: return createAutoApprovalAttestation({ autoApprover, issue });
25: }
26:
27: async function main() {
28: for (const name of [
29: "GITHUB_OUTPUT",
30: "APPROVAL_TOKEN",
31: ...
<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: ...
VERCEL_ORG_ID CI_POLLER_APP_CLIENT_ID CI_POLLER_HAS_PENDING
<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__/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 + ...
21 2 .github/workflows/auto-approve.yml 172 23 .github/workflows/ci-poller.yml 1 2 .github/workflows/cocoapods-keepalive.yml 131 8 .github/workflows/publish.yml 3 3 .github/workflows/test.yml 6 0 AGENTS.md 3 1 README.md 20 20 docs/key_rotation_vault_changes.md 5 4 docs/rfc.md 1 1 src/libs/__tests__/github.js 4 2 src/libs/github.js 21 1 src/modules/__tests__/details-from-context.js 28 0 src/modules/__tests__/update-issue.js 14 3 src/modules/details-from-context.js 2 2 src/modules/post-workflow-details.js 2 2 src/modules/process-end-state.js 23 19 src/modules/update-issue.js 8 9 src/publish/inputs.js 4 4 src/publish/post-result.js 4 4 src/publish/post-workflow-details.js 26 7 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__/authorize-approval.js src/publish/__tests__/auto-approv...
diff --git a/.github/workflows/ci-poller-dispatch.yml b/.github/workflows/ci-poller-dispatch.yml
new file mode 100644
index 0000000..781bd08
--- /dev/null
+++ b/.github/workflows/ci-poller-dispatch.yml
@@ -0,0 +1,16 @@
+name: Run CI Status Poller
+
+on:
+ workflow_dispatch:
+
+permissions: {}
+
+jobs:
+ dispatch:
+ # production only permits deployments from main. Repository secrets must
+ # never be used by this workflow; protected environment secrets enforce
+ # that an arbitrary dispatched ref cannot add them.
+ environment: production
+ runs-on: ubuntu-latest
+ steps:
+ - run: ":"
diff --git a/src/modules/approval-attestation.js b/src/modules/approval-attestation.js
new file mode 100644
index 0000000..096024a
--- /dev/null
+++ b/src/modules/approval-attestation.js
@@ -0,0 +1,259 @@
+const crypto = require("crypto");
+
+const APPROVAL_ATTESTATION_PREFIX = "<!-- publish-approval ";
+const AUTO_APPROVAL_ATTESTATION_PREFIX = "<!-- publish-auto-approval ...
.github/workflows/auto-approve.yml | 23 +- .github/workflows/ci-poller.yml | 245 +++- .github/workflows/cocoapods-keepalive.yml | 1 - .github/workflows/publish.yml | 219 ++- .github/workflows/test.yml | 6 +- AGENTS.md | 6 + README.md | 9 +- docs/key_rotation_vault_changes.md | 40 +- docs/publish-issue-format.md | 107 -- docs/rfc.md | 9 +- package.json | 5 +- scripts/generate-publish-issue-title-parser.js | 104 -- src/libs/__tests__/github.js | 2 +- src/libs/github.js | 6 +- src/modules/__tests__/ci-poller-input.js | 97 -- src/modules/__tests__/ci-poller-workflow.js | 197 --- ...
* 4f4b00f (origin/fix/root-location-bootstrap, fix/root-location-bootstrap) fix: bootstrap root publish locations * 7c60ddb (origin/main, origin/HEAD) feat: accept workspace publish requests | * fd1e115 (origin/feat/workspace-acceptance, feat/workspace-acceptance) fix: address publish workspace review feedback | * a81ab03 feat: resolve workspace publish paths | * 52ef600 feat: resolve compact workspace publish requests | * b658ffa fix: validate publish workspace JSON | * 33de3f1 feat: accept workspace publish requests |/ * 83d210b (HEAD -> main, opencode/tidy-orchid) build(deps): bump js-yaml from 4.3.0 to 4.3.1 (#9174) 7c60ddb feat: accept workspace publish requests .github/workflows/ci-poller.yml | 62 +- .github/workflows/cocoapods-keepalive.yml | 2 +- .github/workflows/publish.yml | 84 +- README.md | 5 + docs/publish-issue-format.md | 107 ++ ...
<path>/home/byk/Code/getsentry/publish/.github/workflows/auto-approve.yml</path>
<type>file</type>
<content>
1: name: auto-approve non-sdks
2: on:
3: issues:
4: types: [opened]
5:
6: permissions:
7: contents: read
8: issues: write
9:
10: jobs:
11: auto-approve:
12: runs-on: ubuntu-latest
13: environment: production
14: if: |
15: (github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') &&
16: startsWith(github.event.issue.title, 'publish: ')
17: steps:
18: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
19: with:
20: sparse-checkout: auto-approve-repos.txt
21: sparse-checkout-cone-mode: false
22: - name: Get auth token
23: id: token
24: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
25: with:
26: client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
27: private-key: ${{ ...
## main...origin/main [behind 1] 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/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__/ ?? ...
<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 = ...
diff --git a/src/modules/__tests__/approval-attestation.js b/src/modules/__tests__/approval-attestation.js
new file mode 100644
index 0000000..f720052
--- /dev/null
+++ b/src/modules/__tests__/approval-attestation.js
@@ -0,0 +1,174 @@
+import { describe, expect, test } from "vitest";
+
+const {
+ createApprovalAttestation,
+ createCiReadyAttestation,
+ currentAcceptedEvent,
+ hasApprovalAttestation,
+ hasCiReadyAttestation,
+ parseApprovalAttestation,
+ requestDigest,
+} = require("../approval-attestation.js");
+
+function issue(title, { body = "", dryRun = false } = {}) {
+ return {
+ body,
+ labels: dryRun ? [{ name: "dry-run" }] : [],
+ title,
+ };
+}
+
+describe("approval attestations", () => {
+ test("matches the latest accepted event to a trusted attestation", () => {
+ const title = "publish: getsentry/relay/py@1.2.3";
+ const publishIssue = issue(title);
+ const attestation = createApprovalAttestation({
+ actor: "contractor",
+ eventId: ...
<path>/home/byk/Code/getsentry/publish/src/modules/update-issue.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3: TARGETS_SECTION_PARSER_REGEX,
4: TARGETS_PARSER_REGEX,
5: } = require("./details-from-context");
6:
7: async function updateTargets({ octokit, version, publishRepo, issue_number }) {
8: const CRAFT_STATE_FILE_PATH = `${process.env.GITHUB_WORKSPACE}/__repo__/.craft-publish-${version}.json`;
9:
10: if (!fs.existsSync(CRAFT_STATE_FILE_PATH)) {
11: return;
12: }
13:
14: const issueRequest = octokit.rest.issues.get({
15: ...publishRepo,
16: issue_number,
17: });
18:
19: const craftStateRequest = fs.promises
20: .readFile(CRAFT_STATE_FILE_PATH, { encoding: "utf-8" })
21: .then((data) => JSON.parse(data));
22:
23: const [{ data: issue }, craftState] = await Promise.all([
24: issueRequest,
25: craftStateRequest,
26: ]);
27:
28: const newIssueBody = transformIssueBody(craftState, issue.body);
29: ...
diff --git a/README.md b/README.md index 4cbc9b9..8b716c8 100644 --- a/README.md +++ b/README.md @@ -39,11 +39,6 @@ flowchart TD 1. Observe the issue for information about the triggered run 1. The issue will automatically be closed when publishing succeeds -## Publish Issue Format - -The release workflow creates publish requests with a stable title and body contract. See -[Publish Issue Format](docs/publish-issue-format.md) for the accepted syntax and fields. - ## CalVer To enable calendar versioning, add the following to your `.craft.yml`: @@ -99,6 +94,8 @@ Automatic approvals are managed in the [`auto-approve.yml`](https://github.com/g The system uses [Craft](https://github.com/getsentry/craft) under the hood to prepare and publish releases. It uses tokens from [Sentry Release Bot](https://github.com/apps/sentry-release-bot), which is a GitHub App that is installed on all repos in `getsentry` with read and write access to code, PRs, and actions. ...
diff --git a/src/publish/__tests__/auto-approval-workflow.js b/src/publish/__tests__/auto-approval-workflow.js
new file mode 100644
index 0000000..9009a04
--- /dev/null
+++ b/src/publish/__tests__/auto-approval-workflow.js
@@ -0,0 +1,27 @@
+import { readFileSync } from "fs";
+import { describe, expect, test } from "vitest";
+
+const workflow = readFileSync(".github/workflows/auto-approve.yml", "utf8");
+
+describe("auto-approval workflow", () => {
+ test("records a request-bound attestation before adding accepted", () => {
+ expect(workflow).toContain(
+ "node src/publish/record-auto-approval-attestation.js"
+ );
+ expect(
+ workflow.indexOf("record-auto-approval-attestation.js")
+ ).toBeLessThan(workflow.indexOf("--add-label accepted"));
+ });
+
+ test("posts the proof as github-actions before the internal app labels", () => {
+ const proof = workflow.indexOf("Post automated approval attestation");
+ const label = workflow.indexOf("--add-label ...
diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml
index d9763c7..b2b489b 100644
--- a/.github/workflows/auto-approve.yml
+++ b/.github/workflows/auto-approve.yml
@@ -5,25 +5,44 @@ on:
permissions:
contents: read
+ issues: write
jobs:
auto-approve:
runs-on: ubuntu-latest
+ environment: production
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: ${{ vars.SENTRY_INTERNAL_APP_ID ...
<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.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: && ...
# 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. ...
Found 1 matches /home/byk/.local/share/opencode/tool-output/tool_086e3fd06001SKuFRxjAjbUQYh: Line 1232: ## `workflow_run`
<path>/home/byk/.local/share/opencode/tool-output/tool_086e3fd06001SKuFRxjAjbUQYh</path> <type>file</type> <content> 1232: ## `workflow_run` 1233: 1234: | Webhook event payload | Activity types | `GITHUB_SHA` | `GITHUB_REF` | 1235: | ----------------------------------------------------------------------- | --------------------------------------------------- | ----------------------------- | -------------- | 1236: | [`workflow_run`](/en/webhooks/webhook-events-and-payloads#workflow_run) | - `completed`<br/>- `requested`<br/>- `in_progress` | Last commit on default branch | Default branch | 1237: 1238: > \[!NOTE] 1239: > 1240: > * More than one activity type triggers this event. The `requested` activity type does not occur when a workflow is re-run. For information about each activity type, see [Webhook events and payloads](/en/webhooks/webhook-events-and-payloads#workflow_run). ...
{
"message": "Not Found",
"documentation_url": "https://docs.github.com/rest",
"status": "404"
}gh: Not Found (HTTP 404)
<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>
yarn run v1.22.22 $ vitest run --run --printConsoleTrace [1m[46m RUN [49m[22m [36mv4.0.17 [39m[90m/home/byk/Code/getsentry/publish[39m [32m✓[39m src/publish/__tests__/validate-approval-attestation.js [2m([22m[2m9 tests[22m[2m)[22m[32m 53[2mms[22m[39m [32m✓[39m src/modules/__tests__/approval-attestation.js [2m([22m[2m5 tests[22m[2m)[22m[32m 21[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 49[2mms[22m[39m [32m✓[39m src/modules/__tests__/update-issue.js [2m([22m[2m6 tests[22m[2m)[22m[32m 27[2mms[22m[39m [32m✓[39m src/modules/__tests__/approval-authorizer.js [2m([22m[2m15 tests[22m[2m)[22m[32m 33[2mms[22m[39m [32m✓[39m src/modules/__tests__/process-end-state.js [2m([22m[2m4 tests[22m[2m)[22m[32m 88[2mms[22m[39m [32m✓[39m ...
yarn run v1.22.22 $ eslint src .github --ignore-pattern '!.github' Done in 1.92s.
b2b489b4e6427c0dece46fd79a25de419ecdc868 17d5e91f956f3b5d8018d11d1a5f07499941b02d 781bd0850f4ea15ae89417acabdd353fc98b6bd0 9ff9a9d55b07734dcbb33e7d3f4fa69178edb39c 096024af8c999595952a6f029cdb06794be23e11 d64534399a1a23a4d5578e2fc1c7d6f98c17c0f7 69f70a434528fa2adf1333d5045ad6b473304118 baa0ef86bfc0d7fc0db974197cc7a227833eb271 5c0baa5457a54f9c444de3cd1bf1b473c065f9b7 58b43354ebad6c25f3a689f9e6672a60632c9461d537dd9229c85a03bfc44ef4 -
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 D docs/publish-issue-format.md M docs/rfc.md M package.json D scripts/generate-publish-issue-title-parser.js M src/libs/__tests__/github.js M src/libs/github.js D src/modules/__tests__/ci-poller-input.js D src/modules/__tests__/ci-poller-workflow.js M src/modules/__tests__/details-from-context.js D src/modules/__tests__/generate-publish-issue-title-parser.js D src/modules/__tests__/publish-location.js D src/modules/__tests__/publish-workflow.js D src/modules/__tests__/release-revision.js M src/modules/__tests__/update-issue.js D src/modules/ci-poller-input.js M src/modules/details-from-context.js M src/modules/post-workflow-details.js M src/modules/process-end-state.js D src/modules/publish-issue-title.js D src/modules/publish-issue-title.peggy D src/mo...
<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: ...
No files found
<path>/home/byk/Code/getsentry/publish/.github/workflows/auto-approve.yml</path>
<type>file</type>
<content>
1: name: auto-approve non-sdks
2: on:
3: issues:
4: types: [opened]
5:
6: permissions:
7: contents: read
8: issues: write
9:
10: jobs:
11: auto-approve:
12: runs-on: ubuntu-latest
13: environment: production
14: if: |
15: (github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') &&
16: startsWith(github.event.issue.title, 'publish: ')
17: steps:
18: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
19: with:
20: sparse-checkout: auto-approve-repos.txt
21: sparse-checkout-cone-mode: false
22: - name: Get auth token
23: id: token
24: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
25: with:
26: client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
27: private-key: ${{ ...
## main...origin/main [behind 1] 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/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__/ ?? ...
I’m resuming the read-only review against the exact current worktree and current origin/main. I will not edit files.
<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/src/publish/record-ci-ready-attestation.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3: createCiReadyAttestation,
4: currentAcceptedEvent,
5: } = require("../modules/approval-attestation.js");
6: const {
7: getAllPages,
8: getGitHubResponse,
9: getIssue,
10: } = require("./validate-approval-attestation.js");
11:
12: async function getAuthenticatedLogin() {
13: const response = await getGitHubResponse("user");
14:
15: if (!response.ok) {
16: throw new Error(
17: `Could not retrieve the authenticated user: GitHub returned ${response.status}`
18: );
19: }
20:
21: const { login } = await response.json();
22:
23: if (typeof login !== "string") {
24: throw new Error("GitHub returned no authenticated user login");
25: }
26:
27: return login;
28: }
29:
30: async function recordCiReadyAttestation({
31: expectedAcceptedEvent,
32: getAuthenticatedLogin,
33: getIssue,
34: ...
<path>/home/byk/Code/getsentry/publish/src/modules/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: ...
No files found
<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__/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 = ...
# REST API endpoints for users Use the REST API to get public and private information about authenticated users. > [!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. ## Get the authenticated user ``` GET /user ``` OAuth app tokens and personal access tokens (classic) need the read:user scope, or the broader user scope, for this endpoint to return the private user response. The private user response includes additional fields such as private_gists, total_private_repos, owned_private_repos, disk_usage, collaborators, and two_factor_authentication. Tokens without these scopes receive the public user response. The private and public user response types are unrelated to the private profile setting. ...
<path>/home/byk/Code/getsentry/publish/src/modules/details-from-context.js</path> <type>file</type> <content> 1: /** 2: * Matches the entire "Targets" section of a github publish issue body. 3: */ 4: const TARGETS_SECTION_PARSER_REGEX = 5: /^(?!### Targets$\s)(?: *- \[[ xX]\] \S+\s*$(?:\r?\n)?)+/m; 6: 7: /** 8: * Matches all targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`. 9: * The "id" of the targets is captured within a capture group. 10: */ 11: const TARGETS_PARSER_REGEX = /^\s*- \[[ x]\] (\S+)/gim; 12: 13: /** 14: * Matches checked targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`. 15: * The "id" of the targets is captured within a capture group. 16: */ 17: const CHECKED_TARGETS_PARSER_REGEX = /^\s*- \[x\] (\S+)/gim; 18: const PUBLISH_TITLE_REGEX = 19: /^publish: ...
<path>/home/byk/Code/getsentry/publish/src/modules/update-issue.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3: TARGETS_SECTION_PARSER_REGEX,
4: TARGETS_PARSER_REGEX,
5: } = require("./details-from-context");
6:
7: async function updateTargets({ octokit, version, publishRepo, issue_number }) {
8: const CRAFT_STATE_FILE_PATH = `${process.env.GITHUB_WORKSPACE}/__repo__/.craft-publish-${version}.json`;
9:
10: if (!fs.existsSync(CRAFT_STATE_FILE_PATH)) {
11: return;
12: }
13:
14: const issueRequest = octokit.rest.issues.get({
15: ...publishRepo,
16: issue_number,
17: });
18:
19: const craftStateRequest = fs.promises
20: .readFile(CRAFT_STATE_FILE_PATH, { encoding: "utf-8" })
21: .then((data) => JSON.parse(data));
22:
23: const [{ data: issue }, craftState] = await Promise.all([
24: issueRequest,
25: craftStateRequest,
26: ]);
27:
28: const newIssueBody = transformIssueBody(craftState, issue.body);
29: ...
<path>/home/byk/Code/getsentry/publish/src/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/.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>
yarn run v1.22.22 $ vitest run --run --printConsoleTrace [1m[46m RUN [49m[22m [36mv4.0.17 [39m[90m/home/byk/Code/getsentry/publish[39m [32m✓[39m src/publish/__tests__/authorize-approval.js [2m([22m[2m7 tests[22m[2m)[22m[32m 36[2mms[22m[39m [32m✓[39m src/publish/__tests__/validate-approval-attestation.js [2m([22m[2m9 tests[22m[2m)[22m[32m 58[2mms[22m[39m [32m✓[39m src/modules/__tests__/approval-authorizer.js [2m([22m[2m15 tests[22m[2m)[22m[32m 15[2mms[22m[39m [32m✓[39m src/publish/__tests__/current-accepted-event.js [2m([22m[2m2 tests[22m[2m)[22m[32m 38[2mms[22m[39m [32m✓[39m src/modules/__tests__/update-issue.js [2m([22m[2m6 tests[22m[2m)[22m[32m 26[2mms[22m[39m [32m✓[39m src/modules/__tests__/post-workflow-details.js [2m([22m[2m1 test[22m[2m)[22m[32m 22[2mms[22m[39m [32m✓[39m src/modules/__tests__/process-end-state.js [2m([22m[2m4 tests[22m[2m)[22m[32m 80[2mms[22m[39m [32m✓[39m ...
Date: Sep 9, 2026 * 🔴 (16:41) GitHub’s REST API endpoint for the authenticated user is `GET /user`; most endpoints use `Authorization: Bearer <YOUR-TOKEN>`, `Accept: application/vnd.github+json`, and `X-GitHub-Api-Version: 2026-03-10`. * 🔴 (16:41) OAuth app tokens and personal access tokens (classic) require the `read:user` scope or broader `user` scope for `GET /user` to return the private-use…
* 🔴 (16:33) `.github/workflows/publish.yml` is 435 lines and defines workflow `Publish`, triggered by issue `labeled` events, with concurrency group `${{ github.event.issue.title }}`, `cancel-in-progress: false`, and permissions `contents: read`, `issues: write`, and `packages: write`. * 🔴 (16:33) The `waiting-for-ci` job runs on `ubuntu-latest` in the `production` environment only when the add…
* 🔴 (16:15) Tool output listed identifiers in this exact order: `b2b489b4e6427c0dece46fd79a25de419ecdc868`, `17d5e91f956f3b5d8018d11d1a5f07499941b02d`, `781bd0850f4ea15ae89417acabdd353fc98b6bd0`, `9ff9a9d55b07734dcbb33e7d3f4fa69178edb39c`, `096024af8c999595952a6f029cdb06794be23e11`, `d64534399a1a23a4d5578e2fc1c7d6f98c17c0f7`, `69f70a434528fa2adf1333d5045ad6b473304118`, `baa0ef86bfc0d7fc0db974197…
* 🔴 (15:58) GitHub Actions documentation states `workflow_run` supports activity types `completed`, `requested`, and `in_progress`; `requested` does not occur when a workflow is re-run, and the triggered workflow file must exist on the default branch. * 🔴 (15:58) GitHub Actions documentation states `workflow_run` chains are limited to 3 levels: in an `A` → `B` → `C` → `D` → `E` → `F` sequence, …
* 🔴 (15:57) `.github/workflows/ci-poller.yml` lines 99–113 lists up to 200 open issues carrying both `ci-pending` and `accepted`; when none exist, it prints `No ci-pending + accepted issues found.` and exits successfully. * 🔴 (15:57) `.github/workflows/ci-poller.yml` lines 170–217 extracts a 40-character commit SHA from the issue body’s `/commit/{SHA}/checks` link, discovers the release branch …
* 🔴 (15:57) User directive: “Always allow workflow_dispatch for manual recovery.” Manual recovery is implemented through the protected `.github/workflows/ci-poller-dispatch.yml` relay rather than direct `workflow_dispatch` in `.github/workflows/ci-poller.yml`. * 🔴 (15:57) User directive: “Always run trusted code.” `.github/workflows/ci-poller.yml` checks out `${{ github.event.repository.default…
* 🔴 (15:55) New test file `src/publish/__tests__/auto-approval-workflow.js` contains 2 tests: (1) `.github/workflows/auto-approve.yml` must run `node src/publish/record-auto-approval-attestation.js` before `--add-label accepted`; (2) the “Post automated approval attestation” step must use `GH_TOKEN: ${{ github.token }}` before labeling, while the later label operation uses `GH_TOKEN: ${{ steps.t…
* 🔴 (15:52) Git history shows `fix/root-location-bootstrap` at `4f4b00f`, `origin/main` at `7c60ddb` (`feat: accept workspace publish requests`), feature branch `feat/workspace-acceptance` ending at `fd1e115`, and local `main`/`opencode/tidy-orchid` at `83d210b29553ea6f4d97508821724a834c43856e`, one commit behind `origin/main`. * 🔴 (15:52) Commit `7c60ddb7f43040fe8fbfea70efc833f689c04e75` chang…
* 🔴 (15:52) The repository change set spans 44 files with 557 insertions and 3,317 deletions. Modified files include `.github/workflows/auto-approve.yml`, `.github/workflows/ci-poller.yml`, `.github/workflows/cocoapods-keepalive.yml`, `.github/workflows/publish.yml`, `.github/workflows/test.yml`, `AGENTS.md`, `README.md`, `docs/key_rotation_vault_changes.md`, `docs/rfc.md`, `package.json`, `src/…
* 🔴 (15:48) New workflow `.github/workflows/ci-poller-dispatch.yml` is named `Run CI Status Poller`, is triggered only by `workflow_dispatch:`, declares `permissions: {}`, and has one `dispatch` job using `environment: production`, `runs-on: ubuntu-latest`, and a single no-op step `run: ":"`. * 🔴 (15:48) Repository secrets must never be used by `.github/workflows/ci-poller-dispatch.yml`; the `p…
* 🔴 (15:41) `/home/byk/Code/getsentry/publish/src/publish/record-auto-approval-attestation.js` defines `recordAutoApprovalAttestation({ autoApprover, getIssue, issueNumber, issueTitle, repository })`; it retrieves the live issue with `getIssue({ repository, issueNumber })` and rejects with `Error("The automated approval request changed before approval")` if the issue is not open, its title diffe…
* 🔴 (15:41) GitHub REST API examples use `Authorization: Bearer <YOUR-TOKEN>`, `Accept: application/vnd.github+json`, and `X-GitHub-Api-Version: 2026-03-10`; the provided curl examples omit these standard headers. * 🔴 (15:41) `GET /user` retrieves the authenticated user. OAuth app tokens and personal access tokens (classic) need `read:user` or broader `user` scope for the private-user response;…
* 🔴 (15:37) Attempting to list selected repositories for an organization Actions secret failed with GitHub HTTP 403: `You must be an org admin or have the actions secrets fine-grained permission.` The API operation requires the `admin:org` scope; GitHub suggested `gh auth refresh -h github.com -s admin:org`. * 🔴 (15:37) `/home/byk/Code/getsentry/publish/src/modules/update-issue.js` is 82 lines …
* 🔴 (15:34) `/home/byk/Code/getsentry/publish/src/publish/validate-approval-attestation.js` is 153 lines and imports `currentAcceptedEvent`, `currentCiReadyEvent`, `hasApprovalAttestation`, and `hasCiReadyAttestation` from `../modules/approval-attestation.js`. * 🔴 (15:34) `validate-approval-attestation.js` sends authenticated GitHub requests to `https://api.github.com/${path}` using `APPROVAL_T…
* 🔴 (15:32) In `/home/byk/Code/getsentry/publish/.github/workflows/publish.yml`, the `publish` job requires an open issue, a `ci-ready` label event, both `accepted` and `ci-ready` currently present, and neither `ci-pending` nor `ci-failed`; timeout is 90 minutes and `SENTRY_DSN` is configured. * 🔴 (15:32) User stated the poller “always adds ci-ready” after checking CI; `waiting-for-ci` first re…
* 🔴 (15:31) File `/home/byk/Code/getsentry/publish/.github/workflows/publish.yml` defines the GitHub Actions workflow `Publish`, triggered by `issues` with `types: [labeled]`. * 🔴 (15:31) `/home/byk/Code/getsentry/publish/.github/workflows/publish.yml` uses concurrency group `${{ github.event.issue.title }}` with `cancel-in-progress: false`, so duplicate issues for the same `repo@version` title…
* 🔴 (15:23) GitHub Actions documentation states that the `workflow_run` activity type `requested` does not occur when a workflow is re-run; by default all supported activity types trigger the workflow, and `types` can restrict them. * 🔴 (15:23) GitHub Actions documentation states that a workflow using `workflow_run` triggers only if its workflow file exists on the default branch. * 🔴 (15:23) G…
* 🔴 (15:18) A checksum command returned two SHA-256-like digests for stdin (`-`): `58b43354ebad6c25f3a689f9e6672a60632c9461d537dd9229c85a03bfc44ef4` and `8ca4aa7afcb6a1c171f695d488a06b8cf4d08436a414a3223a18b33a08db4fc6`. * 🔴 (15:18) `src/publish/record-ci-ready-attestation.js` is 109 lines and imports `createCiReadyAttestation` and `currentAcceptedEvent` from `../modules/approval-attestation.js…
* 🔴 (15:16) Repository `/home/byk/Code/getsentry/publish` was on `main...origin/main [behind 1]`, with 21 tracked files modified, 11 untracked paths, and a tracked diff totaling 499 insertions and 117 deletions. * 🔴 (15:16) Modified tracked files were `.github/workflows/auto-approve.yml`, `.github/workflows/ci-poller.yml`, `.github/workflows/cocoapods-keepalive.yml`, `.github/workflows/publish.…
* 🔴 (15:13) GitHub REST user endpoints generally use `Authorization: Bearer <YOUR-TOKEN>`, `Accept: application/vnd.github+json`, and `X-GitHub-Api-Version: 2026-03-10`; the provided curl examples omit these standard headers. * 🔴 (15:13) `GET /user` returns the authenticated user. OAuth app tokens and personal access tokens (classic) require `read:user` or broader `user` scope for the private r…
* 🔴 (15:05) `/home/byk/Code/getsentry/publish/.github/workflows/ci-poller-dispatch.yml` defines the manual `Run CI Status Poller` relay with only a `workflow_dispatch` trigger, `permissions: {}`, and one `dispatch` job in the protected `production` environment on `ubuntu-latest`; its sole step runs `":"`. * 🔴 (15:05) User directed that repository secrets must never be used by `.github/workflows…
* 🔴 (15:01) `/home/byk/Code/getsentry/publish/.github/workflows/ci-poller.yml` defines `CI Status Poller`, triggered every 5 minutes by cron `*/5 * * * *`, by `repository_dispatch` type `ci-poller`, and when workflow `Run CI Status Poller` completes. * 🔴 (15:01) The `check-ci` job runs on `ubuntu-latest` in the `production` environment with concurrency group `ci-status-poller` and `cancel-in-pr…
* 🔴 (14:42) `/home/byk/Code/getsentry/publish/package.json` defines package `publish` version `0.0.1`, private, Apache-2.0, repository `git@github.com:getsentry/publish.git`, description `Approval-based publishing system for Sentry`, author `Sentry Open Source <oss@sentry.io>`, Volta Node `24.0.0` and Yarn `1.22.22`, with resolution `undici: ^6.23.0`. * 🔴 (14:42) `/home/byk/Code/getsentry/publi…
* 🔴 (14:38) Test run `yarn vitest run --printConsoleTrace` using Vitest `v4.0.17` in `/home/byk/Code/getsentry/publish` passed all `16` test files and all `84` tests: `src/modules/__tests__/approval-attestation.js` (5), `src/publish/__tests__/authorize-approval.js` (7), `src/modules/__tests__/approval-authorizer.js` (15), `src/publish/__tests__/validate-approval-attestation.js` (9), `src/modules…
* 🔴 (14:34) `src/publish/__tests__/publish-workflow.js` contains 2 Vitest tests reading `.github/workflows/publish.yml`: (1) `revalidates approval immediately before Craft` asserts `Revalidate approval attestation` occurs after `Set targets`, before `Publish using Craft`, and its remaining workflow slice contains `REQUIRE_CI_READY_ATTESTATION: "true"`; (2) `pins Craft to an immutable digest` req…
* 🔴 (14:25) User’s repository was on `main...origin/main [behind 1]`; reported revisions were `83d210b29553ea6f4d97508821724a834c43856e` and `7c60ddb7f43040fe8fbfea70efc833f689c04e75`. * 🔴 (14:25) Working-tree diff covered 44 files with 557 insertions and 3317 deletions. Modified files included `.github/workflows/auto-approve.yml`, `.github/workflows/ci-poller.yml`, `.github/workflows/cocoapods…
* 🔴 (14:21) SHA-256 output reported `14720c4c499a2de70a06fffa6cfa3312ab6e3c16dbdcbf19fec1f1aca34c9897` for standard input (`-`). * 🔴 (14:21) File SHA-256 values were: `.github/workflows/ci-poller-dispatch.yml` → `570dcb023ae08f4fb8338923496a55bf1c2e012a944f9969650bc31feadd485c`; `src/modules/approval-attestation.js` → `51898ad7a79e99d1a22a116a40953e5b52030a8e90c9eda5c46eb3ed3998b701`; `src/modu…
* 🔴 (14:13) `src/publish/__tests__/` contains 9 test files: `workflow-action-pinning.js`, `record-auto-approval-attestation.js`, `publish-workflow.js`, `auto-approval-workflow.js`, `ci-poller-workflow.js`, `current-accepted-event.js`, `record-ci-ready-attestation.js`, `validate-approval-attestation.js`, and `authorize-approval.js`. * 🔴 (14:13) `src/modules/details-from-context.js` defines `PUBL…
* 🔴 (13:50) User provided GitHub REST API details for `users/get-authenticated`: OAuth app tokens and classic personal access tokens need `read:user` or broader `user` scope for a private-user response; unscoped tokens receive a public-user response while still authenticating as the token owner. Responses include `200`, `304`, `401`, and `403`; the endpoint is not enabled for GitHub Apps. * 🔴 (…
* 🔴 (13:25) `.github/workflows/auto-approve.yml` defines `auto-approve non-sdks`, triggered when issues are opened; job `auto-approve` runs only when `github.actor` is `sentry-release-bot[bot]` or `getsantry[bot]` and the issue title starts with `publish: `. * 🔴 (13:25) `.github/workflows/auto-approve.yml` uses the `production` environment, `contents: read` and `issues: write` permissions, `act…
Date: Sep 9, 2026 * 🔴 (13:47) A worktree was prepared at detached HEAD `7c60ddb` (`feat: accept workspace publish requests`), but patch application failed for 7 files: `.github/workflows/ci-poller.yml:92`, `.github/workflows/cocoapods-keepalive.yml:1`, `.github/workflows/publish.yml:117`, `src/modules/__tests__/update-issue.js:129`, `src/modules/details-from-context.js:15`, `src/modules/update-i…
Date: Sep 9, 2026 * 🔴 (13:30) Diff statistics for 21 tracked files were: `.github/workflows/auto-approve.yml` +21/-2; `.github/workflows/ci-poller.yml` +172/-23; `.github/workflows/cocoapods-keepalive.yml` +1/-2; `.github/workflows/publish.yml` +131/-8; `.github/workflows/test.yml` +3/-3; `AGENTS.md` +6/-0; `README.md` +3/-1; `docs/key_rotation_vault_changes.md` +20/-20; `docs/rfc.md` +5/-4; `sr…
Date: Sep 9, 2026 * 🔴 (12:58) Git status for `/home/byk/Code/getsentry/publish` showed branch `main` tracking `origin/main` and behind by 1 commit. * 🔴 (12:58) Git status showed 21 modified tracked files: `.github/workflows/auto-approve.yml`, `.github/workflows/ci-poller.yml`, `.github/workflows/cocoapods-keepalive.yml`, `.github/workflows/publish.yml`, `.github/workflows/test.yml`, `AGENTS.md`…
Date: Sep 9, 2026 * 🔴 (13:22) `/home/byk/Code/getsentry/publish/src/publish/__tests__/validate-approval-attestation.js` is a 420-line Vitest suite importing `validateApprovalAttestation`, `createApprovalAttestation`, and `createCiReadyAttestation`; `afterEach()` calls `vi.restoreAllMocks()`, `jsonResponse(json)` creates an OK mocked response, and `issue(title, { body = "", labels = [{ name: "acc…
🔴 (13:19) [enforced-workflow] User directed: “Always run trusted code.” Because `workflow_dispatch` can target any ref, the CI poller workflow must check out trusted code rather than code from the trigger’s arbitrary ref. 🔴 (13:19) [enforced-workflow] User directed: “Never move a release to ci-ready after it changes.” Each polling cycle must bind itself to the current accepted-label event befo…
🔴 (13:17) User directed that the `events_url` URI template’s `{/privacy}` component be replaced with `/public` to retrieve only public events; omitting it retrieves public events and, when authenticated as that user, private events. 🔴 (13:17) GitHub REST API requests generally use `Authorization: Bearer <YOUR-TOKEN>`, `Accept: application/vnd.github+json`, and `X-GitHub-Api-Version: 2026-03-10…
🔴 (13:10) User directed that the `events_url` URI template’s `{/privacy}` component be replaced with `/public` to retrieve only public events; omitting it retrieves public events and, when authenticated as that user, private events. 🔴 (13:10) The `GET /user` private-user response adds `private_gists`, `total_private_repos`, `owned_private_repos`, `disk_usage`, `collaborators`, and `two_factor_…
🟡 (13:02) [requested-review] User asked to resume and complete the previously requested independent read-only review, provide substantive `file:line` evidence, make no file edits, and end the response with exactly `MERGE` or `DO-NOT-MERGE`. 🔴 (13:03) User directed that the `events_url` URI template’s `{/privacy}` component be replaced with `/public` to retrieve only public events; omitting it …
🔴 (12:56) `/home/byk/Code/getsentry/publish/src/publish/authorize-approval.js` is 241 lines. It imports `createApprovalAttestation`, `currentAcceptedEvent`, and `hasAutoApprovalAttestation` from `../modules/approval-attestation.js`, plus `AUTO_APPROVAL_LABELER`, `AUTO_APPROVERS`, `authorizeApproval`, and `isAutoApprovedRepository` from `../modules/approval-authorizer.js`. 🔴 (12:56) `getAutoApp…
🔴 (12:55) User’s manual-recovery directive is exact: “Always allow workflow_dispatch for manual recovery.” The current design preserves manual recovery through a protected `workflow_dispatch` relay whose successful default-branch `workflow_run` can start `.github/workflows/ci-poller.yml`. 🔴 (12:55) User’s trusted-execution directive is exact: “Always run trusted code.” `.github/workflows/ci-po…
🔴 (12:53) Current checked-out branch is `main`. 🔴 (12:53) `/home/byk/Code/getsentry/publish/.github/workflows/auto-approve.yml` is 54 lines. Workflow `auto-approve non-sdks` runs on opened issues with `contents: read` and `issues: write`; job `auto-approve` uses `environment: production` only when actor is `sentry-release-bot[bot]` or `getsantry[bot]` and the title starts with `publish: `. 🔴…
🔴 (10:23) `yarn prettier --write src --check .` ran under Yarn `v1.22.22`; formatting check warned on exactly 2 files, `.lore.md` and `AGENTS.md`, reported `Code style issues found in 2 files.`, and completed in `1.73s`. 🔴 (10:23) Current `/home/byk/Code/getsentry/publish/src/modules/details-from-context.js` is 75 lines and uses `PUBLISH_TITLE_REGEX = /^publish: (?:getsentry\/)?(?<repo>[^/@]+)…
🔴 (10:18) User requires `workflow_dispatch` always remain available for manual recovery. 🔴 (10:18) User requires manually dispatched workflows to always run trusted code from the repository default branch rather than code from an arbitrary dispatch ref. 🔴 (10:18) User requires that a release never move to `ci-ready` after its issue or approval changes. 🔴 (10:18) User requires a renamed or …
🔴 (10:16) GitHub App permissions define which REST API resources an app may access; responses include `X-Accepted-GitHub-Permissions` to identify permissions required for an endpoint. Permissions are required for private resources, while some endpoints allow public-resource access without them. 🔴 (10:16) GitHub App permission tables use token abbreviations `UAT` = user access token and `IAT` =…
🔴 (10:15) For GitHub’s `events_url` URI template, replace `{/privacy}` with `/public` to retrieve only public events; omit `{/privacy}` to retrieve public events plus private events when authenticated as that user. 🔴 (10:15) GitHub REST endpoint `GET /user` returns the authenticated user; OAuth app tokens and personal access tokens (classic) need `read:user` or broader `user` scope for the pri…
🔴 (10:13) `src/modules/approval-authorizer.js` defines `ALLOWED_ROLE_NAMES = new Set(["write", "maintain", "admin"])`, `AUTO_APPROVAL_LABELER = "sentry-internal-app[bot]"`, and `AUTO_APPROVERS = new Set(["getsantry[bot]", "sentry-release-bot[bot]"])`. 🔴 (10:13) `isAutoApprovedRepository({ actor, autoApprovedRepositories, issueTitle })` rejects actors outside `AUTO_APPROVERS`, parses the issue …
🔴 (10:11) Repository `/home/byk/Code/getsentry/publish` was on `main...origin/main [behind 1]` with 21 tracked files changed: 499 insertions and 117 deletions. 🔴 (10:11) Modified tracked files were `.github/workflows/auto-approve.yml`, `.github/workflows/ci-poller.yml`, `.github/workflows/cocoapods-keepalive.yml`, `.github/workflows/publish.yml`, `.github/workflows/test.yml`, `AGENTS.md`, `REA…
🔴 (10:10) Supplied token-creation implementation imports `pRetry` from `"p-retry"` and `isNetworkError` from `"is-network-error"`; exported `main(clientId, privateKey, enterprise, owner, repositories, permissions, core, createAppAuth, request, skipTokenRevoke)` orchestrates target resolution, GitHub App authentication, installation-token creation, output publication, and optional post-job revoca…
🔴 (10:09) User directed that when creating workflows and actions, always consider whether code might execute untrusted input from possible attackers; attacker-controlled GitHub contexts must be treated as untrusted input. 🔴 (10:09) GitHub Actions contexts documented in the supplied reference are: `github`, `env`, `vars`, `job`, `jobs` (reusable workflows only), `steps`, `runner`, `secrets`, `s…
🔴 (10:07) `/home/byk/Code/getsentry/publish/package.json` defines package `publish` version `0.0.1`, private, with Node `24.0.0` and Yarn `1.22.22` via Volta. Scripts are `test: vitest run`, `test:watch: vitest`, `lint: eslint src .github --ignore-pattern '!.github'`, and `prettier: prettier --write src`. Dependencies are `@actions/core ^2.0.0`, `@actions/github ^7.0.0`, and `@sentry/node ^10.0.…