feat: add external python module deployment #1760
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: pr-labels | |
| # `ready-to-merge`: adds/removes the label based on the `ci-complete` job's | |
| # conclusion (which is what branch protection gates on). The `workflow_run` | |
| # event fires after the `ci` workflow finishes; `pull_request_target: | |
| # synchronize` clears the stale label on a new push before CI finishes on | |
| # the new commit. | |
| # | |
| # `backport-check`: when a `backport release/X.Y.Z` label exists (release in | |
| # flight), every PR to main must declare backport intent via that label or | |
| # `backport:skip`. | |
| # | |
| # Runs on the default branch via these events, so the GITHUB_TOKEN is | |
| # write-scoped even for PRs from forks — and the workflow file itself can't | |
| # be tampered with by a fork PR. | |
| on: | |
| workflow_run: | |
| workflows: [ci] | |
| types: [completed] | |
| pull_request_target: | |
| types: [synchronize, converted_to_draft, ready_for_review, opened, reopened, labeled, unlabeled] | |
| permissions: {} | |
| jobs: | |
| ready-to-merge: | |
| if: >- | |
| (github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request') || | |
| (github.event_name == 'pull_request_target' && | |
| (github.event.action == 'synchronize' || | |
| github.event.action == 'converted_to_draft' || | |
| github.event.action == 'ready_for_review')) | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: read # For listWorkflowRuns / listJobsForWorkflowRun | |
| contents: read # For listPullRequestsAssociatedWithCommit | |
| pull-requests: write # To add/remove label | |
| steps: | |
| - uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const label = 'ready-to-merge'; | |
| const { owner, repo } = context.repo; | |
| const event = context.eventName; | |
| const payload = context.payload; | |
| async function setLabel(prNumber, present) { | |
| if (present) { | |
| await github.rest.issues.addLabels({ | |
| owner, repo, issue_number: prNumber, labels: [label], | |
| }); | |
| } else { | |
| try { | |
| await github.rest.issues.removeLabel({ | |
| owner, repo, issue_number: prNumber, name: label, | |
| }); | |
| } catch (e) { | |
| if (e.status !== 404) throw e; | |
| } | |
| } | |
| } | |
| async function ciCompleteOk(headSha) { | |
| const { data } = await github.rest.actions.listWorkflowRuns({ | |
| owner, repo, workflow_id: 'ci.yml', head_sha: headSha, per_page: 1, | |
| }); | |
| const run = data.workflow_runs[0]; | |
| if (!run || run.status !== 'completed') return false; | |
| const jobs = await github.paginate( | |
| github.rest.actions.listJobsForWorkflowRun, | |
| { owner, repo, run_id: run.id, per_page: 100 }, | |
| ); | |
| return jobs.find(j => j.name === 'ci-complete')?.conclusion === 'success'; | |
| } | |
| if (event === 'pull_request_target') { | |
| const prNumber = payload.pull_request.number; | |
| const action = payload.action; | |
| core.info(`event=${event} action=${action} pr=#${prNumber}`); | |
| if (action === 'converted_to_draft' || action === 'synchronize') { | |
| await setLabel(prNumber, false); | |
| } else if (action === 'ready_for_review') { | |
| // Refetch — draft state could have flipped again between the event and now. | |
| const { data: pr } = await github.rest.pulls.get({ | |
| owner, repo, pull_number: prNumber, | |
| }); | |
| const ciOk = await ciCompleteOk(pr.head.sha); | |
| await setLabel(prNumber, ciOk && !pr.draft); | |
| } | |
| return; | |
| } | |
| // workflow_run | |
| const run = payload.workflow_run; | |
| const ciOk = await ciCompleteOk(run.head_sha); | |
| let prNumbers = run.pull_requests?.map(p => p.number) ?? []; | |
| if (prNumbers.length === 0) { | |
| // Fork PRs: workflow_run.pull_requests is empty; resolve via head SHA. | |
| const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({ | |
| owner, repo, commit_sha: run.head_sha, | |
| }); | |
| prNumbers = prs.filter(p => p.state === 'open').map(p => p.number); | |
| } | |
| core.info(`event=${event} prs=${prNumbers.join(',')} ciOk=${ciOk} runHeadSha=${run.head_sha}`); | |
| for (const prNumber of prNumbers) { | |
| const { data: pr } = await github.rest.pulls.get({ | |
| owner, repo, pull_number: prNumber, | |
| }); | |
| // workflow_run events can be delivered after a newer push; | |
| // skip if the PR has moved past the SHA we evaluated. | |
| if (pr.head.sha !== run.head_sha) { | |
| core.info(`stale: #${prNumber} head=${pr.head.sha} run=${run.head_sha}; skipping`); | |
| continue; | |
| } | |
| await setLabel(prNumber, ciOk && !pr.draft); | |
| } | |
| backport-check: | |
| name: "Admins: Backport label added" | |
| if: github.event_name == 'pull_request_target' && github.event.pull_request.base.ref == 'main' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pull-requests: read | |
| steps: | |
| - name: Verify backport intent | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| # Skip when no release is in flight (`backport:skip` is excluded by the prefix filter). | |
| release_labels=$(gh api "repos/$REPO/labels" --paginate \ | |
| --jq '.[].name | select(startswith("backport release/"))') | |
| if [[ -z "$release_labels" ]]; then | |
| echo "::notice::No active 'backport release/*' labels; nothing to enforce." | |
| exit 0 | |
| fi | |
| pr_labels=$(gh pr view "$PR_NUMBER" --repo "$REPO" \ | |
| --json labels --jq '.labels[].name') | |
| if echo "$pr_labels" | grep -qE '^(backport release/|backport:skip$)'; then | |
| exit 0 | |
| fi | |
| echo "::error::PR needs a 'backport release/X.Y.Z' label, or 'backport:skip' to opt out. Active release labels:" | |
| echo "$release_labels" | sed 's/^/ - /' | |
| exit 1 |