From 14cd544e0572f846173c6ceb7328d470e57c7030 Mon Sep 17 00:00:00 2001 From: Gronod Date: Tue, 15 Sep 2026 13:10:19 +0100 Subject: [PATCH] =?UTF-8?q?feat(#189):=20release-driven=20versioning=20?= =?UTF-8?q?=E2=80=94=20tag-stamped=20About=20+=20monotonic=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - scripts/version.sh resolves ICCERY_RELEASE_TAG / MARKETING_VERSION / CURRENT_PROJECT_VERSION from RELEASE_TAG env or `git describe`; tag builds hard-fail when the tag's X.Y.Z != project.yml MARKETING_VERSION - build number = `git rev-list --count HEAD` (Apple: macOS CFBundleVersion must monotonically increase, no per-version reset) - tag ships as a bundled ICCeryReleaseTag resource — a generated Info.plist can't carry custom keys (INFOPLIST_KEY_* allowlist, and ProcessInfoPlistFile runs after script phases); About shows "tag (marketing)", e.g. v2.0.0-pre2-grok (2.0.0) - DMG named ICCery--.dmg for tagged/described builds - CI twins: fetch-depth 0 + RELEASE_TAG env + stamped test builds --- .gitea/workflows/macos.yml | 21 ++++- .github/workflows/macos.yml | 21 ++++- AGENTS.md | 7 ++ .../ICCeryCore/Files/ArtefactFiles.swift | 19 +++- README.md | 32 ++++++- .../ICCeryCoreTests/ArtefactFilesTests.swift | 56 ++++++++++++ docs/21-ui-reference.md | 2 +- docs/PREUAT.md | 5 +- project.yml | 16 ++++ scripts/package-release.sh | 19 +++- scripts/version.sh | 88 +++++++++++++++++++ 11 files changed, 279 insertions(+), 7 deletions(-) create mode 100755 scripts/version.sh diff --git a/.gitea/workflows/macos.yml b/.gitea/workflows/macos.yml index 029a94a..6beb628 100644 --- a/.gitea/workflows/macos.yml +++ b/.gitea/workflows/macos.yml @@ -17,9 +17,16 @@ jobs: runs-on: macos-12 env: DERIVED: build/DerivedData-test + # Tag name on tag pushes, branch name otherwise — scripts/version.sh + # only honours values matching 'v[0-9]*'. + RELEASE_TAG: ${{ github.ref_name }} steps: - name: Checkout uses: actions/checkout@v4 + with: + # Full history + tags: scripts/version.sh needs `git describe` and + # `git rev-list --count`; the default depth-1 clone has neither. + fetch-depth: 0 - name: Assert Xcode 14+ toolchain run: | @@ -51,6 +58,10 @@ jobs: - name: Build for testing (host arch) if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))" run: | + # Stamp the test build with the same version triple packaging uses + # so CI-built apps are traceable (and mis-tags fail early) (#189). + eval "$(scripts/version.sh)" + echo "version=$MARKETING_VERSION build=$CURRENT_PROJECT_VERSION tag=$ICCERY_RELEASE_TAG" xcodebuild build-for-testing \ -scheme ICCery \ -destination 'platform=macOS' \ @@ -59,7 +70,10 @@ jobs: ARCHS="$(uname -m)" \ ONLY_ACTIVE_ARCH=NO \ CODE_SIGNING_ALLOWED=YES \ - CODE_SIGN_IDENTITY='-' + CODE_SIGN_IDENTITY='-' \ + MARKETING_VERSION="$MARKETING_VERSION" \ + CURRENT_PROJECT_VERSION="$CURRENT_PROJECT_VERSION" \ + ICCERY_RELEASE_TAG="$ICCERY_RELEASE_TAG" # Xcode embeds the shared ICCeryCore package framework into the app # and the test bundle without signing it. Ad-hoc hosts still require @@ -191,6 +205,10 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + # Full history + tags: scripts/version.sh needs `git describe` and + # `git rev-list --count`; the default depth-1 clone has neither. + fetch-depth: 0 # scripts/package-release.sh runs `xcodegen generate` and dmgbuild; # see build-and-test for why brew is not used on macOS 12 (#109). @@ -202,6 +220,7 @@ jobs: - name: Package release run: scripts/package-release.sh env: + RELEASE_TAG: ${{ github.ref_name }} GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} CODESIGN_IDENTITY: ${{ secrets.CODESIGN_IDENTITY }} DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }} diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 0ff14c9..8792757 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -30,9 +30,16 @@ jobs: runs-on: macos-14 env: DERIVED: build/DerivedData-test + # Tag name on tag pushes, branch name otherwise — scripts/version.sh + # only honours values matching 'v[0-9]*'. + RELEASE_TAG: ${{ github.ref_name }} steps: - name: Checkout uses: actions/checkout@v4 + with: + # Full history + tags: scripts/version.sh needs `git describe` and + # `git rev-list --count`; the default depth-1 clone has neither. + fetch-depth: 0 - name: Assert Xcode 14+ toolchain run: | @@ -64,6 +71,10 @@ jobs: - name: Build for testing (host arch) if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))" run: | + # Stamp the test build with the same version triple packaging uses + # so CI-built apps are traceable (and mis-tags fail early) (#189). + eval "$(scripts/version.sh)" + echo "version=$MARKETING_VERSION build=$CURRENT_PROJECT_VERSION tag=$ICCERY_RELEASE_TAG" xcodebuild build-for-testing \ -scheme ICCery \ -destination 'platform=macOS' \ @@ -72,7 +83,10 @@ jobs: ARCHS="$(uname -m)" \ ONLY_ACTIVE_ARCH=NO \ CODE_SIGNING_ALLOWED=YES \ - CODE_SIGN_IDENTITY='-' + CODE_SIGN_IDENTITY='-' \ + MARKETING_VERSION="$MARKETING_VERSION" \ + CURRENT_PROJECT_VERSION="$CURRENT_PROJECT_VERSION" \ + ICCERY_RELEASE_TAG="$ICCERY_RELEASE_TAG" # Xcode embeds the shared ICCeryCore package framework into the app # and the test bundle without signing it. Ad-hoc hosts still require @@ -191,6 +205,10 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + # Full history + tags: scripts/version.sh needs `git describe` and + # `git rev-list --count`; the default depth-1 clone has neither. + fetch-depth: 0 # scripts/package-release.sh runs `xcodegen generate` and dmgbuild; # see build-and-test for why brew is not used on macOS 12 (#109). @@ -202,6 +220,7 @@ jobs: - name: Package release run: scripts/package-release.sh env: + RELEASE_TAG: ${{ github.ref_name }} CODESIGN_IDENTITY: ${{ secrets.CODESIGN_IDENTITY }} DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }} NOTARIZE_APPLE_ID: ${{ secrets.NOTARIZE_APPLE_ID }} diff --git a/AGENTS.md b/AGENTS.md index 6344dd5..552bff2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -39,6 +39,13 @@ Empty cwd illegal (#59). Atomic writes = `.tmp` + rename (#213). User-supplied strings via SwiftUI `Text` only (#114). TIFF never rendered directly — host-side PNG preview (#58). +## Versioning +`scripts/version.sh` is the single source: tag/describe → `ICCERY_RELEASE_TAG` +(About shows `tag (marketing)`), `MARKETING_VERSION` = strict `X.Y.Z`, +`CURRENT_PROJECT_VERSION` = `git rev-list --count HEAD` (#189). +`v*` tag builds hard-fail if tag's X.Y.Z ≠ `project.yml` MARKETING_VERSION — +bump `project.yml` on `develop` before tagging. CI needs `fetch-depth: 0`. + ## Branching `develop` ← `milestone/mN-` ← `feat/-`. PRs via Gitea MCP. Every issue/PR: `Project/ICCery-v2` + `Feature/*` or `Bug/*` + `Priority/*`. diff --git a/Packages/ICCeryCore/Sources/ICCeryCore/Files/ArtefactFiles.swift b/Packages/ICCeryCore/Sources/ICCeryCore/Files/ArtefactFiles.swift index d40b262..8bda93c 100644 --- a/Packages/ICCeryCore/Sources/ICCeryCore/Files/ArtefactFiles.swift +++ b/Packages/ICCeryCore/Sources/ICCeryCore/Files/ArtefactFiles.swift @@ -20,8 +20,14 @@ public enum ArtefactFiles { bundle: Bundle = .main ) -> (version: String, build: String, buildDate: String) { let info = bundle.infoDictionary ?? [:] - let version = info["CFBundleShortVersionString"] as? String ?? "0.0.0" + let short = info["CFBundleShortVersionString"] as? String ?? "0.0.0" let build = info["CFBundleVersion"] as? String ?? "0" + var tag = "" + if let url = bundle.url(forResource: "ICCeryReleaseTag", withExtension: nil), + let raw = try? String(contentsOf: url, encoding: .utf8) { + tag = raw + } + let version = displayVersion(shortVersion: short, releaseTag: tag) let url = bundle.executableURL ?? bundle.bundleURL let buildDate: String @@ -37,4 +43,15 @@ public enum ArtefactFiles { return (version, build, buildDate) } + + /// About "Version:" string from the release tag + marketing version + /// (#189): `v2.0.0-pre2 (2.0.0)`; dedupes to `v2.0.0` on an exact + /// release tag; falls back to the short version when untagged. + static func displayVersion(shortVersion: String, releaseTag: String) -> String { + let tag = releaseTag.trimmingCharacters(in: .whitespacesAndNewlines) + if tag.isEmpty || tag == shortVersion || tag == "v\(shortVersion)" { + return tag.isEmpty ? shortVersion : tag + } + return "\(tag) (\(shortVersion))" + } } diff --git a/README.md b/README.md index ccbdfe1..59f4158 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,35 @@ The script builds with a fixed derived data path (`build/DerivedData`), locates `Release/ICCery.app` from it, signs the bundle, recursively verifies every bundled Mach-O sidecar (`scripts/verify-sidecar-signatures.sh`), builds a HiDPI TIFF from `Resources/dmg-background.png` (+ `@2x`) via `tiffutil`, and -writes `ICCery-${VERSION}-${BUILD_NUM}.dmg` with `dmgbuild==1.6.7`. +writes the DMG with `dmgbuild==1.6.7`. + +### Versioning + +`scripts/version.sh` resolves the version triple and is the single source for +packaging and CI: + +- **`ICCERY_RELEASE_TAG`** — `RELEASE_TAG` env when it matches `v[0-9]*` (CI + tag builds), else `git describe --tags --always --dirty --match 'v[0-9]*'`. + Stamped into the bundle's `ICCeryReleaseTag` resource (a generated + Info.plist cannot carry custom keys); the About dialog shows + `tag (marketing)` — e.g. `v2.0.0-pre2-grok (2.0.0)` — falling back to the + plain version when untagged. +- **`MARKETING_VERSION`** (`CFBundleShortVersionString`) — first three numeric + components of the tag core. Must equal `project.yml`'s + `MARKETING_VERSION` on tag builds — packaging hard-fails on mismatch, so + bump `project.yml` on `develop` *before* tagging. +- **`CURRENT_PROJECT_VERSION`** (`CFBundleVersion`) — `BUILD_NUMBER` env + override, else `git rev-list --count HEAD`. A single monotonically + increasing integer per Apple's macOS convention (Mac build numbers never + reset per version, unlike iOS). + +Tagged/described DMGs carry the tag: `ICCery-2.0.0-pre2-grok-.dmg`; +plain releases keep `ICCery--.dmg`. + +Release procedure: bump `MARKETING_VERSION` in `project.yml` on `develop` → +merge `develop` → `main` → tag the release commit `vX.Y.Z[-suffix]` → push +the tag. CI builds, signs, notarizes (when secrets exist) and attaches the +DMG to the release. Sidecars stay ad-hoc signed inside the bundle — the app is never `codesign --deep`ed. @@ -145,6 +173,8 @@ Environment variables read by the pipeline: | `CODESIGN_IDENTITY` | Developer ID identity for the outer `.app`; unset or `-` = ad-hoc | | `DEVELOPMENT_TEAM` | team ID passed to `xcodebuild` when signing | | `NOTARIZE_APPLE_ID` / `NOTARIZE_PASSWORD` / `APPLE_TEAM_ID` | `notarytool` + staple when all three are set | +| `RELEASE_TAG` | release tag string (CI sets `github.ref_name`); stamped into the bundle + DMG name | +| `BUILD_NUMBER` | `CFBundleVersion` override; default `git rev-list --count HEAD` | ## Layout diff --git a/Tests/ICCeryCoreTests/ArtefactFilesTests.swift b/Tests/ICCeryCoreTests/ArtefactFilesTests.swift index f5c7346..41a606f 100644 --- a/Tests/ICCeryCoreTests/ArtefactFilesTests.swift +++ b/Tests/ICCeryCoreTests/ArtefactFilesTests.swift @@ -102,6 +102,62 @@ final class TiffPreviewTests: XCTestCase { } } +final class AppInfoTests: XCTestCase { + // MARK: displayVersion (#189) + + func testNoTagFallsBackToShortVersion() { + XCTAssertEqual( + ArtefactFiles.displayVersion(shortVersion: "2.0.0", releaseTag: ""), + "2.0.0" + ) + } + + func testWhitespaceTagFallsBackToShortVersion() { + XCTAssertEqual( + ArtefactFiles.displayVersion(shortVersion: "2.0.0", releaseTag: " "), + "2.0.0" + ) + } + + func testExactReleaseTagDedupes() { + XCTAssertEqual( + ArtefactFiles.displayVersion(shortVersion: "2.0.0", releaseTag: "v2.0.0"), + "v2.0.0" + ) + } + + func testBareVersionTagDedupes() { + XCTAssertEqual( + ArtefactFiles.displayVersion(shortVersion: "2.0.0", releaseTag: "2.0.0"), + "2.0.0" + ) + } + + func testPrereleaseTagShowsMarketingInParens() { + XCTAssertEqual( + ArtefactFiles.displayVersion( + shortVersion: "2.0.0", releaseTag: "v2.0.0-pre2-grok"), + "v2.0.0-pre2-grok (2.0.0)" + ) + } + + func testDescribeStringShowsMarketingInParens() { + XCTAssertEqual( + ArtefactFiles.displayVersion( + shortVersion: "2.0.0", releaseTag: "v2.0.0-5-gdeadbee"), + "v2.0.0-5-gdeadbee (2.0.0)" + ) + } + + func testAppInfoUsesComposedVersion() { + // .main in the test host is the ICCery app under test; whatever it + // resolves to, build must be a non-empty digits-or-default string. + let info = ArtefactFiles.appInfo() + XCTAssertFalse(info.version.isEmpty) + XCTAssertFalse(info.build.isEmpty) + } +} + final class ArtefactFilesTests: XCTestCase { func testBase64RoundTrip() throws { let url = tempURL("a.txt") diff --git a/docs/21-ui-reference.md b/docs/21-ui-reference.md index 72ff8ce..07acf38 100644 --- a/docs/21-ui-reference.md +++ b/docs/21-ui-reference.md @@ -69,7 +69,7 @@ Keyboard: **R** resets gamut camera when Stage 5 is visible. Bind to a focusable | Dialog | Root id | Controls | |--------|---------|----------| | Settings | `settingsDialog` | `settingsDeltaEGood`, `settingsDeltaEWarning`, `settingsCalStaleDays`; other controls listed in [22](22-settings-presets.md) | -| About | `aboutDialog` | `aboutVersion`, `aboutBuildDate` from `get_app_info`, `closeAboutBtn` | +| About | `aboutDialog` | `aboutVersion` (release tag + marketing, e.g. `v2.0.0-pre2 (2.0.0)`), `aboutBuildDate` from `get_app_info`, `closeAboutBtn` | | Save preset | `savePresetDialog` | `savePresetName`, `savePresetDesc`, `btnConfirmSavePreset`, `btnCloseSavePresetDialog` | | Manage presets | `managePresetsDialog` | `managePresetsList`, `btnExportActivePreset`, `btnImportPreset`, `btnCloseManagePresetsDialog` | | Save media recipe | `saveMediaRecipeDialog` | `saveMediaName`, `saveMediaNotes`, `saveMediaPaper`, `saveMediaInk`, `saveMediaPrinter`, `saveMediaPreset`, `saveMediaColourSpace`, `saveMediaCal`, `saveMediaApplyCal`, `btnConfirmSaveMedia`, `btnCloseSaveMediaDialog` | diff --git a/docs/PREUAT.md b/docs/PREUAT.md index e7041c1..b2f740c 100644 --- a/docs/PREUAT.md +++ b/docs/PREUAT.md @@ -43,6 +43,8 @@ Do not install Argyll to `$PATH`. ICCery never searches `$PATH` for binaries. | `NOTARIZE_APPLE_ID` | Apple ID for `notarytool` | | `NOTARIZE_PASSWORD` | App-specific password for `notarytool` | | `APPLE_TEAM_ID` | Team ID for `notarytool` | +| `RELEASE_TAG` | Optional. Simulates a tag build (`vX.Y.Z[-suffix]`); stamped into the bundle and DMG name | +| `BUILD_NUMBER` | Optional `CFBundleVersion` override; default `git rev-list --count HEAD` | Invocation for a signed, notarized DMG: @@ -56,7 +58,8 @@ export APPLE_TEAM_ID="TEAMID" scripts/package-release.sh ``` -The script will emit `ICCery--.dmg`. After mounting, verify Gatekeeper acceptance: +The script will emit `ICCery--.dmg` (or `ICCery--.dmg` +when `RELEASE_TAG`/a `v*` git tag applies). After mounting, verify Gatekeeper acceptance: ``` spctl -a -t open --context context:primary-signature -v ICCery-*.dmg diff --git a/project.yml b/project.yml index b706f78..68caf45 100644 --- a/project.yml +++ b/project.yml @@ -42,6 +42,15 @@ targets: echo "note: Vendor/Argyll absent — run scripts/fetch-argyll.sh" fi basedOnDependencyAnalysis: false + - name: Stamp release tag + # GENERATE_INFOPLIST_FILE writes Info.plist after script phases and + # only honours Apple's INFOPLIST_KEY_* allowlist — so the tag ships as + # a bundled resource read by ArtefactFiles.appInfo (#189). + script: | + DEST="$TARGET_BUILD_DIR/$UNLOCALIZED_RESOURCES_FOLDER_PATH" + mkdir -p "$DEST" + printf '%s' "${ICCERY_RELEASE_TAG:-}" > "$DEST/ICCeryReleaseTag" + basedOnDependencyAnalysis: false settings: base: PRODUCT_BUNDLE_IDENTIFIER: com.gronod.iccery2 @@ -54,6 +63,13 @@ targets: INFOPLIST_KEY_NSHumanReadableCopyright: "Copyright © 2026 Gronod. AGPLv3." MARKETING_VERSION: "2.0.0" CURRENT_PROJECT_VERSION: "1" + # Stamped by scripts/version.sh at packaging/CI time (release tag or + # `git describe` string); About shows it next to the marketing version. + # Written to the bundled ICCeryReleaseTag resource by the + # "Stamp release tag" phase — GENERATE_INFOPLIST_FILE drops custom + # INFOPLIST_KEY_* names and generates the plist after script phases. + # Tag builds hard-fail when the tag's X.Y.Z != MARKETING_VERSION above. + ICCERY_RELEASE_TAG: "" ENABLE_HARDENED_RUNTIME: YES CODE_SIGN_ENTITLEMENTS: Resources/ICCery.entitlements CODE_SIGN_IDENTITY: "-" diff --git a/scripts/package-release.sh b/scripts/package-release.sh index 872688e..7c13ed6 100755 --- a/scripts/package-release.sh +++ b/scripts/package-release.sh @@ -45,6 +45,13 @@ DEST="platform=macOS" IDENTITY="${CODESIGN_IDENTITY:--}" DEVELOPMENT_TEAM="${DEVELOPMENT_TEAM:-}" +# Resolve the version triple from the release tag / git state. On tag builds +# (RELEASE_TAG=v*) this hard-fails if the tag's X.Y.Z != project.yml +# MARKETING_VERSION, so a mis-tag cannot ship mismatched metadata (#189). +echo "==> Resolving version" +eval "$(scripts/version.sh)" +echo "version=$MARKETING_VERSION build=$CURRENT_PROJECT_VERSION tag=$ICCERY_RELEASE_TAG" + echo "==> Building universal Release app" BUILD_EXTRA="" if [ -n "$DEVELOPMENT_TEAM" ]; then @@ -60,6 +67,9 @@ xcodebuild \ ONLY_ACTIVE_ARCH=NO \ CODE_SIGNING_ALLOWED=YES \ CODE_SIGN_IDENTITY="$IDENTITY" \ + MARKETING_VERSION="$MARKETING_VERSION" \ + CURRENT_PROJECT_VERSION="$CURRENT_PROJECT_VERSION" \ + ICCERY_RELEASE_TAG="$ICCERY_RELEASE_TAG" \ $BUILD_EXTRA \ build @@ -142,7 +152,14 @@ fi echo "==> Building DMG" VERSION="$(plutil -extract CFBundleShortVersionString raw "$APP/Contents/Info.plist" 2>/dev/null || printf '2.0.0')" BUILD_NUM="$(plutil -extract CFBundleVersion raw "$APP/Contents/Info.plist" 2>/dev/null || printf '1')" -DMG="ICCery-${VERSION}-${BUILD_NUM}.dmg" +# Tagged/described builds carry the tag in the DMG name so prerelease +# artefacts are self-describing (ICCery-2.0.0-pre2-grok-236.dmg); a plain +# release tag keeps the classic ICCery--.dmg form. +TAG_NAME="${ICCERY_RELEASE_TAG#v}" +case "$TAG_NAME" in + ''|"$VERSION"|dev) DMG="ICCery-${VERSION}-${BUILD_NUM}.dmg" ;; + *) DMG="ICCery-${TAG_NAME}-${BUILD_NUM}.dmg" ;; +esac VOLUME_NAME="ICCery ${VERSION}" DMG_APP="$APP" \ diff --git a/scripts/version.sh b/scripts/version.sh new file mode 100755 index 0000000..28f76ea --- /dev/null +++ b/scripts/version.sh @@ -0,0 +1,88 @@ +#!/bin/sh +# scripts/version.sh +# +# Resolve the ICCery version triple and print KEY=value lines for eval: +# +# ICCERY_RELEASE_TAG release tag / describe string stamped into the +# bundle's ICCeryReleaseTag Info.plist key and shown +# in the About dialog. +# MARKETING_VERSION CFBundleShortVersionString — strict X.Y.Z only +# (Apple forbids suffixes; tag payload never lands here). +# CURRENT_PROJECT_VERSION CFBundleVersion — monotonically increasing integer. +# macOS/App-Store convention: never reset per version. +# +# Resolution: +# RELEASE_TAG env matching 'v[0-9]*' wins (CI tag builds; branch pushes pass +# the branch name and are ignored). Otherwise `git describe` on the worktree. +# Marketing version = first three numeric components of the tag core; a tag +# with no numeric core falls back to project.yml's MARKETING_VERSION. +# Build number = BUILD_NUMBER env override, else `git rev-list --count HEAD`. +# +# Hard fails (release-tag builds only): tag core not 1-3 numeric components, +# or parsed X.Y.Z != project.yml MARKETING_VERSION — bump project.yml or fix +# the tag before packaging. +# +# Usage: eval "$(scripts/version.sh)" + +set -eu + +ROOT="$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)" +cd "$ROOT" + +YML_MARKETING="$(sed -n 's/^ *MARKETING_VERSION: *"\(.*\)"/\1/p' project.yml | head -n 1)" + +TAG="${RELEASE_TAG:-}" +case "$TAG" in + v[0-9]*) ;; # real release tag + *) TAG="" ;; # branch name (develop, feat/...) or empty +esac +IS_RELEASE_TAG=0 +[ -n "$TAG" ] && IS_RELEASE_TAG=1 + +if [ -z "$TAG" ]; then + TAG="$(git describe --tags --always --dirty --match 'v[0-9]*' 2>/dev/null || true)" +fi +[ -n "$TAG" ] || TAG="dev" + +# Marketing version: strip leading v, cut at first '-', keep first 3 dot +# components. "v2.0.0-pre2" -> "2.0.0"; "v2.0.0.0-x" -> "2.0.0"; +# "v2.0.0-5-gsha" -> "2.0.0"; bare sha/"dev" -> no numeric core -> fallback. +CORE="${TAG#v}" +CORE="${CORE%%-*}" +MV="" +case "$CORE" in + *[!0-9.]*|'') ;; # non-numeric core — not a version tag + *) + MV="$(printf '%s' "$CORE" | cut -d. -f1-3)" + # Every kept component must be non-empty digits. + case "$MV" in + *[!0-9.]*|''|*..*|.*|*.) MV="" ;; + esac + ;; +esac + +if [ "$IS_RELEASE_TAG" -eq 1 ]; then + if [ -z "$MV" ]; then + echo "error: release tag '$TAG' has no X.Y.Z numeric core" >&2 + exit 1 + fi + if [ -n "$YML_MARKETING" ] && [ "$MV" != "$YML_MARKETING" ]; then + echo "error: tag '$TAG' resolves to $MV but project.yml MARKETING_VERSION=$YML_MARKETING; bump project.yml or fix the tag" >&2 + exit 1 + fi +fi +[ -n "$MV" ] || MV="$YML_MARKETING" +[ -n "$MV" ] || MV="0.0.0" + +BUILD="${BUILD_NUMBER:-}" +if [ -z "$BUILD" ]; then + BUILD="$(git rev-list --count HEAD 2>/dev/null || true)" +fi +case "$BUILD" in + ''|*[!0-9]*) BUILD="1" ;; +esac +[ "$BUILD" -gt 0 ] 2>/dev/null || BUILD="1" + +printf 'ICCERY_RELEASE_TAG=%s\n' "$TAG" +printf 'MARKETING_VERSION=%s\n' "$MV" +printf 'CURRENT_PROJECT_VERSION=%s\n' "$BUILD" -- 2.39.5