diff --git a/.github/workflows/qbit-release-rc.yml b/.github/workflows/qbit-release-rc.yml deleted file mode 100644 index cd9dce2..0000000 --- a/.github/workflows/qbit-release-rc.yml +++ /dev/null @@ -1,193 +0,0 @@ -# Cut the next release candidate of a qbit (QRun-IO/qqq docs/QRUN-QBIT-REPO-STANDARD.md, C6). -# -# 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). 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 X.Y.Z-SNAPSHOT picks release/X.Y.Z if that branch -# exists, else release/X.Y (created from develop when missing). -name: qbit release candidate - -on: - workflow_call: - inputs: - dry_run: - 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 - -jobs: - cut-rc: - runs-on: ubuntu-24.04 - timeout-minutes: 10 - 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 }} - DRY_RUN: ${{ inputs.dry_run }} - run: | - set -euo pipefail - develop_sha=$(gh api "repos/$REPO/branches/develop" -q .commit.sha) - revision=$(gh api "repos/$REPO/contents/pom.xml?ref=$develop_sha" -q .content | base64 -d \ - | sed -n 's:.*\(.*\).*:\1:p' | head -n 1) - if [[ ! "$revision" =~ ^([0-9]+)\.([0-9]+)\.([0-9]+)-SNAPSHOT$ ]]; then - echo "::error::develop pom.xml must be X.Y.Z-SNAPSHOT, found '$revision'" - exit 1 - fi - full="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}.${BASH_REMATCH[3]}" - line="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}" - branch_exists() { gh api "repos/$REPO/branches/$1" --silent 2>/dev/null; } - if branch_exists "release/$full"; then branch="release/$full" - elif branch_exists "release/$line"; then branch="release/$line" - else branch=""; fi - - if [[ -z "$branch" ]]; then - branch="release/$line" - 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 - # 201 merged, 204 already up to date (nothing new to release), 409 conflict. - response=$(gh api --include "repos/$REPO/merges" -f base="$branch" -f head="$develop_sha" \ - -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) 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 block (qbit-build-parent). - artifact=$(gh api "repos/$REPO/contents/pom.xml?ref=$MERGE_SHA" -q .content | base64 -d \ - | sed '//,/<\/parent>/d' \ - | sed -n 's:.*\(qbit-[^<]*\).*:\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("#(?[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"