Dashboard › publish › Session 15V7cMKYLoni
15V7cMKYLoniFN8SoPerform a fresh, independent, READ-ONLY correctness and security review of the exact current worktree at /home/byk/Code/getsentry/publish. Never edit files. You must return a NON-EMPTY final response.
Inspect every tracked and untracked changed file relevant to this publishing hardening. Verify with exact current file:line citations:
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 unless this diff worsens its exposure.
Run yarn test --printConsoleTrace, yarn lint, and git diff --check. Findings must come first, ordered by severity, each marked MUST-FIX, CONCERN, or PASS and backed by exact current file:line evidence. Then provide compact test evidence. ...
<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 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 | 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 +- 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__/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 ...
<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/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/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/.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/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/validate-approval-attestation.js</path>
<type>file</type>
<content>
1: const {
2: currentAcceptedEvent,
3: currentCiReadyEvent,
4: hasApprovalAttestation,
5: hasCiReadyAttestation,
6: } = require("../modules/approval-attestation.js");
7:
8: async function getGitHubResponse(path) {
9: return fetch(`https://api.github.com/${path}`, {
10: headers: {
11: Accept: "application/vnd.github+json",
12: Authorization: `Bearer ${process.env.APPROVAL_TOKEN}`,
13: "X-GitHub-Api-Version": "2026-03-10",
14: },
15: });
16: }
17:
18: async function getIssue({ repository, issueNumber }) {
19: const response = await getGitHubResponse(
20: `repos/${repository}/issues/${issueNumber}`
21: );
22:
23: if (!response.ok) {
24: throw new Error(
25: `Could not retrieve issue #${issueNumber}: GitHub returned ${response.status}`
26: );
27: }
28:
29: return response.json();
30: }
31:
32: async ...
<path>/home/byk/Code/getsentry/publish/src/publish/record-auto-approval-attestation.js</path>
<type>file</type>
<content>
1: const fs = require("fs");
2: const {
3: createAutoApprovalAttestation,
4: } = require("../modules/approval-attestation.js");
5: const { 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/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: ({ ...
Found 71 matches
/home/byk/Code/getsentry/publish/src/publish/record-auto-approval-attestation.js:
Line 21: throw new Error("The automated approval request changed before approval");
Line 50: `auto_approval_attestation=${attestation}\n`
/home/byk/Code/getsentry/publish/src/publish/record-ci-ready-attestation.js:
Line 44: const acceptedEvent = currentAcceptedEvent(events);
Line 50: !acceptedEvent ||
Line 52: (acceptedEvent.actor !== expectedAcceptedEvent.actor ||
Line 53: acceptedEvent.eventId !== expectedAcceptedEvent.eventId))
Line 55: throw new Error("The approval changed before CI could be marked ready");
Line 58: return createCiReadyAttestation({ acceptedEvent, ciReadyActor, issue });
/home/byk/Code/getsentry/publish/src/publish/validate-approval-attestation.js:
Line 94: acceptedEvent: event,
/home/byk/Code/getsentry/publish/src/publish/__tests__/record-auto-approval-attestation.js:
Line 8: } = ...
# 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. ...
name: "Create GitHub App Token"
description: "GitHub Action for creating a GitHub App installation access token"
author: "Gregor Martynus and Parker Brown"
branding:
icon: "lock"
color: "gray-dark"
inputs:
client-id:
description: "GitHub App Client ID"
required: false
app-id:
description: "GitHub App ID"
required: false
deprecationMessage: "Use 'client-id' instead."
private-key:
description: "GitHub App private key"
required: true
owner:
description: "The owner of the GitHub App installation (defaults to current repository owner)"
required: false
repositories:
description: "Comma or newline-separated list of repositories to grant the token access to (defaults to current repository if owner is unset)"
required: false
enterprise:
description: "The slug of the enterprise account where the GitHub App is installed (cannot be used with 'owner' or 'repositories')"
required: false
skip-token-revoke:
description: "If ...
{
"operationId": "users/get-authenticated",
"fineGrainedAccess": null,
"enabledForGitHubApps": false,
"githubCloudOnly": false
}
<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/modules/__tests__/approval-attestation.js</path>
<type>file</type>
<content>
1: import { describe, expect, test } from "vitest";
2:
3: const {
4: createApprovalAttestation,
5: createCiReadyAttestation,
6: currentAcceptedEvent,
7: hasApprovalAttestation,
8: hasCiReadyAttestation,
9: parseApprovalAttestation,
10: requestDigest,
11: } = require("../approval-attestation.js");
12:
13: function issue(title, { body = "", dryRun = false } = {}) {
14: return {
15: body,
16: labels: dryRun ? [{ name: "dry-run" }] : [],
17: title,
18: };
19: }
20:
21: describe("approval attestations", () => {
22: test("matches the latest accepted event to a trusted attestation", () => {
23: const title = "publish: getsentry/relay/py@1.2.3";
24: const publishIssue = issue(title);
25: const attestation = createApprovalAttestation({
26: actor: "contractor",
27: eventId: "200",
28: issue: publishIssue,
29: ...
<path>/home/byk/Code/getsentry/publish/src/publish/__tests__/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 = ...
Date: Sep 9, 2026 * 🟡 (23:39) `actions/create-github-app-token` metadata identifies the action as “Create GitHub App Token,” authored by Gregor Martynus and Parker Brown; it runs on `node24` with `dist/main.cjs` and post-step `dist/post.cjs`, and outputs `token`, `installation-id`, and `app-slug`. * 🟡 (23:39) `actions/create-github-app-token` requires `private-key`; supports `client-id`, deprec…
Date: Sep 9, 2026 * 🟡 (23:33) `.github/workflows/cocoapods-keepalive.yml` defines a daily `0 0 * * *` CocoaPods token keep-alive on `macos-15` in the `production` environment, with `contents: read` and `COCOAPODS_TRUNK_TOKEN`; it installs CocoaPods using unversioned `gem install cocoapods`, prints `pod --version`, then runs `pod trunk me > /dev/null 2>&1` (`.github/workflows/cocoapods-keepalive.…
Date: Sep 9, 2026 * 🔴 [requested-review] (23:26) User requested a fresh, independent, READ-ONLY correctness and security review of the exact current worktree at `/home/byk/Code/getsentry/publish`, covering every relevant tracked and untracked changed file. * 🔴 (23:26) User directed: “Never edit files.” * 🔴 (23:26) User directed: “Never return empty”; the review must produce a NON-EMPTY final r…