Skip to content

API-Only Suite Plugin - Release #55

API-Only Suite Plugin - Release

API-Only Suite Plugin - Release #55

name: API-Only Suite Plugin - Release
on:
push:
branches:
- main
paths:
- 'api-only-suite/*.gradle'
- 'api-only-suite/gradle/**'
- 'api-only-suite/src/main/**'
- 'api-only-suite/docs/**'
# A sibling detector plugin releasing a new version - even one that touches none of the paths
# above - still means the published api-only-suite plugin should pick up that new version, since
# Publish below resolves each sibling's real version from its own release tags. Without this,
# a sibling-only change (nothing under api-only-suite/ itself) never re-triggers this workflow at
# all, and api-only-suite stays silently pinned to whatever sibling versions it last resolved.
workflow_run:
workflows:
- "Shadow API Detector Plugin - Release"
- "Mirage API Detector Plugin - Release"
- "Doppelganger API Detector Plugin - Release"
types:
- completed
branches:
- main
jobs:
Calculate-Version:
# Skip entirely when the triggering sibling release workflow itself failed or was skipped -
# its own Publish step won't have created a new tag, so there's nothing new to resolve here.
if: ${{ github.event_name == 'push' || github.event.workflow_run.conclusion == 'success' }}
uses: ./.github/workflows/semantic-version-calculate.yml
with:
# A direct push to api-only-suite's own files should always yield a new version - an empty
# result there means a conventional-commit mistake worth failing loudly on. A workflow_run
# trigger, by contrast, fires once for every sibling detector plugin release, and commits are
# analyzed repo-wide with no per-project scope filtering: whichever of those runs happens to
# execute first picks up every real fix/feat commit and genuinely releases, leaving every
# other sibling's run in the same burst with nothing new to find - a normal, expected no-op,
# not a failure. The jobs below already gate on next_version_empty instead of next_version
# itself, so there is nothing for a hard failure here to protect once that path is disabled.
#
# fail_on_existing_tag must be disabled the same way, and for the same reason every
# *-build.yml workflow already disables both together: when nothing new was calculated,
# next_version falls back to the *current* version, whose tag trivially already exists -
# that's not a real conflict, just what the fallback always looks like. Left enabled, it
# would misfire as a "tag conflict" every time this path finds nothing to release, exactly
# like fail_on_empty_version would.
fail_on_empty_version: ${{ github.event_name == 'push' }}
fail_on_existing_tag: ${{ github.event_name == 'push' }}
project_dir: api-only-suite
tag_format: 'api-only-suite-v${version}'
DEBUG: true
secrets:
sem_rel_token_token: ${{ secrets.SEMANTIC_RELEASE_TOKEN }}
Build:
name: Build API-Only Suite Plugin
needs: Calculate-Version
permissions:
contents: write
issues: write
pull-requests: write
runs-on: ubuntu-latest
env:
GRADLE_PROPERTIES_FILE: ./api-only-suite/gradle.properties
GRADLE_VERSION_REGEX: version\s*=\s*[0-9]\\+\.[0-9]\\+\.[0-9]\\+[a-zA-Z0-9.-]*
VERSION: ${{ needs.Calculate-Version.outputs.next_version }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
# api-only-suite pulls shadow/mirage/doppelganger-api-detector in as subprojects, so they
# resolve api-detector-core from api-only-suite's own catalog rather than their own - a
# hand-copied pin here is easy to forget to bump and has broken this exact Build job three
# times in a row after the siblings adopted api-detector-core 1.5.0. Derive it instead of
# trusting a human to keep it in sync.
- name: Align api-only-suite's api-detector-core pin with sibling plugins
uses: ./.github/actions/align-api-detector-core-version
- name: Setup Java
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin'
java-version: 21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
- name: Update version in Gradle property file
run: |
echo "VERSION=${{ env.VERSION }}"
echo ===== Before - ${{ env.GRADLE_PROPERTIES_FILE }} =====
cat ${{ env.GRADLE_PROPERTIES_FILE }}
sed -i "s/${{ env.GRADLE_VERSION_REGEX }}/version = ${{ env.VERSION }}/g" ${{ env.GRADLE_PROPERTIES_FILE }}
echo ===== After - ${{ env.GRADLE_PROPERTIES_FILE }} =====
cat ${{ env.GRADLE_PROPERTIES_FILE }}
- name: Build and test API-Only Suite Plugin
run: |
cd api-only-suite
chmod +x ./gradlew
./gradlew build
env:
GITHUB_ACTOR: ${{ github.actor }}
GITHUB_TOKEN: ${{ secrets.SEDR_AUTOMATION_TOKEN }}
- name: Upload test report
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: api-only-suite-plugin-test-results
path: api-only-suite/build/reports/tests/**
Release-Plugin:
name: Release API-Only Suite Plugin
needs: [Calculate-Version, Build]
# next_version itself is never empty - it falls back to the current version so a build-only
# caller always has something to compile with - so next_version_empty is what actually signals
# "there is nothing new to release", not a string-emptiness check on next_version.
if: ${{ needs.Calculate-Version.outputs.next_version_empty == 'false' }}
uses: ./.github/workflows/semantic-version-apply.yml
with:
project_dir: api-only-suite
secrets:
sem_rel_token_token: ${{ secrets.SEMANTIC_RELEASE_TOKEN }}
Wait-For-Sibling-Releases:
name: Wait for sibling detector plugin releases
needs: Calculate-Version
# next_version itself is never empty - it falls back to the current version so a build-only
# caller always has something to compile with - so next_version_empty is what actually signals
# "there is nothing new to release", not a string-emptiness check on next_version.
if: ${{ needs.Calculate-Version.outputs.next_version_empty == 'false' }}
runs-on: ubuntu-latest
steps:
- name: Wait for any sibling detector plugin release still in flight
# api-only-suite pulls shadow/mirage/doppelganger-api-detector in as subprojects and the
# Publish job below resolves their real released version from git tags. This workflow fires
# both when api-only-suite's own files change (push) and whenever any sibling's own release
# workflow completes (workflow_run, added so a sibling-only change still reaches a new
# api-only-suite release). Either way, a *different* sibling's release triggered by the same
# underlying push can still be in flight when we reach this point, which would silently pin
# api-only-suite to that sibling's stale previous version.
#
# Rather than diffing the triggering push - workflow_run events don't carry a comparable
# before/after pair - check each sibling's own release workflow's most recent run on main
# directly and wait for it if still in progress. This is correct for both trigger types and
# for any number of siblings releasing concurrently, at the cost of not knowing in advance
# which siblings are actually relevant to this run (so it checks all three, unconditionally),
# and of no longer aborting outright when a sibling's most recent run failed: unlike the
# old push-diff version, this can't tell "a sibling relevant to this exact push failed" from
# "some unrelated earlier run of that sibling failed", so it simply waits for in-progress
# runs and otherwise proceeds - the Resolve sibling plugin versions step below then reads
# each sibling's last successfully tagged version regardless, never a broken one.
#
# This traces back to a real incident: api-only-suite 0.2.0 was published referencing
# shadow-api-detector 1.2.0 and mirage-api-detector 0.2.0 instead of the 1.3.0/0.3.0 that
# same push actually released, because this job didn't exist yet.
#
# This job has no checkout step - it never touches git, only the GitHub API via `gh` - so
# every `gh run list` call below passes --repo explicitly rather than relying on `gh`
# auto-detecting the repository from a local .git directory that doesn't exist here.
# Omitting --repo broke this job's very first real run: "failed to determine base repo:
# fatal: not a git repository", which silently skipped Publish and left a real release
# (api-only-suite 0.4.2) tagged but never uploaded to the Gradle Plugin Portal.
env:
GH_TOKEN: ${{ secrets.SEDR_AUTOMATION_TOKEN }}
run: |
set -euo pipefail
SIBLING_WORKFLOWS=(
"Shadow API Detector Plugin - Release"
"Mirage API Detector Plugin - Release"
"Doppelganger API Detector Plugin - Release"
)
TIMEOUT_SECONDS=1200
INTERVAL_SECONDS=20
for WF in "${SIBLING_WORKFLOWS[@]}"; do
echo "Checking '$WF' for an in-progress run on main..."
ELAPSED=0
while :; do
STATUS=$(gh run list --repo "${{ github.repository }}" --workflow "$WF" --branch main --limit 1 --json status -q '.[0].status // empty')
if [[ -z "$STATUS" || "$STATUS" == "completed" ]]; then
echo "'$WF' has no run in progress."
break
fi
if (( ELAPSED >= TIMEOUT_SECONDS )); then
echo "::error::Timed out after ${TIMEOUT_SECONDS}s waiting for '$WF' to finish (last status: '$STATUS'). Re-run this workflow once it has completed."
exit 1
fi
echo "'$WF' status: '$STATUS' - waiting ${INTERVAL_SECONDS}s..."
sleep "$INTERVAL_SECONDS"
ELAPSED=$((ELAPSED + INTERVAL_SECONDS))
done
done
Publish:
name: Publish API-Only Suite Plugin to Gradle Portal
needs: [Calculate-Version, Release-Plugin, Wait-For-Sibling-Releases]
# next_version itself is never empty - it falls back to the current version so a build-only
# caller always has something to compile with - so next_version_empty is what actually signals
# "there is nothing new to release", not a string-emptiness check on next_version.
if: ${{ needs.Calculate-Version.outputs.next_version_empty == 'false' }}
runs-on: ubuntu-latest
env:
GRADLE_PROPERTIES_FILE: ./api-only-suite/gradle.properties
GRADLE_VERSION_REGEX: version\s*=\s*[0-9]\\+\.[0-9]\\+\.[0-9]\\+[a-zA-Z0-9.-]*
VERSION: ${{ needs.Calculate-Version.outputs.next_version }}
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
ref: ${{ github.ref_name }}
# Same reasoning as the Build job above - this job has its own separate checkout, so it
# needs the same alignment to publish a POM that references the api-detector-core version
# the siblings actually built against.
- name: Align api-only-suite's api-detector-core pin with sibling plugins
uses: ./.github/actions/align-api-detector-core-version
- name: Setup Java
uses: actions/setup-java@ad2b38190b15e4d6bdf0c97fb4fca8412226d287 # v5.3.0
with:
distribution: 'temurin'
java-version: 21
- name: Setup Gradle
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
- name: Update version in Gradle property file
run: |
sed -i "s/${{ env.GRADLE_VERSION_REGEX }}/version = ${{ env.VERSION }}/g" ${{ env.GRADLE_PROPERTIES_FILE }}
- name: Resolve sibling plugin versions
# shadow/mirage/doppelganger-api-detector are pulled in as subprojects of this build, but
# their own gradle.properties always stays at the unpublished placeholder version (each
# plugin's own release workflow only patches it transiently, in its own separate CI run,
# never commits the bump back to main). Left as-is, api-only-suite's published POM would
# declare a runtime dependency on that placeholder version, which nobody could resolve -
# exactly the bug this repo just finished fixing for api-detector-core. Resolve each
# sibling's real latest released version from its own tag namespace instead.
id: sibling-versions
run: |
SHADOW_VERSION=$(git tag -l 'shadow-api-detector-v*' | sed 's/^shadow-api-detector-v//' | sort -V | tail -1)
MIRAGE_VERSION=$(git tag -l 'mirage-api-detector-v*' | sed 's/^mirage-api-detector-v//' | sort -V | tail -1)
DOPPELGANGER_VERSION=$(git tag -l 'doppelganger-api-detector-v*' | sed 's/^doppelganger-api-detector-v//' | sort -V | tail -1)
{
echo "shadow_version=${SHADOW_VERSION}"
echo "mirage_version=${MIRAGE_VERSION}"
echo "doppelganger_version=${DOPPELGANGER_VERSION}"
} >> "$GITHUB_OUTPUT"
echo "::notice title=Resolved Sibling Versions::shadow-api-detector=${SHADOW_VERSION} mirage-api-detector=${MIRAGE_VERSION} doppelganger-api-detector=${DOPPELGANGER_VERSION}"
- name: Update sibling plugin versions in their Gradle property files
run: |
sed -i "s/${{ env.GRADLE_VERSION_REGEX }}/version = ${{ steps.sibling-versions.outputs.shadow_version }}/g" shadow-api-detector/gradle.properties
sed -i "s/${{ env.GRADLE_VERSION_REGEX }}/version = ${{ steps.sibling-versions.outputs.mirage_version }}/g" mirage-api-detector/gradle.properties
sed -i "s/${{ env.GRADLE_VERSION_REGEX }}/version = ${{ steps.sibling-versions.outputs.doppelganger_version }}/g" doppelganger-api-detector/gradle.properties
- name: Publish to Gradle Plugin Portal
run: |
cd api-only-suite
chmod +x ./gradlew
# Scoped to the root project only: shadow/mirage/doppelganger-api-detector are pulled in
# as subprojects of this build, and an unscoped `publishPlugins` matches the task by name
# across every project that has it - including those three, causing it to try to
# republish them too. That collides with their own independent release workflows (which
# may be publishing the same coordinates concurrently) and gets rejected by the Portal.
./gradlew :publishPlugins --no-daemon --no-configuration-cache
env:
GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }}
GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }}
GITHUB_ACTOR: ${{ github.actor }}
GITHUB_TOKEN: ${{ secrets.SEDR_AUTOMATION_TOKEN }}
Determine-Release-Type:
needs: [Calculate-Version]
# next_version itself is never empty - it falls back to the current version so a build-only
# caller always has something to compile with - so next_version_empty is what actually signals
# "there is nothing new to release", not a string-emptiness check on next_version.
if: ${{ needs.Calculate-Version.outputs.next_version_empty == 'false' }}
uses: ./.github/workflows/determine-release-type.yml
Workflow-Summary:
name: Workflow Summary
needs: [Build, Publish, Release-Plugin, Wait-For-Sibling-Releases, Determine-Release-Type]
if: always()
runs-on: ubuntu-latest
steps:
- name: Output release completion
run: |
echo "API-Only Suite Plugin ${{ needs.Determine-Release-Type.outputs.release_type }} release workflow completed"
echo "::notice title=Release Complete::API-Only Suite Plugin ${{ needs.Determine-Release-Type.outputs.release_type }} release workflow completed"