Dashboard › publish › Session 0FSl746ahuuJ
0FSl746ahuuJXxKq3Perform a READ-ONLY, substantive adversarial security review of the exact current worktree at /home/byk/Code/getsentry/publish against base commit 7c60ddb7f43040fe8fbfea70efc833f689c04e75 (origin/main). DO NOT edit, format, stage, stash, commit, or otherwise mutate any file or repository state. Inspect every changed tracked file (staged and unstaged together) and every untracked file; use git diff HEAD plus untracked reads, not only git diff. Treat GitHub event payloads/issue content as attacker-controlled. Review approval authorization/provenance, request snapshot binding, accepted/ci-ready event binding, canonical title/workspace/revision parsing, poller trusted-code execution/manual recovery, CI transitions/races, exact-revision checkout, path/state containment, secret exposure, action/container pinning, final pre-Craft fence, and terminal cleanup/reconciliation. Accepted scope decision: SENTRY_INTERNAL_APP_PRIVATE_KEY is an existing organization-level secret available broadly; do not block this change solely because it cannot be migrated here, but report any NEW widening introduced by this diff. Preserve these invariants in your assessment: direct bot labels never fall through to collaborator permission; requester cannot self-approve; renamed/body/dry-run/re-approved/revision-changed releases never reach ci-ready in the stale cycle; poller removes stale ci-ready, revalidates, and always re-adds ci-ready; workflow_dispatch manual recovery executes only trusted default-branch controller code. Run read-only tests/checks if useful. Compute an initial and final fingerprint that covers HEAD, tracked diff, staged diff, and untracked file paths+contents; verify unchanged. Findings must come first, ordered by severity, each with current file:line evidence and classification MUST-FIX, CONCERN, or PASS. Explicitly state every changed file was inspected. If blocked, begin with BLOCKED and name the exact tool/error. ...
<path>/home/byk/Code/getsentry/publish</path> <type>directory</type> <entries> .eslintrc.js .git/ .github/ .gitignore .lore.md AGENTS.md auto-approve-repos.txt docs/ LICENSE node_modules/ package.json README.md scripts/ src/ vitest.config.js yarn.lock (16 entries) </entries>
HEAD 7c60ddb7f43040fe8fbfea70efc833f689c04e75 TRACKED_HEAD_DIFF_SHA256 679ed3e87d137e0bc55b2d8fd9ffde9efa900cd91b63c382447e750d6766aaaa - UNSTAGED_TRACKED_DIFF_SHA256 16c8e3d564c98865ee0d3c1412f21e156a09d8790b579ba8b8092d5b246a21f4 - STAGED_DIFF_SHA256 8e4530c4a3b36b00445028365ab293870bc3d19520ef829cb92b6c60eefe187d - UNTRACKED_MANIFEST_SHA256 a3cd906e09f8c3c7f7c380a66742ab595bbe49391649c96478d69a831b990bc7 - UNTRACKED_COUNT 23 STATUS MM .github/workflows/auto-approve.yml MM .github/workflows/ci-poller.yml M .github/workflows/cocoapods-keepalive.yml MM .github/workflows/publish.yml M .github/workflows/test.yml M AGENTS.md M README.md M docs/key_rotation_vault_changes.md M docs/rfc.md M src/libs/__tests__/github.js M src/libs/github.js M src/modules/__tests__/ci-poller-input.js M src/modules/__tests__/ci-poller-workflow.js MM src/modules/__tests__/details-from-context.js M src/modules/__tests__/process-end-state.js M src/modules/__tests__/publish-location.js M ...
BRANCH main UPSTREAM origin/main ORIGIN_MAIN 7c60ddb7f43040fe8fbfea70efc833f689c04e75 MERGE_BASE_HEAD_ORIGIN_MAIN 7c60ddb7f43040fe8fbfea70efc833f689c04e75 RECENT_LOG 7c60ddb (HEAD -> main, origin/main, origin/HEAD) feat: accept workspace publish requests 83d210b (opencode/tidy-orchid) build(deps): bump js-yaml from 4.3.0 to 4.3.1 (#9174) 5268a23 feat: Add Vercel creds 92eba3f feat: Add CLOUDFLARE_API_TOKEN (#9122) ada6147 build(deps): bump js-yaml from 4.1.1 to 4.3.0 (#8731) a4f961a fix: Add GITHUB_API_TOKEN for commit-on-git target (#9069) bf10f82 build(deps): bump postcss from 8.5.15 to 8.5.23 (#9017) 837edc0 docs: fix possessive apostrophe placement (#9015) 4da4a25 chore: replace deprecated trimRight() with trimEnd() (#9012) 22fd64d docs: fix typo 'acces' -> 'access' (#9011) 24b9ce3 docs: fix missing article in rfc.md (#9014) c1bded6 docs: fix run-on sentence in README (#9010) HEAD_TO_ORIGIN_MAIN_COUNTS 0 0
COMBINED_DIFF_STAT .github/workflows/auto-approve.yml | 30 ++- .github/workflows/ci-poller.yml | 232 +++++++++++++++--- .github/workflows/cocoapods-keepalive.yml | 1 - .github/workflows/publish.yml | 271 +++++++++++++++++++--- .github/workflows/test.yml | 6 +- AGENTS.md | 6 + README.md | 4 +- docs/key_rotation_vault_changes.md | 40 ++-- docs/rfc.md | 9 +- src/libs/__tests__/github.js | 2 +- src/libs/github.js | 6 +- src/modules/__tests__/ci-poller-input.js | 22 +- src/modules/__tests__/ci-poller-workflow.js | 207 ++++++++++++++--- src/modules/__tests__/details-from-context.js | 22 +- src/modules/__tests__/process-end-state.js | 27 +++ ...
<path>/home/byk/Code/getsentry/publish/.github/workflows/auto-approve.yml</path>
<type>file</type>
<content>
1: name: auto-approve non-sdks
2: on:
3: issues:
4: types: [opened]
5:
6: permissions:
7: contents: read
8: issues: write
9:
10: jobs:
11: auto-approve:
12: runs-on: ubuntu-latest
13: environment: production
14: if: |
15: (github.actor == 'sentry-release-bot[bot]' || github.actor == 'getsantry[bot]') &&
16: startsWith(github.event.issue.title, 'publish: ')
17: steps:
18: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262
19: with:
20: persist-credentials: false
21: - name: Get auth token
22: id: token
23: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
24: with:
25: client-id: ${{ vars.SENTRY_INTERNAL_APP_ID }}
26: private-key: ${{ secrets.SENTRY_INTERNAL_APP_PRIVATE_KEY }}
27: - name: Bind request snapshot
28: ...
<path>/home/byk/Code/getsentry/publish/.github/workflows/ci-poller.yml</path> <type>file</type> <content> 1: name: CI Status Poller 2: 3: on: 4: schedule: 5: - cron: "*/5 * * * *" 6: repository_dispatch: 7: types: [ci-poller] 8: workflow_run: 9: workflows: [Run CI Status Poller] 10: types: [completed] 11: 12: permissions: 13: contents: read 14: issues: write 15: 16: jobs: 17: check-ci: 18: runs-on: ubuntu-latest 19: environment: production 20: # Skip entirely (no runner provisioned) when there's nothing to check. 21: # Set to "true" by ci-pending.yml, reset to "false" here when done. 22: # repository_dispatch and a successful main-branch run of the protected 23: # workflow_dispatch relay are the trusted manual-recovery paths. 24: if: >- 25: vars.CI_POLLER_HAS_PENDING == 'true' 26: || github.event_name == 'repository_dispatch' 27: || ( 28: github.event_name == 'workflow_run' 29: && ...
<path>/home/byk/Code/getsentry/publish/.github/workflows/publish.yml</path>
<type>file</type>
<content>
1: name: Publish
2: on:
3: issues:
4: types: [labeled]
5:
6: concurrency:
7: # Use the issue title (e.g. "publish: getsentry/foo@1.2.3") so duplicate
8: # issues for the same repo@version share a concurrency group.
9: group: ${{ github.event.issue.title }}
10: cancel-in-progress: false
11:
12: permissions:
13: contents: read
14: issues: write
15: packages: write
16:
17: jobs:
18: # When accepted is added to a publish issue:
19: # - Add ci-pending (and remove ci-failed if retrying)
20: # - Enable the poller via CI_POLLER_HAS_PENDING=true
21: # - Comment on the issue
22: # - Trigger the poller immediately so we don't wait for the next cron tick
23: # The publish job below requires ci-ready, so it will not fire until the
24: # poller flips ci-pending → ci-ready (which also prevents publishing without
25: # CI verification in the ...
<path>/home/byk/Code/getsentry/publish/.github/workflows/ci-poller-dispatch.yml</path>
<type>file</type>
<content>
1: name: Run CI Status Poller
2:
3: on:
4: workflow_dispatch:
5:
6: permissions: {}
7:
8: jobs:
9: dispatch:
10: # production only permits deployments from main. Repository secrets must
11: # never be used by this workflow; protected environment secrets enforce
12: # that an arbitrary dispatched ref cannot add them.
13: environment: production
14: runs-on: ubuntu-latest
15: steps:
16: - run: ":"
(End of file - total 16 lines)
</content>
<path>/home/byk/Code/getsentry/publish/.github/workflows/cocoapods-keepalive.yml</path>
<type>file</type>
<content>
1: name: CocoaPods Token Keep-Alive
2:
3: on:
4: schedule:
5: # CocoaPods sessions currently expire after 3 days of inactivity (VALIDITY_LENGTH)
6: # This runs every day at midnight UTC to keep the current token alive
7: # See: https://github.com/CocoaPods/trunk.cocoapods.org/blob/a1869790e9ae2229b6985b6af4532da814202558/app/models/session.rb#L9
8: - cron: "0 0 * * *" # Runs every day at midnight UTC
9:
10: permissions:
11: contents: read
12:
13: jobs:
14: keepalive:
15: name: Refresh CocoaPods Session
16: runs-on: macos-15
17: environment: production
18: env:
19: COCOAPODS_TRUNK_TOKEN: ${{ secrets.COCOAPODS_TRUNK_TOKEN }}
20:
21: steps:
22: - name: Install CocoaPods
23: run: gem install cocoapods
24: - name: Print CocoaPods Version
25: run: pod --version
26: - name: Refresh CocoaPods ...
<path>/home/byk/Code/getsentry/publish/.github/workflows/test.yml</path>
<type>file</type>
<content>
1: name: Test
2: on:
3: push:
4: branches:
5: - main
6: pull_request:
7:
8: permissions:
9: contents: read
10:
11: defaults:
12: run:
13: shell: bash
14: jobs:
15: unit-test:
16: runs-on: ubuntu-latest
17: name: unit tests
18: steps:
19: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803
20: - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38
21: with:
22: node-version: 24
23: - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
24: id: cache
25: with:
26: path: node_modules
27: key: ${{ runner.os }}-node_modules-${{ hashFiles('package.json', 'yarn.lock') }}
28:
29: - name: Install Dependencies
30: if: steps.cache.outputs.cache-hit != 'true'
31: run: yarn install --frozen-lockfile
32:
33: - name: Test
34: ...
/home/byk/Code/getsentry/publish/src/publish/request-digest-from-event.js /home/byk/Code/getsentry/publish/src/publish/update-issue.js /home/byk/Code/getsentry/publish/src/publish/post-workflow-details.js /home/byk/Code/getsentry/publish/src/publish/post-result.js /home/byk/Code/getsentry/publish/src/publish/inputs.js /home/byk/Code/getsentry/publish/src/publish/resolve-release-revision.js /home/byk/Code/getsentry/publish/src/publish/resolve-location.js /home/byk/Code/getsentry/publish/src/publish/resolve-ci-poller-input.js /home/byk/Code/getsentry/publish/src/publish/discover-location.js /home/byk/Code/getsentry/publish/src/publish/record-auto-approval-attestation.js /home/byk/Code/getsentry/publish/src/publish/current-accepted-event.js /home/byk/Code/getsentry/publish/src/publish/record-ci-ready-attestation.js /home/byk/Code/getsentry/publish/src/publish/validate-approval-attestation.js /home/byk/Code/getsentry/publish/src/publish/__tests__/post-result.js /home/byk/Code/getsentry/pub...
<path>/home/byk/Code/getsentry/publish/src/modules/approval-attestation.js</path>
<type>file</type>
<content>
1: const crypto = require("crypto");
2:
3: const APPROVAL_ATTESTATION_PREFIX = "<!-- publish-approval ";
4: const AUTO_APPROVAL_ATTESTATION_PREFIX = "<!-- publish-auto-approval ";
5: const CI_READY_ATTESTATION_PREFIX = "<!-- publish-ci-ready ";
6: const ATTESTATION_SUFFIX = " -->";
7:
8: function createAttestation(prefix, value) {
9: return `${prefix}${Buffer.from(JSON.stringify(value)).toString(
10: "base64url"
11: )}${ATTESTATION_SUFFIX}`;
12: }
13:
14: function requestDigest({ body, labels, title }) {
15: if (
16: typeof body !== "string" ||
17: !Array.isArray(labels) ||
18: typeof title !== "string"
19: ) {
20: throw new Error("Invalid publish request");
21: }
22:
23: return crypto
24: .createHash("sha256")
25: .update(
26: JSON.stringify({
27: body,
28: dryRun: labels.some((label) => label.name === ...
<path>/home/byk/Code/getsentry/publish/src/modules/approval-authorizer.js</path>
<type>file</type>
<content>
1: const ALLOWED_ROLE_NAMES = new Set(["write", "maintain", "admin"]);
2: const { parsePublishTitle } = require("./details-from-context.js");
3:
4: const AUTO_APPROVAL_LABELER = "sentry-internal-app[bot]";
5: const AUTO_APPROVERS = new Set(["getsantry[bot]", "sentry-release-bot[bot]"]);
6:
7: function isAutoApprovedRepository({
8: actor,
9: autoApprovedRepositories,
10: issueTitle,
11: }) {
12: if (!AUTO_APPROVERS.has(actor)) {
13: return false;
14: }
15:
16: const title = parsePublishTitle(issueTitle);
17:
18: if (!title) {
19: return false;
20: }
21:
22: return autoApprovedRepositories.has(
23: `getsentry/${title.repo}${title.path || ""}`
24: );
25: }
26:
27: async function authorizeApproval({ actor, issueTitle, getPermission }) {
28: const title = parsePublishTitle(issueTitle);
29:
30: if (!title) {
31: return { authorized: ...
<path>/home/byk/Code/getsentry/publish/src/modules/details-from-context.js</path>
<type>file</type>
<content>
1: const { parse: parsePublishIssueTitle } = require("./publish-issue-title");
2: const { getReleaseRevisionDetails } = require("./release-revision");
3: const { isPublishPath } = require("./publish-location");
4: const {
5: isPublishRepository,
6: isReleaseVersion,
7: } = require("./publish-issue-validation");
8:
9: /**
10: * Matches the entire "Targets" section of a github publish issue body.
11: */
12: const TARGETS_SECTION_PARSER_REGEX =
13: /^(?!### Targets$\s)(?: *- \[[ xX]\] \S+\s*$(?:\r?\n)?)+/m;
14:
15: /**
16: * Matches all targets of a github publish issue body in a section that was already matched and extracted with `TARGETS_PARSER_REGEX`.
17: * The "id" of the targets is captured within a capture group. ...
<path>/home/byk/Code/getsentry/publish/src/modules/ci-poller-input.js</path>
<type>file</type>
<content>
1: const { parse: parsePublishIssueTitle } = require("./publish-issue-title");
2: const {
3: getReleaseRevision,
4: updateReleaseRevision,
5: } = require("./release-revision");
6: const {
7: isPublishRepository,
8: isReleaseVersion,
9: } = require("./publish-issue-validation");
10: const { isPublishPath } = require("./publish-location");
11: const { requestDigest } = require("./approval-attestation");
12:
13: function getCiPollerInput({ title, issueBody, labels, revision }) {
14: let parsedTitle;
15: try {
16: parsedTitle = parsePublishIssueTitle(title);
17: } catch {
18: throw new Error(`Invalid publish issue title: '${title}'`);
19: }
20: const { repo, version } = parsedTitle;
21: if (!isPublishRepository(repo)) {
22: throw new Error(`Invalid publish issue repository: '${repo}'`);
23: }
24: if (!isReleaseVersion(version)) {
25: throw new ...
<path>/home/byk/Code/getsentry/publish/src/modules/release-revision.js</path>
<type>file</type>
<content>
1: const { parse } = require("./publish-issue-title");
2:
3: function isRevision(revision) {
4: return /^[0-9a-f]{40}$/.test(revision);
5: }
6:
7: function getReleaseRevisionDetails({ issueBody, repo }) {
8: if (parse(issueBody, { startRule: "CheckRunsLinkCount" }) !== 1) {
9: throw new Error(
10: `Expected exactly one View check runs link in Quick links for getsentry/${repo}.`
11: );
12: }
13:
14: let details;
15: try {
16: details = parse(issueBody, { startRule: "ReleaseRevision" });
17: } catch {
18: throw new Error(
19: `Expected a View check runs link for getsentry/${repo} in the publish issue body.`
20: );
21: }
22:
23: if (details.repo !== repo) {
24: throw new Error(
25: `Expected a View check runs link for getsentry/${repo} in the publish issue body.`
26: );
27: }
28:
29: return details;
30: }
31:
32: ...
<path>/home/byk/Code/getsentry/publish/src/modules/publish-location.js</path>
<type>file</type>
<content>
1: function resolvePublishLocation({ path, workspaceNames }) {
2: if (!isPublishPath(path)) {
3: throw new Error("Invalid publish path.");
4: }
5:
6: if (!workspaceNames.every(isWorkspaceName)) {
7: throw new Error(
8: "Craft workspace discovery returned an invalid workspace list."
9: );
10: }
11:
12: if (path === ".") {
13: return { path };
14: }
15:
16: const workspace = path.slice(2);
17: if (workspaceNames.includes(workspace)) {
18: return { path: ".", workspace };
19: }
20:
21: return { path };
22: }
23:
24: function isPublishPath(path) {
25: return (
26: typeof path === "string" &&
27: (path === "." ||
28: (path.startsWith("./") &&
29: path.slice(2).split("/").every(isSafeWorkspaceSegment)))
30: );
31: }
32:
33: function isWorkspaceName(name) {
34: return (
35: typeof name === "string" && ...
<path>/home/byk/Code/getsentry/publish/src/modules/process-end-state.js</path>
<type>file</type>
<content>
1: const Sentry = require("@sentry/node");
2:
3: async function processEndState({ context, octokit, inputs = {}, status }) {
4: const { repo, version } = inputs;
5: const { repo: publishRepo, runId: run_id } = context;
6: const { number: issue_number } = context.payload.issue;
7:
8: // Validate the status before applying any terminal state transition.
9: sentryInfoFromDetails({ status, repo });
10:
11: if (status === "success") {
12: await octokit.rest.issues.update({
13: ...publishRepo,
14: issue_number,
15: state: "closed",
16: });
17: }
18:
19: const workflowInfo = (
20: await octokit.rest.actions.getWorkflowRun({
21: ...publishRepo,
22: run_id,
23: })
24: ).data;
25:
26: const details = {
27: repo,
28: version,
29: publishRepo,
30: run_id,
31: issue_number,
32: workflowInfo,
33: ...
<path>/home/byk/Code/getsentry/publish/src/modules/post-workflow-details.js</path>
<type>file</type>
<content>
1: async function postWorkflowDetails({ context, octokit }) {
2: const { repo: publishRepo, runId: run_id } = context;
3: const { number: issue_number } = context.payload.issue;
4:
5: const workflowInfo = (
6: await octokit.rest.actions.getWorkflowRun({
7: ...publishRepo,
8: run_id,
9: })
10: ).data;
11:
12: return octokit.rest.issues.createComment({
13: ...publishRepo,
14: issue_number,
15: body: `Publishing: [run#${run_id}](${workflowInfo.html_url})`,
16: });
17: }
18:
19: module.exports = postWorkflowDetails;
(End of file - total 19 lines)
</content>
<path>/home/byk/Code/getsentry/publish/src/publish/authorize-approval.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3: createApprovalAttestation,
4: currentAcceptedEvent,
5: hasAutoApprovalAttestation,
6: requestDigest,
7: } = require("../modules/approval-attestation.js");
8: const {
9: AUTO_APPROVAL_LABELER,
10: AUTO_APPROVERS,
11: authorizeApproval,
12: isAutoApprovedRepository,
13: } = require("../modules/approval-authorizer.js");
14:
15: function getAutoApprovedRepositories() {
16: return new Set(
17: fs
18: .readFileSync("auto-approve-repos.txt", "utf8")
19: .split(/\r?\n/)
20: .filter(Boolean)
21: );
22: }
23:
24: async function getPermission({ owner, repository, username }) {
25: const response = await getGitHubResponse(
26: `repos/${encodeURIComponent(owner)}/${encodeURIComponent(
27: repository
28: )}/collaborators/${encodeURIComponent(username)}/permission`
29: );
30:
31: if ...
<path>/home/byk/Code/getsentry/publish/src/publish/validate-approval-attestation.js</path>
<type>file</type>
<content>
1: const {
2: currentAcceptedEvent,
3: currentCiReadyEvent,
4: hasApprovalAttestation,
5: hasCiReadyAttestation,
6: requestDigest,
7: } = require("../modules/approval-attestation.js");
8:
9: async function getGitHubResponse(path) {
10: return fetch(`https://api.github.com/${path}`, {
11: headers: {
12: Accept: "application/vnd.github+json",
13: Authorization: `Bearer ${process.env.APPROVAL_TOKEN}`,
14: "X-GitHub-Api-Version": "2026-03-10",
15: },
16: });
17: }
18:
19: async function getIssue({ repository, issueNumber }) {
20: const response = await getGitHubResponse(
21: `repos/${repository}/issues/${issueNumber}`
22: );
23:
24: if (!response.ok) {
25: throw new Error(
26: `Could not retrieve issue #${issueNumber}: GitHub returned ${response.status}`
27: );
28: }
29:
30: return ...
<path>/home/byk/Code/getsentry/publish/src/publish/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: requestDigest,
5: } = require("../modules/approval-attestation.js");
6: const { getIssue } = require("./validate-approval-attestation.js");
7:
8: async function recordAutoApprovalAttestation({
9: autoApprover,
10: expectedRequestDigest,
11: getIssue,
12: issueNumber,
13: issueTitle,
14: repository,
15: }) {
16: const issue = await getIssue({ repository, issueNumber });
17:
18: if (
19: issue.state !== "open" ||
20: issue.title !== issueTitle ||
21: requestDigest(issue) !== expectedRequestDigest ||
22: issue.user?.login?.toLowerCase() !== autoApprover.toLowerCase()
23: ) {
24: throw new Error("The automated approval request changed before approval");
25: }
26:
27: return createAutoApprovalAttestation({ autoApprover, issue });
28: }
29:
30: ...
<path>/home/byk/Code/getsentry/publish/src/publish/record-ci-ready-attestation.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3: createCiReadyAttestation,
4: currentAcceptedEvent,
5: requestDigest,
6: } = require("../modules/approval-attestation.js");
7: const {
8: getAllPages,
9: getGitHubResponse,
10: getIssue,
11: } = require("./validate-approval-attestation.js");
12:
13: async function getAuthenticatedLogin() {
14: const response = await getGitHubResponse("user");
15:
16: if (!response.ok) {
17: throw new Error(
18: `Could not retrieve the authenticated user: GitHub returned ${response.status}`
19: );
20: }
21:
22: const { login } = await response.json();
23:
24: if (typeof login !== "string") {
25: throw new Error("GitHub returned no authenticated user login");
26: }
27:
28: return login;
29: }
30:
31: async function recordCiReadyAttestation({
32: expectedAcceptedEvent,
33: ...
<path>/home/byk/Code/getsentry/publish/src/publish/request-digest-from-event.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const { requestDigest } = require("../modules/approval-attestation.js");
3:
4: function requestDigestFromEvent(event) {
5: if (!event || typeof event.issue !== "object" || event.issue === null) {
6: throw new Error("The GitHub event has no issue snapshot");
7: }
8:
9: return requestDigest(event.issue);
10: }
11:
12: function main() {
13: if (!process.env.GITHUB_EVENT_PATH) {
14: throw new Error('No "GITHUB_EVENT_PATH" environment variable found');
15: }
16:
17: if (!process.env.GITHUB_OUTPUT) {
18: throw new Error('No "GITHUB_OUTPUT" environment variable found');
19: }
20:
21: const event = JSON.parse(
22: fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8")
23: );
24: fs.appendFileSync(
25: process.env.GITHUB_OUTPUT,
26: `request_digest=${requestDigestFromEvent(event)}\n`
27: );
28: ...
<path>/home/byk/Code/getsentry/publish/src/publish/discover-location.js</path>
<type>file</type>
<content>
1: const { execFileSync } = require("child_process");
2: const { existsSync } = require("fs");
3: const path = require("path");
4:
5: const core = require("@actions/core");
6:
7: const { resolvePublishLocation } = require("../modules/publish-location");
8:
9: const CRAFT_IMAGE =
10: "getsentry/craft@sha256:9a4a5d5efa44a00c2215078ead39800d4aaa5a97908b94f45a64d7d506d6e14b";
11:
12: function getWorkspaceNames({
13: repositoryDirectory,
14: exists = existsSync,
15: execFile = execFileSync,
16: }) {
17: if (!exists(path.join(repositoryDirectory, ".craft.yml"))) {
18: return [];
19: }
20:
21: const output = execFile(
22: "docker",
23: [
24: "run",
25: "--rm",
26: "--volume",
27: `${path.resolve(repositoryDirectory)}:/github/workspace/__repo__`,
28: "--workdir",
29: "/github/workspace/__repo__",
30: ...
<path>/home/byk/Code/getsentry/publish/src/publish/inputs.js</path>
<type>file</type>
<content>
1: const github = require("@actions/github");
2: const core = require("@actions/core");
3: const { detailsFromContext } = require("../modules/details-from-context");
4:
5: async function inputs() {
6: const result = await detailsFromContext({
7: context: github.context,
8: });
9: core.setOutput("result", JSON.stringify(result));
10: }
11: inputs();
(End of file - total 11 lines)
</content>
<path>/home/byk/Code/getsentry/publish/src/publish/post-result.js</path>
<type>file</type>
<content>
1: const processEndState = require("../modules/process-end-state.js");
2: const { getGitHubToken } = require("../libs/github.js");
3: const github = require("@actions/github");
4:
5: async function main({
6: context = github.context,
7: octokit = github.getOctokit(getGitHubToken()),
8: report = processEndState,
9: status = process.argv.slice(2)[0],
10: } = {}) {
11: let inputs = {};
12:
13: if (process.env.PUBLISH_ARGS) {
14: try {
15: inputs = JSON.parse(process.env.PUBLISH_ARGS);
16: } catch {
17: console.warn("Could not parse publish inputs; reporting without them");
18: }
19: }
20:
21: await report({ context, octokit, inputs, status });
22: }
23:
24: if (require.main === module) {
25: main().catch((error) => {
26: console.error(error);
27: process.exitCode = 1;
28: });
29: }
30:
31: module.exports = { main };
(End of file - ...
<path>/home/byk/Code/getsentry/publish/src/publish/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/resolve-ci-poller-input.js</path>
<type>file</type>
<content>
1: const { getCiPollerInput } = require("../modules/ci-poller-input");
2: const { readFileSync } = require("fs");
3:
4: const issueBody = process.env.PUBLISH_ISSUE_BODY_FILE
5: ? readFileSync(process.env.PUBLISH_ISSUE_BODY_FILE, "utf8")
6: : process.env.PUBLISH_ISSUE_BODY || "";
7:
8: process.stdout.write(
9: JSON.stringify(
10: getCiPollerInput({
11: issueBody,
12: labels: JSON.parse(process.env.PUBLISH_ISSUE_LABELS || ""),
13: title: process.env.PUBLISH_TITLE || "",
14: revision: process.env.PUBLISH_REVISION || "",
15: })
16: )
17: );
(End of file - total 17 lines)
</content>
<path>/home/byk/Code/getsentry/publish/src/publish/resolve-release-revision.js</path>
<type>file</type>
<content>
1: const core = require("@actions/core");
2: const { getReleaseRevision } = require("../modules/release-revision");
3:
4: function resolveReleaseRevision() {
5: const { repo } = JSON.parse(process.env.PUBLISH_ARGS || "{}");
6: if (!repo) {
7: throw new Error("Publish input must define a repository.");
8: }
9:
10: core.setOutput(
11: "revision",
12: getReleaseRevision({
13: issueBody: process.env.PUBLISH_ISSUE_BODY || "",
14: repo,
15: })
16: );
17: }
18:
19: resolveReleaseRevision();
(End of file - total 19 lines)
</content>
<path>/home/byk/Code/getsentry/publish/src/publish/update-issue.js</path>
<type>file</type>
<content>
1: const { updateIssue } = require("../modules/update-issue.js");
2: const { getGitHubToken } = require("../libs/github");
3: const github = require("@actions/github");
4:
5: async function main() {
6: const context = github.context;
7: const octokit = github.getOctokit(getGitHubToken());
8: let inputs;
9:
10: if (process.env.PUBLISH_ARGS) {
11: try {
12: inputs = JSON.parse(process.env.PUBLISH_ARGS);
13: } catch {
14: console.warn("Could not parse publish inputs; skipping target update");
15: }
16: }
17:
18: await updateIssue({ context, octokit, inputs });
19: }
20:
21: if (require.main === module) {
22: main().catch((error) => {
23: console.error(error);
24: process.exitCode = 1;
25: });
26: }
27:
28: module.exports = { main };
(End of file - total 28 lines)
</content>
<path>/home/byk/Code/getsentry/publish/src/libs/github.js</path>
<type>file</type>
<content>
1: /**
2: * This is a helper function that throws a useful error message if the
3: * workflow environment is not configured correctly.
4: *
5: * @returns string
6: */
7: function getGitHubToken() {
8: if (!process.env.GITHUB_TOKEN) {
9: throw new Error(
10: 'No "GITHUB_TOKEN" environment variable found. ' +
11: "Please ensure the workflow is configured correctly"
12: );
13: }
14: return process.env.GITHUB_TOKEN;
15: }
16:
17: module.exports = {
18: getGitHubToken,
19: };
(End of file - total 19 lines)
</content>
<path>/home/byk/Code/getsentry/publish/src/modules/publish-issue-validation.js</path>
<type>file</type>
<content>
1: function isPublishRepository(name) {
2: return (
3: /^[A-Za-z0-9_.-]+$/.test(name) &&
4: name !== "." &&
5: name !== ".." &&
6: name !== "__proto__" &&
7: !name.startsWith("-")
8: );
9: }
10:
11: function isReleaseVersion(version) {
12: return /^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-?([0-9a-z-]+(?:\.[0-9a-z-]+)*))?(?:\+([0-9a-z-]+(?:\.[0-9a-z-]+)*))?$/i.test(
13: version
14: );
15: }
16:
17: module.exports = { isPublishRepository, isReleaseVersion };
(End of file - total 17 lines)
</content>
<path>/home/byk/Code/getsentry/publish/src/modules/publish-issue-title.js</path>
<type>file</type>
<content>
1: /* eslint-disable */
2: // @generated by Peggy 5.1.0.
3: //
4: // https://peggyjs.org/
5:
6: "use strict";
7:
8: class peg$SyntaxError extends SyntaxError {
9: constructor(message, expected, found, location) {
10: super(message);
11: this.expected = expected;
12: this.found = found;
13: this.location = location;
14: this.name = "SyntaxError";
15: }
16:
17: format(sources) {
18: let str = "Error: " + this.message;
19: if (this.location) {
20: let src = null;
21: const st = sources.find((s) => s.source === this.location.source);
22: if (st) {
23: src = st.text.split(/\r\n|\n|\r/g);
24: }
25: const s = this.location.start;
26: const offset_s =
27: this.location.source &&
28: typeof this.location.source.offset === "function"
29: ? this.location.source.offset(s)
30: ...
<path>/home/byk/Code/getsentry/publish/src/modules/update-issue.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3: TARGETS_SECTION_PARSER_REGEX,
4: TARGETS_PARSER_REGEX,
5: } = require("./details-from-context");
6:
7: async function updateTargets({
8: octokit,
9: stateFilePath,
10: publishRepo,
11: issue_number,
12: }) {
13: if (!stateFilePath || !fs.existsSync(stateFilePath)) {
14: return;
15: }
16:
17: const issueRequest = octokit.rest.issues.get({
18: ...publishRepo,
19: issue_number,
20: });
21:
22: const craftStateRequest = fs.promises
23: .readFile(stateFilePath, { encoding: "utf-8" })
24: .then((data) => JSON.parse(data));
25:
26: const [{ data: issue }, craftState] = await Promise.all([
27: issueRequest,
28: craftStateRequest,
29: ]);
30:
31: const newIssueBody = transformIssueBody(craftState, issue.body);
32:
33: await octokit.rest.issues.update({
34: ...publishRepo,
35: ...
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/approval-attestation.js</path>
<type>file</type>
<content>
1: import { describe, expect, test } from "vitest";
2:
3: const {
4: createApprovalAttestation,
5: createCiReadyAttestation,
6: currentAcceptedEvent,
7: hasApprovalAttestation,
8: hasCiReadyAttestation,
9: parseApprovalAttestation,
10: requestDigest,
11: } = require("../approval-attestation.js");
12:
13: function issue(title, { body = "", dryRun = false } = {}) {
14: return {
15: body,
16: labels: dryRun ? [{ name: "dry-run" }] : [],
17: title,
18: };
19: }
20:
21: describe("approval attestations", () => {
22: test("matches the latest accepted event to a trusted attestation", () => {
23: const title = "publish: getsentry/relay/py@1.2.3";
24: const publishIssue = issue(title);
25: const attestation = createApprovalAttestation({
26: actor: "contractor",
27: eventId: "200",
28: issue: publishIssue,
29: ...
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/approval-authorizer.js</path>
<type>file</type>
<content>
1: import { describe, expect, test, vi } from "vitest";
2:
3: const { authorizeApproval } = require("../approval-authorizer.js");
4:
5: const issueTitle = "publish: getsentry/sentry-javascript@10.0.0";
6:
7: describe("authorizeApproval", () => {
8: test.each(["getsantry[bot]", "sentry-release-bot[bot]"])(
9: "rejects direct approval by %s without a target repository lookup",
10: async (actor) => {
11: const getPermission = vi.fn();
12:
13: await expect(
14: authorizeApproval({
15: actor,
16: issueTitle,
17: getPermission,
18: autoApprovedRepositories: new Set(["getsentry/sentry-javascript"]),
19: })
20: ).resolves.toEqual({
21: authorized: false,
22: repository: "getsentry/sentry-javascript",
23: });
24:
25: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/authorize-approval.js</path>
<type>file</type>
<content>
1: import { afterEach, describe, expect, test, vi } from "vitest";
2: import fs from "fs";
3: const {
4: createAutoApprovalAttestation,
5: requestDigest,
6: } = require("../../modules/approval-attestation.js");
7: const {
8: getAutoApprovedRepositories,
9: main,
10: } = require("../authorize-approval.js");
11:
12: const originalEnvironment = { ...process.env };
13:
14: afterEach(() => {
15: process.env = { ...originalEnvironment };
16: vi.restoreAllMocks();
17: vi.resetModules();
18: });
19:
20: function jsonResponse(json) {
21: return { ok: true, json: vi.fn().mockResolvedValue(json) };
22: }
23:
24: async function runAuthorization({ actor, issueTitle, responses }) {
25: process.env.GITHUB_OUTPUT = "/tmp/github-output";
26: process.env.APPROVAL_TOKEN = "release-bot-token";
27: process.env.APPROVAL_ACTOR = actor;
28: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/validate-approval-attestation.js</path>
<type>file</type>
<content>
1: import { afterEach, describe, expect, test, vi } from "vitest";
2:
3: const {
4: validateApprovalAttestation,
5: } = require("../validate-approval-attestation.js");
6: const {
7: createApprovalAttestation,
8: createCiReadyAttestation,
9: requestDigest,
10: } = require("../../modules/approval-attestation.js");
11:
12: afterEach(() => {
13: vi.restoreAllMocks();
14: });
15:
16: function jsonResponse(json) {
17: return { ok: true, json: vi.fn().mockResolvedValue(json) };
18: }
19:
20: function issue(
21: title,
22: { body = "", labels = [{ name: "accepted" }], state = "open" } = {}
23: ) {
24: return { body, labels, state, title };
25: }
26:
27: describe("validateApprovalAttestation", () => {
28: test("accepts the current accepted event and its trusted attestation", async () => {
29: const title = "publish: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/record-auto-approval-attestation.js</path>
<type>file</type>
<content>
1: import { describe, expect, test, vi } from "vitest";
2:
3: const {
4: parseAutoApprovalAttestation,
5: requestDigest,
6: } = require("../../modules/approval-attestation.js");
7: const {
8: recordAutoApprovalAttestation,
9: } = require("../record-auto-approval-attestation.js");
10:
11: describe("recordAutoApprovalAttestation", () => {
12: test("binds an automated requester to the live publish request", async () => {
13: const title = "publish: getsentry/sentry-javascript@10.0.0";
14:
15: await expect(
16: recordAutoApprovalAttestation({
17: autoApprover: "getsantry[bot]",
18: expectedRequestDigest: requestDigest({
19: body: "Merge target: main",
20: labels: [],
21: title,
22: }),
23: getIssue: vi.fn().mockResolvedValue({
24: body: "Merge target: main",
25: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/record-ci-ready-attestation.js</path>
<type>file</type>
<content>
1: import { describe, expect, test, vi } from "vitest";
2:
3: const {
4: parseCiReadyAttestation,
5: requestDigest,
6: } = require("../../modules/approval-attestation.js");
7: const {
8: recordCiReadyAttestation,
9: } = require("../record-ci-ready-attestation.js");
10:
11: describe("recordCiReadyAttestation", () => {
12: test("binds the current approval to the app that will add ci-ready", async () => {
13: const title = "publish: getsentry/sentry-javascript@10.0.0";
14:
15: await expect(
16: recordCiReadyAttestation({
17: expectedRequestDigest: requestDigest({
18: body: "Merge target: main",
19: labels: [{ name: "accepted" }],
20: title,
21: }),
22: getAuthenticatedLogin: vi
23: .fn()
24: .mockResolvedValue("sentry-internal-app[bot]"),
25: getIssue: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/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__/request-digest-from-event.js</path>
<type>file</type>
<content>
1: import { describe, expect, test } from "vitest";
2:
3: const { requestDigest } = require("../../modules/approval-attestation.js");
4: const { requestDigestFromEvent } = require("../request-digest-from-event.js");
5:
6: describe("requestDigestFromEvent", () => {
7: test("digests the exact issue event snapshot", () => {
8: const issue = {
9: body: "Merge target: main",
10: labels: [{ name: "accepted" }, { name: "dry-run" }],
11: title: "publish: getsentry/relay@1.2.3",
12: };
13:
14: expect(requestDigestFromEvent({ issue })).toBe(requestDigest(issue));
15: });
16:
17: test("rejects events without a complete issue snapshot", () => {
18: expect(() => requestDigestFromEvent({})).toThrow(
19: "The GitHub event has no issue snapshot"
20: );
21: expect(() =>
22: requestDigestFromEvent({ issue: { body: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/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 + ...
Found 18 matches /home/byk/Code/getsentry/publish/.github/workflows/test.yml: Line 19: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 Line 20: - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 Line 23: - uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 /home/byk/Code/getsentry/publish/.github/workflows/publish.yml: Line 36: uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 Line 42: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 Line 56: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 Line 167: uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 Line 220: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 Line 256: uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 Line 284: uses: ...
No files found
/home/byk/Code/getsentry/publish/.github/workflows/test.yml /home/byk/Code/getsentry/publish/.github/workflows/publish.yml /home/byk/Code/getsentry/publish/.github/workflows/cocoapods-keepalive.yml /home/byk/Code/getsentry/publish/.github/workflows/ci-poller.yml /home/byk/Code/getsentry/publish/.github/workflows/auto-approve.yml /home/byk/Code/getsentry/publish/.github/workflows/ci-poller-dispatch.yml
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/ci-poller-workflow.js</path>
<type>file</type>
<content>
1: import {
2: mkdtempSync,
3: mkdirSync,
4: readFileSync,
5: readdirSync,
6: rmSync,
7: writeFileSync,
8: } from "fs";
9: import { tmpdir } from "os";
10: import { join } from "path";
11: import { spawnSync } from "child_process";
12:
13: import { afterEach, expect, test } from "vitest";
14:
15: const temporaryDirectories = [];
16: const INITIAL_REVISION = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
17: const UPDATED_REVISION = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
18:
19: function getPollerScript() {
20: const workflow = readFileSync(
21: join(__dirname, "../../../.github/workflows/ci-poller.yml"),
22: "utf8"
23: );
24: const section = workflow.match(
25: / {6}- name: Check CI status for ci-pending issues[\s\S]*? {8}run: \|\n(?<script>[\s\S]*?)\n {6}- name: Check for remaining pending issues/
26: );
27:
28: if ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/ci-poller-workflow.js</path>
<type>file</type>
<content>
1: import { readFileSync } from "fs";
2: import { describe, expect, test } from "vitest";
3:
4: const workflow = readFileSync(".github/workflows/ci-poller.yml", "utf8");
5: const manualDispatchWorkflow = readFileSync(
6: ".github/workflows/ci-poller-dispatch.yml",
7: "utf8"
8: );
9:
10: describe("CI poller workflow", () => {
11: test("checks out trusted code before requesting app tokens", () => {
12: expect(workflow).toMatch(
13: /- name: Get publish code\n\s+uses: actions\/checkout@[a-f0-9]{40}\n\s+with:\n(?:\s+#.*\n)*\s+ref: \$\{\{ github\.event\.repository\.default_branch \}\}/
14: );
15:
16: expect(workflow.indexOf("Get publish code")).toBeLessThan(
17: workflow.indexOf("Get auth token")
18: );
19: });
20:
21: test("uses a protected workflow dispatch relay for manual recovery", () => {
22: ...
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/publish-workflow.js</path>
<type>file</type>
<content>
1: import { spawnSync } from "child_process";
2: import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
3: import { tmpdir } from "os";
4: import { join } from "path";
5:
6: import { afterEach, expect, test } from "vitest";
7:
8: const { resolvePublishLocation } = require("../publish-location.js");
9:
10: const temporaryDirectories = [];
11:
12: function getWorkflow() {
13: return readFileSync(
14: join(__dirname, "../../../.github/workflows/publish.yml"),
15: "utf8"
16: );
17: }
18:
19: function getSetTargetsScript() {
20: const workflow = getWorkflow();
21: const section = workflow.match(
22: / {6}- name: Set targets[\s\S]*? {8}run: \|\n(?<script>[\s\S]*?)\n {6}- name: Revalidate approval attestation/
23: );
24: if (!section?.groups?.script) {
25: throw new Error("Missing Set targets workflow script");
26: }
27: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/publish-workflow.js</path>
<type>file</type>
<content>
1: import { readFileSync } from "fs";
2: import { describe, expect, test } from "vitest";
3:
4: const workflow = readFileSync(".github/workflows/publish.yml", "utf8");
5:
6: describe("publish workflow", () => {
7: test("binds approval and publication to the triggering issue snapshot", () => {
8: expect(workflow).toContain("node src/publish/request-digest-from-event.js");
9: expect(
10: workflow.match(
11: /EXPECTED_REQUEST_DIGEST: \$\{\{ steps\.request-digest\.outputs\.request_digest \}\}/g
12: )
13: ).toHaveLength(4);
14: });
15:
16: test("revalidates approval immediately before Craft", () => {
17: const fence = workflow.indexOf("Revalidate approval attestation");
18:
19: expect(fence).toBeGreaterThan(workflow.indexOf("Set targets"));
20: expect(fence).toBeLessThan(workflow.indexOf("Publish using Craft"));
21: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/post-result.js</path>
<type>file</type>
<content>
1: import { afterEach, describe, expect, test, vi } from "vitest";
2:
3: const { main } = require("../post-result.js");
4:
5: const originalEnvironment = { ...process.env };
6:
7: afterEach(() => {
8: process.env = { ...originalEnvironment };
9: });
10:
11: describe("post-result entry point", () => {
12: test.each([undefined, "not-json"])(
13: "reports terminal state when publish inputs are %s",
14: async (publishArgs) => {
15: const report = vi.fn().mockResolvedValue(undefined);
16: if (publishArgs === undefined) {
17: delete process.env.PUBLISH_ARGS;
18: } else {
19: process.env.PUBLISH_ARGS = publishArgs;
20: }
21:
22: await main({
23: context: {},
24: octokit: {},
25: report,
26: status: "failure",
27: });
28:
29: expect(report).toHaveBeenCalledWith(
30: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/auto-approval-workflow.js</path>
<type>file</type>
<content>
1: import { readFileSync } from "fs";
2: import { describe, expect, test } from "vitest";
3:
4: const workflow = readFileSync(".github/workflows/auto-approve.yml", "utf8");
5:
6: describe("auto-approval workflow", () => {
7: test("checks out the code used to record the attestation", () => {
8: const checkout = workflow.slice(
9: workflow.indexOf("uses: actions/checkout@"),
10: workflow.indexOf("Record automated approval attestation")
11: );
12:
13: expect(checkout).not.toContain("sparse-checkout:");
14: });
15:
16: test("records a request-bound attestation before adding accepted", () => {
17: expect(workflow).toContain("node src/publish/request-digest-from-event.js");
18: expect(workflow).toContain(
19: "EXPECTED_REQUEST_DIGEST: ${{ steps.request-digest.outputs.request_digest }}"
20: );
21: ...
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/ci-poller-input.js</path>
<type>file</type>
<content>
1: import { readFileSync } from "fs";
2: import { join } from "path";
3:
4: import { expect, test } from "vitest";
5:
6: const { getCiPollerInput } = require("../ci-poller-input.js");
7: const { requestDigest } = require("../approval-attestation.js");
8:
9: const REVISION = "7e5ca7ed5581552de066e2a8bc295b8306be38ac";
10: const labels = [{ name: "accepted" }, { name: "dry-run" }];
11: const issueBody = `Requested by: @byk
12:
13: Merge target: (default)
14:
15: Quick links:
16: - [View changes](https://github.com/getsentry/toolkit/compare/1.2.2...release/1.2.3)
17: - [View check runs](https://github.com/getsentry/toolkit/commit/${REVISION}/checks/)`;
18:
19: test("parses root and full-path publish titles", () => {
20: const rootTitle = "publish: getsentry/toolkit/cli@1.2.3";
21: expect(
22: getCiPollerInput({
23: issueBody,
24: labels,
25: ...
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/release-revision.js</path>
<type>file</type>
<content>
1: import { expect, test } from "vitest";
2:
3: const {
4: getReleaseRevision,
5: updateReleaseRevision,
6: } = require("../release-revision.js");
7:
8: const REVISION = "7e5ca7ed5581552de066e2a8bc295b8306be38ac";
9:
10: function requestBody(quickLinks) {
11: return `Requested by: @byk
12:
13: Merge target: (default)
14:
15: Quick links:
16: ${quickLinks}`;
17: }
18:
19: function canonicalQuickLinks(revision = REVISION) {
20: return `- [View changes](https://github.com/getsentry/toolkit/compare/1.2.2...release/1.2.3)
21: - [View check runs](https://github.com/getsentry/toolkit/commit/${revision}/checks/)`;
22: }
23:
24: test("gets the CI-approved revision from the request header", () => {
25: expect(
26: getReleaseRevision({
27: repo: "toolkit",
28: issueBody: requestBody(canonicalQuickLinks()),
29: })
30: ).toBe(REVISION);
31: ...
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/publish-location.js</path>
<type>file</type>
<content>
1: import { expect, test } from "vitest";
2:
3: const { resolvePublishLocation } = require("../publish-location.js");
4:
5: test("classifies an exact full workspace path without normalizing it", () => {
6: expect(
7: resolvePublishLocation({
8: path: "./packages/CLI",
9: workspaceNames: ["packages/cli", "packages/CLI"],
10: })
11: ).toStrictEqual({ path: ".", workspace: "packages/CLI" });
12: });
13:
14: test("keeps a non-workspace suffix as a checkout path", () => {
15: expect(
16: resolvePublishLocation({
17: path: "./packages",
18: workspaceNames: ["cli"],
19: })
20: ).toStrictEqual({ path: "./packages" });
21: });
22:
23: test("keeps a multi-segment suffix that is not an exact workspace path", () => {
24: expect(
25: resolvePublishLocation({
26: path: "./packages/cli",
27: workspaceNames: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/discover-location.js</path>
<type>file</type>
<content>
1: import { afterEach, expect, test, vi } from "vitest";
2:
3: const {
4: discoverLocation,
5: getWorkspaceNames,
6: } = require("../discover-location.js");
7:
8: afterEach(() => {
9: vi.restoreAllMocks();
10: });
11:
12: test("retains checkout-path behavior when the root config is absent", () => {
13: expect(
14: discoverLocation({
15: input: { path: "./packages/cli" },
16: repositoryDirectory: "__repo__",
17: exists: () => false,
18: })
19: ).toEqual({ path: "./packages/cli" });
20: });
21:
22: test("discovers exact workspace paths using the released Craft image", () => {
23: const execFile = vi.fn(() => '["packages/CLI"]');
24:
25: expect(
26: discoverLocation({
27: input: { path: "./packages/CLI" },
28: repositoryDirectory: "__repo__",
29: exists: () => true,
30: execFile,
31: })
32: ...
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/details-from-context.js</path>
<type>file</type>
<content>
1: import { test, expect } from "vitest";
2:
3: const {
4: detailsFromContext,
5: parsePublishTitle,
6: } = require("../details-from-context.js");
7:
8: const inputsArgs = {
9: context: {
10: repo: { owner: "getsentry", repo: "publish" },
11: payload: {
12: issue: {
13: number: "223",
14: title: "publish: getsentry/sentry@21.3.1",
15: body: `Requested by: @BYK
16:
17: Merge target: custom-branch
18:
19: Quick links:
20: - [View changes](https://github.com/getsentry/sentry/compare/21.3.0...refs/heads/releases/21.3.1)
21: - [View check runs](https://github.com/getsentry/sentry/commit/7e5ca7ed5581552de066e2a8bc295b8306be38ac/checks/)
22:
23: Assign the **accepted** label to this issue to approve the release. ...
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/process-end-state.js</path>
<type>file</type>
<content>
1: import { vi, describe, test, expect } from "vitest";
2:
3: vi.mock("fs");
4:
5: const processEndState = require("../process-end-state.js");
6:
7: describe("publish failed", () => {
8: const failureArgs = {
9: status: "failure",
10: inputs: { repo: "sentry", version: "21.3.1" },
11: context: {
12: runId: "1234",
13: repo: { owner: "getsentry", repo: "publish" },
14: payload: { issue: { number: "211" } },
15: },
16: octokit: {
17: rest: {
18: actions: {
19: getWorkflowRun: async () => ({
20: data: {
21: html_url: "https://github.com/getsentry/sentry/actions/runs/1234",
22: },
23: }),
24: },
25: issues: {
26: createComment: vi.fn(),
27: },
28: },
29: },
30: Sentry: {
31: Scope: class Scope {
32: ...
<path>/home/byk/Code/getsentry/publish/src/modules/__tests__/update-issue.js</path>
<type>file</type>
<content>
1: import { vi, describe, test, expect, beforeAll, beforeEach, it } from "vitest";
2: import fs from "fs";
3:
4: const { updateIssue, transformIssueBody } = require("../update-issue.js");
5:
6: let mockExistsSync;
7:
8: const updateTargetsArgs = {
9: inputs: { repo: "sentry", version: "21.3.1" },
10: context: {
11: runId: "1234",
12: repo: { owner: "getsentry", repo: "publish" },
13: payload: { issue: { number: "211" } },
14: },
15: octokit: {
16: rest: {
17: actions: {
18: getWorkflowRun: async () => ({
19: data: {
20: html_url: "https://github.com/getsentry/sentry/actions/runs/1234",
21: },
22: }),
23: },
24: issues: {
25: get: vi.fn(),
26: update: vi.fn(),
27: removeLabel: vi.fn(),
28: },
29: },
30: },
31: Sentry: {
32: Scope: class Scope ...
<path>/home/byk/Code/getsentry/publish/src/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/publish/__tests__/resolve-release-revision.js</path>
<type>file</type>
<content>
1: import { spawnSync } from "child_process";
2: import { expect, test } from "vitest";
3:
4: const script = new URL("../resolve-release-revision.js", import.meta.url)
5: .pathname;
6:
7: test("rejects missing workflow input through release revision validation", () => {
8: const result = spawnSync(process.execPath, [script], {
9: encoding: "utf8",
10: env: {
11: ...process.env,
12: PUBLISH_ARGS: "",
13: PUBLISH_ISSUE_BODY: "",
14: },
15: });
16:
17: expect(result.status).not.toBe(0);
18: expect(result.stderr).toContain("Publish input must define a repository.");
19: expect(result.stderr).not.toContain("Unexpected end of JSON input");
20: });
(End of file - total 20 lines)
</content>
<path>/home/byk/Code/getsentry/publish/README.md</path>
<type>file</type>
<content>
1: # Sentry Publish 🏠
2:
3: This is a meta/control repository that implements the [Central Publish Repository](docs/rfc.md) RFC
4:
5: ## Quick Start
6:
7: [craft quick start](https://craft.sentry.dev/github-actions/)
8:
9: ## Release Flow
10:
11: ```mermaid
12: flowchart TD
13: A[Developer triggers release workflow] --> B["SDK Repo: craft prepare"]
14: B --> C[Build artifacts & create release branch]
15: C --> D[Upload artifacts to GitHub]
16: D --> E["Create issue in getsentry/publish"]
17: E --> F{Release Manager Review}
18: F -->|"Add 'accepted' label"| G[Publish workflow triggers]
19: G --> I[Download artifacts from GitHub]
20: I --> J["craft publish to registries"]
21: J --> K{Publish successful?}
22: K -->|Yes| L[Issue closed - success]
23: K -->|No| M[Issue updated with failure]
24: ```
25:
26: ## Goals
27:
28: 1. ...
<path>/home/byk/Code/getsentry/publish/docs/key_rotation_vault_changes.md</path> <type>file</type> <content> 1: # Key Rotation & Vault Changes 2: 3: Currently there are three vaults that contain secrets: 4: 5: - SDKs/OSS: both available to a range of employees, some of which departed 6: - Shared: available to all engineers, some of which departed 7: - Ops: available to operations, none of which departed 8: 9: ## Goal 10: 11: We do not want employees to publish through their own accounts, but we also do not want them to have access to the global credentials. We have enabled the first part of this process as the preparation of releases no longer requires credentials when [Craft](https://github.com/getsentry/craft) is used. 12: 13: The actual publish via Craft however still requires access to the credentials. ...
<path>/home/byk/Code/getsentry/publish/docs/rfc.md</path> <type>file</type> <content> 1: # [RFC] Central Publish Repository 2: 3: This design document is written to address the concerns mentioned in [Key Rotation & Vault Changes](key_rotation_vault_changes.md). Implementation pending feedback from engineering team members, specifically the SDK team. 4: 5: # Problem Statement 6: 7: 1. We need to be able to quickly recycle all publishing tokens (NPM, PyPI, Cargo, Docker, etc.) at will without disrupting our releases or our developer flow 8: 2. We need to limit the publishing tokens to authorized personnel only and not leak them in any way 9: 1. GitHub secrets is not enough as it lets anyone with write access to the repo to access the secrets and scuttle them out, even in a brand new PR. Example: [https://github.com/getsentry/sentry/pull/21930](https://github.com/getsentry/sentry/pull/21930) 10: 3. ...
<path>/home/byk/Code/getsentry/publish/AGENTS.md</path> <type>file</type> <content> 1: # AGENTS.md - Coding Agent Guidelines 2: 3: This document provides guidelines for AI coding agents working in this repository. 4: 5: ## Project Overview 6: 7: This is the **Sentry Publish Repository** - a GitHub Actions-based approval system for publishing releases across multiple Sentry projects using [Craft](https://github.com/getsentry/craft). It's a pure JavaScript (Node.js 24.0.0) project using CommonJS modules. 8: 9: ## Build/Lint/Test Commands 10: 11: ### Package Manager 12: 13: - **Yarn 1.22.22** (classic) - Do NOT use npm 14: - Version management via Volta (pinned in package.json) 15: 16: ### Commands 17: 18: ```bash 19: # Install dependencies 20: yarn install 21: 22: # Run all tests 23: yarn test 24: 25: # Run tests in watch mode 26: yarn test:watch 27: 28: # Run a single test file 29: yarn test src/modules/__tests__/update-issue.js 30: 31: # Run tests matching a pattern 32: ...
<path>/home/byk/Code/getsentry/publish/.lore.md</path> <type>file</type> <content> 1: <!-- Managed by lore (https://github.com/BYK/loreai) — manual edits are imported on next session. --> 2: 3: ## Long-term Knowledge 4: 5: ### Architecture 6: 7: <!-- lore:01a043d7-502d-7de9-9416-66f3050c12b7 --> 8: * **approval-authorizer GitHub roles**: Chose live GitHub collaborator permission checks over private \`security-as-code\` because Publish is public and GitHub reflects effective target-repository access without exposing RBAC. Humans authorize only with \`write\`, \`maintain\`, or \`admin\`; \`triage\` never authorizes. \`getsantry\[bot]\` must never fall through to collaborator lookup: it authorizes only when its exact parsed repository/release path is in \`auto-approve-repos.txt\`, because bot repository access does not prove the event followed an allowlisted auto-approval path. 9: 10: <!-- lore:01a082db-d8b9-7438-927a-07bf7aa5e369 --> 11: * **production environment secret boundary**: ...
<path>/home/byk/Code/getsentry/publish/src/publish/request-digest-from-event.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const { requestDigest } = require("../modules/approval-attestation.js");
3:
4: function requestDigestFromEvent(event) {
5: if (!event || typeof event.issue !== "object" || event.issue === null) {
6: throw new Error("The GitHub event has no issue snapshot");
7: }
8:
9: return requestDigest(event.issue);
10: }
11:
12: function main() {
13: if (!process.env.GITHUB_EVENT_PATH) {
14: throw new Error('No "GITHUB_EVENT_PATH" environment variable found');
15: }
16:
17: if (!process.env.GITHUB_OUTPUT) {
18: throw new Error('No "GITHUB_OUTPUT" environment variable found');
19: }
20:
21: const event = JSON.parse(
22: fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8")
23: );
24: fs.appendFileSync(
25: process.env.GITHUB_OUTPUT,
26: `request_digest=${requestDigestFromEvent(event)}\n`
27: );
28: ...
diff --git a/.github/workflows/auto-approve.yml b/.github/workflows/auto-approve.yml
index d9763c7..5f86245 100644
--- a/.github/workflows/auto-approve.yml
+++ b/.github/workflows/auto-approve.yml
@@ -5,25 +5,47 @@ 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
+ persist-credentials: false
- name: Get auth token
id: token
- uses: actions/create-github-app-token@v3
+ uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1
with:
...
diff --git a/.github/workflows/ci-poller.yml b/.github/workflows/ci-poller.yml
index 487e9da..92ce6f0 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,46 @@ 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 == ...
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 2635ea6..88a4030 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -32,13 +32,90 @@ 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 }}
+ - name: Bind request snapshot
+ id: request-digest
+ run: node src/publish/request-digest-from-event.js
+
+ # The release bot sees all target repositories. ...
diff --git a/src/modules/ci-poller-input.js b/src/modules/ci-poller-input.js
index ca08a72..bf2d23d 100644
--- a/src/modules/ci-poller-input.js
+++ b/src/modules/ci-poller-input.js
@@ -8,8 +8,9 @@ const {
isReleaseVersion,
} = require("./publish-issue-validation");
const { isPublishPath } = require("./publish-location");
+const { requestDigest } = require("./approval-attestation");
-function getCiPollerInput({ title, issueBody, revision }) {
+function getCiPollerInput({ title, issueBody, labels, revision }) {
let parsedTitle;
try {
parsedTitle = parsePublishIssueTitle(title);
@@ -28,12 +29,14 @@ function getCiPollerInput({ title, issueBody, revision }) {
throw new Error(`Invalid publish issue path: '${path}'`);
}
const currentRevision = getReleaseRevision({ issueBody, repo });
+ const resolvedIssueBody = revision
+ ? updateReleaseRevision({ issueBody, repo, revision })
+ : issueBody;
return {
- ...(revision
- ? ...
diff --git a/src/libs/github.js b/src/libs/github.js
index e4d506f..f1188c1 100644
--- a/src/libs/github.js
+++ b/src/libs/github.js
@@ -6,8 +6,10 @@
*/
function getGitHubToken() {
if (!process.env.GITHUB_TOKEN) {
- throw new Error('No "GITHUB_TOKEN" environment variable found. ' +
- 'Please ensure the workflow is configured correctly');
+ throw new Error(
+ 'No "GITHUB_TOKEN" environment variable found. ' +
+ "Please ensure the workflow is configured correctly"
+ );
}
return process.env.GITHUB_TOKEN;
}
diff --git a/src/modules/post-workflow-details.js b/src/modules/post-workflow-details.js
index 278d23e..a02534a 100644
--- a/src/modules/post-workflow-details.js
+++ b/src/modules/post-workflow-details.js
@@ -1,4 +1,4 @@
-async function postWorkflowDetails({context, octokit}) {
+async function postWorkflowDetails({ context, octokit }) {
const { repo: publishRepo, runId: run_id } = context;
const { number: issue_number } = ...
diff --git a/src/modules/__tests__/process-end-state.js b/src/modules/__tests__/process-end-state.js
index 940f2d7..f983bfb 100644
--- a/src/modules/__tests__/process-end-state.js
+++ b/src/modules/__tests__/process-end-state.js
@@ -171,6 +171,33 @@ describe("publish success", () => {
}
`);
});
+
+ test("closes the issue before workflow lookup and comments", async () => {
+ const update = vi.fn();
+ const getWorkflowRun = vi.fn().mockRejectedValue(new Error("unavailable"));
+
+ await expect(
+ processEndState({
+ ...successArgs,
+ octokit: {
+ rest: {
+ actions: { getWorkflowRun },
+ issues: { createComment: vi.fn(), update },
+ },
+ },
+ })
+ ).rejects.toThrow("unavailable");
+
+ expect(update).toHaveBeenCalledWith({
+ issue_number: "211",
+ owner: "getsentry",
+ repo: "publish",
+ state: "closed",
+ });
+ ...
diff --git a/AGENTS.md b/AGENTS.md index cf473df..51bf3a8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -273,3 +273,9 @@ This codebase interacts heavily with GitHub Actions. Entry points in `src/publis ## Code Owners All code is owned by `@getsentry/releng` (Release Engineering team). + +<!-- This section is maintained by the coding agent via lore (https://github.com/BYK/loreai) --> +## Long-term Knowledge + +For long-term knowledge entries managed by [lore](https://github.com/BYK/loreai) (gotchas, patterns, decisions, architecture), see [`.lore.md`](.lore.md) in the project root. +<!-- End lore-managed section --> diff --git a/README.md b/README.md index 4cbc9b9..82bc3f5 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,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 ...
<path>/home/byk/Code/getsentry/publish/README.md</path> <type>file</type> <content> 88: ## Approvals 89: 90: Packages we release into the wider world that our customers install, require an explicit approval. This for instance applies to 91: `sentry-cli`, our SDKs or the `symbolicator` distributed utilities. Internal dependencies such as `arroyo` can be published 92: with an auto approval. The reasoning here is that the bump of the dependency requires an explicit approval again in Sentry 93: proper. In theory if an independent package gets sufficient independent use of Sentry we might want to reconsider an auto 94: approval process for such package as it might become an interesting target for an attacker. 95: 96: Automatic approvals are managed in the [`auto-approve.yml`](https://github.com/getsentry/publish/blob/main/.github/workflows/auto-approve.yml) workflow. 97: 98: ## Under the hood 99: 100: The system uses [Craft](https://github.com/getsentry/craft) under the hood to prepare ...
<path>/home/byk/Code/getsentry/publish/package.json</path>
<type>file</type>
<content>
1: {
2: "name": "publish",
3: "private": true,
4: "version": "0.0.1",
5: "description": "Approval-based publishing system for Sentry",
6: "main": "index.js",
7: "repository": "git@github.com:getsentry/publish.git",
8: "author": "Sentry Open Source <oss@sentry.io>",
9: "license": "Apache-2.0",
10: "volta": {
11: "node": "24.0.0",
12: "yarn": "1.22.22"
13: },
14: "resolutions": {
15: "undici": "^6.23.0"
16: },
17: "scripts": {
18: "generate": "node scripts/generate-publish-issue-title-parser.js",
19: "check:generated": "node scripts/generate-publish-issue-title-parser.js --check",
20: "test": "yarn check:generated && vitest run",
21: "test:watch": "vitest",
22: "lint": "eslint src .github --ignore-pattern '!.github'",
23: "prettier": "prettier --write src"
24: },
25: "devDependencies": {
26: "eslint": "^8.9.0",
27: ...
Found 2 matches /home/byk/Code/getsentry/publish/src/publish/validate-approval-attestation.js: Line 14: "X-GitHub-Api-Version": "2026-03-10", /home/byk/Code/getsentry/publish/src/publish/authorize-approval.js: Line 45: "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. ...
# Events that trigger workflows You can configure your workflows to run when specific activity on GitHub happens, at a scheduled time, or when an event outside of GitHub occurs. ## About events that trigger workflows Workflow triggers are events that cause a workflow to run. For more information about how to use workflow triggers, see [Triggering a workflow](/en/actions/how-tos/write-workflows/choose-when-workflows-run/trigger-a-workflow). Some events have multiple activity types. For these events, you can specify which activity types will trigger a workflow run. For more information about what each activity type means, see [Webhook events and payloads](/en/webhooks/webhook-events-and-payloads). > \[!NOTE] > Not all webhook events trigger workflows. Like GitHub Actions workflows, agentic workflows can be triggered by repository events and schedules. ...
<path>/home/byk/Code/getsentry/publish/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: ...
# 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 $ yarn check:generated && vitest run --printConsoleTrace src/modules/__tests__/approval-attestation.js src/modules/__tests__/approval-authorizer.js src/modules/__tests__/ci-poller-input.js src/modules/__tests__/ci-poller-workflow.js src/modules/__tests__/details-from-context.js src/modules/__tests__/publish-location.js src/modules/__tests__/publish-workflow.js src/modules/__tests__/release-revision.js src/publish/__tests__/authorize-approval.js src/publish/__tests__/auto-approval-workflow.js src/publish/__tests__/ci-poller-workflow.js src/publish/__tests__/current-accepted-event.js src/publish/__tests__/discover-location.js src/publish/__tests__/publish-workflow.js src/publish/__tests__/record-auto-approval-attestation.js src/publish/__tests__/record-ci-ready-attestation.js src/publish/__tests__/request-digest-from-event.js src/publish/__tests__/validate-approval-attestation.js src/publish/__tests__/workflow-action-pinning.js $ node ...
🔴 (12:26) Test command completed successfully in `/home/byk/Code/getsentry/publish`: `yarn check:generated && vitest run --printConsoleTrace src/modules/__tests__/approval-attestation.js src/modules/__tests__/approval-authorizer.js src/modules/__tests__/ci-poller-input.js src/modules/__tests__/ci-poller-workflow.js src/modules/__tests__/details-from-context.js src/modules/__tests__/publish-locat…
🔴 (12:16) GitHub Actions workflow triggers can originate from repository activity, schedules, or events outside GitHub; not all webhook events trigger workflows, and events with multiple activity types can be filtered by activity type. 🔴 (12:16) The `branch_protection_rule` workflow trigger supports activity types `created`, `edited`, and `deleted`; `GITHUB_SHA` is the last commit on the defaul…
🔴 (12:14) `/home/byk/Code/getsentry/publish/package.json` defines the private package `publish` version `0.0.1`, described as “Approval-based publishing system for Sentry,” with entry point `index.js`, repository `git@github.com:getsentry/publish.git`, author `Sentry Open Source <oss@sentry.io>`, and license `Apache-2.0`. 🔴 (12:14) `/home/byk/Code/getsentry/publish/package.json` pins Volta tool…
🔴 (12:11) `README.md` approval policy requires explicit approval for customer-installed public releases such as `sentry-cli`, SDKs, and `symbolicator` utilities; internal dependencies such as `arroyo` may receive automatic approval because their later dependency bump in Sentry proper requires another explicit approval. Automatic approvals are managed by `.github/workflows/auto-approve.yml`. 🔴 (…
🔴 (12:08) Working tree contains new `src/publish/request-digest-from-event.js`: `requestDigestFromEvent(event)` rejects events without an `issue` object, computes `requestDigest(event.issue)` via `src/modules/approval-attestation.js`, and `main()` reads JSON from `GITHUB_EVENT_PATH` and appends `request_digest=…` to `GITHUB_OUTPUT`; missing environment variables fail closed. 🔴 (12:08) `.github/…
🔴 (12:07) User requires the CI poller’s deliberate `ci-ready` transition to remain intact: after CI succeeds, remove any stale `ci-ready` label, revalidate the exact approved release snapshot, and always add `ci-ready` again so a fresh label event triggers publishing; adding the label must never be treated as an idempotent optimization. 🔴 (12:07) User requires the CI poller to revoke or stop pr…
🔴 (12:04) `/home/byk/Code/getsentry/publish/src/modules/__tests__/process-end-state.js` tests `processEndState()` for statuses `"failure"`, `"cancelled"`, `"success"`, and undefined using repository `sentry`, version `21.3.1`, run ID `"1234"`, and publish issue `"211"`. 🔴 (12:04) For status `"failure"`, `processEndState()` creates exactly 1 issue comment beginning `Failed to publish.` with run …
🔴 (12:01) `/home/byk/Code/getsentry/publish/src/publish/__tests__/ci-poller-workflow.js` reads `.github/workflows/ci-poller.yml` and `.github/workflows/ci-poller-dispatch.yml` and tests the CI poller workflow. 🔴 (12:01) The CI poller must check out trusted code at `github.event.repository.default_branch` in `"Get publish code"` before `"Get auth token"`. 🔴 (12:01) The CI poller uses `repositor…
🔴 (11:57) `src/modules/__tests__/approval-authorizer.js` contains tests for `authorizeApproval()` using issue title `publish: getsentry/sentry-javascript@10.0.0`. 🔴 (11:57) `authorizeApproval()` rejects direct approval by actors `"getsantry[bot]"` and `"sentry-release-bot[bot]"`, returns `{ authorized: false, repository: "getsentry/sentry-javascript" }`, and does not call `getPermission`, even …
🔴 (11:57) `updateTargets({ octokit, stateFilePath, publishRepo, issue_number })` in `src/modules/update-issue.js` returns without making API calls when `stateFilePath` is absent or `fs.existsSync(stateFilePath)` is false. 🔴 (11:57) When a state file exists, `updateTargets()` concurrently fetches the issue via `octokit.rest.issues.get({ ...publishRepo, issue_number })` and reads/parses the UTF-8…
🔴 (11:55) `src/publish/request-digest-from-event.js` defines `requestDigestFromEvent(event)`, which throws `The GitHub event has no issue snapshot` unless `event.issue` is a non-null object, then returns `requestDigest(event.issue)`. 🔴 (11:55) `main()` in `src/publish/request-digest-from-event.js` requires `GITHUB_EVENT_PATH` and `GITHUB_OUTPUT`, throwing `No "GITHUB_EVENT_PATH" environment var…
🔴 (11:51) `/home/byk/Code/getsentry/publish/.github/workflows/test.yml` defines workflow `Test`, triggered by pushes to `main` and all `pull_request` events, with `contents: read`, default `bash` shell, and one `unit-test` job named `unit tests` on `ubuntu-latest`. 🔴 (11:51) `.github/workflows/test.yml` runs ordered steps: 1. `actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803`; 2. `acti…
🔴 (11:50) User’s `.github/workflows/ci-poller-dispatch.yml` defines the manually triggered `Run CI Status Poller` workflow with `permissions: {}` and one `dispatch` job running on `ubuntu-latest` in the protected `production` environment; its only step runs `":"`. 🔴 (11:50) User stated repository secrets must never be used by `.github/workflows/ci-poller-dispatch.yml`; protected `production` en…
🔴 (11:50) User stated the CI poller always adds the `ci-ready` label.
🔴 (11:49) Repository was on branch `main`, tracking `origin/main`; `HEAD`, `ORIGIN_MAIN`, and `MERGE_BASE_HEAD_ORIGIN_MAIN` were all `7c60ddb7f43040fe8fbfea70efc833f689c04e75`, with `HEAD_TO_ORIGIN_MAIN_COUNTS 0 0`. 🔴 (11:49) Recent commit history included, in order: `7c60ddb feat: accept workspace publish requests`; `83d210b build(deps): bump js-yaml from 4.3.0 to 4.3.1 (#9174)` on `opencode/t…
🔴 (11:48) Repository root `/home/byk/Code/getsentry/publish` contained 16 entries: `.eslintrc.js`, `.git/`, `.github/`, `.gitignore`, `.lore.md`, `AGENTS.md`, `auto-approve-repos.txt`, `docs/`, `LICENSE`, `node_modules/`, `package.json`, `README.md`, `scripts/`, `src/`, `vitest.config.js`, `yarn.lock`. 🔴 (11:48) Initial repository fingerprint reported `HEAD 7c60ddb7f43040fe8fbfea70efc833f689c04…
🟡 (11:47) [requested-review] User requested a READ-ONLY, substantive adversarial security review of the exact current worktree at `/home/byk/Code/getsentry/publish` against base commit `7c60ddb7f43040fe8fbfea70efc833f689c04e75` (`origin/main`). 🔴 (11:47) [enforced-read-only-workflow] User prohibited editing, formatting, staging, stashing, committing, or otherwise mutating any file or repository…