|
|
|
@@ -3,8 +3,14 @@
|
|
|
|
|
# Called from each qbit's .github/workflows/release-rc.yml on a manual dispatch or when a
|
|
|
|
|
# develop PR labelled `release:rc` is merged. It merges develop into the release line through
|
|
|
|
|
# the GitHub API (GitHub signs the merge commit), and the push of release/* runs the existing
|
|
|
|
|
# CircleCI publish_release_candidate workflow (qqq-orb: RC bump, Maven Central, tag). Nothing
|
|
|
|
|
# here publishes, tags or touches hotfix/* branches.
|
|
|
|
|
# CircleCI publish_release_candidate workflow (qqq-orb: RC bump, Maven Central). The second job,
|
|
|
|
|
# tag-and-release, then waits for the orb's "Bump version to X [skip ci]" commit and for X on Maven
|
|
|
|
|
# Central, checks that the Central artifact was published after this merge (provenance), and only
|
|
|
|
|
# then creates the signed annotated tag vX on the bump commit and a GitHub prerelease whose notes
|
|
|
|
|
# list the merged PRs. Nothing here publishes artifacts or touches hotfix/* branches.
|
|
|
|
|
#
|
|
|
|
|
# Signing: the tag is signed with the SSH key in the QRUN_TAG_SIGNING_KEY secret (callers pass
|
|
|
|
|
# `secrets: inherit`). Without it the job fails before tagging; it never creates an unsigned tag.
|
|
|
|
|
#
|
|
|
|
|
# Release line: develop's <revision>X.Y.Z-SNAPSHOT</revision> picks release/X.Y.Z if that branch
|
|
|
|
|
# exists, else release/X.Y (created from develop when missing).
|
|
|
|
@@ -17,6 +23,10 @@ on:
|
|
|
|
|
description: Print the plan without merging or creating branches
|
|
|
|
|
type: boolean
|
|
|
|
|
default: false
|
|
|
|
|
secrets:
|
|
|
|
|
QRUN_TAG_SIGNING_KEY:
|
|
|
|
|
description: SSH private key that signs release tags
|
|
|
|
|
required: false
|
|
|
|
|
|
|
|
|
|
permissions:
|
|
|
|
|
contents: write
|
|
|
|
@@ -28,8 +38,13 @@ jobs:
|
|
|
|
|
concurrency:
|
|
|
|
|
group: qbit-release-rc-${{ github.repository }}
|
|
|
|
|
cancel-in-progress: false
|
|
|
|
|
outputs:
|
|
|
|
|
branch: ${{ steps.merge.outputs.branch }}
|
|
|
|
|
merge_sha: ${{ steps.merge.outputs.merge_sha }}
|
|
|
|
|
merged_at: ${{ steps.merge.outputs.merged_at }}
|
|
|
|
|
steps:
|
|
|
|
|
- name: Merge develop into the release line
|
|
|
|
|
id: merge
|
|
|
|
|
env:
|
|
|
|
|
GH_TOKEN: ${{ github.token }}
|
|
|
|
|
REPO: ${{ github.repository }}
|
|
|
|
@@ -55,6 +70,7 @@ jobs:
|
|
|
|
|
echo "Plan: create $branch from develop $develop_sha (first RC of $full)"
|
|
|
|
|
[[ "$DRY_RUN" == true ]] && exit 0
|
|
|
|
|
gh api "repos/$REPO/git/refs" -f ref="refs/heads/$branch" -f sha="$develop_sha" --silent
|
|
|
|
|
merge_sha="$develop_sha"
|
|
|
|
|
else
|
|
|
|
|
echo "Plan: merge develop $develop_sha into $branch"
|
|
|
|
|
[[ "$DRY_RUN" == true ]] && exit 0
|
|
|
|
@@ -63,11 +79,115 @@ jobs:
|
|
|
|
|
-f commit_message="Merge develop into $branch for the next $full RC" 2>&1) || true
|
|
|
|
|
status=$(printf '%s\n' "$response" | sed -n 's:^HTTP/[0-9.]* \([0-9][0-9]*\).*:\1:p' | head -n 1)
|
|
|
|
|
case "$status" in
|
|
|
|
|
201) ;;
|
|
|
|
|
201) merge_sha=$(gh api "repos/$REPO/branches/$branch" -q .commit.sha) ;;
|
|
|
|
|
204) echo "::warning::$branch already contains develop; no new RC"; exit 0 ;;
|
|
|
|
|
409) echo "::error::Merge conflict between develop and $branch; resolve it in a PR into $branch"; exit 1 ;;
|
|
|
|
|
*) echo "::error::Merge of develop into $branch failed (HTTP ${status:-unknown})"; exit 1 ;;
|
|
|
|
|
esac
|
|
|
|
|
fi
|
|
|
|
|
{
|
|
|
|
|
echo "branch=$branch"
|
|
|
|
|
echo "merge_sha=$merge_sha"
|
|
|
|
|
echo "merged_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
|
|
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
|
echo "Pushed $branch; CircleCI publish_release_candidate cuts the RC." >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
|
echo "Release line: $branch" >> "$GITHUB_STEP_SUMMARY"
|
|
|
|
|
|
|
|
|
|
tag-and-release:
|
|
|
|
|
needs: cut-rc
|
|
|
|
|
if: needs.cut-rc.outputs.merge_sha != ''
|
|
|
|
|
runs-on: ubuntu-24.04
|
|
|
|
|
timeout-minutes: 150
|
|
|
|
|
concurrency:
|
|
|
|
|
group: qbit-release-rc-tag-${{ github.repository }}
|
|
|
|
|
cancel-in-progress: false
|
|
|
|
|
steps:
|
|
|
|
|
- name: Wait for the orb bump and the Maven Central artifact, then tag and release
|
|
|
|
|
env:
|
|
|
|
|
GH_TOKEN: ${{ github.token }}
|
|
|
|
|
REPO: ${{ github.repository }}
|
|
|
|
|
BRANCH: ${{ needs.cut-rc.outputs.branch }}
|
|
|
|
|
MERGE_SHA: ${{ needs.cut-rc.outputs.merge_sha }}
|
|
|
|
|
MERGED_AT: ${{ needs.cut-rc.outputs.merged_at }}
|
|
|
|
|
SIGNING_KEY: ${{ secrets.QRUN_TAG_SIGNING_KEY }}
|
|
|
|
|
run: |
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
if [[ -z "$SIGNING_KEY" ]]; then
|
|
|
|
|
echo "::error::QRUN_TAG_SIGNING_KEY is not available (the caller needs 'secrets: inherit'); not creating an unsigned tag. The release captain tags this RC."
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
# The project's own artifactId: skip the <parent> block (qbit-build-parent).
|
|
|
|
|
artifact=$(gh api "repos/$REPO/contents/pom.xml?ref=$MERGE_SHA" -q .content | base64 -d \
|
|
|
|
|
| sed '/<parent>/,/<\/parent>/d' \
|
|
|
|
|
| sed -n 's:.*<artifactId>\(qbit-[^<]*\)</artifactId>.*:\1:p' | head -n 1)
|
|
|
|
|
group_path=com/kingsrook/qbits
|
|
|
|
|
|
|
|
|
|
# 1. The orb pushes "Bump version to X [skip ci]" on top of the merge after it deploys.
|
|
|
|
|
bump_sha=""; version=""
|
|
|
|
|
for _ in $(seq 1 100); do
|
|
|
|
|
line=$(gh api "repos/$REPO/commits?sha=$BRANCH&per_page=1" -q '.[0] | .sha + " " + (.commit.message | split("\n")[0])')
|
|
|
|
|
sha=${line%% *}; msg=${line#* }
|
|
|
|
|
if [[ "$msg" =~ ^Bump\ version\ to\ ([0-9]+\.[0-9]+\.[0-9]+(-RC\.[0-9]+)?)\ \[skip\ ci\]$ ]] \
|
|
|
|
|
&& [[ "$(gh api "repos/$REPO/compare/$MERGE_SHA...$sha" -q .status)" == ahead ]]; then
|
|
|
|
|
bump_sha=$sha; version=${BASH_REMATCH[1]}; break
|
|
|
|
|
fi
|
|
|
|
|
sleep 60
|
|
|
|
|
done
|
|
|
|
|
if [[ -z "$bump_sha" ]]; then
|
|
|
|
|
echo "::error::No orb bump commit on $BRANCH after the merge; the release captain tags this RC."
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
if gh api "repos/$REPO/git/refs/tags/v$version" --silent 2>/dev/null; then
|
|
|
|
|
echo "::warning::Tag v$version already exists; nothing to do."
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# 2. Provenance: the Central artifact must exist and be newer than this merge.
|
|
|
|
|
pom="https://repo1.maven.org/maven2/$group_path/$artifact/$version/$artifact-$version.pom"
|
|
|
|
|
for _ in $(seq 1 60); do
|
|
|
|
|
[[ "$(curl -s -o /dev/null -w '%{http_code}' "$pom")" == 200 ]] && break
|
|
|
|
|
sleep 60
|
|
|
|
|
done
|
|
|
|
|
last_modified=$(curl -sI "$pom" | tr -d '\r' | sed -n 's/^[Ll]ast-[Mm]odified: //p')
|
|
|
|
|
if [[ "$(curl -s -o /dev/null -w '%{http_code}' "$pom")" != 200 || -z "$last_modified" ]]; then
|
|
|
|
|
echo "::error::$artifact $version is not on Maven Central"
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
if (( $(date -d "$last_modified" +%s) <= $(date -d "$MERGED_AT" +%s) )); then
|
|
|
|
|
echo "::error::$artifact $version on Central ($last_modified) predates this merge ($MERGED_AT); not tagging."
|
|
|
|
|
exit 1
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# 3. Signed annotated tag on the bump commit.
|
|
|
|
|
workdir=$(mktemp -d)
|
|
|
|
|
key="$workdir/signing_key"
|
|
|
|
|
install -m 600 /dev/null "$key"
|
|
|
|
|
printf '%s\n' "$SIGNING_KEY" > "$key"
|
|
|
|
|
git init -q "$workdir/repo"
|
|
|
|
|
git -C "$workdir/repo" fetch -q --depth=1 "https://x-access-token:$GH_TOKEN@github.com/$REPO.git" "$bump_sha"
|
|
|
|
|
git -C "$workdir/repo" -c user.name="James Maes" -c user.email=james@kof22.com -c gpg.format=ssh \
|
|
|
|
|
-c user.signingkey="$key" tag -s "v$version" "$bump_sha" -m "$artifact $version"
|
|
|
|
|
rm -f "$key"
|
|
|
|
|
git -C "$workdir/repo" push -q "https://x-access-token:$GH_TOKEN@github.com/$REPO.git" "v$version"
|
|
|
|
|
|
|
|
|
|
# 4. GitHub prerelease whose notes list the merged PRs since the previous tag.
|
|
|
|
|
previous=$(gh api "repos/$REPO/tags?per_page=100" -q '.[].name' | grep -E '^v[0-9]' | grep -vx "v$version" | sort -V | tail -n 1 || true)
|
|
|
|
|
prs=$(gh api "repos/$REPO/compare/${previous:-$MERGE_SHA}...$bump_sha" \
|
|
|
|
|
-q '.commits[].commit.message | select(startswith("Merge pull request #")) | split("\n") | (.[0] | capture("#(?<n>[0-9]+)").n) + "\t" + (.[2] // "")')
|
|
|
|
|
notes="$workdir/notes.md"
|
|
|
|
|
{
|
|
|
|
|
echo "$artifact **$version** release candidate (Maven Central, $last_modified)."
|
|
|
|
|
echo
|
|
|
|
|
echo "## Changes"
|
|
|
|
|
if [[ -n "$prs" ]]; then
|
|
|
|
|
while IFS=$'\t' read -r n title; do echo "- $title ([#$n](https://github.com/$REPO/pull/$n))"; done <<< "$prs"
|
|
|
|
|
else
|
|
|
|
|
echo "- See the commits since ${previous:-the previous release}."
|
|
|
|
|
fi
|
|
|
|
|
echo
|
|
|
|
|
echo "The signed annotated tag \`v$version\` fixes source \`${bump_sha:0:7}\` on \`$BRANCH\`."
|
|
|
|
|
echo
|
|
|
|
|
echo "No GA approval is implied."
|
|
|
|
|
} > "$notes"
|
|
|
|
|
gh release create "v$version" -R "$REPO" --verify-tag --prerelease --title "$artifact $version" --notes-file "$notes"
|
|
|
|
|
echo "Tagged and released v$version ($artifact) on ${bump_sha:0:7}." >> "$GITHUB_STEP_SUMMARY"
|