mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-25 14:53:34 +00:00
fix(ci): repair staging promotion workflow behavior (#1091)
* fix(ci): repair staging-ci workflow parsing * fix(ci): chain staging promotion to latest open branch * feat(ci): carry staging batch summaries into release PRs * test(ci): add dry-run dispatch for promotion metadata workflows * fix(ci): fetch only release tags for batch summaries * fix(ci): address review feedback on batch summaries * fix(ci): harden metadata workflows and dedupe body helpers * fix(ci): pass repo explicitly to gh pr list
This commit is contained in:
@@ -0,0 +1,59 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
load_commit_summary() {
|
||||||
|
local range="$1"
|
||||||
|
local max_commits="${2:-50}"
|
||||||
|
local commit_list overflow
|
||||||
|
|
||||||
|
commit_list="$(git log --oneline --no-merges --reverse "${range}" 2>/dev/null || echo "")"
|
||||||
|
if [ -n "${commit_list}" ]; then
|
||||||
|
COMMIT_COUNT="$(printf '%s\n' "${commit_list}" | wc -l | tr -d ' ')"
|
||||||
|
if [ "${COMMIT_COUNT}" -gt "${max_commits}" ]; then
|
||||||
|
COMMIT_MD="$(printf '%s\n' "${commit_list}" | head -n "${max_commits}" | sed 's/^/- /')"
|
||||||
|
overflow=$((COMMIT_COUNT - max_commits))
|
||||||
|
COMMIT_MD+=$'\n'"- ... and ${overflow} more (see compare view)"
|
||||||
|
else
|
||||||
|
COMMIT_MD="$(printf '%s\n' "${commit_list}" | sed 's/^/- /')"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
COMMIT_COUNT=0
|
||||||
|
COMMIT_MD="- (no non-merge commits in range)"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
replace_marked_section() {
|
||||||
|
local body_file="$1"
|
||||||
|
local section_file="$2"
|
||||||
|
local section_start="$3"
|
||||||
|
local section_end="$4"
|
||||||
|
local output_file="$5"
|
||||||
|
|
||||||
|
if grep -qF "${section_start}" "${body_file}" && grep -qF "${section_end}" "${body_file}"; then
|
||||||
|
awk -v start="${section_start}" -v end="${section_end}" -v replacement_file="${section_file}" '
|
||||||
|
BEGIN {
|
||||||
|
while ((getline line < replacement_file) > 0) {
|
||||||
|
replacement = replacement line ORS
|
||||||
|
}
|
||||||
|
in_block = 0
|
||||||
|
}
|
||||||
|
$0 == start {
|
||||||
|
printf "%s", replacement
|
||||||
|
in_block = 1
|
||||||
|
next
|
||||||
|
}
|
||||||
|
$0 == end {
|
||||||
|
in_block = 0
|
||||||
|
next
|
||||||
|
}
|
||||||
|
!in_block {
|
||||||
|
print
|
||||||
|
}
|
||||||
|
' "${body_file}" > "${output_file}"
|
||||||
|
else
|
||||||
|
cp "${body_file}" "${output_file}"
|
||||||
|
if [ -s "${output_file}" ]; then
|
||||||
|
printf '\n\n' >> "${output_file}"
|
||||||
|
fi
|
||||||
|
cat "${section_file}" >> "${output_file}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${PR_NUMBER:?PR_NUMBER is required}"
|
||||||
|
: "${REPO:?REPO is required}"
|
||||||
|
|
||||||
|
MAIN_BRANCH="${MAIN_BRANCH:-main}"
|
||||||
|
DRY_RUN="${DRY_RUN:-false}"
|
||||||
|
SECTION_START="<!-- staging-promotion-release-summary:start -->"
|
||||||
|
SECTION_END="<!-- staging-promotion-release-summary:end -->"
|
||||||
|
TMP_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "${TMP_DIR}"' EXIT
|
||||||
|
|
||||||
|
# shellcheck source=.github/scripts/pr-body-utils.sh
|
||||||
|
source "$(dirname "$0")/pr-body-utils.sh"
|
||||||
|
|
||||||
|
gh pr view "${PR_NUMBER}" --repo "${REPO}" --json body > "${TMP_DIR}/pr.json"
|
||||||
|
jq -r '.body // ""' < "${TMP_DIR}/pr.json" > "${TMP_DIR}/body.md"
|
||||||
|
|
||||||
|
git fetch origin "${MAIN_BRANCH}"
|
||||||
|
git fetch origin "+refs/tags/v*:refs/tags/v*"
|
||||||
|
|
||||||
|
LAST_TAG="$(git describe --tags --match 'v*' --abbrev=0 "origin/${MAIN_BRANCH}" 2>/dev/null || true)"
|
||||||
|
if [ -n "${LAST_TAG}" ]; then
|
||||||
|
RANGE="${LAST_TAG}..origin/${MAIN_BRANCH}"
|
||||||
|
HEADER="## Staging promotion batches since ${LAST_TAG}"
|
||||||
|
EMPTY_MESSAGE="_No structured staging promotion merges found since ${LAST_TAG}._"
|
||||||
|
else
|
||||||
|
RANGE="origin/${MAIN_BRANCH}"
|
||||||
|
HEADER="## Staging promotion batches on ${MAIN_BRANCH}"
|
||||||
|
EMPTY_MESSAGE="_No structured staging promotion merges found on ${MAIN_BRANCH}._"
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "${SECTION_START}"
|
||||||
|
echo "${HEADER}"
|
||||||
|
echo
|
||||||
|
} > "${TMP_DIR}/section.md"
|
||||||
|
|
||||||
|
FOUND_SUMMARY=false
|
||||||
|
while IFS= read -r sha; do
|
||||||
|
[ -n "${sha}" ] || continue
|
||||||
|
BODY="$(git show -s --format=%b "${sha}")"
|
||||||
|
if ! printf '%s\n' "${BODY}" | grep -q '^staging-promotion-summary-v1$'; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
FOUND_SUMMARY=true
|
||||||
|
SUBJECT="$(git show -s --format=%s "${sha}")"
|
||||||
|
PR_REF="$(printf '%s\n' "${BODY}" | sed -n 's/^promotion-pr: //p' | head -n 1)"
|
||||||
|
COMMIT_COUNT="$(printf '%s\n' "${BODY}" | sed -n 's/^current-commit-count: //p' | head -n 1)"
|
||||||
|
CURRENT_RANGE="$(printf '%s\n' "${BODY}" | sed -n 's/^current-range: //p' | head -n 1)"
|
||||||
|
COMMIT_BLOCK="$(printf '%s\n' "${BODY}" | awk 'capture { print } /^Current commits in this promotion \([0-9]+\):$/ { capture = 1 }')"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "### ${SUBJECT}"
|
||||||
|
echo
|
||||||
|
if [ -n "${PR_REF}" ]; then
|
||||||
|
echo "**Promotion PR:** ${PR_REF}"
|
||||||
|
fi
|
||||||
|
if [ -n "${COMMIT_COUNT}" ]; then
|
||||||
|
echo "**Commit count:** ${COMMIT_COUNT}"
|
||||||
|
fi
|
||||||
|
if [ -n "${CURRENT_RANGE}" ]; then
|
||||||
|
echo "**Range:** \`${CURRENT_RANGE}\`"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
if [ -n "${COMMIT_BLOCK}" ]; then
|
||||||
|
echo "${COMMIT_BLOCK}"
|
||||||
|
else
|
||||||
|
echo "- (no commit summary found)"
|
||||||
|
fi
|
||||||
|
echo
|
||||||
|
} >> "${TMP_DIR}/section.md"
|
||||||
|
done < <(git log --merges --reverse --format='%H' "${RANGE}")
|
||||||
|
|
||||||
|
if [ "${FOUND_SUMMARY}" = false ]; then
|
||||||
|
{
|
||||||
|
echo "${EMPTY_MESSAGE}"
|
||||||
|
echo
|
||||||
|
} >> "${TMP_DIR}/section.md"
|
||||||
|
fi
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "*Auto-updated from structured staging promotion merge bodies on ${MAIN_BRANCH}.*"
|
||||||
|
echo "${SECTION_END}"
|
||||||
|
} >> "${TMP_DIR}/section.md"
|
||||||
|
|
||||||
|
replace_marked_section \
|
||||||
|
"${TMP_DIR}/body.md" \
|
||||||
|
"${TMP_DIR}/section.md" \
|
||||||
|
"${SECTION_START}" \
|
||||||
|
"${SECTION_END}" \
|
||||||
|
"${TMP_DIR}/new-body.md"
|
||||||
|
|
||||||
|
if [ "${DRY_RUN}" = "true" ]; then
|
||||||
|
echo "Dry run enabled. Computed PR body for #${PR_NUMBER}:"
|
||||||
|
cat "${TMP_DIR}/new-body.md"
|
||||||
|
else
|
||||||
|
gh pr edit "${PR_NUMBER}" --repo "${REPO}" --body-file "${TMP_DIR}/new-body.md"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
: "${PR_NUMBER:?PR_NUMBER is required}"
|
||||||
|
: "${REPO:?REPO is required}"
|
||||||
|
|
||||||
|
MAX_COMMITS="${MAX_COMMITS:-50}"
|
||||||
|
DRY_RUN="${DRY_RUN:-false}"
|
||||||
|
SECTION_START="<!-- staging-ci-current:start -->"
|
||||||
|
SECTION_END="<!-- staging-ci-current:end -->"
|
||||||
|
TMP_DIR="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "${TMP_DIR}"' EXIT
|
||||||
|
|
||||||
|
# shellcheck source=.github/scripts/pr-body-utils.sh
|
||||||
|
source "$(dirname "$0")/pr-body-utils.sh"
|
||||||
|
|
||||||
|
gh pr view "${PR_NUMBER}" --repo "${REPO}" --json body,baseRefName,headRefName > "${TMP_DIR}/pr.json"
|
||||||
|
jq -r '.body // ""' < "${TMP_DIR}/pr.json" > "${TMP_DIR}/body.md"
|
||||||
|
BASE="$(jq -r '.baseRefName' < "${TMP_DIR}/pr.json")"
|
||||||
|
HEAD="$(jq -r '.headRefName' < "${TMP_DIR}/pr.json")"
|
||||||
|
RANGE="origin/${BASE}..origin/${HEAD}"
|
||||||
|
|
||||||
|
git fetch origin "${BASE}" "${HEAD}"
|
||||||
|
|
||||||
|
load_commit_summary "${RANGE}" "${MAX_COMMITS}"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "${SECTION_START}"
|
||||||
|
echo "### Current commits in this promotion (${COMMIT_COUNT})"
|
||||||
|
echo
|
||||||
|
echo "**Current base:** \`${BASE}\`"
|
||||||
|
echo "**Current head:** \`${HEAD}\`"
|
||||||
|
echo "**Current range:** \`${RANGE}\`"
|
||||||
|
echo
|
||||||
|
echo "${COMMIT_MD}"
|
||||||
|
echo
|
||||||
|
echo "*Auto-updated by staging promotion metadata workflow*"
|
||||||
|
echo "${SECTION_END}"
|
||||||
|
} > "${TMP_DIR}/section.md"
|
||||||
|
|
||||||
|
replace_marked_section \
|
||||||
|
"${TMP_DIR}/body.md" \
|
||||||
|
"${TMP_DIR}/section.md" \
|
||||||
|
"${SECTION_START}" \
|
||||||
|
"${SECTION_END}" \
|
||||||
|
"${TMP_DIR}/new-body.md"
|
||||||
|
|
||||||
|
if [ "${DRY_RUN}" = "true" ]; then
|
||||||
|
echo "Dry run enabled. Computed PR body for #${PR_NUMBER}:"
|
||||||
|
cat "${TMP_DIR}/new-body.md"
|
||||||
|
else
|
||||||
|
gh pr edit "${PR_NUMBER}" --repo "${REPO}" --body-file "${TMP_DIR}/new-body.md"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
name: Release-plz Batch Summary
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
pr_number:
|
||||||
|
description: "release-plz PR number to refresh"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
dry_run:
|
||||||
|
description: "Compute the body update without editing the PR"
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
update-release-pr:
|
||||||
|
if: >
|
||||||
|
(github.event_name == 'pull_request_target' &&
|
||||||
|
github.event.pull_request.head.repo.full_name == github.repository &&
|
||||||
|
startsWith(github.event.pull_request.head.ref, 'release-plz-')) ||
|
||||||
|
github.event_name == 'workflow_dispatch'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout base branch
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event_name == 'workflow_dispatch' && 'main' || github.event.pull_request.base.ref }}
|
||||||
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- name: Update release-plz PR body with staging batch summary
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || 'false' }}
|
||||||
|
run: bash .github/scripts/update-release-plz-body.sh
|
||||||
@@ -58,10 +58,16 @@ jobs:
|
|||||||
- *checkout
|
- *checkout
|
||||||
- *install-rust
|
- *install-rust
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
- name: Generate GitHub token
|
||||||
|
uses: actions/create-github-app-token@v2
|
||||||
|
id: generate-token
|
||||||
|
with:
|
||||||
|
app-id: ${{ secrets.GH_RELEASES_MANAGER_APP_ID }}
|
||||||
|
private-key: ${{ secrets.GH_RELEASES_MANAGER_APP_PRIVATE_KEY }}
|
||||||
- name: Run release-plz
|
- name: Run release-plz
|
||||||
uses: release-plz/[email protected]
|
uses: release-plz/[email protected]
|
||||||
with:
|
with:
|
||||||
command: release-pr
|
command: release-pr
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ steps.generate-token.outputs.token }}
|
||||||
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
|
||||||
|
|||||||
@@ -25,9 +25,35 @@ concurrency:
|
|||||||
cancel-in-progress: false # Let running suites finish
|
cancel-in-progress: false # Let running suites finish
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
# ── Resolve promotion base branch ───────────────────────────────
|
||||||
|
resolve-promotion-base:
|
||||||
|
name: Resolve promotion base
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
promotion_base: ${{ steps.resolve.outputs.promotion_base }}
|
||||||
|
steps:
|
||||||
|
- name: Resolve promotion base
|
||||||
|
id: resolve
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
FALLBACK_BRANCH: main
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
LATEST=$(gh pr list --repo "${REPO}" --label staging-promotion --state open \
|
||||||
|
--json headRefName,createdAt \
|
||||||
|
--jq '[.[] | select(.headRefName | startswith("staging-promote/"))] | sort_by(.createdAt) | last | .headRefName // empty')
|
||||||
|
if [ -n "$LATEST" ]; then
|
||||||
|
echo "promotion_base=${LATEST}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Using open promotion branch as base: ${LATEST}"
|
||||||
|
else
|
||||||
|
echo "promotion_base=${FALLBACK_BRANCH}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "No open promotion branch found. Using ${FALLBACK_BRANCH}."
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Check for new commits ──────────────────────────────────────
|
# ── Check for new commits ──────────────────────────────────────
|
||||||
check-changes:
|
check-changes:
|
||||||
name: Check for new commits
|
name: Check for new commits
|
||||||
|
needs: resolve-promotion-base
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
has_changes: ${{ steps.check.outputs.has_changes }}
|
has_changes: ${{ steps.check.outputs.has_changes }}
|
||||||
@@ -44,7 +70,7 @@ jobs:
|
|||||||
id: check
|
id: check
|
||||||
env:
|
env:
|
||||||
FORCE_RUN: ${{ inputs.force }}
|
FORCE_RUN: ${{ inputs.force }}
|
||||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
PROMOTION_BASE: ${{ needs.resolve-promotion-base.outputs.promotion_base }}
|
||||||
run: |
|
run: |
|
||||||
CURRENT_HEAD=$(git rev-parse HEAD)
|
CURRENT_HEAD=$(git rev-parse HEAD)
|
||||||
echo "current_head=${CURRENT_HEAD}" >> "$GITHUB_OUTPUT"
|
echo "current_head=${CURRENT_HEAD}" >> "$GITHUB_OUTPUT"
|
||||||
@@ -66,9 +92,9 @@ jobs:
|
|||||||
echo "Found ${COMMIT_COUNT} new commit(s) since last tested"
|
echo "Found ${COMMIT_COUNT} new commit(s) since last tested"
|
||||||
DIFF_RANGE="${LAST_TESTED}..${CURRENT_HEAD}"
|
DIFF_RANGE="${LAST_TESTED}..${CURRENT_HEAD}"
|
||||||
else
|
else
|
||||||
git fetch origin "${DEFAULT_BRANCH}"
|
git fetch origin "${PROMOTION_BASE}"
|
||||||
MERGE_BASE=$(git merge-base "origin/${DEFAULT_BRANCH}" HEAD)
|
MERGE_BASE=$(git merge-base "origin/${PROMOTION_BASE}" HEAD)
|
||||||
echo "First run -- reviewing from merge-base ${MERGE_BASE}"
|
echo "First run -- reviewing from merge-base ${MERGE_BASE} against ${PROMOTION_BASE}"
|
||||||
DIFF_RANGE="${MERGE_BASE}..${CURRENT_HEAD}"
|
DIFF_RANGE="${MERGE_BASE}..${CURRENT_HEAD}"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
@@ -102,13 +128,12 @@ jobs:
|
|||||||
# ── Create promotion PR (triggers claude-review.yml on the PR) ──
|
# ── Create promotion PR (triggers claude-review.yml on the PR) ──
|
||||||
create-promotion-pr:
|
create-promotion-pr:
|
||||||
name: Create Promotion PR
|
name: Create Promotion PR
|
||||||
needs: check-changes
|
needs: [resolve-promotion-base, check-changes]
|
||||||
if: needs.check-changes.outputs.has_changes == 'true'
|
if: needs.check-changes.outputs.has_changes == 'true'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
pr_number: ${{ steps.create-pr.outputs.pr_number }}
|
pr_number: ${{ steps.create-pr.outputs.pr_number }}
|
||||||
promotion_branch: ${{ steps.branch.outputs.branch }}
|
promotion_branch: ${{ steps.branch.outputs.branch }}
|
||||||
commit_summary: ${{ steps.create-pr.outputs.commit_summary }}
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
@@ -135,15 +160,15 @@ jobs:
|
|||||||
id: ahead-check
|
id: ahead-check
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ steps.token.outputs.token }}
|
GH_TOKEN: ${{ steps.token.outputs.token }}
|
||||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
PROMOTION_BASE: ${{ needs.resolve-promotion-base.outputs.promotion_base }}
|
||||||
run: |
|
run: |
|
||||||
git fetch origin "${DEFAULT_BRANCH}"
|
git fetch origin "${PROMOTION_BASE}"
|
||||||
AHEAD=$(git rev-list --count "origin/${DEFAULT_BRANCH}..origin/staging")
|
AHEAD=$(git rev-list --count "origin/${PROMOTION_BASE}..origin/staging")
|
||||||
echo "commits_ahead=${AHEAD}" >> "$GITHUB_OUTPUT"
|
echo "commits_ahead=${AHEAD}" >> "$GITHUB_OUTPUT"
|
||||||
if [ "$AHEAD" -eq 0 ]; then
|
if [ "$AHEAD" -eq 0 ]; then
|
||||||
echo "Staging is not ahead of ${DEFAULT_BRANCH}. Nothing to promote."
|
echo "Staging is not ahead of ${PROMOTION_BASE}. Nothing to promote."
|
||||||
else
|
else
|
||||||
echo "Staging is ${AHEAD} commits ahead of ${DEFAULT_BRANCH}."
|
echo "Staging is ${AHEAD} commits ahead of ${PROMOTION_BASE}."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Create promotion branch
|
- name: Create promotion branch
|
||||||
@@ -157,51 +182,20 @@ jobs:
|
|||||||
echo "branch=${BRANCH}" >> "$GITHUB_OUTPUT"
|
echo "branch=${BRANCH}" >> "$GITHUB_OUTPUT"
|
||||||
echo "Created promotion branch: ${BRANCH}"
|
echo "Created promotion branch: ${BRANCH}"
|
||||||
|
|
||||||
- name: Find base branch
|
|
||||||
id: find-base
|
|
||||||
if: steps.ahead-check.outputs.commits_ahead != '0'
|
|
||||||
env:
|
|
||||||
GH_TOKEN: ${{ steps.token.outputs.token }}
|
|
||||||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
|
||||||
run: |
|
|
||||||
# Find the newest open promotion PR with a staging-promote/* head branch
|
|
||||||
LATEST=$(gh pr list --label staging-promotion --state open \
|
|
||||||
--json headRefName,createdAt \
|
|
||||||
--jq '[.[] | select(.headRefName | startswith("staging-promote/"))] | sort_by(.createdAt) | last | .headRefName // empty')
|
|
||||||
if [ -n "$LATEST" ]; then
|
|
||||||
echo "base=${LATEST}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "Chaining onto existing promotion branch: ${LATEST}"
|
|
||||||
else
|
|
||||||
echo "base=${DEFAULT_BRANCH}" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "No existing promotion PR — targeting ${DEFAULT_BRANCH}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Create promotion PR
|
- name: Create promotion PR
|
||||||
id: create-pr
|
id: create-pr
|
||||||
if: steps.ahead-check.outputs.commits_ahead != '0'
|
if: steps.ahead-check.outputs.commits_ahead != '0'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ steps.token.outputs.token }}
|
GH_TOKEN: ${{ steps.token.outputs.token }}
|
||||||
run: |
|
run: |
|
||||||
|
source .github/scripts/pr-body-utils.sh
|
||||||
RANGE="${{ needs.check-changes.outputs.diff_range }}"
|
RANGE="${{ needs.check-changes.outputs.diff_range }}"
|
||||||
TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M UTC")
|
TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M UTC")
|
||||||
BRANCH="${{ steps.branch.outputs.branch }}"
|
BRANCH="${{ steps.branch.outputs.branch }}"
|
||||||
BASE="${{ steps.find-base.outputs.base }}"
|
BASE="${{ needs.resolve-promotion-base.outputs.promotion_base }}"
|
||||||
|
|
||||||
# Enumerate commits in this batch (exclude merge commits, cap at 50)
|
|
||||||
MAX_COMMITS=50
|
MAX_COMMITS=50
|
||||||
COMMIT_LIST=$(git log --oneline --no-merges --reverse "${RANGE}" 2>/dev/null || echo "")
|
load_commit_summary "${RANGE}" "${MAX_COMMITS}"
|
||||||
if [ -n "$COMMIT_LIST" ]; then
|
|
||||||
COMMIT_COUNT=$(echo "$COMMIT_LIST" | wc -l | tr -d ' ')
|
|
||||||
if [ "$COMMIT_COUNT" -gt "$MAX_COMMITS" ]; then
|
|
||||||
COMMIT_MD=$(echo "$COMMIT_LIST" | head -n "$MAX_COMMITS" | sed 's/^/- /')
|
|
||||||
COMMIT_MD+=$'\n'"- ... and $((COMMIT_COUNT - MAX_COMMITS)) more (see compare view)"
|
|
||||||
else
|
|
||||||
COMMIT_MD=$(echo "$COMMIT_LIST" | sed 's/^/- /')
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
COMMIT_COUNT=0
|
|
||||||
COMMIT_MD="- (no non-merge commits in range)"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build PR body via concatenation to avoid heredoc shell expansion
|
# Build PR body via concatenation to avoid heredoc shell expansion
|
||||||
# (commit messages in COMMIT_MD may contain $, backticks, or backslashes)
|
# (commit messages in COMMIT_MD may contain $, backticks, or backslashes)
|
||||||
@@ -212,6 +206,17 @@ jobs:
|
|||||||
PR_BODY+=$'\n'"**Triggered by:** Staging CI batch at ${TIMESTAMP}"
|
PR_BODY+=$'\n'"**Triggered by:** Staging CI batch at ${TIMESTAMP}"
|
||||||
PR_BODY+=$'\n\n'"### Commits in this batch (${COMMIT_COUNT}):"
|
PR_BODY+=$'\n\n'"### Commits in this batch (${COMMIT_COUNT}):"
|
||||||
PR_BODY+=$'\n'"${COMMIT_MD}"
|
PR_BODY+=$'\n'"${COMMIT_MD}"
|
||||||
|
PR_BODY+=$'\n\n'"<!-- staging-ci-current:start -->"
|
||||||
|
PR_BODY+=$'\n'"### Current commits in this promotion (${COMMIT_COUNT})"
|
||||||
|
PR_BODY+=$'\n'
|
||||||
|
PR_BODY+=$'\n'"**Current base:** \`${BASE}\`"
|
||||||
|
PR_BODY+=$'\n'"**Current head:** \`${BRANCH}\`"
|
||||||
|
PR_BODY+=$'\n'"**Current range:** \`origin/${BASE}..origin/${BRANCH}\`"
|
||||||
|
PR_BODY+=$'\n'
|
||||||
|
PR_BODY+=$'\n'"${COMMIT_MD}"
|
||||||
|
PR_BODY+=$'\n'
|
||||||
|
PR_BODY+=$'\n'"*Auto-updated by staging promotion metadata workflow*"
|
||||||
|
PR_BODY+=$'\n'"<!-- staging-ci-current:end -->"
|
||||||
PR_BODY+=$'\n\n'"Waiting for gates:"
|
PR_BODY+=$'\n\n'"Waiting for gates:"
|
||||||
PR_BODY+=$'\n'"- Tests: pending"
|
PR_BODY+=$'\n'"- Tests: pending"
|
||||||
PR_BODY+=$'\n'"- E2E: pending"
|
PR_BODY+=$'\n'"- E2E: pending"
|
||||||
@@ -230,15 +235,6 @@ jobs:
|
|||||||
echo "pr_number=${PR_NUM}" >> "$GITHUB_OUTPUT"
|
echo "pr_number=${PR_NUM}" >> "$GITHUB_OUTPUT"
|
||||||
echo "Created promotion PR #${PR_NUM}"
|
echo "Created promotion PR #${PR_NUM}"
|
||||||
|
|
||||||
# Output commit summary for use in merge commit message
|
|
||||||
DELIM="COMMIT_SUMMARY_EOF_$(date +%s)"
|
|
||||||
{
|
|
||||||
echo "commit_summary<<${DELIM}"
|
|
||||||
echo "Commits in this batch (${COMMIT_COUNT}):"
|
|
||||||
echo "${COMMIT_MD}"
|
|
||||||
echo "${DELIM}"
|
|
||||||
} >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
# ── Gate: wait for review, process findings, merge or block ─────
|
# ── Gate: wait for review, process findings, merge or block ─────
|
||||||
gate:
|
gate:
|
||||||
name: Staging Gate
|
name: Staging Gate
|
||||||
@@ -257,7 +253,8 @@ jobs:
|
|||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v6
|
||||||
with:
|
with:
|
||||||
ref: staging
|
ref: staging
|
||||||
fetch-depth: 1
|
# Need full history to recompute the final promoted range before merge.
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Generate GitHub App token
|
- name: Generate GitHub App token
|
||||||
id: app-token
|
id: app-token
|
||||||
@@ -356,8 +353,10 @@ jobs:
|
|||||||
# Use process substitution so variables propagate to parent shell
|
# Use process substitution so variables propagate to parent shell
|
||||||
while read -r line; do
|
while read -r line; do
|
||||||
TAG=$(echo "$line" | grep -oE '^\[(CRITICAL|HIGH|MEDIUM|LOW):[0-9]+\]')
|
TAG=$(echo "$line" | grep -oE '^\[(CRITICAL|HIGH|MEDIUM|LOW):[0-9]+\]')
|
||||||
SEVERITY=$(echo "$TAG" | sed 's/\[\(.*\):\(.*\)\]/\1/')
|
SEVERITY="${TAG#\[}"
|
||||||
CONFIDENCE=$(echo "$TAG" | sed 's/\[\(.*\):\(.*\)\]/\2/')
|
SEVERITY="${SEVERITY%%:*}"
|
||||||
|
CONFIDENCE="${TAG##*:}"
|
||||||
|
CONFIDENCE="${CONFIDENCE%\]}"
|
||||||
DESC=$(echo "$line" | sed "s/\[${SEVERITY}:${CONFIDENCE}\] *//" | head -1)
|
DESC=$(echo "$line" | sed "s/\[${SEVERITY}:${CONFIDENCE}\] *//" | head -1)
|
||||||
|
|
||||||
echo "Found: [${SEVERITY}:${CONFIDENCE}] ${DESC}"
|
echo "Found: [${SEVERITY}:${CONFIDENCE}] ${DESC}"
|
||||||
@@ -448,18 +447,30 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ steps.token.outputs.token }}
|
GH_TOKEN: ${{ steps.token.outputs.token }}
|
||||||
PR_NUMBER: ${{ needs.create-promotion-pr.outputs.pr_number }}
|
PR_NUMBER: ${{ needs.create-promotion-pr.outputs.pr_number }}
|
||||||
COMMIT_SUMMARY: ${{ needs.create-promotion-pr.outputs.commit_summary }}
|
|
||||||
run: |
|
run: |
|
||||||
|
source .github/scripts/pr-body-utils.sh
|
||||||
if [ -n "$PR_NUMBER" ]; then
|
if [ -n "$PR_NUMBER" ]; then
|
||||||
BASE=$(gh pr view "$PR_NUMBER" --json baseRefName --jq '.baseRefName')
|
BASE=$(gh pr view "$PR_NUMBER" --json baseRefName --jq '.baseRefName')
|
||||||
if [ "$BASE" = "main" ]; then
|
if [ "$BASE" = "main" ]; then
|
||||||
echo "Merging promotion PR #${PR_NUMBER} (targets main)"
|
echo "Merging promotion PR #${PR_NUMBER} (targets main)"
|
||||||
TITLE=$(gh pr view "$PR_NUMBER" --json title --jq '.title')
|
TITLE=$(gh pr view "$PR_NUMBER" --json title --jq '.title')
|
||||||
if [ -n "$COMMIT_SUMMARY" ]; then
|
HEAD_BRANCH=$(gh pr view "$PR_NUMBER" --json headRefName --jq '.headRefName')
|
||||||
gh pr merge "$PR_NUMBER" --merge --subject "#${PR_NUMBER} $TITLE" --body-file <(printf '%s' "$COMMIT_SUMMARY")
|
git fetch origin "${BASE}" "${HEAD_BRANCH}"
|
||||||
else
|
CURRENT_RANGE="origin/${BASE}..origin/${HEAD_BRANCH}"
|
||||||
gh pr merge "$PR_NUMBER" --merge
|
MAX_COMMITS=50
|
||||||
fi
|
load_commit_summary "${CURRENT_RANGE}" "${MAX_COMMITS}"
|
||||||
|
{
|
||||||
|
echo "staging-promotion-summary-v1"
|
||||||
|
echo "promotion-pr: #${PR_NUMBER}"
|
||||||
|
echo "base: ${BASE}"
|
||||||
|
echo "head: ${HEAD_BRANCH}"
|
||||||
|
echo "current-range: ${CURRENT_RANGE}"
|
||||||
|
echo "current-commit-count: ${COMMIT_COUNT}"
|
||||||
|
echo ""
|
||||||
|
echo "Current commits in this promotion (${COMMIT_COUNT}):"
|
||||||
|
echo "${COMMIT_MD}"
|
||||||
|
} > /tmp/staging-promotion-merge-body.md
|
||||||
|
gh pr merge "$PR_NUMBER" --merge --subject "#${PR_NUMBER} $TITLE" --body-file /tmp/staging-promotion-merge-body.md
|
||||||
echo "merged=true" >> "$GITHUB_OUTPUT"
|
echo "merged=true" >> "$GITHUB_OUTPUT"
|
||||||
else
|
else
|
||||||
echo "PR #${PR_NUMBER} targets '${BASE}' (not main) — leaving open for chain resolution"
|
echo "PR #${PR_NUMBER} targets '${BASE}' (not main) — leaving open for chain resolution"
|
||||||
@@ -499,18 +510,20 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Summary
|
- name: Summary
|
||||||
run: |
|
run: |
|
||||||
echo "## Staging CI Batch Results" >> "$GITHUB_STEP_SUMMARY"
|
{
|
||||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
echo "## Staging CI Batch Results"
|
||||||
echo "| Check | Result |" >> "$GITHUB_STEP_SUMMARY"
|
echo ""
|
||||||
echo "|-------|--------|" >> "$GITHUB_STEP_SUMMARY"
|
echo "| Check | Result |"
|
||||||
echo "| Tests | ${{ needs.tests.result }} |" >> "$GITHUB_STEP_SUMMARY"
|
echo "|-------|--------|"
|
||||||
echo "| E2E | ${{ needs.e2e.result }} |" >> "$GITHUB_STEP_SUMMARY"
|
echo "| Tests | ${{ needs.tests.result }} |"
|
||||||
echo "| Promotion PR | ${{ needs.create-promotion-pr.result }} |" >> "$GITHUB_STEP_SUMMARY"
|
echo "| E2E | ${{ needs.e2e.result }} |"
|
||||||
echo "| Gate | ${{ needs.gate.result }} |" >> "$GITHUB_STEP_SUMMARY"
|
echo "| Promotion PR | ${{ needs.create-promotion-pr.result }} |"
|
||||||
echo "| Tag Updated | ${{ needs.update-tag.result }} |" >> "$GITHUB_STEP_SUMMARY"
|
echo "| Gate | ${{ needs.gate.result }} |"
|
||||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
echo "| Tag Updated | ${{ needs.update-tag.result }} |"
|
||||||
echo "Range: ${{ needs.check-changes.outputs.diff_range }}" >> "$GITHUB_STEP_SUMMARY"
|
echo ""
|
||||||
|
echo "Range: ${{ needs.check-changes.outputs.diff_range }}"
|
||||||
PR_NUM="${{ needs.create-promotion-pr.outputs.pr_number }}"
|
PR_NUM="${{ needs.create-promotion-pr.outputs.pr_number }}"
|
||||||
if [ -n "$PR_NUM" ]; then
|
if [ -n "$PR_NUM" ]; then
|
||||||
echo "Promotion PR: #${PR_NUM}" >> "$GITHUB_STEP_SUMMARY"
|
echo "Promotion PR: #${PR_NUM}"
|
||||||
fi
|
fi
|
||||||
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
name: Staging Promotion Metadata
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
pr_number:
|
||||||
|
description: "Staging promotion PR number to refresh"
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
dry_run:
|
||||||
|
description: "Compute the body update without editing the PR"
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
refresh-single-pr:
|
||||||
|
if: >
|
||||||
|
(github.event_name == 'pull_request_target' &&
|
||||||
|
github.event.pull_request.head.repo.full_name == github.repository &&
|
||||||
|
startsWith(github.event.pull_request.head.ref, 'staging-promote/')) ||
|
||||||
|
github.event_name == 'workflow_dispatch'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout base branch
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: ${{ github.event_name == 'workflow_dispatch' && 'main' || github.event.pull_request.base.ref }}
|
||||||
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- name: Refresh staging promotion PR body
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
PR_NUMBER: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || github.event.pull_request.number }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run || 'false' }}
|
||||||
|
run: bash .github/scripts/update-staging-promotion-body.sh
|
||||||
|
|
||||||
|
refresh-open-prs-after-main-push:
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout main
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
|
|
||||||
|
- name: Refresh all open staging promotion PR bodies
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
run: |
|
||||||
|
# ubuntu-latest uses bash 5.x, so mapfile is available here.
|
||||||
|
mapfile -t prs < <(gh pr list --repo "${REPO}" --label staging-promotion --state open \
|
||||||
|
--json number,headRefName \
|
||||||
|
--jq '.[] | select(.headRefName | startswith("staging-promote/")) | .number')
|
||||||
|
if [ "${#prs[@]}" -eq 0 ]; then
|
||||||
|
echo "No open staging promotion PRs to refresh."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
for pr in "${prs[@]}"; do
|
||||||
|
echo "Refreshing staging promotion PR #${pr}"
|
||||||
|
PR_NUMBER="${pr}" bash .github/scripts/update-staging-promotion-body.sh
|
||||||
|
done
|
||||||
Reference in New Issue
Block a user