Create PR staging branch writer #303
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: Create PR staging branch writer | |
| on: | |
| workflow_run: | |
| workflows: ["Create PR staging branch"] | |
| types: [completed] | |
| schedule: | |
| - cron: "*/10 * * * *" | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: Pull request number to process | |
| required: true | |
| type: number | |
| permissions: | |
| actions: read | |
| contents: write | |
| pull-requests: write | |
| jobs: | |
| ensure-base-is-staging: | |
| if: ${{ github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request') }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Ensure base is staging | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| REPOSITORY: ${{ github.repository }} | |
| WORKFLOW_RUN_ID: ${{ github.event.workflow_run.id }} | |
| WORKFLOW_RUN_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} | |
| WORKFLOW_RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} | |
| WORKFLOW_RUN_HEAD_REPOSITORY: ${{ github.event.workflow_run.head_repository.full_name }} | |
| WORKFLOW_RUN_PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }} | |
| DISPATCH_PR_NUMBER: ${{ inputs.pr_number }} | |
| run: | | |
| set -euo pipefail | |
| is_pr_number() { | |
| [[ "$1" =~ ^[0-9]+$ ]] | |
| } | |
| recover_pr_number_from_artifact() { | |
| local run_id="$1" | |
| local artifact_count artifact_dir artifact_id artifact_json artifact_size entries pr_number | |
| is_pr_number "${run_id}" || return 1 | |
| if ! artifact_json="$(gh api "repos/${REPOSITORY}/actions/runs/${run_id}/artifacts" \ | |
| --jq '[.artifacts[] | select(.name == "create-staging-pr-number" and .expired == false)]')"; then | |
| return 1 | |
| fi | |
| artifact_count="$(jq -r 'length' <<<"${artifact_json}")" | |
| [[ "${artifact_count}" == "1" ]] || return 1 | |
| artifact_id="$(jq -r '.[0].id' <<<"${artifact_json}")" | |
| artifact_size="$(jq -r '.[0].size_in_bytes' <<<"${artifact_json}")" | |
| is_pr_number "${artifact_id}" || return 1 | |
| is_pr_number "${artifact_size}" || return 1 | |
| (( artifact_size <= 4096 )) || return 1 | |
| artifact_dir="workflow-run-artifacts/create-staging-${run_id}" | |
| rm -rf "${artifact_dir}" | |
| mkdir -p "${artifact_dir}" | |
| if ! gh api "repos/${REPOSITORY}/actions/artifacts/${artifact_id}/zip" > "${artifact_dir}/artifact.zip"; then | |
| rm -rf "${artifact_dir}" | |
| return 1 | |
| fi | |
| if ! entries="$(unzip -Z1 "${artifact_dir}/artifact.zip" 2>/dev/null)"; then | |
| rm -rf "${artifact_dir}" | |
| return 1 | |
| fi | |
| if [[ "${entries}" != "pr_number.txt" ]]; then | |
| rm -rf "${artifact_dir}" | |
| return 1 | |
| fi | |
| if ! pr_number="$(unzip -p "${artifact_dir}/artifact.zip" pr_number.txt 2>/dev/null | head -c 32)"; then | |
| rm -rf "${artifact_dir}" | |
| return 1 | |
| fi | |
| rm -rf "${artifact_dir}" | |
| is_pr_number "${pr_number}" || return 1 | |
| printf '%s\n' "${pr_number}" | |
| } | |
| recover_pr_number_from_head_sha() { | |
| local head_sha="$1" | |
| local head_repository="$2" | |
| local matches pulls_json | |
| [[ "${head_sha}" =~ ^[0-9a-fA-F]{40}$ ]] || return 1 | |
| [[ -n "${head_repository}" && "${head_repository}" != "null" ]] || return 1 | |
| if ! pulls_json="$(gh api -H "Accept: application/vnd.github+json" "repos/${REPOSITORY}/commits/${head_sha}/pulls")"; then | |
| return 1 | |
| fi | |
| matches="$(jq -r --arg head_sha "${head_sha}" --arg head_repository "${head_repository}" ' | |
| [ | |
| .[] | |
| | select(.state == "open") | |
| | select(.base.ref == "main") | |
| | select(.head.sha == $head_sha) | |
| | select(.head.repo.full_name == $head_repository) | |
| | .number | |
| ] | |
| ' <<<"${pulls_json}")" | |
| [[ "$(jq -r 'length' <<<"${matches}")" == "1" ]] || return 1 | |
| jq -r '.[0]' <<<"${matches}" | |
| } | |
| encode_ref() { | |
| jq -rn --arg value "$1" '$value | @uri' | |
| } | |
| process_pr() { | |
| local advisory_file_pages base_ref base_repo branch_name encoded_branch head_ref head_repo head_sha | |
| local main_sha pr_author pr_json pr_number="$1" state | |
| if ! is_pr_number "${pr_number}"; then | |
| echo "::error::Unexpected pull request number: ${pr_number}" | |
| return 1 | |
| fi | |
| pr_json="$(gh api "repos/${REPOSITORY}/pulls/${pr_number}")" | |
| state="$(jq -r '.state' <<<"${pr_json}")" | |
| base_ref="$(jq -r '.base.ref' <<<"${pr_json}")" | |
| base_repo="$(jq -r '.base.repo.full_name' <<<"${pr_json}")" | |
| pr_author="$(jq -r '.user.login' <<<"${pr_json}")" | |
| head_sha="$(jq -r '.head.sha // empty' <<<"${pr_json}")" | |
| head_repo="$(jq -r '.head.repo.full_name // empty' <<<"${pr_json}")" | |
| head_ref="$(jq -r '.head.ref // empty' <<<"${pr_json}")" | |
| if [[ "${GITHUB_EVENT_NAME}" == "workflow_run" ]]; then | |
| if [[ ! "${WORKFLOW_RUN_HEAD_SHA:-}" =~ ^[0-9a-fA-F]{40}$ || | |
| -z "${WORKFLOW_RUN_HEAD_REPOSITORY:-}" || | |
| "${WORKFLOW_RUN_HEAD_REPOSITORY}" == "null" || | |
| -z "${WORKFLOW_RUN_HEAD_BRANCH:-}" || | |
| "${WORKFLOW_RUN_HEAD_BRANCH}" == "null" ]]; then | |
| echo "::error::The workflow run is missing trusted head identity metadata." | |
| return 1 | |
| fi | |
| if [[ "${head_sha}" != "${WORKFLOW_RUN_HEAD_SHA}" || | |
| "${head_repo}" != "${WORKFLOW_RUN_HEAD_REPOSITORY}" || | |
| "${head_ref}" != "${WORKFLOW_RUN_HEAD_BRANCH}" ]]; then | |
| echo "::error::Pull request ${pr_number} does not match the triggering workflow run." | |
| return 1 | |
| fi | |
| fi | |
| if [[ "${state}" != "open" ]]; then | |
| echo "Pull request ${pr_number} is ${state}; skipping." | |
| return 0 | |
| fi | |
| if [[ "${base_ref}" != "main" ]]; then | |
| echo "Pull request ${pr_number} base is ${base_ref}, not main; skipping." | |
| return 0 | |
| fi | |
| if [[ "${base_repo}" != "${REPOSITORY}" ]]; then | |
| echo "Pull request ${pr_number} targets ${base_repo}, not ${REPOSITORY}; skipping." | |
| return 0 | |
| fi | |
| advisory_file_pages="$(gh api --paginate "repos/${REPOSITORY}/pulls/${pr_number}/files?per_page=100" \ | |
| --jq 'any(.[]; .filename | startswith("advisories/"))')" | |
| if ! grep -qx 'true' <<<"${advisory_file_pages}"; then | |
| echo "Pull request ${pr_number} does not modify advisories/; skipping." | |
| return 0 | |
| fi | |
| branch_name="${pr_author}/advisory-improvement-${pr_number}" | |
| if ! git check-ref-format "refs/heads/${branch_name}" >/dev/null; then | |
| echo "::error::Unexpected staging branch name: ${branch_name}" | |
| return 1 | |
| fi | |
| encoded_branch="$(encode_ref "${branch_name}")" | |
| if gh api "repos/${REPOSITORY}/git/ref/heads/${encoded_branch}" --silent >/dev/null 2>&1; then | |
| echo "Staging branch ${branch_name} already exists." | |
| else | |
| main_sha="$(gh api "repos/${REPOSITORY}/git/ref/heads/main" --jq '.object.sha')" | |
| if gh api -X POST "repos/${REPOSITORY}/git/refs" \ | |
| -f ref="refs/heads/${branch_name}" \ | |
| -f sha="${main_sha}" \ | |
| --silent; then | |
| echo "Created staging branch ${branch_name} from main." | |
| elif gh api "repos/${REPOSITORY}/git/ref/heads/${encoded_branch}" --silent >/dev/null 2>&1; then | |
| echo "Staging branch ${branch_name} was created by another run." | |
| else | |
| echo "::error::Failed to create staging branch ${branch_name}." | |
| return 1 | |
| fi | |
| fi | |
| gh api -X PATCH "repos/${REPOSITORY}/pulls/${pr_number}" \ | |
| -f base="${branch_name}" \ | |
| --silent | |
| echo "Retargeted pull request ${pr_number} to ${branch_name}." | |
| } | |
| if [[ "${GITHUB_EVENT_NAME}" == "workflow_run" ]]; then | |
| if is_pr_number "${WORKFLOW_RUN_PR_NUMBER:-}"; then | |
| PR_NUMBERS="${WORKFLOW_RUN_PR_NUMBER}" | |
| elif PR_NUMBERS="$(recover_pr_number_from_artifact "${WORKFLOW_RUN_ID:-}")"; then | |
| echo "Recovered pull request number ${PR_NUMBERS} from signal artifact." | |
| elif PR_NUMBERS="$(recover_pr_number_from_head_sha "${WORKFLOW_RUN_HEAD_SHA:-}" "${WORKFLOW_RUN_HEAD_REPOSITORY:-}")"; then | |
| echo "Recovered pull request number ${PR_NUMBERS} from workflow_run head SHA." | |
| else | |
| echo "No pull request number could be recovered; skipping." | |
| exit 0 | |
| fi | |
| elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then | |
| PR_NUMBERS="${DISPATCH_PR_NUMBER}" | |
| else | |
| PR_NUMBERS="$(gh api --paginate "repos/${REPOSITORY}/pulls?state=open&base=main&per_page=100" --jq '.[].number')" | |
| if [[ -z "${PR_NUMBERS}" ]]; then | |
| echo "No open pull requests targeting main need reconciliation." | |
| exit 0 | |
| fi | |
| fi | |
| while IFS= read -r PR_NUMBER; do | |
| [[ -n "${PR_NUMBER}" ]] || continue | |
| process_pr "${PR_NUMBER}" | |
| done <<<"${PR_NUMBERS}" |