Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 16 additions & 17 deletions .github/workflows/create_staging_branch.yaml
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
name: Create PR staging branch

on:
pull_request_target:
pull_request:
branches: [main]
types: [opened, synchronize, reopened, edited]
paths:
- "advisories/**"
workflow_dispatch:

permissions:
contents: write # Required to create and push branches
pull-requests: write # Required to edit PR base branch
contents: read

jobs:
ensure-base-is-staging:
signal:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: ensure base is staging
env:
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
PR_NUMBER: ${{ github.event.pull_request.number }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -xeo pipefail
BRANCH_NAME="$PR_AUTHOR"/advisory-improvement-"$PR_NUMBER"
git checkout -b "$BRANCH_NAME"
git push origin "$BRANCH_NAME"
gh pr edit --repo ${{ github.repository }} $PR_NUMBER --base "$BRANCH_NAME"
- name: Record pull request signal
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
mkdir -p workflow-signal
printf '%s\n' "${PR_NUMBER}" > workflow-signal/pr_number.txt
- name: Upload pull request signal
uses: actions/upload-artifact@v4
with:
name: create-staging-pr-number
path: workflow-signal/pr_number.txt
retention-days: 1
if-no-files-found: error
172 changes: 172 additions & 0 deletions .github/workflows/create_staging_branch_writer.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
name: Create PR staging branch writer

on:
workflow_run:
workflows: ["Create PR staging branch"]
types: [completed]
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 == '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_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_id artifact_dir pr_number

is_pr_number "${run_id}" || return 1

if ! artifact_id="$(gh api "repos/${REPOSITORY}/actions/runs/${run_id}/artifacts" \
--jq '.artifacts[] | select(.name == "create-staging-pr-number" and .expired == false) | .id' | head -n 1)"; then
return 1
fi
[[ -n "${artifact_id}" ]] || 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 ! pr_number="$(unzip -p "${artifact_dir}/artifact.zip" pr_number.txt 2>/dev/null | tr -d '[:space:]')"; 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 pulls_json pr_number

[[ "${head_sha}" =~ ^[0-9a-fA-F]{40}$ ]] || return 1

[[ "${head_repository}" != "null" ]] || head_repository=""

if ! pulls_json="$(gh api -H "Accept: application/vnd.github+json" "repos/${REPOSITORY}/commits/${head_sha}/pulls")"; then
return 1
fi
pr_number="$(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_repository == "" or .head.repo.full_name == $head_repository)
| .number
' <<<"${pulls_json}" | head -n 1)"

is_pr_number "${pr_number}" || return 1
printf '%s\n' "${pr_number}"
}

if [[ "${GITHUB_EVENT_NAME}" == "workflow_run" ]]; then
if is_pr_number "${WORKFLOW_RUN_PR_NUMBER:-}"; then
PR_NUMBER="${WORKFLOW_RUN_PR_NUMBER}"
elif PR_NUMBER="$(recover_pr_number_from_artifact "${WORKFLOW_RUN_ID:-}")"; then
echo "Recovered pull request number ${PR_NUMBER} from signal artifact."
elif PR_NUMBER="$(recover_pr_number_from_head_sha "${WORKFLOW_RUN_HEAD_SHA:-}" "${WORKFLOW_RUN_HEAD_REPOSITORY:-}")"; then
echo "Recovered pull request number ${PR_NUMBER} from workflow_run head SHA."
else
echo "No pull request number could be recovered; skipping."
exit 0
fi
else
PR_NUMBER="${DISPATCH_PR_NUMBER}"
fi

if ! is_pr_number "${PR_NUMBER}"; then
echo "::error::Unexpected pull request number: ${PR_NUMBER}"
exit 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}")"

if [[ "${state}" != "open" ]]; then
echo "Pull request ${PR_NUMBER} is ${state}; skipping."
exit 0
fi

if [[ "${base_ref}" != "main" ]]; then
echo "Pull request ${PR_NUMBER} base is ${base_ref}, not main; skipping."
exit 0
fi

if [[ "${base_repo}" != "${REPOSITORY}" ]]; then
echo "Pull request ${PR_NUMBER} targets ${base_repo}, not ${REPOSITORY}; skipping."
exit 0
fi

if [[ ! "${pr_author}" =~ ^[A-Za-z0-9]([A-Za-z0-9-]*[A-Za-z0-9])?$ ]]; then
echo "::error::Unexpected pull request author login: ${pr_author}"
exit 1
fi

files="$(gh api --paginate "repos/${REPOSITORY}/pulls/${PR_NUMBER}/files?per_page=100" --jq '.[].filename')"
if ! grep -q '^advisories/' <<<"${files}"; then
echo "Pull request ${PR_NUMBER} does not modify advisories/; skipping."
exit 0
fi

branch_name="${pr_author}/advisory-improvement-${PR_NUMBER}"
if [[ ! "${branch_name}" =~ ^[A-Za-z0-9][A-Za-z0-9-]*/advisory-improvement-[0-9]+$ ]]; then
echo "::error::Unexpected staging branch name: ${branch_name}"
exit 1
fi

