Skip to content
Merged
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
69 changes: 67 additions & 2 deletions .github/workflows/auto-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,14 @@
# workflows). Until that secret exists, this job fails loudly BEFORE merging — bump PRs still open and
# run CI, but nothing merges or publishes. Adding RELEASE_TOKEN arms the loop.
#
# DISPATCH, NOT JUST THE TAG PUSH (#253): v0.3.3's tag went up with RELEASE_TOKEN and no Publish run
# ever started — whether a pushed tag triggers `on: push: tags` depends on what kind of credential the
# secret holds, which this workflow cannot see. So after pushing the tag the job also dispatches
# `publish.yml` on the tag ref (a workflow_dispatch made with GITHUB_TOKEN always triggers, which is
# why the job needs `actions: write`), then waits until a Publish run for the tag exists and fails
# loudly if none shows up. If the tag push does fire too, publish.yml's per-ref concurrency group and
# its already-released guard turn the second run into a clean no-op.
#
# The release version is the root pom's current `-SNAPSHOT` base (e.g. 0.1.3-SNAPSHOT -> 0.1.3), the
# Maven-idiomatic "version master is heading toward"; `publish.yml` advances the snapshot afterward.

Expand All @@ -31,6 +39,7 @@ on:
permissions:
contents: write
pull-requests: write
actions: write # dispatch publish.yml on the tag and watch for its run

env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Expand Down Expand Up @@ -122,7 +131,7 @@ jobs:
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
echo "publish=true" >> "$GITHUB_OUTPUT"

- name: Tag the merge commit (triggers publish.yml via RELEASE_TOKEN)
- name: Tag the merge commit
if: steps.ver.outputs.publish == 'true'
shell: bash
env:
Expand All @@ -139,4 +148,60 @@ jobs:
# Central, creates the GitHub Release, and advances master's -SNAPSHOT. The token is passed
# in the remote URL via env (GitHub Actions masks secrets in logs).
git push "https://x-access-token:${RELEASE_TOKEN}@github.com/${{ github.repository }}.git" "refs/tags/${TAG}"
echo "Pushed ${TAG}; publish.yml will deploy it and bump the snapshot."
echo "Pushed ${TAG}."

# Do not rely on the tag push alone to start publish.yml (#253). Give the push-triggered run a
# short grace period to appear; if it does not, dispatch publish.yml on the tag ref. Either way,
# require a Publish run for the tag to exist before this job succeeds, so a silent no-publish
# fails here instead of leaving a tagged-but-unpublished release.
- name: Make sure publish.yml runs for the tag
if: steps.ver.outputs.publish == 'true'
shell: bash
env:
TAG: ${{ steps.ver.outputs.tag }}
REPO: ${{ github.repository }}
run: |
set -euo pipefail

# A Publish run for a tag (push- or dispatch-triggered) reports the tag name as its head
# branch. The tag is brand new, so any such run belongs to this release.
publish_runs() {
gh run list --repo "${REPO}" --workflow publish.yml --branch "${TAG}" --limit 20 \
--json databaseId,event,status,url \
--jq '.[] | "\(.databaseId) \(.event) \(.status) \(.url)"'
}

# wait_for_run <seconds>: poll until a Publish run for the tag exists; print it and succeed,
# or fail once the deadline passes.
wait_for_run() {
local deadline=$(( SECONDS + $1 )) runs
while :; do
runs="$(publish_runs)"
if [ -n "${runs}" ]; then
printf '%s\n' "${runs}"
return 0
fi
[ "${SECONDS}" -ge "${deadline}" ] && return 1
sleep 10
done
}

if runs="$(wait_for_run 45)"; then
echo "The tag push started publish.yml for ${TAG}; no dispatch needed:"
printf '%s\n' "${runs}"
exit 0
fi

echo "No Publish run for ${TAG} after the tag push; dispatching publish.yml on the tag ref."
gh workflow run publish.yml --repo "${REPO}" --ref "${TAG}"

if runs="$(wait_for_run 300)"; then
echo "publish.yml is running for ${TAG}; it deploys the release and bumps the snapshot:"
printf '%s\n' "${runs}"
exit 0
fi

echo "::error::No Publish run for ${TAG} appeared within 5 minutes of the tag push and an explicit"
echo "::error::dispatch. ${TAG} is tagged but NOT published. Publish it by hand:"
echo "::error:: gh workflow run publish.yml --repo ${REPO} --ref ${TAG}"
exit 1
50 changes: 46 additions & 4 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,20 @@ on:
push:
branches: [master]
tags: ['v*']
# A release can also arrive as a dispatch on the tag ref: auto-release.yml runs
# `gh workflow run publish.yml --ref vX.Y.Z` after pushing the tag, so the release does not depend
# on the tag push triggering this workflow (#253). On such a dispatch github.ref is refs/tags/vX.Y.Z
# and GITHUB_REF_NAME is vX.Y.Z, exactly as on a tag push, so every tag-gated step below behaves
# the same either way.
workflow_dispatch:

# The tag push AND auto-release's dispatch can both start a run for the same tag. Serialize runs per
# ref (never cancel one mid-deploy) so the second starts only after the first has finished, and let
# the "already released" guard below turn it into a clean no-op.
concurrency:
group: publish-${{ github.ref }}
cancel-in-progress: false

