Skip to content

Commit a8080a9

Browse files
committed
docs: define snapshot version strategy
1 parent 737a9cc commit a8080a9

1 file changed

Lines changed: 157 additions & 7 deletions

File tree

docs/plans/2026-05-16-semantic-versioning-script.md

Lines changed: 157 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,56 @@ git fetch --no-tags origin '+refs/tags/token-rotate/v1.5.0-rc.*:refs/tags/token-
139139

140140
Avoid branch reachability checks such as `git tag --merged` in the first version because they require commit history and defeat shallow-checkout performance. Tags are global refs, not branch-local refs. If the business rule needs branch-specific versions, encode the release line into the tag prefix or branch name instead of trying to infer it from commit reachability.
141141

142+
### Snapshot version strategy
143+
144+
Snapshots should be treated as ephemeral CI artifact versions, not permanent version reservations. The script should not scan tags, create tags, or update a central counter just to produce a snapshot. For performance, generate snapshots from the next release base plus GitLab CI's already-available unique identifiers.
145+
146+
Recommended snapshot format for SemVer-compatible consumers:
147+
148+
```text
149+
<base-version>-snapshot.<pipeline-iid>.<commit-short-sha>
150+
```
151+
152+
Examples:
153+
154+
```text
155+
1.4.8-snapshot.732.a1b2c3d4
156+
1.5.0-rc.3.snapshot.733.a1b2c3d4
157+
```
158+
159+
If a target package ecosystem requires Maven-style snapshots, support a template override:
160+
161+
```bash
162+
--snapshot-template '{base_version}-SNAPSHOT'
163+
```
164+
165+
Default behavior:
166+
167+
- `snapshot` mode never creates a tag.
168+
- `snapshot` mode never increments or reserves the patch/RC counter.
169+
- Snapshot uniqueness comes from `CI_PIPELINE_IID` plus `CI_COMMIT_SHORT_SHA`.
170+
- Snapshot ordering is good enough for CI artifacts because `CI_PIPELINE_IID` is monotonic inside a GitLab project.
171+
- Module name should be part of the artifact path or package coordinates, not necessarily part of the SemVer string.
172+
173+
Base version rules:
174+
175+
| Branch/mode | Base version | Snapshot output example |
176+
| --- | --- | --- |
177+
| `main` / `snapshot` | next patch from latest stable | `1.4.8-snapshot.732.a1b2c3d4` |
178+
| feature branch / `snapshot` | next patch from latest stable | `1.4.8-snapshot.feature-login.732.a1b2c3d4` if branch slug is enabled |
179+
| `release/1.5` / `snapshot` | target release line | `1.5.0-snapshot.732.a1b2c3d4` |
180+
| after existing `1.5.0-rc.2` | next RC base without reserving it | `1.5.0-rc.3.snapshot.732.a1b2c3d4` |
181+
182+
Important: if strict SemVer precedence matters, avoid publishing snapshots to the same channel as immutable releases/RCs. SemVer prerelease ordering can be surprising for mixed labels like `rc.3.snapshot.732`. The safest operational rule is: snapshots go to a snapshot/dev repository or package channel; releases and RCs go to release channels.
183+
184+
If the business requires per-module sequential snapshot numbers, do not derive that from Git tags. Use one of these explicit state stores instead:
185+
186+
1. GitLab project/group variable per module, updated only by a serialized release/snapshot job.
187+
2. GitLab Generic Package Registry state file such as `version-state/<module>/state.json`.
188+
3. A small external version service.
189+
190+
For the first implementation, avoid stateful snapshot counters. Use `CI_PIPELINE_IID` for unique snapshots and keep state only for immutable releases/RCs.
191+
142192
### Branch/mode strategy
143193

144194
Support both auto-detection and explicit mode:
@@ -165,6 +215,7 @@ Modes:
165215
| `auto` | Detect from branch pattern | `1.4.8` or `1.5.0-rc.1` |
166216
| `patch` | Always bump patch | `1.4.8` |
167217
| `rc` | Bump/start/increment release candidate | `1.5.0-rc.1` |
218+
| `snapshot` | Generate an ephemeral CI artifact version without reserving it | `1.4.8-snapshot.732.a1b2c3d4` |
168219

