Author SHA1 Message Date
james.maesandCodex 407fc25522 ci: align canonical shared orb workers
Co-Authored-By: Codex <noreply@openai.com>
2026-10-21 01:23:22 -05:00
james.maesandCodex 395f51b93a ci: centralize organization workflow lint (#4)
* ci: delegate organization workflow lint to shared orb worker

Co-Authored-By: Codex <noreply@openai.com>

* ci: retire duplicate organization Circle lint job

Co-Authored-By: Codex <noreply@openai.com>

---------

Co-authored-by: Codex <noreply@openai.com>
2026-10-20 20:26:36 -05:00
james.maes 8f7cf886f2 Merge pull request #3 from QRun-IO/fix/release-rc-artifact-id
fix(release-rc): read the project's own artifactId, not the parent's
2026-10-16 11:34:45 -05:00
james.maesandClaude Opus 5.5 56e76e4bf3 fix(release-rc): read the project's own artifactId, not the parent's
tag-and-release took the first qbit-* artifactId in the pom, which is the
<parent> qbit-build-parent, so it waited for qbit-build-parent X on Maven
Central and failed (qbit-crm 0.2.0-RC.6). Skip the <parent> block.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 11:27:21 -05:00
james.maes b28dbbe3f0 Merge pull request #2 from QRun-IO/feature/rc-tag-and-release
feat(release-rc): tag and release each RC after Maven Central confirms it
2026-10-16 08:02:26 -05:00
james.maesandClaude Opus 5.5 d494052a33 feat(release-rc): tag and release each RC after Central confirms it
A new tag-and-release job waits for the orb's bump commit and for the
version on Maven Central, checks that the Central artifact was published
after this merge, then creates the signed annotated tag (SSH key from the
QRUN_TAG_SIGNING_KEY secret) on the bump commit and a GitHub prerelease
listing the merged PRs. It never creates an unsigned tag and skips a tag
that already exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 16:17:37 -05:00
james.maes a308e88c55 Merge pull request #1 from QRun-IO/feature/qbit-release-rc
feat: reusable qbit release-candidate workflow
2026-10-13 14:27:10 -05:00
3 changed files with 137 additions and 21 deletions

No files matched your search

-18
View File
@@ -1,18 +0,0 @@
# The org's CircleCI GitHub App builds every repository; without a config this repository
# reported "No configuration was found" as an error on every commit. Lint the workflows instead.
version: 2.1
jobs:
actionlint:
docker:
- image: cimg/go:1.24
steps:
- checkout
- run:
name: Lint GitHub Actions workflows
command: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.7 -color .github/workflows/*.yml
workflows:
lint:
jobs:
- actionlint
+123 -3
View File
@@ -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"
+14
View File
@@ -0,0 +1,14 @@
name: Workflow lint
'on':
push:
branches:
- '**'
pull_request: null
workflow_dispatch: null
permissions:
contents: read
jobs:
actionlint:
uses: QRun-IO/qqq-orb/.github/workflows/workflow-lint.yml@0f33ddadf88b9a1b940b8883782fe2000ee1d2bd
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name
!= github.repository