Skip to content

Commit 3342ac7

Browse files
authored
feat(build)!: derive the version from git, and adopt the verified release flow (#173)
* 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> * 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 e15f00b commit 3342ac7

8 files changed

Lines changed: 149 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:
@@ -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-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_maven.yml‎

Lines changed: 0 additions & 38 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+
# `mvn deploy` builds from the tag itself, so there is no separate build step:
79+
# the artifacts it signs and uploads are the ones Nisse stamped from the tag.
80+
# `version` makes a disagreement a hard stop before anything reaches Central.
81+
publish-to-maven-central:
82+
needs: [prepare, tag]
83+
uses: reqstool/.github/.github/workflows/java-publish-to-maven.yml@main
84+
permissions:
85+
contents: read
86+
packages: write
87+
secrets: inherit
88+
with:
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-maven-central]
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' }}

‎.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)