169220
Default RC branch patterns:
170221

@@ -220,7 +271,7 @@ Required:
220271
Logical module name. Used to filter module-scoped tags.
221272
222273
Options:
223-
--mode auto|patch|rc
274+
--mode auto|patch|rc|snapshot
224275
Version generation mode. Default: auto.
225276
226277
--branch BRANCH
@@ -238,6 +289,13 @@ Options:
238289
--write-env PATH
239290
Write CI dotenv output.
240291
292+
--snapshot-template TEMPLATE
293+
Format for snapshot versions. Default:
294+
{base_version}-snapshot.{pipeline_iid}.{commit_short_sha}
295+
296+
--snapshot-include-branch
297+
Include the sanitized branch slug in snapshot versions.
298+
241299
--fetch-tags
242300
Run git fetch --tags before reading tags. Intended only for small repos.
243301
@@ -758,7 +816,7 @@ git commit -m "feat: calculate next patch version"
758816

759817
---
760818

761-
### Task 6: Implement RC branch detection and minor bump
819+
### Task 7: Implement RC branch detection and minor bump
762820

763821
**Objective:** Auto-detect release-candidate branches and calculate minor RC versions.
764822

@@ -823,7 +881,93 @@ git commit -m "feat: calculate release candidate minor versions"
823881

824882
---
825883

826-
### Task 7: Wire end-to-end calculation into CLI output
884+
### Task 8: Implement snapshot version calculation
885+
886+
**Objective:** Generate unique ephemeral snapshot versions without fetching all tags or reserving a release number.
887+
888+
**Files:**
889+
- Modify: `semantic-version/semantic_version_bumper.py`
890+
- Modify: `semantic-version/test_semantic_version_bumper.py`
891+
892+
**Step 1: Add failing tests**
893+
894+
Add tests for:
895+
896+
```python
897+
class SnapshotCalculationTests(unittest.TestCase):
898+
def test_snapshot_uses_next_patch_base_and_ci_identifiers(self):
899+
module = load_module()
900+
versions = [module.SemVer.parse("1.4.7")]
901+
result = module.next_snapshot_version(
902+
versions=versions,
903+
initial_version=module.SemVer.parse("0.0.0"),
904+
branch="main",
905+
pipeline_iid="732",
906+
commit_short_sha="a1b2c3d4",
907+
include_branch=False,
908+
)
909+
self.assertEqual(result, "1.4.8-snapshot.732.a1b2c3d4")
910+
911+
def test_release_branch_snapshot_uses_release_line_without_reserving_rc(self):
912+
module = load_module()
913+
versions = [module.SemVer.parse("1.4.7")]
914+
result = module.next_snapshot_version(
915+
versions=versions,
916+
initial_version=module.SemVer.parse("0.0.0"),
917+
branch="release/1.5",
918+
pipeline_iid="733",
919+
commit_short_sha="b2c3d4e5",
920+
include_branch=False,
921+
)
922+
self.assertEqual(result, "1.5.0-snapshot.733.b2c3d4e5")
923+
```
924+
925+
**Step 2: Implement snapshot helpers**
926+
927+
Implement:
928+
929+
- `sanitize_prerelease_identifier(value)` for branch slugs.
930+
- `resolve_snapshot_base(versions, initial_version, branch)`.
931+
- `next_snapshot_version(...)`.
932+
933+
Use `CI_PIPELINE_IID` and `CI_COMMIT_SHORT_SHA` by default when CLI flags are not provided. If they are missing outside CI, fall back to `0` and the local Git short SHA, or fail with a clear message when Git is unavailable.
934+
935+
**Step 3: Add CLI options**
936+
937+
Add:
938+
939+
```text
940+
--mode snapshot
941+
--snapshot-template
942+
--snapshot-include-branch
943+
--pipeline-iid
944+
--commit-short-sha
945+
```
946+
947+
Default template:
948+
949+
```text
950+
{base_version}-snapshot.{pipeline_iid}.{commit_short_sha}
951+
```
952+
953+
**Step 4: Verify**
954+
955+
```bash
956+
python -m unittest semantic-version/test_semantic_version_bumper.py -v
957+
```
958+
959+
Expected: PASS.
960+
961+
**Step 5: Commit**
962+
963+
```bash
964+
git add semantic-version/semantic_version_bumper.py semantic-version/test_semantic_version_bumper.py
965+
git commit -m "feat: calculate ephemeral snapshot versions"
966+
```
967+
968+
---
969+
970+
### Task 9: Wire end-to-end calculation into CLI output
827971

828972
**Objective:** Make the CLI calculate and print version output from real tags.
829973

@@ -875,7 +1019,7 @@ git commit -m "feat: expose semantic version calculation through CLI"
8751019

8761020
---
8771021

878-
### Task 8: Add dotenv output for CI
1022+
### Task 10: Add dotenv output for CI
8791023

8801024
**Objective:** Support CI systems that pass variables between jobs.
8811025

@@ -920,7 +1064,7 @@ git commit -m "feat: write semantic version output for CI"
9201064

9211065
---
9221066

923-
### Task 9: Add README documentation
1067+
### Task 11: Add README documentation
9241068

9251069
**Objective:** Document usage for single-module and multi-module projects.
9261070

@@ -980,7 +1124,7 @@ git commit -m "docs: document semantic version bumper"
9801124

9811125
---
9821126

983-
### Task 10: Integrate with existing quality checks
1127+
### Task 12: Integrate with existing quality checks
9841128

9851129
**Objective:** Ensure the new script is included in repository quality gates.
9861130

@@ -1035,7 +1179,9 @@ git commit -m "ci: include semantic version bumper tests"
10351179
- No prior tags produce deterministic initial versions.
10361180
- Output is CI-friendly key/value text by default.
10371181
- Optional `--write-env` writes a dotenv file.
1038-
- Unit tests cover SemVer parsing, tag matching, patch generation, RC generation, and end-to-end CLI output.
1182+
- Snapshot mode generates unique versions from `CI_PIPELINE_IID` and `CI_COMMIT_SHORT_SHA` without creating tags or reserving counters.
1183+
- Large repositories can use GitLab API tag filtering instead of fetching all tags/history.
1184+
- Unit tests cover SemVer parsing, tag matching, patch generation, RC generation, snapshot generation, and end-to-end CLI output.
10391185
- Existing repository quality workflow remains green.
10401186

10411187
---
@@ -1051,6 +1197,8 @@ git commit -m "ci: include semantic version bumper tests"
10511197
| `module/v1.2.3`, `module/v1.3.0-rc.1` | `release/1.3` | `auto` | `1.3.0-rc.2` | `module/v1.3.0-rc.2` |
10521198
| `module/v1.2.3`, `other/v9.9.9` | `main` | `auto` | `1.2.4` | `module/v1.2.4` |
10531199
| `v2.0.0` with `--tag-template 'v{version}'` | `main` | `auto` | `2.0.1` | `v2.0.1` |
1200+
| `module/v1.4.7` | `main` | `snapshot` | `1.4.8-snapshot.732.a1b2c3d4` | none |
1201+
| `module/v1.4.7` | `release/1.5` | `snapshot` | `1.5.0-snapshot.733.b2c3d4e5` | none |
10541202

10551203
---
10561204

@@ -1063,5 +1211,7 @@ These are worth confirming before coding if the script will be used across many
10631211
3. Should the script ever create Git tags, or only calculate the next version?
10641212
4. Should module names allow slashes, for example `services/api`, or should they be simple slugs only?
10651213
5. Should version files be updated, for example `pyproject.toml`, `package.json`, or `__init__.py`, or should CI consume the generated version without editing files?
1214+
6. Should snapshots use strict SemVer prerelease style, Maven-style `-SNAPSHOT`, or an ecosystem-specific template?
1215+
7. Should snapshots be unique only per pipeline, or is a per-module sequential snapshot counter required?
10661216

10671217
Recommended first implementation: calculate only, do not tag, do not edit version files. This keeps the script safe, reusable, and easy to test. Add mutation features only after version calculation is trusted.

0 commit comments

Comments
 (0)