jobs:
publish:
name: Publish to Maven Central
Expand All @@ -30,19 +42,49 @@ jobs:
CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
# Require the full credential set; a partial config would red-fail the signing/deploy
# steps, so skip cleanly instead and tell the maintainer what is missing.
if [ -n "$CENTRAL_USERNAME" ] && [ -n "$CENTRAL_PASSWORD" ] \
&& [ -n "$GPG_PRIVATE_KEY" ] && [ -n "$GPG_PASSPHRASE" ]; then
echo "deploy=true" >> "$GITHUB_OUTPUT"
else
if ! { [ -n "$CENTRAL_USERNAME" ] && [ -n "$CENTRAL_PASSWORD" ] \
&& [ -n "$GPG_PRIVATE_KEY" ] && [ -n "$GPG_PASSPHRASE" ]; }; then
echo "Publishing credentials incomplete — skipping deploy."
echo "Configure all of MAVEN_CENTRAL_USERNAME, MAVEN_CENTRAL_PASSWORD, GPG_PRIVATE_KEY"
echo "and MAVEN_GPG_PASSPHRASE repository secrets to enable Central deployment."
echo "deploy=false" >> "$GITHUB_OUTPUT"
exit 0
fi

# Double-publish guard: on a release tag, no-op if this version already shipped (a second run
# for the same tag — tag push + dispatch, or a re-run). Central refuses to overwrite a release,
# so redeploying would only fail. Two independent signals, either one sufficient:
# - the GitHub Release object, which a successful run creates as its last publish step;
# - the Central Portal's own "is this published" answer, for a run that deployed but died
# before creating the Release. Only a definitive `"published":true` counts; any other
# answer (error, false) proceeds to deploy, where a real duplicate fails loudly.
case "$GITHUB_REF" in
refs/tags/v*)
VERSION="${GITHUB_REF_NAME#v}"
if gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
echo "::notice::Release $GITHUB_REF_NAME already exists — $VERSION was published by an earlier run; skipping."
echo "deploy=false" >> "$GITHUB_OUTPUT"
exit 0
fi
AUTH="$(printf '%s:%s' "$CENTRAL_USERNAME" "$CENTRAL_PASSWORD" | base64 -w0)"
PUBLISHED="$(curl -sS --max-time 30 -H "Authorization: Bearer $AUTH" \
"https://central.sonatype.com/api/v1/publisher/published?namespace=io.github.achird-labs&name=rift-java-core&version=$VERSION" \
|| echo '{}')"
echo "Central Portal: rift-java-core $VERSION -> $PUBLISHED"
if printf '%s' "$PUBLISHED" | grep -Eq '"published"[[:space:]]*:[[:space:]]*true'; then
echo "::notice::rift-java-core $VERSION is already on Maven Central; skipping the redeploy."
echo "deploy=false" >> "$GITHUB_OUTPUT"
exit 0
fi
;;
esac
echo "deploy=true" >> "$GITHUB_OUTPUT"

# The reactor runs on JDK 21 (LTS): it builds the zero-dep modules and the JDK 21 preview
# embedded variant (rift-java-embedded-jdk21) natively. JDK 22 is installed alongside so the
# stable-FFM rift-java-embedded jar can be compiled via a toolchain in the same reactor and
Expand Down
11 changes: 9 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,11 @@ Artifacts publish to Maven Central under the `io.github.achird-labs` namespace v

- **Snapshots** deploy automatically on every push to `master`, at whatever `-SNAPSHOT` version the
root pom currently carries.
- **Releases are cut by pushing a `vX.Y.Z` tag** — that is the only trigger. The `Publish` workflow
- **Releases are cut by a `vX.Y.Z` tag** — pushing it triggers `Publish`, and so does dispatching
`Publish` on the tag ref (`gh workflow run publish.yml --ref vX.Y.Z`), which is how to publish a
tag whose push did not start a run. Runs are serialized per tag, and a run for a version that
already has a GitHub Release (or is already on Central) skips the deploy, so a duplicate trigger is
a harmless no-op. The `Publish` workflow
stamps every module with the version from the tag, deploys, *then* creates the GitHub Release
object and pushes a follow-up commit advancing `master` to the next `-SNAPSHOT` (which also syncs
the README's install snippets to the released version).
Expand All @@ -159,7 +163,10 @@ separate `RELEASE_TOKEN` secret.

`Engine Bump` (weekly, plus `workflow_dispatch`) polls `achird-labs/rift` and opens a
`chore/engine-<version>` PR through the reusable `dep-bump.yml`; when CI on that PR is green,
`auto-release.yml` merges it and pushes the release tag that `Publish` acts on. The loop needs **two**
`auto-release.yml` merges it and pushes the release tag that `Publish` acts on. It does not trust the
tag push alone to start `Publish`: if no run for the tag appears within a short grace period it
dispatches `publish.yml` on the tag ref itself, and it fails if no `Publish` run for the tag exists
five minutes later — so a tagged-but-unpublished release is a red run, not a silent gap (#253). The loop needs **two**
repository secrets, and it stalls in a different place if either is missing:

| secret | used for | if absent |
Expand Down
Loading