diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1fe2b21..a3e7d09 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,5 +1,19 @@ on: workflow_call: + inputs: + ref: + description: "Branch, tag or SHA to build. Empty = the caller's ref." + required: false + type: string + default: "" + artifact-name: + description: > + Name to upload the build output under. The release flow runs this + workflow twice in one run -- once on the branch, once on the tag -- and + upload-artifact rejects a duplicate name. + required: false + type: string + default: "dist" workflow_dispatch: push: branches: @@ -21,7 +35,14 @@ jobs: matrix: reqstool-source: [pypi, main] steps: + # Full history and tags: git-dyn-semver derives the version from git state, so a + # shallow clone would build the wrong number rather than fail. - uses: actions/checkout@v7 + with: + persist-credentials: false + fetch-depth: 0 + fetch-tags: true + ref: ${{ inputs.ref || github.ref }} - name: Set up Java uses: actions/setup-java@v5 with: diff --git a/.github/workflows/check-semantic-pr.yml b/.github/workflows/check-semantic-pr.yml index b26224b..81a00ea 100644 --- a/.github/workflows/check-semantic-pr.yml +++ b/.github/workflows/check-semantic-pr.yml @@ -10,4 +10,4 @@ permissions: jobs: check: - uses: reqstool/.github/.github/workflows/common-check-semantic-pr.yml@e1d67194373e4da7ccfdf400f46201f18ca14f23 # main 2026-03-07 + uses: reqstool/.github/.github/workflows/common-check-semantic-pr.yml@main diff --git a/.github/workflows/check_release.yml b/.github/workflows/check_release.yml deleted file mode 100644 index 62fe017..0000000 --- a/.github/workflows/check_release.yml +++ /dev/null @@ -1,11 +0,0 @@ -name: Check rules for release -on: - workflow_call: - -jobs: - check-release: - runs-on: ubuntu-latest - steps: - - name: Check branch and tag - if: github.event_name == 'push' && !(github.ref == 'refs/heads/main' && startsWith(github.ref, 'refs/tags/')) - run: exit 1 diff --git a/.github/workflows/publish_plugin_portal.yml b/.github/workflows/publish_plugin_portal.yml deleted file mode 100644 index b544196..0000000 --- a/.github/workflows/publish_plugin_portal.yml +++ /dev/null @@ -1,30 +0,0 @@ - -name: Publish Gradle Plugin to Plugin Portal -on: - release: - types: [created] - -jobs: - check-release: - name: Reuse check release - uses: ./.github/workflows/check_release.yml - build: - name: Reuse build - uses: ./.github/workflows/build.yml - publish: - needs: - - build - - check-release - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - name: Set up Java for publishing to Gradle Plugin Portal - uses: actions/setup-java@v5 - with: - java-version: "21" - distribution: "temurin" - - name: Publish plugin to Gradle Plugin Portal - run: ./gradlew publishPlugins - env: - GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} - GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..646905f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,114 @@ +name: Release + +# The whole release, start to finish. See RELEASING.md in reqstool/.github for +# what each step does, and for why the release is created as a prerelease rather +# than a draft. + +on: + workflow_dispatch: + inputs: + version: + description: "Version to release (Maven, no v prefix), e.g. 1.1.0. Leave empty to auto-detect from Conventional Commits." + required: false + type: string + prerelease: + description: "Publish as a release candidate instead of a release: verified like any release, but never promoted to latest. The number is chosen for you (1.1.0 -> 1.1.0-rc1, then the next)." + required: false + type: choice + options: [none, rc, b, a] + default: none + ref: + description: "Branch to release from. Leave empty for the branch this workflow was dispatched on." + required: false + type: string + force: + description: "Allow a version that disagrees with the auto-detected one." + required: false + type: boolean + default: false + dry-run: + description: "Validate and preview only -- nothing tagged, nothing published." + required: false + type: boolean + default: true + +concurrency: + group: release + cancel-in-progress: false + +permissions: + contents: read + +jobs: + prepare: + uses: reqstool/.github/.github/workflows/common-release-prepare.yml@main + permissions: + contents: read + with: + version-format: maven + version: ${{ inputs.version }} + prerelease: ${{ inputs.prerelease }} + ref: ${{ inputs.ref }} + force: ${{ inputs.force }} + dry-run: ${{ inputs.dry-run }} + + # The same checks that guard main, called rather than reimplemented, and run + # before the approval gate so the reviewer approves something already green + # rather than a version string. + checks: + needs: prepare + if: ${{ !inputs.dry-run }} + uses: ./.github/workflows/build.yml + permissions: + contents: read + + # THE APPROVAL GATE -- bound to the `stable` environment, so it sits pending + # until a required reviewer approves it on the run page. + tag: + needs: [prepare, checks] + if: ${{ !inputs.dry-run }} + uses: reqstool/.github/.github/workflows/common-release-tag.yml@main + permissions: + contents: write + with: + version: ${{ needs.prepare.outputs.version }} + version-format: maven + ref: ${{ inputs.ref }} + + # `./gradlew publishPlugins` builds from the tag itself, so there is no + # separate build step. `version` makes a disagreement a hard stop before + # anything reaches the portal. + publish-to-plugin-portal: + needs: [prepare, tag] + uses: reqstool/.github/.github/workflows/java-publish-to-gradle.yml@main + permissions: + contents: read + secrets: inherit + with: + target: portal + ref: ${{ needs.prepare.outputs.version }} + version: ${{ needs.prepare.outputs.version }} + environment: stable + + # Last, deliberately. Everything above can fail, and until this runs nothing + # resolving "the latest release" can see what was built -- the release is still + # a prerelease. Promotion itself is one API call against a release that already + # has its artifacts. + # + # The guard is `no job failed`, not the default `every job succeeded`: a release + # candidate deliberately skips the publish jobs that a real release runs, and a + # skipped dependency would otherwise cascade and skip this too -- leaving the + # candidate unpromoted, which is right, and every *real* release unpromoted the + # moment any optional job is skipped, which is not. + # + # `!inputs.dry-run` has to be spelled out for the same reason: on a dry run + # every job above is skipped, and "nothing failed" would otherwise be true. + promote: + needs: [prepare, publish-to-plugin-portal] + if: ${{ !inputs.dry-run && !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }} + uses: reqstool/.github/.github/workflows/common-release-promote.yml@main + permissions: + contents: write + with: + version: ${{ needs.prepare.outputs.version }} + prerelease: ${{ needs.prepare.outputs.prerelease == 'true' }} diff --git a/build.gradle b/build.gradle index 056bd5c..e37f2b7 100644 --- a/build.gradle +++ b/build.gradle @@ -4,18 +4,13 @@ plugins { id 'maven-publish' id 'io.spring.javaformat' version '0.0.47' id 'com.gradle.plugin-publish' version '2.1.1' - id 'pl.allegro.tech.build.axion-release' version '1.21.3' -} - -scmVersion { - tag { - prefix = '' - } - versionCreator 'simple' + // Sets the project version from git tags using Conventional Commits bump + // logic -- the Gradle counterpart to Nisse on the Maven side, so the tag is + // the only version across the org. See RELEASING.md in reqstool/.github. + id 'io.github.jimisola.git-dyn-semver' version '0.1.2' } group = 'io.github.reqstool' -version = scmVersion.version java { sourceCompatibility = JavaVersion.VERSION_21