Dashboard › publish › Distillation
c183a7f6-444c-4ced-9526-42c4e4b93e51["lore_tm_v1_t3Igr2q2Ck7roJZVFwlYEnAesd4X8NOrdr6iO87KTJo","lore_tm_v1_LeYoHVeES8J74y0_xfa-4Z62kT_uk8c1Z2SJ_De9JBo","lore_tm_v1_JdmOEfR0P7tcRxqeQ0kHWZBjnp-8DAUXCkdsbzsADSs","lore_tm_v1_Nvtsm7SYBo35HQk3EmXQrfu4SJ7faZ9NMkkimLg_dEA","lore_tm_v1_mMzbdOPwD-2N6U9uayufMbJ6K9MFYesVWsRL_DckxvY","lore_tm_v1_Bi4FCvd1Ggf6WRDBL276DKwwoTiw679HMaTw4QRi0Lg","lore_tm_v1_Af-848mIn24Q8ZOFAkxx9xhekIg9Eb4zC4UVYRYgjIQ","lore_tm_v1_Kv1LyOacoppbw5DLd3ZaZxXaVhl0SSqpa4zF09dLt-Q","lore_tm_v1_AZTqLVIYemkasDq-fPmOt_Eth1Dm4JCJjAoT3rpuMMU","lore_tm_v1_duryAXCk_aqy-FlanZOMvMzDdJMd5cxtEHyxHTyaxhA","lore_tm_v1_LuWNkjDqzKz7XZUpfAwGMxGABSz4jmEowZ9cDGrSf0g","lore_tm_v1_zO5EYzgT9H-Y80IlS2QGugopTS1KrkbVKwnJ9Eo8z0U","lore_tm_v1__JnPqxC_YrkIDBqOAzG5BlFPOdq9QUbbResM4mf9fwQ","lore_tm_v1_oqQxyBf5szpoel3SqGQO1icJFnXLFaotHMs1lX-6Y2Q","lore_tm_v1_7Z1rXr1O-D3PRCEOdPuaPWtXOocBdY2Gk3jDZVGqSzg","lore_tm_v1_0__L1-nFrmWeMFa8eesdIjF7-Zf_3VyoQi5aA8tBWXI","lore_tm_v1_AEXkTi7iEDD5ZKmjlNN7uwSlWo-98VzkYculABC4hJs"]
Date: Aug 28, 2026
src/publish/authorize-approval.js (157 lines). getAutoApprovedRepositories() reads auto-approve-repos.txt as UTF-8, splits on /\r?\n/, filters blank lines, and returns a Set.authorize-approval.js GitHub API behavior: getGitHubResponse(path) uses fetch("https://api.github.com/${path}") with Accept: application/vnd.github+json, Authorization: Bearer ${process.env.APPROVAL_TOKEN}, and X-GitHub-Api-Version: 2026-03-10; getPermission() calls repos/{owner}/{repository}/collaborators/{username}/permission, while getIssue() calls repos/{repository}/issues/{issueNumber} and both throw with GitHub status on non-OK responses.authorize-approval.js pagination: getIssueEvents() fetches repos/{repository}/issues/{issueNumber}/events?per_page=100&page=${page} from page 1 onward, aggregates results, and returns when a page contains fewer than 100 events; non-OK responses throw.authorize-approval.js authorization/attestation flow: main() requires GITHUB_OUTPUT, APPROVAL_TOKEN, APPROVAL_ISSUE_NUMBER, and APPROVAL_ISSUE_REPOSITORY; it calls authorizeApproval() with APPROVAL_ACTOR, APPROVAL_ISSUE_TITLE, getPermission, and getAutoApprovedRepositories(). If unauthorized, it appends authorized=false; otherwise it concurrently retrieves issue title and events, requires fetched title to equal APPROVAL_ISSUE_TITLE, requires a current accepted event, and requires its actor to equal APPROVAL_ACTOR. It then creates an attestation from the event actor/event ID/title and appends authorized=true plus approval_attestation=${attestation} to GITHUB_OUTPUT; failures are logged and set process.exitCode = 1.src/publish/validate-approval-attestation.js (119 lines). It uses the same GitHub API headers/version (2026-03-10), retrieves an issue, and getAllPages() paginates either events or comments at per_page=100, throwing on non-OK responses.validateApprovalAttestation(): concurrently fetch issue labels/title, all events, and all comments; derive currentAcceptedEvent(events); return true only if the current title equals supplied issueTitle, labels include accepted, a current accepted event exists, and hasApprovalAttestation() finds an attestation matching the event and title from the specified attestationAuthor.main() requires APPROVAL_TOKEN, APPROVAL_ISSUE_NUMBER, APPROVAL_ISSUE_REPOSITORY, APPROVAL_ISSUE_TITLE, and APPROVAL_ATTESTATION_AUTHOR; invalid validation throws "The current accepted label has no matching approval attestation".src/modules/approval-attestation.js (108 lines). Attestations have prefix <!-- publish-approval and suffix -->; createApprovalAttestation({ actor, eventId, title }) base64url-encodes JSON { actor, eventId: String(eventId), title } and wraps it in those markers.parseApprovalAttestation(body) finds the first marker pair, base64url-decodes/parses JSON, and accepts only string actor, eventId, and title, otherwise returns null; compareEventIds(left, right) normalizes leading zeroes, permits only decimal numeric IDs, compares by length then localeCompare, and throws "Invalid issue event ID" for invalid IDs.currentAcceptedEvent(events) behavior: it considers only labeled events whose label is accepted, whose IDs are strings or numbers, and whose actors have logins; it selects the greatest numeric event ID, returning { actor: event.actor.login, eventId: String(event.id) }; no valid events or invalid IDs return null.hasApprovalAttestation({ comments, event, title, attestationAuthor }) accepts only a comment whose comment.user?.login equals attestationAuthor and whose parsed attestation actor, event ID, and title exactly equal the current accepted event actor/event ID and current title..github/workflows/publish.yml lines 340–408: Craft publish environment includes XDG_STATE_HOME=/github/workspace/.craft-state, CRAFT_MERGE_TARGET, CRAFT_LOG_LEVEL defaulting to Info, CRAFT_DRY_RUN, bot author/committer identity, release-bot GITHUB_TOKEN/GITHUB_API_TOKEN, GHCR credentials, and numerous registry/cloud credentials. On cancellation or failure it runs node .__publish__/src/publish/update-issue.js; it posts cancellation/failure via post-result.js cancelled or post-result.js failure; on success it runs post-result.js success.src/modules/details-from-context.js (66 lines). PUBLISH_TITLE_REGEX is /^publish: (?:getsentry\/)?(?<repo>[^/@]+)(?<path>\/[\w./-]+)?@(?<version>[\w.+-]+)$/; detailsFromContext() requires context.payload.issue, parses the title, sets dry_run to "1" iff label dry-run is present, sets path to "." + (titleMatch.path || ""), parses Merge target: {merge_target} using /^Merge target: (?<merge_target>[\w.\-/]+)$/m, and extracts checked target IDs from the matched targets section.src/publish/inputs.js (12 lines): it imports @actions/github, @actions/core, and detailsFromContext; inputs() invokes detailsFromContext({ context: github.context }) then uses core.setOutput("result", result), and is immediately invoked.publish.yml at lines 14, 77, 87, 105, 108, 125, 140, 201, 212, and 215; auto-approve.yml line 34; and ci-poller.yml lines 80, 103, 109, 112, 175, 232, 238, 246, 253, 258, 266, and 279.src/publish/__tests__/validate-approval-attestation.js with 4 tests: (1) accepts a current accepted event ID "100" by actor "contractor" with matching github-actions[bot] attestation; (2) rejects when title changes from publish: getsentry/sentry-javascript@10.0.0 to publish: getsentry/sentry-python@10.0.0; (3) rejects when the accepted label is absent; and (4) rejects when accepted is re-added with later event ID "200" while only the original event "100" has an attestation..github/workflows/ci-poller.yml now checks out publish code via actions/checkout@v6 with persist-credentials: false, and before CI work invokes node src/publish/validate-approval-attestation.js using APPROVAL_TOKEN="$GH_TOKEN", author github-actions[bot], current issue number/repository/title; invalid approval removes ci-pending and accepted, posts the invalid/unverifiable approval message, and continues.accepted-label event and issue title..github/workflows/publish.yml: checkout is now persist-credentials: false; a getsentry-wide release-bot token is acquired with continue-on-error: true; Authorize approval runs node src/publish/authorize-approval.js; successful authorization records an approval comment; validation runs node src/publish/validate-approval-attestation.js; any authorization, attestation, or validation failure removes accepted, comments the invalid/unverifiable approval message, and exits 1..__publish__ with persist-credentials: false, .github/workflows/publish.yml validates via node .__publish__/src/publish/validate-approval-attestation.js; failure removes accepted, comments the invalid/unverifiable approval message, and exits 1.PUBLISH_TITLE_REGEX from src/modules/details-from-context.js, replacing a function-local titleParser; it also adds the new src/modules/approval-attestation.js implementation described above.src/modules/__tests__/approval-authorizer.js (144 lines). The test suite allows getsantry[bot] for allowlisted getsentry/sentry-javascript without a permission lookup; rejects it outside the allowlist without lookup; allows target-repository collaborators with roles write, maintain, or admin; rejects roles none, read, triage, Elevated Bot, or undefined; accepts unqualified publish: sentry-javascript/packages/core@10.0.0 as getsentry/sentry-javascript; rejects malformed titles without lookup; and propagates GitHub permission-lookup errors such as "Not Found".ci-ready after the bound label event or title changed. It stated it would revalidate immediately before that transition and make the poller consume Publish’s shared title parser..github/workflows/ci-poller.yml lines 1–336. The workflow runs on schedule every 5 minutes ("*/5 * * * *") and workflow_dispatch with optional attempt default "0", requires contents: read and issues: read, runs check-ci in production, and uses concurrency group ci-status-poller with cancel-in-progress: false.ci-poller.yml runs when vars.CI_POLLER_HAS_PENDING == 'true' || github.event_name == 'workflow_dispatch'.actions/checkout@v6 with persist-credentials: false; creates a sentry-internal-app token for label changes and a getsentry-wide release-bot token for cross-repository CI API access, as the internal app may 404 on private repositories including sentry-xbox, sentry-playstation, sentry-switch, and service-registry.ci-pending and accepted, retrieving number/title/labels/body; before checking CI, it validates each issue using src/publish/validate-approval-attestation.js against its current title, latest accepted event, and github-actions[bot] attestation. Invalid approval removes ci-pending and accepted, comments the invalid/unverifiable approval message, and skips the issue.sed, extracts a 40-hex commit SHA from a commit/{SHA}/checks issue-body link, derives a release branch from the original commit’s first check suite, attempts to resolve branch HEAD, and updates the issue body’s SHA link if the branch moved.success, neutral, and skipped check conclusions passing; requires status success or zero statuses, zero pending checks, and zero unsuccessful checks before moving from ci-pending to ci-ready.ci-pending, adds ci-ready, and comments CI checks passed for ${repo}@${version}. Publishing is starting now.; on failed check runs it replaces ci-pending with ci-failed, removes accepted, and comments failed check names plus a checks URL; on failed commit status with otherwise passing check runs it likewise sets ci-failed, removes accepted, and comments failed status contexts plus a checks URL.ci-pending and accepted; if a dedicated poller app token succeeds, it sets CI_POLLER_HAS_PENDING to "false" when none remain or "true" otherwise. If pending issues remain, the internal-app token self-dispatches ci-poller.yml with incremented attempt; self-dispatch stops at 60 attempts (about 30 minutes), after which cron is the fallback.