Skip to content
Open
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:
Comment thread
mrecachinas marked this conversation as resolved.
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
228 changes: 228 additions & 0 deletions .github/workflows/create_staging_branch_writer.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
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}"
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:
Comment thread
mrecachinas marked this conversation as resolved.
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."
Loading