Skip to content

feat: add external python module deployment #1760

feat: add external python module deployment

feat: add external python module deployment #1760

Workflow file for this run

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