Skip to content

Commit 58ac708

Browse files
authored
feat(build)!: derive the version from git, and adopt the verified release flow (#77)
* feat(build)!: derive the version from git, and adopt the verified release flow The old arrangement created a draft, a human clicked Publish, and only then did the publish workflows run -- so everything that verified a release ran after it was already public. The release is now created as a prerelease and promoted only once everything that can fail has succeeded; the confirmation step is an environment approval on the job that tags, reached after lint and build are already green. The version no longer has to be typed: it is auto-detected from Conventional Commits, and passing one that disagrees needs `force`. `prerelease: rc` cuts a release candidate, verified exactly like a release but never promoted to latest. See RELEASING.md in reqstool/.github for the whole flow. Replaces axion-release with git-dyn-semver. Both derive the version from git tags, so this is not a change of principle -- it is the same plugin the rest of the org's Gradle builds will use, and it applies Conventional Commits bump logic to compute the next version rather than only reading the last tag. Pure JGit, no git CLI required. Verified locally -- tagging 9.9.9 makes `./gradlew printVersion` print exactly 9.9.9; off-tag it yields {next}-{distance}-SNAPSHOT. `./gradlew publishPlugins` builds from the tag itself, so the release flow has no separate build-and-attach step. `java-publish-to-gradle.yml` asserts the resolved version matches the tag before publishing, which catches the one failure that matters here: a shallow clone, which makes the plugin compute a version rather than fail. BREAKING CHANGE: the version is now computed by a different plugin. An off-tag build's version string changes shape (git-dyn-semver's {next}-{distance}-SNAPSHOT, not axion-release's). Tagged builds are unaffected. Signed-off-by: Jimisola Laursen <jimisola@jimisola.com> * fix(ci): point the semantic-PR check at a ref that has the workflow check-semantic-pr.yml pinned common-check-semantic-pr.yml at e1d67194373e4da7ccfdf400f46201f18ca14f23. That commit predates the file: the `common-` rename came later, so the pinned tree has no such workflow. GitHub cannot resolve a workflow_call to a path that does not exist, so the run failed at startup with no jobs -- and because that produces no check run, it never appeared in the PR checks list. PR titles have therefore not been validated here since the pin was written. Following @main, as the other callers do, until the org settles on re-pinning. Signed-off-by: Jimisola Laursen <jimisola@jimisola.com> * fix(release): pass version-format to the tag job common-release-tag.yml now validates the version and the ref itself rather than trusting that prepare validated the same values (CodeQL flagged the privileged checkout on an unvalidated ref in reqstool/.github#66). Validating the version needs to know which format to validate against. Signed-off-by: Jimisola Laursen <jimisola@jimisola.com> --------- Signed-off-by: Jimisola Laursen <jimisola@jimisola.com>
1 parent 45cca38 commit 58ac708

6 files changed

Lines changed: 140 additions & 51 deletions

File tree

.github/workflows/build.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,19 @@
11
on:
22
workflow_call:
3+
inputs:
4+
ref:
5+
description: "Branch, tag or SHA to build. Empty = the caller's ref."
6+
required: false
7+
type: string
8+
default: ""
9+
artifact-name:
10+
description: >
11+
Name to upload the build output under. The release flow runs this
12+
workflow twice in one run -- once on the branch, once on the tag -- and
13+
upload-artifact rejects a duplicate name.
14+
required: false
15+
type: string
16+
default: "dist"
317
workflow_dispatch:
418
push:
519
branches:
@@ -21,7 +35,14 @@ jobs:
2135
matrix:
2236
reqstool-source: [pypi, main]
2337
steps:
38+
# Full history and tags: git-dyn-semver derives the version from git state, so a
39+
# shallow clone would build the wrong number rather than fail.
2440
- uses: actions/checkout@v7
41+
with:
42+
persist-credentials: false
43+
fetch-depth: 0
44+
fetch-tags: true
45+
ref: ${{ inputs.ref || github.ref }}
2546
- name: Set up Java
2647
uses: actions/setup-java@v5
2748
with:

.github/workflows/check-semantic-pr.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ permissions:
1010

1111
jobs:
1212
check:
13-
uses: reqstool/.github/.github/workflows/common-check-semantic-pr.yml@e1d67194373e4da7ccfdf400f46201f18ca14f23 # main 2026-03-07
13+
uses: reqstool/.github/.github/workflows/common-check-semantic-pr.yml@main

.github/workflows/check_release.yml

Lines changed: 0 additions & 11 deletions
This file was deleted.

.github/workflows/publish_plugin_portal.yml

Lines changed: 0 additions & 30 deletions
This file was deleted.

.github/workflows/release.yml

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
name: Release
2+
3+
# The whole release, start to finish. See RELEASING.md in reqstool/.github for
4+
# what each step does, and for why the release is created as a prerelease rather
5+
# than a draft.
6+
7+
on:
8+
workflow_dispatch:
9+
inputs:
10+
version:
11+
description: "Version to release (Maven, no v prefix), e.g. 1.1.0. Leave empty to auto-detect from Conventional Commits."
12+
required: false
13+
type: string
14+
prerelease:
15+
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)."
16+
required: false
17+
type: choice
18+
options: [none, rc, b, a]
19+
default: none
20+
ref:
21+
description: "Branch to release from. Leave empty for the branch this workflow was dispatched on."
22+
required: false
23+
type: string
24+
force:
25+
description: "Allow a version that disagrees with the auto-detected one."
26+
required: false
27+
type: boolean
28+
default: false
29+
dry-run:
30+
description: "Validate and preview only -- nothing tagged, nothing published."
31+
required: false
32+
type: boolean
33+
default: true
34+
35+
concurrency:
36+
group: release
37+
cancel-in-progress: false
38+
39+
permissions:
40+
contents: read
41+
42+
jobs:
43+
prepare:
44+
uses: reqstool/.github/.github/workflows/common-release-prepare.yml@main
45+
permissions:
46+
contents: read
47+
with:
48+
version-format: maven
49+
version: ${{ inputs.version }}
50+
prerelease: ${{ inputs.prerelease }}
51+
ref: ${{ inputs.ref }}
52+
force: ${{ inputs.force }}
53+
dry-run: ${{ inputs.dry-run }}
54+
55+
# The same checks that guard main, called rather than reimplemented, and run
56+
# before the approval gate so the reviewer approves something already green
57+
# rather than a version string.
58+
checks:
59+
needs: prepare
60+
if: ${{ !inputs.dry-run }}
61+
uses: ./.github/workflows/build.yml
62+
permissions:
63+
contents: read
64+
65+
# THE APPROVAL GATE -- bound to the `stable` environment, so it sits pending
66+
# until a required reviewer approves it on the run page.
67+
tag:
68+
needs: [prepare, checks]
69+
if: ${{ !inputs.dry-run }}
70+
uses: reqstool/.github/.github/workflows/common-release-tag.yml@main
71+
permissions:
72+
contents: write
73+
with:
74+
version: ${{ needs.prepare.outputs.version }}
75+
version-format: maven
76+
ref: ${{ inputs.ref }}
77+
78+
# `./gradlew publishPlugins` builds from the tag itself, so there is no
79+
# separate build step. `version` makes a disagreement a hard stop before
80+
# anything reaches the portal.
81+
publish-to-plugin-portal:
82+
needs: [prepare, tag]
83+
uses: reqstool/.github/.github/workflows/java-publish-to-gradle.yml@main
84+
permissions:
85+
contents: read
86+
secrets: inherit
87+
with:
88+
target: portal
89+
ref: ${{ needs.prepare.outputs.version }}
90+
version: ${{ needs.prepare.outputs.version }}
91+
environment: stable
92+
93+
# Last, deliberately. Everything above can fail, and until this runs nothing
94+
# resolving "the latest release" can see what was built -- the release is still
95+
# a prerelease. Promotion itself is one API call against a release that already
96+
# has its artifacts.
97+
#
98+
# The guard is `no job failed`, not the default `every job succeeded`: a release
99+
# candidate deliberately skips the publish jobs that a real release runs, and a
100+
# skipped dependency would otherwise cascade and skip this too -- leaving the
101+
# candidate unpromoted, which is right, and every *real* release unpromoted the
102+
# moment any optional job is skipped, which is not.
103+
#
104+
# `!inputs.dry-run` has to be spelled out for the same reason: on a dry run
105+
# every job above is skipped, and "nothing failed" would otherwise be true.
106+
promote:
107+
needs: [prepare, publish-to-plugin-portal]
108+
if: ${{ !inputs.dry-run && !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
109+
uses: reqstool/.github/.github/workflows/common-release-promote.yml@main
110+
permissions:
111+
contents: write
112+
with:
113+
version: ${{ needs.prepare.outputs.version }}
114+
prerelease: ${{ needs.prepare.outputs.prerelease == 'true' }}

build.gradle

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,13 @@ plugins {
44
id 'maven-publish'
55
id 'io.spring.javaformat' version '0.0.47'
66
id 'com.gradle.plugin-publish' version '2.1.1'
7-
id 'pl.allegro.tech.build.axion-release' version '1.21.3'
8-
}
9-
10-
scmVersion {
11-
tag {
12-
prefix = ''
13-
}
14-
versionCreator 'simple'
7+
// Sets the project version from git tags using Conventional Commits bump
8+
// logic -- the Gradle counterpart to Nisse on the Maven side, so the tag is
9+
// the only version across the org. See RELEASING.md in reqstool/.github.
10+
id 'io.github.jimisola.git-dyn-semver' version '0.1.2'
1511
}
1612

1713
group = 'io.github.reqstool'
18-
version = scmVersion.version
1914

2015
java {
2116
sourceCompatibility = JavaVersion.VERSION_21

0 commit comments

Comments
 (0)