API-Only Suite Plugin - Release #55
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |