Skip to content

Create PR staging branch writer #295

Create PR staging branch writer

Create PR staging branch writer #295

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}"