Skip to content

Commit dea3c37

Browse files
committed
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. `<version>1.0.0</version>` was hand-edited, so the version in the POM and the version in the tag were two facts that had to be kept in agreement by hand. The Maveniverse Nisse core extension derives it from git state instead (`${nisse.jgit.dynamicVersion}`), matching hatch-vcs and poetry-dynamic-versioning on the Python side: there is now no version string in this repo to edit. Verified locally -- tagging 9.9.9 makes `mvn help:evaluate -Dexpression= project.version` print exactly 9.9.9, and `${project.version}` in the annotationProcessorPath resolves unchanged. `mvn deploy` builds from the tag itself, so the release flow has no separate build-and-attach step; the artifacts Central receives are the ones Nisse stamped. `java-publish-to-maven.yml` asserts the resolved version matches the tag before deploying, which catches the one failure that matters here: a shallow clone, which makes Nisse compute a version rather than fail. Note for whoever cuts the next release: the existing 1.0.0 tag is not an ancestor of main, so until the first release from this flow, off-tag builds report 0.1.0-N-SNAPSHOT rather than 1.0.x-N-SNAPSHOT. Nothing publishes snapshots, and git-cliff reads the tag regardless (it proposes 1.1.0), so this is cosmetic and self-corrects at the first release. BREAKING CHANGE: the POM no longer carries a literal version. A build from a shallow clone or an export without .git resolves a different version than before. Signed-off-by: Jimisola Laursen <jimisola@jimisola.com>
1 parent e15f00b commit dea3c37

7 files changed

Lines changed: 147 additions & 50 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:
@@ -24,7 +38,14 @@ jobs:
2438
matrix:
2539
reqstool-source: [pypi, main]
2640
steps:
41+
# Full history and tags: Nisse derives the version from git state, so a
42+
# shallow clone would build the wrong number rather than fail.
2743
- uses: actions/checkout@v7
44+
with:
45+
persist-credentials: false
46+
fetch-depth: 0
47+
fetch-tags: true
48+
ref: ${{ inputs.ref || github.ref }}
2849
- name: Set up JDK
2950
uses: actions/setup-java@v5
3051
with:

‎.github/workflows/check_release.yml‎

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

‎.github/workflows/publish_maven.yml‎

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

‎.github/workflows/release.yml‎

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
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+
ref: ${{ inputs.ref }}
76+
77+
# `mvn deploy` builds from the tag itself, so there is no separate build step:
78+
# the artifacts it signs and uploads are the ones Nisse stamped from the tag.
79+
# `version` makes a disagreement a hard stop before anything reaches Central.
80+
publish-to-maven-central:
81+
needs: [prepare, tag]
82+
uses: reqstool/.github/.github/workflows/java-publish-to-maven.yml@main
83+
permissions:
84+
contents: read
85+
packages: write
86+
secrets: inherit
87+
with:
88+
ref: ${{ needs.prepare.outputs.version }}
89+
version: ${{ needs.prepare.outputs.version }}
90+
environment: stable
91+
92+
# Last, deliberately. Everything above can fail, and until this runs nothing
93+
# resolving "the latest release" can see what was built -- the release is still
94+
# a prerelease. Promotion itself is one API call against a release that already
95+
# has its artifacts.
96+
#
97+
# The guard is `no job failed`, not the default `every job succeeded`: a release
98+
# candidate deliberately skips the publish jobs that a real release runs, and a
99+
# skipped dependency would otherwise cascade and skip this too -- leaving the
100+
# candidate unpromoted, which is right, and every *real* release unpromoted the
101+
# moment any optional job is skipped, which is not.
102+
#
103+
# `!inputs.dry-run` has to be spelled out for the same reason: on a dry run
104+
# every job above is skipped, and "nothing failed" would otherwise be true.
105+
promote:
106+
needs: [prepare, publish-to-maven-central]
107+
if: ${{ !inputs.dry-run && !cancelled() && !contains(needs.*.result, 'failure') && !contains(needs.*.result, 'cancelled') }}
108+
uses: reqstool/.github/.github/workflows/common-release-promote.yml@main
109+
permissions:
110+
contents: write
111+
with:
112+
version: ${{ needs.prepare.outputs.version }}
113+
prerelease: ${{ needs.prepare.outputs.prerelease == 'true' }}

‎.mvn/extensions.xml‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<extensions>
3+
<!-- Derives ${nisse.jgit.dynamicVersion} from git state, so the tag is the
4+
only version. See RELEASING.md in reqstool/.github. -->
5+
<extension>
6+
<groupId>eu.maveniverse.maven.nisse</groupId>
7+
<artifactId>extension</artifactId>
8+
<version>0.9.5</version>
9+
</extension>
10+
</extensions>

‎.mvn/maven.config‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
-Dnisse.source.jgit.dynamicVersion=true

‎pom.xml‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
<groupId>io.github.reqstool</groupId>
88
<artifactId>reqstool-java-annotations</artifactId>
9-
<version>1.0.0</version>
9+
<!-- Derived from git state by the Nisse core extension (.mvn/extensions.xml). -->
10+
<version>${nisse.jgit.dynamicVersion}</version>
1011
<packaging>jar</packaging>
1112

1213
<name>${project.artifactId}</name>

0 commit comments

Comments
 (0)