diff --git a/.github/workflows/add-pr-to-project.yml b/.github/workflows/add-pr-to-project.yml deleted file mode 100644 index f9b9a36e7..000000000 --- a/.github/workflows/add-pr-to-project.yml +++ /dev/null @@ -1,126 +0,0 @@ -name: Add PR to Project Board - -# Adds new PRs to the shader-slang project board and sets the "Source" field -# to "Internal" (org member) or "Community" (everyone else). -# Also tags any bot-created PRs as "Bot". -# -# Uses pull_request_target so secrets are available for fork PRs. -# This workflow only reads PR metadata and makes API calls — it never -# checks out or executes PR code. - -on: - pull_request_target: - types: [opened] - -jobs: - add-to-project: - runs-on: ubuntu-latest - permissions: - contents: read - steps: - - name: Check if PR author is in shader-slang org - id: check_org - uses: actions/github-script@v7 - with: - github-token: ${{ secrets.SLANGBOT_MEMBERS_READONLY }} - script: | - const user = context.payload.pull_request.user; - const username = user.login; - - let source; - if (user.type === 'Bot') { - source = 'Bot'; - } else { - let isOrgMember = false; - - try { - await github.rest.orgs.checkMembershipForUser({ - org: 'shader-slang', - username, - }); - isOrgMember = true; - } catch (error) { - if (error.status !== 404) { - throw error; - } - } - source = isOrgMember ? 'Internal' : 'Community'; - - console.log(`PR author: ${username}`); - console.log(`Is org member: ${isOrgMember}`); - } - - core.setOutput('source', source); - - - name: Add PR to project board and set Source field - uses: actions/github-script@v7 - env: - SOURCE: ${{ steps.check_org.outputs.source }} - with: - github-token: ${{ secrets.ADD_TO_PROJECT_PAT }} - script: | - // Project and field IDs for: - // https://github.com/orgs/shader-slang/projects/13 - // - // To re-query these IDs, run: - // gh api graphql -f query='{ organization(login: "shader-slang") { - // projectV2(number: 13) { id fields(first: 20) { nodes { - // ... on ProjectV2SingleSelectField { id name options { id name } } - // } } } } }' - const PROJECT_ID = 'PVT_kwDOAb2kZs4BSJKy'; - const SOURCE_FIELD_ID = 'PVTSSF_lADOAb2kZs4BSJKyzg_w_AM'; - const SOURCE_OPTIONS = { - 'Internal': '407f2ea2', - 'Community': '31a0b1ae', - 'Bot': 'e3f25255', - }; - - const source = process.env.SOURCE; - const optionId = SOURCE_OPTIONS[source]; - if (!optionId) { - throw new Error(`Unknown source value: ${source}`); - } - const prNodeId = context.payload.pull_request.node_id; - - // Add the PR to the project (idempotent if already present) - const addResult = await github.graphql(` - mutation($projectId: ID!, $contentId: ID!) { - addProjectV2ItemById(input: {projectId: $projectId, contentId: $contentId}) { - item { id } - } - } - `, { - projectId: PROJECT_ID, - contentId: prNodeId, - }); - - const itemId = addResult?.addProjectV2ItemById?.item?.id; - if (!itemId) { - throw new Error('Failed to add PR to project: no item ID returned'); - } - console.log(`Added PR to project, item ID: ${itemId}`); - - // Set the Source field - const updateResult = await github.graphql(` - mutation($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) { - updateProjectV2ItemFieldValue(input: { - projectId: $projectId, - itemId: $itemId, - fieldId: $fieldId, - value: { singleSelectOptionId: $optionId } - }) { - projectV2Item { id } - } - } - `, { - projectId: PROJECT_ID, - itemId: itemId, - fieldId: SOURCE_FIELD_ID, - optionId: optionId, - }); - - if (!updateResult?.updateProjectV2ItemFieldValue?.projectV2Item?.id) { - throw new Error('Failed to set Source field: unexpected response'); - } - - console.log(`Set Source field to: ${source}`); diff --git a/.github/workflows/pr-checks-complete.yml b/.github/workflows/pr-checks-complete.yml new file mode 100644 index 000000000..c383c97d5 --- /dev/null +++ b/.github/workflows/pr-checks-complete.yml @@ -0,0 +1,46 @@ +name: PR Checks Complete Sync + +# Relays a gating GitHub Actions workflow's completion to the board, so a PR is +# moved to Snagged when its checks fail (and recovered when they pass) in real +# time rather than waiting for the nightly sweep. Copy to +# `.github/workflows/pr-checks-complete.yml`. +# +# Why this is needed: GitHub deliberately does NOT deliver check_suite/check_run +# events for suites created by GitHub Actions (recursion prevention), so a +# check_suite trigger never fires for Actions-based CI. workflow_run IS delivered +# for Actions runs, so it is the event that carries "a gating workflow finished". +# +# IMPORTANT -- customize the `workflows:` list below for THIS repo. workflow_run +# can only reference workflows in the same repo, by their `name:`. List your +# repo's gating workflows: at least the heavyweight CI workflow (usually the last +# to finish, so the whole check rollup has settled when it completes), plus any +# lighter required checks you want reflected the moment they finish rather than +# when CI does. The reusable workflow always recomputes from the WHOLE check +# rollup, so listing a workflow only changes WHEN the recompute runs, not which +# checks it reads. +# +# Do NOT list the board-sync's own callers ("PR Maintenance", "PR Review Fork +# Bridge", "PR Review Fork Apply", "PR Commit Status Sync", and this one) -- a +# workflow_run keyed on them would trigger this caller in a loop. +# +# No conclusion guard: react to every completion (failure -> Snagged, success -> +# recovery); the recompute is idempotent. (workflow_run only fires for workflow +# files on the default branch.) + +on: + workflow_run: + workflows: + # slangpy gating Actions workflows (their `name:` fields, not filenames). + # Listing one only affects WHEN board Status is recomputed; the reusable + # workflow always reads the whole check rollup. + - "ci" + - "checks" + types: [completed] + +permissions: {} + +jobs: + board-sync: + uses: shader-slang/slang/.github/workflows/pr-board-sync.yml@master + secrets: + SLANG_PR_BOT_TOKEN: ${{ secrets.SLANG_PR_BOT_TOKEN }} diff --git a/.github/workflows/pr-commit-status.yml b/.github/workflows/pr-commit-status.yml new file mode 100644 index 000000000..f6c45aaba --- /dev/null +++ b/.github/workflows/pr-commit-status.yml @@ -0,0 +1,30 @@ +name: PR Commit Status Sync + +# Relays external commit statuses (e.g. "SlangPy Tests", "license/cla", +# "CodeRabbit") to the board. Copy to `.github/workflows/pr-commit-status.yml`. +# +# Some PR checks are not GitHub Actions check runs but external commit statuses; +# the reusable workflow folds those into its check rollup (a failing status -> the +# PR is Snagged), but only the `status` event carries them -- check_suite and +# workflow_run cover only Actions suites. Unlike check_suite for Actions CI, the +# `status` event IS delivered here, because these statuses are posted by external +# apps/PATs, not the recursion-suppressed GITHUB_TOKEN. +# +# Generic -- copy verbatim, no per-repo customization needed. Note: `status` is +# repo-wide (it fires for any commit, including non-PR pushes); for a non-PR +# commit the reusable workflow resolves zero open PRs and no-ops. + +on: + status: {} + +permissions: {} + +jobs: + board-sync: + # Skip the initial "pending" each external check posts; only a settled status + # (success -> recovery, failure/error -> Snagged) changes the outcome, and the + # recompute is idempotent regardless. + if: ${{ github.event.state != 'pending' }} + uses: shader-slang/slang/.github/workflows/pr-board-sync.yml@master + secrets: + SLANG_PR_BOT_TOKEN: ${{ secrets.SLANG_PR_BOT_TOKEN }} diff --git a/.github/workflows/pr-maintenance.yml b/.github/workflows/pr-maintenance.yml new file mode 100644 index 000000000..35a8f4550 --- /dev/null +++ b/.github/workflows/pr-maintenance.yml @@ -0,0 +1,57 @@ +name: PR Maintenance + +# Thin caller for the shared PR board-sync reusable workflow. Copy this file to +# `.github/workflows/pr-maintenance.yml` in the consuming repo. It declares the +# PR lifecycle / review triggers and delegates all logic to the central reusable +# workflow in shader-slang/slang. All board IDs default to the shared +# "Slang PR Tracking" board, so no `with:` block is needed -- map only the one +# org-level secret (SLANG_PR_BOT_TOKEN). +# +# Pair this with the other example callers in this directory for full coverage: +# - example-pr-checks-complete.yml CI / gating-check results -> board +# - example-pr-commit-status.yml external commit statuses -> board +# - example-pr-review-fork-bridge.yml + example-pr-review-fork-apply.yml +# real-time fork-PR review handling +# +# Why these triggers: pull_request_target and check_suite receive the secret even +# for fork PRs, so they run here directly. pull_request_review only receives the +# secret for ORIGIN PRs, so the job below skips it for forks; fork reviews go +# through the fork-review relay (the two extra files above). CI completion is NOT +# handled here -- GitHub does not deliver check_suite for GitHub Actions-created +# suites (recursion prevention), so Actions-based CI results come via +# example-pr-checks-complete.yml (workflow_run). The check_suite trigger is kept +# only for any non-Actions, GitHub-App-created check suites. + +on: + pull_request_target: + types: + [ + opened, + reopened, + edited, + synchronize, + closed, + ready_for_review, + converted_to_draft, + enqueued, + dequeued, + ] + pull_request_review: + # dismissed matters too: dismissing a CHANGES_REQUESTED review (with no new + # commit) should move the PR out of Revising, and only this event signals it. + types: [submitted, dismissed] + check_suite: + types: [completed] + +permissions: {} + +jobs: + board-sync: + # Run directly for everything except a fork PR's review (no secret there); + # fork reviews go through the workflow_run relay instead. + if: >- + github.event_name != 'pull_request_review' || + github.event.pull_request.head.repo.fork == false + uses: shader-slang/slang/.github/workflows/pr-board-sync.yml@master + secrets: + SLANG_PR_BOT_TOKEN: ${{ secrets.SLANG_PR_BOT_TOKEN }} diff --git a/.github/workflows/pr-review-fork-apply.yml b/.github/workflows/pr-review-fork-apply.yml new file mode 100644 index 000000000..dae9aa2a8 --- /dev/null +++ b/.github/workflows/pr-review-fork-apply.yml @@ -0,0 +1,30 @@ +name: PR Review Fork Apply + +# Stage 2 of the fork-review relay (optional, pairs with +# example-pr-review-fork-bridge.yml). Copy to +# `.github/workflows/pr-review-fork-apply.yml`. +# +# Triggered by the completion of "PR Review Fork Bridge". A workflow_run workflow +# runs in the base repo's PRIVILEGED context with secrets even when the upstream +# run was a fork PR's review -- the only way to update the board for fork-PR +# reviews on a public repo. The reusable workflow resolves the PR from the +# upstream run's head SHA and reconciles; it does NO checkout (metadata only), so +# running it in this privileged context is not a pwn-request vector. +# +# (workflow_run only fires for workflow files on the default branch.) + +on: + workflow_run: + workflows: ["PR Review Fork Bridge"] + types: [completed] + +permissions: {} + +jobs: + board-sync: + # Only when the bridge completed successfully (it always should; this guards + # against a cancelled/failed relay run). + if: ${{ github.event.workflow_run.conclusion == 'success' }} + uses: shader-slang/slang/.github/workflows/pr-board-sync.yml@master + secrets: + SLANG_PR_BOT_TOKEN: ${{ secrets.SLANG_PR_BOT_TOKEN }} diff --git a/.github/workflows/pr-review-fork-bridge.yml b/.github/workflows/pr-review-fork-bridge.yml new file mode 100644 index 000000000..42042331d --- /dev/null +++ b/.github/workflows/pr-review-fork-bridge.yml @@ -0,0 +1,30 @@ +name: PR Review Fork Bridge + +# Stage 1 of the fork-review relay (optional, for fully real-time fork-PR review +# handling). Copy to `.github/workflows/pr-review-fork-bridge.yml`. +# +# On a PUBLIC repo, a pull_request_review on a PR from a FORK runs the fork's head +# with no secrets, so it cannot update the board directly. This unprivileged +# workflow does nothing but complete successfully for fork-PR reviews, which lets +# the privileged workflow_run workflow (example-pr-review-fork-apply.yml) fire and +# do the board update with secrets. It runs NO board logic and checks out NO code. +# Origin-PR reviews are handled directly by pr-maintenance.yml, so this is gated +# to forks. + +on: + pull_request_review: + # Relay both submitted and dismissed: dismissing a fork PR's CHANGES_REQUESTED + # review should recompute its Status, same as a new review. + types: [submitted, dismissed] + +permissions: {} + +jobs: + bridge: + if: ${{ github.event.pull_request.head.repo.fork == true }} + runs-on: ubuntu-latest + steps: + - name: Relay fork PR review to the privileged apply workflow + run: | + echo "Fork PR #${{ github.event.pull_request.number }} review event." + echo "Completing so pr-review-fork-apply.yml (workflow_run) can reconcile with secrets." diff --git a/.github/workflows/pr-sweep-nightly.yml b/.github/workflows/pr-sweep-nightly.yml new file mode 100644 index 000000000..20fc7c17b --- /dev/null +++ b/.github/workflows/pr-sweep-nightly.yml @@ -0,0 +1,31 @@ +name: PR Board Sweep (nightly) + +# Scheduled cadence caller for the reusable board-sync workflow in "sweep" mode. +# Copy to `.github/workflows/pr-sweep-nightly.yml`. +# +# Reconciles every open PR in THIS repo onto the shared "Slang PR Tracking" +# board. It is the periodic backstop for what the per-event path +# (pr-maintenance.yml and the other callers) cannot see -- missed webhooks or +# failed runs, an issue assigned/linked after the fact, board drift -- and +# shares all per-PR logic with them (only the enumeration differs). +# +# Cross-repo safe: the reusable workflow reads `context.repo` of the CALLER, so +# this schedule in slangpy / slang-rhi / etc. sweeps that repo's open PRs, not +# slang's. No per-repo customization needed beyond copying the file. +# +# Generic -- copy verbatim. (workflow_dispatch allows on-demand runs.) + +on: + schedule: + - cron: "0 7 * * *" # nightly ~07:00 UTC (GitHub may delay under load) + workflow_dispatch: {} + +permissions: {} + +jobs: + board-sweep: + uses: shader-slang/slang/.github/workflows/pr-board-sync.yml@master + with: + mode: sweep + secrets: + SLANG_PR_BOT_TOKEN: ${{ secrets.SLANG_PR_BOT_TOKEN }} diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 000000000..9af592f7e --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,10 @@ +# Allow ref-pinning for slang reusable workflows (@master by design) and +# first-party GitHub actions; silence zizmor/CodeRabbit unpinned-uses nags. +rules: + unpinned-uses: + config: + policies: + shader-slang/slang/*: ref-pin + actions/*: ref-pin + github/*: ref-pin + dependabot/*: ref-pin