Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62a45d9c7d | ||
|
|
bb3cc3c858 | ||
|
|
eb73bf0106 | ||
|
|
736fd080cb | ||
|
|
14cd544e05 |
@@ -17,9 +17,16 @@ jobs:
|
|||||||
runs-on: macos-12
|
runs-on: macos-12
|
||||||
env:
|
env:
|
||||||
DERIVED: build/DerivedData-test
|
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:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
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
|
- name: Assert Xcode 14+ toolchain
|
||||||
run: |
|
run: |
|
||||||
@@ -51,6 +58,10 @@ jobs:
|
|||||||
- name: Build for testing (host arch)
|
- name: Build for testing (host arch)
|
||||||
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
|
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
|
||||||
run: |
|
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 \
|
xcodebuild build-for-testing \
|
||||||
-scheme ICCery \
|
-scheme ICCery \
|
||||||
-destination 'platform=macOS' \
|
-destination 'platform=macOS' \
|
||||||
@@ -59,7 +70,10 @@ jobs:
|
|||||||
ARCHS="$(uname -m)" \
|
ARCHS="$(uname -m)" \
|
||||||
ONLY_ACTIVE_ARCH=NO \
|
ONLY_ACTIVE_ARCH=NO \
|
||||||
CODE_SIGNING_ALLOWED=YES \
|
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
|
# Xcode embeds the shared ICCeryCore package framework into the app
|
||||||
# and the test bundle without signing it. Ad-hoc hosts still require
|
# and the test bundle without signing it. Ad-hoc hosts still require
|
||||||
@@ -191,6 +205,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
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;
|
# scripts/package-release.sh runs `xcodegen generate` and dmgbuild;
|
||||||
# see build-and-test for why brew is not used on macOS 12 (#109).
|
# see build-and-test for why brew is not used on macOS 12 (#109).
|
||||||
@@ -202,6 +220,7 @@ jobs:
|
|||||||
- name: Package release
|
- name: Package release
|
||||||
run: scripts/package-release.sh
|
run: scripts/package-release.sh
|
||||||
env:
|
env:
|
||||||
|
RELEASE_TAG: ${{ github.ref_name }}
|
||||||
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }}
|
||||||
CODESIGN_IDENTITY: ${{ secrets.CODESIGN_IDENTITY }}
|
CODESIGN_IDENTITY: ${{ secrets.CODESIGN_IDENTITY }}
|
||||||
DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }}
|
DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }}
|
||||||
|
|||||||
@@ -30,9 +30,16 @@ jobs:
|
|||||||
runs-on: macos-14
|
runs-on: macos-14
|
||||||
env:
|
env:
|
||||||
DERIVED: build/DerivedData-test
|
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:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
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
|
- name: Assert Xcode 14+ toolchain
|
||||||
run: |
|
run: |
|
||||||
@@ -64,6 +71,10 @@ jobs:
|
|||||||
- name: Build for testing (host arch)
|
- name: Build for testing (host arch)
|
||||||
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
|
if: "!(startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, 'prerelease'))"
|
||||||
run: |
|
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 \
|
xcodebuild build-for-testing \
|
||||||
-scheme ICCery \
|
-scheme ICCery \
|
||||||
-destination 'platform=macOS' \
|
-destination 'platform=macOS' \
|
||||||
@@ -72,7 +83,10 @@ jobs:
|
|||||||
ARCHS="$(uname -m)" \
|
ARCHS="$(uname -m)" \
|
||||||
ONLY_ACTIVE_ARCH=NO \
|
ONLY_ACTIVE_ARCH=NO \
|
||||||
CODE_SIGNING_ALLOWED=YES \
|
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
|
# Xcode embeds the shared ICCeryCore package framework into the app
|
||||||
# and the test bundle without signing it. Ad-hoc hosts still require
|
# and the test bundle without signing it. Ad-hoc hosts still require
|
||||||
@@ -191,6 +205,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
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;
|
# scripts/package-release.sh runs `xcodegen generate` and dmgbuild;
|
||||||
# see build-and-test for why brew is not used on macOS 12 (#109).
|
# see build-and-test for why brew is not used on macOS 12 (#109).
|
||||||
@@ -202,6 +220,7 @@ jobs:
|
|||||||
- name: Package release
|
- name: Package release
|
||||||
run: scripts/package-release.sh
|
run: scripts/package-release.sh
|
||||||
env:
|
env:
|
||||||
|
RELEASE_TAG: ${{ github.ref_name }}
|
||||||
CODESIGN_IDENTITY: ${{ secrets.CODESIGN_IDENTITY }}
|
CODESIGN_IDENTITY: ${{ secrets.CODESIGN_IDENTITY }}
|
||||||
DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }}
|
DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }}
|
||||||
NOTARIZE_APPLE_ID: ${{ secrets.NOTARIZE_APPLE_ID }}
|
NOTARIZE_APPLE_ID: ${{ secrets.NOTARIZE_APPLE_ID }}
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ Vendor/Argyll/
|
|||||||
# XcodeGen output (regenerate with `make gen`)
|
# XcodeGen output (regenerate with `make gen`)
|
||||||
ICCery.xcodeproj/
|
ICCery.xcodeproj/
|
||||||
|
|
||||||
|
# Local-only developer helpers (never committed)
|
||||||
|
/set-version.sh
|
||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,13 @@ Empty cwd illegal (#59). Atomic writes = `.tmp` + rename (#213).
|
|||||||
User-supplied strings via SwiftUI `Text` only (#114).
|
User-supplied strings via SwiftUI `Text` only (#114).
|
||||||
TIFF never rendered directly — host-side PNG preview (#58).
|
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
|
## Branching
|
||||||
`develop` ← `milestone/mN-<name>` ← `feat/<issue#>-<slug>`.
|
`develop` ← `milestone/mN-<name>` ← `feat/<issue#>-<slug>`.
|
||||||
PRs via Gitea MCP. Every issue/PR: `Project/ICCery-v2` + `Feature/*` or `Bug/*` + `Priority/*`.
|
PRs via Gitea MCP. Every issue/PR: `Project/ICCery-v2` + `Feature/*` or `Bug/*` + `Priority/*`.
|
||||||
@@ -54,3 +61,14 @@ Universal (`ARCHS='arm64 x86_64' ONLY_ACTIVE_ARCH=NO`) is still required for rel
|
|||||||
2-arg `(PMPrintSession, CFStringRef) -> OSStatus`. Never pass integer `1`.
|
2-arg `(PMPrintSession, CFStringRef) -> OSStatus`. Never pass integer `1`.
|
||||||
Modes: `AP_ApplicationColorMatching` then `ApplicationColorMatching`.
|
Modes: `AP_ApplicationColorMatching` then `ApplicationColorMatching`.
|
||||||
`lp` path and Quartz/`ICCeryPrintKit` path use **different** ColorSync dictionaries. Never mix.
|
`lp` path and Quartz/`ICCeryPrintKit` path use **different** ColorSync dictionaries. Never mix.
|
||||||
|
|
||||||
|
## Gitea issue dependencies
|
||||||
|
Use the `gitea` MCP (custom build with blocking support — verified working):
|
||||||
|
|
||||||
|
- `issue_write` methods:
|
||||||
|
- `add_dependency` — `blocking_issue` blocks `issue_number`.
|
||||||
|
- `remove_dependency` — removes `blocking_issue` from `issue_number`'s blockers.
|
||||||
|
- `block_issue` / `unblock_issue` — `issue_number` blocks/unblocks `blocked_issue`.
|
||||||
|
- `issue_read` methods: `list_dependencies` (issues blocking N),
|
||||||
|
`list_blocks` (issues N blocks).
|
||||||
|
- All issue numbers are *display numbers*, not db ids.
|
||||||
|
|||||||
+4
-3
@@ -1,6 +1,6 @@
|
|||||||
# BUILD-PLAN.md — ICCery v2 Mac
|
# BUILD-PLAN.md — ICCery v2 Mac
|
||||||
|
|
||||||
Spec snapshot: `docs/`. Source of tickets: Gitea milestones M1–M6 + M10 (id 32) + Later.
|
Spec snapshot: `docs/`. Source of tickets: Gitea milestones M1–M6 + M10 (id 32) + M11 (id 34) + Later.
|
||||||
|
|
||||||
## Sprint rule
|
## Sprint rule
|
||||||
Do not start milestone N+1 implementation until milestone N **CI/mock gate** is green.
|
Do not start milestone N+1 implementation until milestone N **CI/mock gate** is green.
|
||||||
@@ -20,11 +20,12 @@ Hardware gates block *release of that sprint*, not filing, and not starting codi
|
|||||||
| M8 | Deduplicate & consolidate | 79–86 | (already shipped on `develop`) | N/A |
|
| M8 | Deduplicate & consolidate | 79–86 | (already shipped on `develop`) | N/A |
|
||||||
| M9 | macOS 12 / Xcode 14.2 retarget | (milestone/m9-monterey, PR #145) | XCTest + ObservableObject + macos-12 CI | N/A |
|
| M9 | macOS 12 / Xcode 14.2 retarget | (milestone/m9-monterey, PR #145) | XCTest + ObservableObject + macos-12 CI | N/A |
|
||||||
| M10 | Studio workflow | 146–149 | Media library + spot-read + gamut compare + project file unit/UI smoke | Real printer+paper+.cal; live spot-read; two `.gam`; reopen `.icceryproj` |
|
| M10 | Studio workflow | 146–149 | Media library + spot-read + gamut compare + project file unit/UI smoke | Real printer+paper+.cal; live spot-read; two `.gam`; reopen `.icceryproj` |
|
||||||
|
| M11 | Printer settings completeness & dialog binding | 183, 180, 181, 186 | Quality/media/paper caps parse + `LpArgs` emit + picker UI smoke + panel capture-return | Real Epson: 7 `EPIJ_Qual` options; real Canon Pro9500: English media labels; dialog bound to non-default queue with paper/quality/media/orientation preselected; captured settings reach `lp` argv |
|
||||||
| Later | Quartz / TargetPrint | 16 | `ICCeryPrintKit` standalone + seam test | 1:1 on paper vs TIFF |
|
| Later | Quartz / TargetPrint | 16 | `ICCeryPrintKit` standalone + seam test | 1:1 on paper vs TIFF |
|
||||||
|
|
||||||
M8 and M9 merged to `develop` via PR #104 / #145; M10 starts from `800c980`.
|
M8–M10 are merged to `develop`; M11 assembles on `milestone/m11-print-settings` from `736fd08`.
|
||||||
|
|
||||||
Issue **16 is not an M3, M6, or M10 exit gate.**
|
Issue **16 is not an M3, M6, M10, or M11 exit gate.**
|
||||||
|
|
||||||
## Branch taxonomy
|
## Branch taxonomy
|
||||||
|
|
||||||
|
|||||||
@@ -20,8 +20,14 @@ public enum ArtefactFiles {
|
|||||||
bundle: Bundle = .main
|
bundle: Bundle = .main
|
||||||
) -> (version: String, build: String, buildDate: String) {
|
) -> (version: String, build: String, buildDate: String) {
|
||||||
let info = bundle.infoDictionary ?? [:]
|
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"
|
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 url = bundle.executableURL ?? bundle.bundleURL
|
||||||
let buildDate: String
|
let buildDate: String
|
||||||
@@ -37,4 +43,15 @@ public enum ArtefactFiles {
|
|||||||
|
|
||||||
return (version, build, buildDate)
|
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))"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ as AGPLv3 child processes. The GUI never `dlopen`s or links Argyll.
|
|||||||
| M7 | Pre-UAT hardening — shipped |
|
| M7 | Pre-UAT hardening — shipped |
|
||||||
| M8 | Deduplication contracts & UAT-ready hardening (#79–#86) — shipped |
|
| M8 | Deduplication contracts & UAT-ready hardening (#79–#86) — shipped |
|
||||||
| M9 | macOS 12 / Xcode 14.2 retarget (PR #145) — shipped |
|
| M9 | macOS 12 / Xcode 14.2 retarget (PR #145) — shipped |
|
||||||
| M10 | Studio workflow: gamut compare (#147), Spot Read (#148), project files (#149) shipped on `develop`; media library (#146) is in the tree, issue still open |
|
| M10 | Studio workflow: media library (#146), gamut compare (#147), Spot Read (#148), project files (#149) — shipped on `develop` |
|
||||||
|
| M11 | Printer settings completeness & dialog binding (#183, #180, #181, #186) — in flight on `milestone/m11-print-settings` |
|
||||||
| Licence | Proprietary source in [`LICENCE.md`](LICENCE.md); bundled Argyll sidecars remain AGPLv3 |
|
| Licence | Proprietary source in [`LICENCE.md`](LICENCE.md); bundled Argyll sidecars remain AGPLv3 |
|
||||||
|
|
||||||
## What it does
|
## What it does
|
||||||
@@ -125,7 +126,35 @@ The script builds with a fixed derived data path (`build/DerivedData`), locates
|
|||||||
`Release/ICCery.app` from it, signs the bundle, recursively verifies every
|
`Release/ICCery.app` from it, signs the bundle, recursively verifies every
|
||||||
bundled Mach-O sidecar (`scripts/verify-sidecar-signatures.sh`), builds a
|
bundled Mach-O sidecar (`scripts/verify-sidecar-signatures.sh`), builds a
|
||||||
HiDPI TIFF from `Resources/dmg-background.png` (+ `@2x`) via `tiffutil`, and
|
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-<build>.dmg`;
|
||||||
|
plain releases keep `ICCery-<ver>-<build>.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
|
Sidecars stay ad-hoc signed inside the bundle — the app is never
|
||||||
`codesign --deep`ed.
|
`codesign --deep`ed.
|
||||||
@@ -145,6 +174,8 @@ Environment variables read by the pipeline:
|
|||||||
| `CODESIGN_IDENTITY` | Developer ID identity for the outer `.app`; unset or `-` = ad-hoc |
|
| `CODESIGN_IDENTITY` | Developer ID identity for the outer `.app`; unset or `-` = ad-hoc |
|
||||||
| `DEVELOPMENT_TEAM` | team ID passed to `xcodebuild` when signing |
|
| `DEVELOPMENT_TEAM` | team ID passed to `xcodebuild` when signing |
|
||||||
| `NOTARIZE_APPLE_ID` / `NOTARIZE_PASSWORD` / `APPLE_TEAM_ID` | `notarytool` + staple when all three are set |
|
| `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
|
## Layout
|
||||||
|
|
||||||
@@ -268,9 +299,9 @@ fix/<issue>-<slug>
|
|||||||
```
|
```
|
||||||
|
|
||||||
Open feature/fix PRs against **`develop`**. A `milestone/m…` integration
|
Open feature/fix PRs against **`develop`**. A `milestone/m…` integration
|
||||||
branch is used only while that milestone is assembling; `milestone/m10-studio`
|
branch is used only while that milestone is assembling — currently
|
||||||
has been merged and deleted. Do not open umbrella “bugfix” branches that mix
|
`milestone/m11-print-settings` (`milestone/m10-studio` was merged and
|
||||||
tickets.
|
deleted). Do not open umbrella “bugfix” branches that mix tickets.
|
||||||
|
|
||||||
`main` is push-protected and requires status check
|
`main` is push-protected and requires status check
|
||||||
`macOS CI / build-and-test (push)`. Protected **file** patterns on `main`
|
`macOS CI / build-and-test (push)`. Protected **file** patterns on `main`
|
||||||
|
|||||||
@@ -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 {
|
final class ArtefactFilesTests: XCTestCase {
|
||||||
func testBase64RoundTrip() throws {
|
func testBase64RoundTrip() throws {
|
||||||
let url = tempURL("a.txt")
|
let url = tempURL("a.txt")
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ Keyboard: **R** resets gamut camera when Stage 5 is visible. Bind to a focusable
|
|||||||
| Dialog | Root id | Controls |
|
| Dialog | Root id | Controls |
|
||||||
|--------|---------|----------|
|
|--------|---------|----------|
|
||||||
| Settings | `settingsDialog` | `settingsDeltaEGood`, `settingsDeltaEWarning`, `settingsCalStaleDays`; other controls listed in [22](22-settings-presets.md) |
|
| 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` |
|
| Save preset | `savePresetDialog` | `savePresetName`, `savePresetDesc`, `btnConfirmSavePreset`, `btnCloseSavePresetDialog` |
|
||||||
| Manage presets | `managePresetsDialog` | `managePresetsList`, `btnExportActivePreset`, `btnImportPreset`, `btnCloseManagePresetsDialog` |
|
| Manage presets | `managePresetsDialog` | `managePresetsList`, `btnExportActivePreset`, `btnImportPreset`, `btnCloseManagePresetsDialog` |
|
||||||
| Save media recipe | `saveMediaRecipeDialog` | `saveMediaName`, `saveMediaNotes`, `saveMediaPaper`, `saveMediaInk`, `saveMediaPrinter`, `saveMediaPreset`, `saveMediaColourSpace`, `saveMediaCal`, `saveMediaApplyCal`, `btnConfirmSaveMedia`, `btnCloseSaveMediaDialog` |
|
| Save media recipe | `saveMediaRecipeDialog` | `saveMediaName`, `saveMediaNotes`, `saveMediaPaper`, `saveMediaInk`, `saveMediaPrinter`, `saveMediaPreset`, `saveMediaColourSpace`, `saveMediaCal`, `saveMediaApplyCal`, `btnConfirmSaveMedia`, `btnCloseSaveMediaDialog` |
|
||||||
|
|||||||
+4
-1
@@ -43,6 +43,8 @@ Do not install Argyll to `$PATH`. ICCery never searches `$PATH` for binaries.
|
|||||||
| `NOTARIZE_APPLE_ID` | Apple ID for `notarytool` |
|
| `NOTARIZE_APPLE_ID` | Apple ID for `notarytool` |
|
||||||
| `NOTARIZE_PASSWORD` | App-specific password for `notarytool` |
|
| `NOTARIZE_PASSWORD` | App-specific password for `notarytool` |
|
||||||
| `APPLE_TEAM_ID` | Team ID 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:
|
Invocation for a signed, notarized DMG:
|
||||||
|
|
||||||
@@ -56,7 +58,8 @@ export APPLE_TEAM_ID="TEAMID"
|
|||||||
scripts/package-release.sh
|
scripts/package-release.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
The script will emit `ICCery-<version>-<build>.dmg`. After mounting, verify Gatekeeper acceptance:
|
The script will emit `ICCery-<version>-<build>.dmg` (or `ICCery-<tag>-<build>.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
|
spctl -a -t open --context context:primary-signature -v ICCery-*.dmg
|
||||||
|
|||||||
+16
@@ -42,6 +42,15 @@ targets:
|
|||||||
echo "note: Vendor/Argyll absent — run scripts/fetch-argyll.sh"
|
echo "note: Vendor/Argyll absent — run scripts/fetch-argyll.sh"
|
||||||
fi
|
fi
|
||||||
basedOnDependencyAnalysis: false
|
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:
|
settings:
|
||||||
base:
|
base:
|
||||||
PRODUCT_BUNDLE_IDENTIFIER: com.gronod.iccery2
|
PRODUCT_BUNDLE_IDENTIFIER: com.gronod.iccery2
|
||||||
@@ -54,6 +63,13 @@ targets:
|
|||||||
INFOPLIST_KEY_NSHumanReadableCopyright: "Copyright © 2026 Gronod. AGPLv3."
|
INFOPLIST_KEY_NSHumanReadableCopyright: "Copyright © 2026 Gronod. AGPLv3."
|
||||||
MARKETING_VERSION: "2.0.0"
|
MARKETING_VERSION: "2.0.0"
|
||||||
CURRENT_PROJECT_VERSION: "1"
|
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
|
ENABLE_HARDENED_RUNTIME: YES
|
||||||
CODE_SIGN_ENTITLEMENTS: Resources/ICCery.entitlements
|
CODE_SIGN_ENTITLEMENTS: Resources/ICCery.entitlements
|
||||||
CODE_SIGN_IDENTITY: "-"
|
CODE_SIGN_IDENTITY: "-"
|
||||||
|
|||||||
@@ -45,6 +45,13 @@ DEST="platform=macOS"
|
|||||||
IDENTITY="${CODESIGN_IDENTITY:--}"
|
IDENTITY="${CODESIGN_IDENTITY:--}"
|
||||||
DEVELOPMENT_TEAM="${DEVELOPMENT_TEAM:-}"
|
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"
|
echo "==> Building universal Release app"
|
||||||
BUILD_EXTRA=""
|
BUILD_EXTRA=""
|
||||||
if [ -n "$DEVELOPMENT_TEAM" ]; then
|
if [ -n "$DEVELOPMENT_TEAM" ]; then
|
||||||
@@ -60,6 +67,9 @@ xcodebuild \
|
|||||||
ONLY_ACTIVE_ARCH=NO \
|
ONLY_ACTIVE_ARCH=NO \
|
||||||
CODE_SIGNING_ALLOWED=YES \
|
CODE_SIGNING_ALLOWED=YES \
|
||||||
CODE_SIGN_IDENTITY="$IDENTITY" \
|
CODE_SIGN_IDENTITY="$IDENTITY" \
|
||||||
|
MARKETING_VERSION="$MARKETING_VERSION" \
|
||||||
|
CURRENT_PROJECT_VERSION="$CURRENT_PROJECT_VERSION" \
|
||||||
|
ICCERY_RELEASE_TAG="$ICCERY_RELEASE_TAG" \
|
||||||
$BUILD_EXTRA \
|
$BUILD_EXTRA \
|
||||||
build
|
build
|
||||||
|
|
||||||
@@ -142,7 +152,14 @@ fi
|
|||||||
echo "==> Building DMG"
|
echo "==> Building DMG"
|
||||||
VERSION="$(plutil -extract CFBundleShortVersionString raw "$APP/Contents/Info.plist" 2>/dev/null || printf '2.0.0')"
|
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')"
|
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-<ver>-<build>.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}"
|
VOLUME_NAME="ICCery ${VERSION}"
|
||||||
|
|
||||||
DMG_APP="$APP" \
|
DMG_APP="$APP" \
|
||||||
|
|||||||
Executable
+88
@@ -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"
|
||||||
Reference in New Issue
Block a user