diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bbf3d1c..07fd7e3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,15 +24,35 @@ concurrency: group: release cancel-in-progress: false -# Needed to push the tag and to create the GitHub release. +# Needed to push the tag/create the release and inspect prior publishing attempts. permissions: contents: write + actions: read jobs: release: name: Tag, release and publish to Sonatype runs-on: ubuntu-latest + defaults: + run: + shell: bash steps: + - name: Check publishing credentials + env: + ORG_GRADLE_PROJECT_centralUsername: ${{ secrets.CENTRAL_USERNAME }} + ORG_GRADLE_PROJECT_centralPassword: ${{ secrets.CENTRAL_PASSWORD }} + ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_KEY }} + ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }} + run: | + if [ "${ORG_GRADLE_PROJECT_centralUsername}" = "" ] || \ + [ "${ORG_GRADLE_PROJECT_centralPassword}" = "" ] || \ + [ "${ORG_GRADLE_PROJECT_signingKey}" = "" ] || \ + [ "${ORG_GRADLE_PROJECT_signingPassword}" = "" ]; then + echo "Configure the following secrets to enable uploading to Sonatype:" + echo "CENTRAL_USERNAME, CENTRAL_PASSWORD, SIGNING_KEY, SIGNING_PASSWORD" + exit 1 + fi + - name: Clone the repo uses: actions/checkout@v4 with: @@ -40,22 +60,43 @@ jobs: fetch-depth: 0 fetch-tags: true - - name: Compute the release version + - name: Reserve or reuse the release version id: version - run: echo "version=$(bash scripts/next-version.sh)" >> "$GITHUB_OUTPUT" + run: | + # Run IDs survive retries; a new dispatch still gets a new version. + marker="Release workflow ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + tags="$(git for-each-ref --format='%(objecttype)%09%(refname:strip=2)%09%(contents:subject)' refs/tags)" + version="$(awk -F '\t' -v marker="$marker" '$1 == "tag" && $3 == marker { print $2 }' <<< "$tags")" + + if [ -n "$version" ]; then + # Also reject multiple matching tags (the result would contain newlines). + if [[ ! "$version" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}(-[0-9]+)?$ ]]; then + echo "Invalid or ambiguous release tag for this workflow run." >&2 + exit 1 + fi + if [ "$(git rev-parse "refs/tags/${version}^{commit}")" != "$GITHUB_SHA" ]; then + echo "The release tag does not point to this workflow's commit." >&2 + exit 1 + fi + echo "Reusing release ${version}" + else + version="$(bash scripts/next-version.sh)" + git -c user.name='github-actions[bot]' \ + -c user.email='41898282+github-actions[bot]@users.noreply.github.com' \ + tag --annotate "$version" "$GITHUB_SHA" --message "$marker" + git push origin "refs/tags/${version}" + fi + echo "version=${version}" >> "$GITHUB_OUTPUT" - - name: Push the tag and create the GitHub release + - name: Create the GitHub release if needed env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.version.outputs.version }} run: | - version='${{ steps.version.outputs.version }}' - echo "Releasing ${version}" - gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" \ - -f ref="refs/tags/${version}" \ - -f sha="${GITHUB_SHA}" - gh release create "${version}" \ - --title "${version}" \ - --generate-notes + if ! gh release view "$VERSION" > /dev/null 2>&1; then + gh release create "$VERSION" \ + --verify-tag --title "$VERSION" --generate-notes + fi - name: Setup the java environment uses: actions/setup-java@v4 @@ -65,19 +106,23 @@ jobs: - name: Publish and close the Sonatype staging repository env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ steps.version.outputs.version }} ORG_GRADLE_PROJECT_centralUsername: ${{ secrets.CENTRAL_USERNAME }} ORG_GRADLE_PROJECT_centralPassword: ${{ secrets.CENTRAL_PASSWORD }} ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_KEY }} ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }} run: | - if [ "${ORG_GRADLE_PROJECT_centralUsername}" = "" ] || \ - [ "${ORG_GRADLE_PROJECT_centralPassword}" = "" ] || \ - [ "${ORG_GRADLE_PROJECT_signingKey}" = "" ] || \ - [ "${ORG_GRADLE_PROJECT_signingPassword}" = "" ]; then - echo "Configure the following secrets to enable uploading to Sonatype:" - echo "CENTRAL_USERNAME, CENTRAL_PASSWORD, SIGNING_KEY, SIGNING_PASSWORD" - exit 1 + if [ "$GITHUB_RUN_ATTEMPT" -gt 1 ]; then + # A later step (including cleanup) may have failed after publication. + published="$(gh api --paginate \ + "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/jobs?filter=all&per_page=100" \ + --jq '.jobs[].steps[]? | select(.name == "Publish and close the Sonatype staging repository" and .conclusion == "success") | .conclusion')" + if [ -n "$published" ]; then + echo "Release ${VERSION} was already published successfully; skipping upload." + exit 0 + fi fi ./gradlew publishToSonatype closeSonatypeStagingRepository \ - -PtestdataVersion="${{ steps.version.outputs.version }}" \ + -PtestdataVersion="$VERSION" \ --console=plain --stacktrace diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index 184bc29..01fd8d1 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -1,18 +1,26 @@ name: Publish snapshot to Maven Central -# Every commit that lands on the default branch publishes a snapshot of +# Every push to the default branch requests a snapshot of # org.jmonkeyengine:jme3-testdata to Sonatype, using the same date based # versioning as the releases (scripts/next-version.sh) plus the -SNAPSHOT # qualifier: -SNAPSHOT. # # Contrary to the releases, no tag and no GitHub release are created, and the -# version is not numbered: a Maven snapshot can be republished, so every commit -# of the day simply updates the -SNAPSHOT version. +# version is not numbered: a Maven snapshot can be republished, so commits of +# the day update the -SNAPSHOT version. Superseded commits are skipped. on: push: branches: [master] +# Snapshot uploads share Maven metadata, so never interrupt or overlap them. +concurrency: + group: publish-testdata-snapshot + cancel-in-progress: false + +permissions: + contents: read + jobs: snapshot: name: Publish snapshot to Sonatype @@ -23,7 +31,9 @@ jobs: - name: Compute the snapshot version id: version - run: echo "version=$(bash scripts/next-version.sh --snapshot)" >> "$GITHUB_OUTPUT" + run: | + version="$(bash scripts/next-version.sh --snapshot)" + echo "version=${version}" >> "$GITHUB_OUTPUT" - name: Setup the java environment uses: actions/setup-java@v4 @@ -38,6 +48,14 @@ jobs: ORG_GRADLE_PROJECT_signingKey: ${{ secrets.SIGNING_KEY }} ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.SIGNING_PASSWORD }} run: | + # Queued runs and manual reruns can target a superseded commit. + master_ref="$(git ls-remote --exit-code origin refs/heads/master)" + master_sha="${master_ref%%[[:space:]]*}" + if [ "${GITHUB_SHA}" != "${master_sha}" ]; then + echo "Skipping snapshot for ${GITHUB_SHA}; master is now ${master_sha}." + exit 0 + fi + if [ "${ORG_GRADLE_PROJECT_centralUsername}" = "" ] || \ [ "${ORG_GRADLE_PROJECT_centralPassword}" = "" ] || \ [ "${ORG_GRADLE_PROJECT_signingKey}" = "" ] || \ diff --git a/README.md b/README.md index 11c47b2..3ff714f 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,18 @@ resets the next day: | 3rd | `2026-09-19-3` | | 1st of the next day | `2026-09-20` | -Snapshots are published on every commit to `master` by the **Publish snapshot to -Maven Central** workflow (`.github/workflows/snapshot.yml`), as -`org.jmonkeyengine:jme3-testdata:-SNAPSHOT`. +Publishing credentials are checked before creating a tag or GitHub release. If a +release run fails, rerun it from the Actions tab: once its tag has been pushed, +every attempt reuses the same version and GitHub release, even on a later day. +An upload that a previous attempt recorded as successful is skipped. A new +manual dispatch starts a separate release and allocates a new version. + +Every push to `master` triggers the **Publish snapshot to Maven Central** workflow +(`.github/workflows/snapshot.yml`), publishing +`org.jmonkeyengine:jme3-testdata:-SNAPSHOT`. Snapshot uploads are +serialized without cancelling an upload in progress. Runs whose commit is no +longer the head of `master` are skipped before uploading, so a queued run or a +rerun of an older commit cannot replace a newer snapshot. The version of the day and the counter are computed by [`scripts/next-version.sh`](scripts/next-version.sh), covered by