if gh api "repos/${REPOSITORY}/git/ref/heads/${branch_name}" --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/${branch_name}" --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}."
exit 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}."
20 changes: 5 additions & 15 deletions .github/workflows/delete_staging_and_head_branches.yaml
Original file line number Diff line number Diff line change
@@ -1,28 +1,18 @@
name: Delete PR staging and head branches

on:
pull_request_target:
pull_request:
branches: ["*/advisory-improvement-*"]
types: [closed]
paths:
- "advisories/**"
workflow_dispatch:

permissions:
contents: write # Required to delete branches
contents: read

jobs:
delete-staging-and-head-branches:
if: ${{ !github.event.pull_request.head.repo.fork }}
signal:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Delete staging and head branches
env:
STAGING_BRANCH: ${{ github.event.pull_request.base.ref }}
HEAD_BRANCH: ${{ github.event.pull_request.head.ref }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -xeo pipefail
git push origin --delete --force $STAGING_BRANCH
git push origin --delete --force $HEAD_BRANCH
- name: Record pull request signal
run: echo "Delete staging and head branches signal received."
107 changes: 107 additions & 0 deletions .github/workflows/delete_staging_and_head_branches_writer.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
name: Delete PR staging and head branches writer

on:
workflow_run:
workflows: ["Delete PR staging and head branches"]
types: [completed]
workflow_dispatch:
inputs:
pr_number:
description: Pull request number to process
required: true
type: number

permissions:
contents: write
pull-requests: read

jobs:
delete-staging-and-head-branches:
if: ${{ github.event_name == 'workflow_dispatch' || (github.event.workflow_run.conclusion == 'success' && github.event.workflow_run.event == 'pull_request') }}
runs-on: ubuntu-latest
steps:
- name: Delete staging and head branches
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPOSITORY: ${{ github.repository }}
WORKFLOW_RUN_PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }}
DISPATCH_PR_NUMBER: ${{ inputs.pr_number }}
run: |
set -euo pipefail

is_staging_branch() {
[[ "$1" =~ ^[A-Za-z0-9][A-Za-z0-9-]*/advisory-improvement-[A-Za-z0-9._-]+$ ]]
}

is_safe_branch() {
local branch="$1"
[[ -n "${branch}" ]] || return 1
[[ "${branch}" != "main" ]] || return 1
[[ "${branch}" =~ ^[A-Za-z0-9._/-]+$ ]] || return 1
[[ "${branch}" != /* ]] || return 1
[[ "${branch}" != */ ]] || return 1
[[ "${branch}" != *..* ]] || return 1
[[ "${branch}" != *//* ]] || return 1
}

delete_branch() {
local branch="$1"
if gh api -X DELETE "repos/${REPOSITORY}/git/refs/heads/${branch}" --silent; then
echo "Deleted branch ${branch}."
elif gh api "repos/${REPOSITORY}/git/ref/heads/${branch}" --silent >/dev/null 2>&1; then
echo "::error::Failed to delete existing branch ${branch}."
exit 1
else
echo "Branch ${branch} is already absent."
fi
}

if [[ "${GITHUB_EVENT_NAME}" == "workflow_run" ]]; then
PR_NUMBER="${WORKFLOW_RUN_PR_NUMBER}"
else
PR_NUMBER="${DISPATCH_PR_NUMBER}"
fi

if [[ -z "${PR_NUMBER}" || "${PR_NUMBER}" == "null" ]]; then
echo "No pull request number was provided; skipping."
exit 0
fi

pr_json="$(gh api "repos/${REPOSITORY}/pulls/${PR_NUMBER}")"
state="$(jq -r '.state' <<<"${pr_json}")"
base_ref="$(jq -r '.base.ref' <<<"${pr_json}")"
head_ref="$(jq -r '.head.ref // empty' <<<"${pr_json}")"
head_repo="$(jq -r '.head.repo.full_name // empty' <<<"${pr_json}")"

if [[ "${state}" != "closed" ]]; then
echo "Pull request ${PR_NUMBER} is ${state}, not closed; skipping."
exit 0
fi

if [[ "${head_repo}" != "${REPOSITORY}" ]]; then
echo "Pull request ${PR_NUMBER} head repo is ${head_repo}, not ${REPOSITORY}; skipping."
exit 0
fi

if ! is_staging_branch "${base_ref}"; then
echo "Pull request ${PR_NUMBER} base branch ${base_ref} is not an advisory improvement branch; skipping."
exit 0
fi

files="$(gh api --paginate "repos/${REPOSITORY}/pulls/${PR_NUMBER}/files?per_page=100" --jq '.[].filename')"
if ! grep -q '^advisories/' <<<"${files}"; then
echo "Pull request ${PR_NUMBER} does not modify advisories/; skipping."
exit 0
fi

delete_branch "${base_ref}"
if [[ "${head_ref}" == "${base_ref}" ]]; then
exit 0
fi

if ! is_safe_branch "${head_ref}"; then
echo "Head branch ${head_ref} is not safe to delete; leaving it in place."
exit 0
fi

delete_branch "${head_ref}"