mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-02 03:21:36 +00:00
refactor(ios): append unpadded App Store revisions
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
## 2026.7.21
|
||||
|
||||
- Fixed background Gateway sessions restarting while waiting for pairing or authentication attention.
|
||||
- Prevented failed offline-data resets from reusing retired chat database handles and kept byte-distinct Gateway owners isolated.
|
||||
- Made Apple Watch message acknowledgments wait for persisted handling and preserved pending voice turns across navigation and relaunches.
|
||||
|
||||
@@ -75,8 +75,8 @@ Release behavior:
|
||||
- `apps/ios/CHANGELOG.md` is the iOS-only changelog and release-note source.
|
||||
- The gateway version must use CalVer like `2026.7.2`.
|
||||
- Gateway `2026.7.2`, App Store revision `1` becomes:
|
||||
- `CFBundleShortVersionString = 2026.7.201`
|
||||
- `CFBundleVersion = next App Store Connect build number for 2026.7.201`
|
||||
- `CFBundleShortVersionString = 2026.7.21`
|
||||
- `CFBundleVersion = next App Store Connect build number for 2026.7.21`
|
||||
- Each App Store version has its own build sequence beginning at `1`.
|
||||
- Local defaults and release planning derive the gateway from root `package.json`; App Store Connect versions and build uploads determine the release revision and build.
|
||||
- See `apps/ios/VERSIONING.md` for the full workflow.
|
||||
|
||||
@@ -18,13 +18,13 @@ the active release identity from the repository and App Store Connect.
|
||||
An iOS release has three independent identifiers:
|
||||
|
||||
- gateway version `G = YYYY.M.P`, for example `2026.7.2`
|
||||
- App Store revision `R`, an integer from `0` through `99`
|
||||
- App Store revision `R`, a single digit from `0` through `9`
|
||||
- build number `B`, a positive integer scoped to the exact App Store version
|
||||
|
||||
The App Store version packs the revision into the third numeric component:
|
||||
The App Store version appends the revision directly to the gateway patch with no padding:
|
||||
|
||||
```text
|
||||
AppStoreVersion(G, R) = YYYY.M.(P * 100 + R)
|
||||
AppStoreVersion(G, R) = YYYY.M.concat(P, R)
|
||||
```
|
||||
|
||||
Examples:
|
||||
@@ -32,13 +32,15 @@ Examples:
|
||||
| Gateway | Revision | App Store version | Candidate builds |
|
||||
| --- | ---: | --- | --- |
|
||||
| `2026.7.2` | legacy `0` | `2026.7.2` | closed history |
|
||||
| `2026.7.2` | `1` | `2026.7.201` | `1`, `2`, `3` |
|
||||
| `2026.7.2` | `2` | `2026.7.202` | `1`, `2`, ... |
|
||||
| `2026.7.3` | `0` | `2026.7.300` | `1`, `2`, ... |
|
||||
| `2026.7.2` | `1` | `2026.7.21` | `1`, `2`, `3` |
|
||||
| `2026.7.2` | `2` | `2026.7.22` | `1`, `2`, ... |
|
||||
| `2026.7.3` | `0` | `2026.7.30` | `1`, `2`, ... |
|
||||
|
||||
Historical exact versions are grandfathered as read-only release history. The
|
||||
release tooling does not target them again. All future uploads use the packed
|
||||
format, including revision zero.
|
||||
Historical exact versions through `2026.7.2` are grandfathered as read-only
|
||||
release history and consume revision zero for their gateway. That explicit
|
||||
cutover keeps later appended versions such as `2026.7.21` from being mistaken
|
||||
for a future gateway's exact legacy release. The release tooling does not target
|
||||
exact versions again; all future uploads use the appended single-digit format.
|
||||
|
||||
## Release commands
|
||||
|
||||
@@ -69,12 +71,12 @@ pnpm ios:release:archive -- --version 2026.7.2 --revision 1 --build-number 3
|
||||
Gateway `2026.7.2`, revision `1`, build `3` maps to:
|
||||
|
||||
- `OpenClawCanonicalVersion = 2026.7.2`
|
||||
- `CFBundleShortVersionString = 2026.7.201`
|
||||
- `CFBundleShortVersionString = 2026.7.21`
|
||||
- `CFBundleVersion = 3`
|
||||
|
||||
Local development builds continue using the normalized gateway version as the
|
||||
marketing version. Release preparation supplies the explicit revision and
|
||||
therefore the packed App Store version.
|
||||
therefore the appended App Store version.
|
||||
|
||||
## Revision and build lifecycle
|
||||
|
||||
@@ -109,7 +111,7 @@ processing, then fails the attempt rather than polling indefinitely.
|
||||
Production release notes require an exact App Store version heading:
|
||||
|
||||
```markdown
|
||||
## 2026.7.201
|
||||
## 2026.7.21
|
||||
|
||||
- Fixed an iOS issue.
|
||||
```
|
||||
@@ -171,7 +173,7 @@ refs/openclaw/mobile-releases/ios/<CFBundleShortVersionString>-<CFBundleVersion>
|
||||
For example:
|
||||
|
||||
```text
|
||||
refs/openclaw/mobile-releases/ios/2026.7.201-3
|
||||
refs/openclaw/mobile-releases/ios/2026.7.21-3
|
||||
```
|
||||
|
||||
The ref is checked before archive/upload work and created only after App Store
|
||||
|
||||
@@ -164,8 +164,8 @@ Versioning rules:
|
||||
- explicit `--version`, `--revision`, and `--build-number` values are checked overrides
|
||||
- `apps/ios/CHANGELOG.md` is the iOS-only changelog and release-note source
|
||||
- Gateway versions use CalVer: `YYYY.M.D`
|
||||
- Fastlane derives the App Store version as `YYYY.M.(D * 100 + revision)`
|
||||
- Gateway `2026.7.2`, revision `1` sets `CFBundleShortVersionString` to `2026.7.201`
|
||||
- Fastlane appends one unpadded revision digit: gateway `YYYY.M.D`, revision `R`, becomes `YYYY.M.DR`
|
||||
- Gateway `2026.7.2`, revision `1` sets `CFBundleShortVersionString` to `2026.7.21`
|
||||
- Fastlane resolves `CFBundleVersion` from the maximum awaiting, processing, failed, or complete build-upload record plus one
|
||||
- Run `pnpm ios:release:cut` after changing `## Unreleased`, then review and commit the exact encoded heading
|
||||
- `pnpm ios:version:check` validates that release notes can be generated from the iOS changelog
|
||||
|
||||
@@ -48,7 +48,7 @@ Or set `APP_STORE_CONNECT_API_KEY_PATH`.
|
||||
- Release notes are generated from `apps/ios/CHANGELOG.md` into temporary Fastlane metadata during upload; use `pnpm ios:release:plan -- --json` and `pnpm ios:release:cut` to prepare the exact encoded section.
|
||||
- Do not check in `release_notes.txt` under locale metadata directories; the lane strips copied release-note files and writes the current generated en-US release notes when requested.
|
||||
- `apps/ios/APP-REVIEW-NOTES.md` is rendered to `apps/ios/build/app-review/APP-REVIEW-NOTES.pdf` and uploaded as the App Review attachment when metadata is uploaded.
|
||||
- Production release notes require the exact encoded App Store heading, such as `## 2026.7.201`; they do not fall back to the gateway or `## Unreleased` section.
|
||||
- Production release notes require the exact encoded App Store heading, such as `## 2026.7.21`; they do not fall back to the gateway or `## Unreleased` section.
|
||||
- Generated App Store release notes begin with the associated gateway version.
|
||||
- The release upload flow uploads release notes, screenshots, and the App Review PDF attachment before the IPA, and never submits for App Review.
|
||||
- `privacy_url.txt` is set to `https://openclaw.ai/privacy`.
|
||||
|
||||
@@ -9,7 +9,7 @@ export function parseArgs(argv: string[]) {
|
||||
|
||||
function printUsage(): void {
|
||||
process.stdout.write(
|
||||
"Usage: node --import tsx scripts/ios-sync-versioning.ts [--write|--check] [--version YYYY.M.D] [--revision 0-99] [--root dir]\n\nValidates that iOS versioning inputs can produce generated local artifacts.\n",
|
||||
"Usage: node --import tsx scripts/ios-sync-versioning.ts [--write|--check] [--version YYYY.M.D] [--revision 0-9] [--root dir]\n\nValidates that iOS versioning inputs can produce generated local artifacts.\n",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { parseVersionQueryArgs } from "./lib/version-script-args.ts";
|
||||
|
||||
function printUsage(): void {
|
||||
process.stdout.write(
|
||||
"Usage: node --import tsx scripts/ios-version.ts [--json|--shell] [--field name] [--version YYYY.M.D] [--revision 0-99] [--root dir]\n\n",
|
||||
"Usage: node --import tsx scripts/ios-version.ts [--json|--shell] [--field name] [--version YYYY.M.D] [--revision 0-9] [--root dir]\n\n",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,15 @@ import path from "node:path";
|
||||
import {
|
||||
encodeIosAppStoreVersion,
|
||||
extractChangelogSection,
|
||||
MAX_IOS_APP_STORE_REVISION,
|
||||
normalizeIosAppStoreRevision,
|
||||
normalizePinnedIosVersion,
|
||||
} from "./ios-version.ts";
|
||||
|
||||
const IOS_BUILD_UPLOAD_STATES = ["AWAITING_UPLOAD", "PROCESSING", "FAILED", "COMPLETE"] as const;
|
||||
// 2026.7.2 is the last exact-version iOS release in App Store Connect.
|
||||
// Later exact CalVer values must not be confused with appended revision versions.
|
||||
const LAST_LEGACY_IOS_APP_STORE_VERSION = "2026.7.2";
|
||||
|
||||
const EDITABLE_APP_STORE_VERSION_STATES = new Set([
|
||||
"PREPARE_FOR_SUBMISSION",
|
||||
@@ -100,20 +104,27 @@ export function decodeIosAppStoreVersion(
|
||||
gatewayVersion: string,
|
||||
appStoreVersion: string,
|
||||
): DecodedVersion | null {
|
||||
const gateway = parseVersionComponents(normalizePinnedIosVersion(gatewayVersion));
|
||||
const canonicalGatewayVersion = normalizePinnedIosVersion(gatewayVersion);
|
||||
const gateway = parseVersionComponents(canonicalGatewayVersion);
|
||||
const candidate = parseVersionComponents(appStoreVersion);
|
||||
if (!gateway || !candidate || gateway[0] !== candidate[0] || gateway[1] !== candidate[1]) {
|
||||
return null;
|
||||
}
|
||||
if (candidate[2] === gateway[2]) {
|
||||
return { legacy: true, revision: 0 };
|
||||
return compareAppStoreVersions(canonicalGatewayVersion, LAST_LEGACY_IOS_APP_STORE_VERSION) <= 0
|
||||
? { legacy: true, revision: 0 }
|
||||
: null;
|
||||
}
|
||||
const firstPackedPatch = gateway[2] * 100;
|
||||
const revision = candidate[2] - firstPackedPatch;
|
||||
if (revision < 0 || revision > 99) {
|
||||
const gatewayPatch = gateway[2].toString();
|
||||
const candidatePatch = candidate[2].toString();
|
||||
if (!candidatePatch.startsWith(gatewayPatch)) {
|
||||
return null;
|
||||
}
|
||||
return { legacy: false, revision };
|
||||
const revision = candidatePatch.slice(gatewayPatch.length);
|
||||
if (!/^\d$/u.test(revision)) {
|
||||
return null;
|
||||
}
|
||||
return { legacy: false, revision: Number(revision) };
|
||||
}
|
||||
|
||||
function normalizeBuildNumber(rawBuildNumber: string): number {
|
||||
@@ -269,8 +280,10 @@ export function resolveIosReleasePlan(input: IosReleasePlanInput): IosReleasePla
|
||||
}
|
||||
}
|
||||
|
||||
if (revision > 99) {
|
||||
throw new Error(`Gateway ${gatewayVersion} has exhausted App Store revisions 0 through 99.`);
|
||||
if (revision > MAX_IOS_APP_STORE_REVISION) {
|
||||
throw new Error(
|
||||
`Gateway ${gatewayVersion} has exhausted App Store revisions 0 through ${MAX_IOS_APP_STORE_REVISION}.`,
|
||||
);
|
||||
}
|
||||
|
||||
const appStoreVersion = encodeIosAppStoreVersion(gatewayVersion, revision);
|
||||
|
||||
@@ -4,8 +4,7 @@ import path from "node:path";
|
||||
import { parseReleaseVersion } from "./release-version.mjs";
|
||||
|
||||
const IOS_CHANGELOG_FILE = "apps/ios/CHANGELOG.md";
|
||||
const IOS_APP_STORE_REVISIONS_PER_GATEWAY_VERSION = 100;
|
||||
const MAX_IOS_APP_STORE_REVISION = IOS_APP_STORE_REVISIONS_PER_GATEWAY_VERSION - 1;
|
||||
export const MAX_IOS_APP_STORE_REVISION = 9;
|
||||
|
||||
type ResolvedIosVersion = {
|
||||
appStoreRevision: number | null;
|
||||
@@ -71,9 +70,9 @@ export function encodeIosAppStoreVersion(
|
||||
}
|
||||
|
||||
const revision = normalizeIosAppStoreRevision(appStoreRevision);
|
||||
// Exact pre-cutover versions are immutable release history. Revision zero is
|
||||
// deliberately packed too, so future trains never return to the old shape.
|
||||
const encodedPatch = parsed.patch * IOS_APP_STORE_REVISIONS_PER_GATEWAY_VERSION + revision;
|
||||
// Append one revision digit without padding. Keeping the revision to one
|
||||
// digit preserves App Store ordering when the gateway patch increments.
|
||||
const encodedPatch = Number(`${parsed.patch}${revision}`);
|
||||
if (!Number.isSafeInteger(encodedPatch)) {
|
||||
throw new Error(`Encoded iOS App Store version is too large for '${gatewayVersion}'.`);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ describe("resolveIosReleasePlan", () => {
|
||||
it("starts a new gateway at revision zero and build one", () => {
|
||||
expect(resolveIosReleasePlan(input())).toMatchObject({
|
||||
appStoreRevision: 0,
|
||||
appStoreVersion: "2026.7.200",
|
||||
appStoreVersion: "2026.7.20",
|
||||
buildNumber: 1,
|
||||
changelogStatus: "needs-cut",
|
||||
decision: "new-revision",
|
||||
@@ -46,7 +46,7 @@ describe("resolveIosReleasePlan", () => {
|
||||
|
||||
expect(plan).toMatchObject({
|
||||
appStoreRevision: 1,
|
||||
appStoreVersion: "2026.7.201",
|
||||
appStoreVersion: "2026.7.21",
|
||||
buildNumber: 1,
|
||||
decision: "new-revision",
|
||||
});
|
||||
@@ -67,7 +67,7 @@ describe("resolveIosReleasePlan", () => {
|
||||
|
||||
expect(plan).toMatchObject({
|
||||
appStoreRevision: 1,
|
||||
appStoreVersion: "2026.7.201",
|
||||
appStoreVersion: "2026.7.21",
|
||||
buildNumber: 1,
|
||||
decision: "new-revision",
|
||||
});
|
||||
@@ -77,7 +77,7 @@ describe("resolveIosReleasePlan", () => {
|
||||
const plan = resolveIosReleasePlan(
|
||||
input({
|
||||
appStoreVersions: [
|
||||
{ id: "editable", state: "PREPARE_FOR_SUBMISSION", versionString: "2026.7.201" },
|
||||
{ id: "editable", state: "PREPARE_FOR_SUBMISSION", versionString: "2026.7.21" },
|
||||
],
|
||||
}),
|
||||
);
|
||||
@@ -99,7 +99,7 @@ describe("resolveIosReleasePlan", () => {
|
||||
buildUploads: [
|
||||
{
|
||||
buildNumber: "1",
|
||||
shortVersion: "2026.7.201",
|
||||
shortVersion: "2026.7.21",
|
||||
state: "FAILED",
|
||||
},
|
||||
],
|
||||
@@ -121,8 +121,8 @@ describe("resolveIosReleasePlan", () => {
|
||||
{ id: "legacy", state: "READY_FOR_DISTRIBUTION", versionString: "2026.7.2" },
|
||||
],
|
||||
buildUploads: [
|
||||
{ buildNumber: "1", shortVersion: "2026.7.201", state: "FAILED" },
|
||||
{ buildNumber: "1", shortVersion: "2026.7.202", state: "FAILED" },
|
||||
{ buildNumber: "1", shortVersion: "2026.7.21", state: "FAILED" },
|
||||
{ buildNumber: "1", shortVersion: "2026.7.22", state: "FAILED" },
|
||||
],
|
||||
}),
|
||||
),
|
||||
@@ -135,11 +135,11 @@ describe("resolveIosReleasePlan", () => {
|
||||
const plan = resolveIosReleasePlan(
|
||||
input({
|
||||
appStoreVersions: [
|
||||
{ id: "editable", state: "READY_FOR_REVIEW", versionString: "2026.7.201" },
|
||||
{ id: "editable", state: "READY_FOR_REVIEW", versionString: "2026.7.21" },
|
||||
],
|
||||
buildUploads: [
|
||||
{ buildNumber: "7", shortVersion: "2026.7.201", state },
|
||||
{ buildNumber: "3", shortVersion: "2026.7.201", state: "COMPLETE" },
|
||||
{ buildNumber: "7", shortVersion: "2026.7.21", state },
|
||||
{ buildNumber: "3", shortVersion: "2026.7.21", state: "COMPLETE" },
|
||||
],
|
||||
}),
|
||||
);
|
||||
@@ -152,7 +152,7 @@ describe("resolveIosReleasePlan", () => {
|
||||
expect(() =>
|
||||
resolveIosReleasePlan(
|
||||
input({
|
||||
appStoreVersions: [{ id: "locked", state: "IN_REVIEW", versionString: "2026.7.201" }],
|
||||
appStoreVersions: [{ id: "locked", state: "IN_REVIEW", versionString: "2026.7.21" }],
|
||||
}),
|
||||
),
|
||||
).toThrow("locked in state IN_REVIEW");
|
||||
@@ -161,7 +161,7 @@ describe("resolveIosReleasePlan", () => {
|
||||
resolveIosReleasePlan(
|
||||
input({
|
||||
appStoreVersions: [
|
||||
{ id: "other", state: "PREPARE_FOR_SUBMISSION", versionString: "2026.7.300" },
|
||||
{ id: "other", state: "PREPARE_FOR_SUBMISSION", versionString: "2026.7.30" },
|
||||
],
|
||||
}),
|
||||
),
|
||||
@@ -173,8 +173,8 @@ describe("resolveIosReleasePlan", () => {
|
||||
resolveIosReleasePlan(
|
||||
input({
|
||||
appStoreVersions: [
|
||||
{ id: "one", state: "PREPARE_FOR_SUBMISSION", versionString: "2026.7.201" },
|
||||
{ id: "two", state: "READY_FOR_REVIEW", versionString: "2026.7.202" },
|
||||
{ id: "one", state: "PREPARE_FOR_SUBMISSION", versionString: "2026.7.21" },
|
||||
{ id: "two", state: "READY_FOR_REVIEW", versionString: "2026.7.22" },
|
||||
],
|
||||
}),
|
||||
),
|
||||
@@ -183,24 +183,22 @@ describe("resolveIosReleasePlan", () => {
|
||||
expect(() =>
|
||||
resolveIosReleasePlan(
|
||||
input({
|
||||
buildUploads: [
|
||||
{ buildNumber: "1", shortVersion: "2026.7.200", state: "NEW_APPLE_STATE" },
|
||||
],
|
||||
buildUploads: [{ buildNumber: "1", shortVersion: "2026.7.20", state: "NEW_APPLE_STATE" }],
|
||||
}),
|
||||
),
|
||||
).toThrow("Unknown App Store build upload state");
|
||||
});
|
||||
|
||||
it("fails after revision 99 is distributed", () => {
|
||||
it("fails after revision 9 is distributed", () => {
|
||||
expect(() =>
|
||||
resolveIosReleasePlan(
|
||||
input({
|
||||
appStoreVersions: [
|
||||
{ id: "last", state: "READY_FOR_DISTRIBUTION", versionString: "2026.7.299" },
|
||||
{ id: "last", state: "READY_FOR_DISTRIBUTION", versionString: "2026.7.29" },
|
||||
],
|
||||
}),
|
||||
),
|
||||
).toThrow("exhausted App Store revisions 0 through 99");
|
||||
).toThrow("exhausted App Store revisions 0 through 9");
|
||||
});
|
||||
|
||||
it("rejects a planned version older than released history from another gateway", () => {
|
||||
@@ -225,27 +223,59 @@ describe("resolveIosReleasePlan", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("decodes only legacy or packed versions for the selected gateway", () => {
|
||||
it("decodes only legacy or single-digit revision versions for the selected gateway", () => {
|
||||
expect(decodeIosAppStoreVersion("2026.7.2", "2026.7.2")).toEqual({
|
||||
legacy: true,
|
||||
revision: 0,
|
||||
});
|
||||
expect(decodeIosAppStoreVersion("2026.7.2", "2026.7.299")).toEqual({
|
||||
expect(decodeIosAppStoreVersion("2026.7.2", "2026.7.20")).toEqual({
|
||||
legacy: false,
|
||||
revision: 99,
|
||||
revision: 0,
|
||||
});
|
||||
expect(decodeIosAppStoreVersion("2026.7.2", "2026.7.21")).toEqual({
|
||||
legacy: false,
|
||||
revision: 1,
|
||||
});
|
||||
expect(decodeIosAppStoreVersion("2026.7.2", "2026.7.29")).toEqual({
|
||||
legacy: false,
|
||||
revision: 9,
|
||||
});
|
||||
expect(decodeIosAppStoreVersion("2026.7.2", "2026.7.201")).toBeNull();
|
||||
expect(decodeIosAppStoreVersion("2026.7.2", "2026.7.30")).toBeNull();
|
||||
expect(decodeIosAppStoreVersion("2026.7.3", "2026.7.3")).toBeNull();
|
||||
expect(decodeIosAppStoreVersion("2026.7.21", "2026.7.21")).toBeNull();
|
||||
expect(decodeIosAppStoreVersion("2026.7.21", "2026.7.210")).toEqual({
|
||||
legacy: false,
|
||||
revision: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("does not mistake an appended version for a future gateway's legacy release", () => {
|
||||
const plan = resolveIosReleasePlan(
|
||||
input({
|
||||
appStoreVersions: [
|
||||
{ id: "older", state: "READY_FOR_DISTRIBUTION", versionString: "2026.7.21" },
|
||||
],
|
||||
gatewayVersion: "2026.7.21",
|
||||
}),
|
||||
);
|
||||
|
||||
expect(plan).toMatchObject({
|
||||
appStoreRevision: 0,
|
||||
appStoreVersion: "2026.7.210",
|
||||
decision: "new-revision",
|
||||
});
|
||||
expect(decodeIosAppStoreVersion("2026.7.2", "2026.7.300")).toBeNull();
|
||||
});
|
||||
|
||||
it("requires another cut when retry notes remain Unreleased", () => {
|
||||
const rootDir = writeIosFixture({
|
||||
packageVersion: "2026.7.2",
|
||||
changelog:
|
||||
"# OpenClaw iOS Changelog\n\n## Unreleased\n\nRetry notes.\n\n## 2026.7.201\n\nOriginal notes.\n",
|
||||
"# OpenClaw iOS Changelog\n\n## Unreleased\n\nRetry notes.\n\n## 2026.7.21\n\nOriginal notes.\n",
|
||||
});
|
||||
const plan = resolveIosReleasePlan({
|
||||
appStoreVersions: [
|
||||
{ id: "editable", state: "PREPARE_FOR_SUBMISSION", versionString: "2026.7.201" },
|
||||
{ id: "editable", state: "PREPARE_FOR_SUBMISSION", versionString: "2026.7.21" },
|
||||
],
|
||||
buildUploads: [],
|
||||
gatewayVersion: "2026.7.2",
|
||||
@@ -260,26 +290,26 @@ describe("cutIosReleaseChangelog", () => {
|
||||
it("cuts Unreleased notes into a new exact App Store version section", () => {
|
||||
const current =
|
||||
"# OpenClaw iOS Changelog\n\n## Unreleased\n\nNew notes.\n\n## 2026.7.2\n\nOld notes.\n";
|
||||
const updated = cutIosReleaseChangelog(current, "2026.7.201");
|
||||
const updated = cutIosReleaseChangelog(current, "2026.7.21");
|
||||
|
||||
expect(updated).toContain("## Unreleased\n\n## 2026.7.201\n\nNew notes.");
|
||||
expect(updated).toContain("## Unreleased\n\n## 2026.7.21\n\nNew notes.");
|
||||
expect(updated).toContain("## 2026.7.2\n\nOld notes.");
|
||||
expect(cutIosReleaseChangelog(updated, "2026.7.201")).toBe(updated);
|
||||
expect(cutIosReleaseChangelog(updated, "2026.7.21")).toBe(updated);
|
||||
});
|
||||
|
||||
it("merges retry notes into the existing release section", () => {
|
||||
const current =
|
||||
"# OpenClaw iOS Changelog\n\n## Unreleased\n\nRetry fix.\n\n## 2026.7.201\n\nOriginal notes.\n";
|
||||
const updated = cutIosReleaseChangelog(current, "2026.7.201");
|
||||
"# OpenClaw iOS Changelog\n\n## Unreleased\n\nRetry fix.\n\n## 2026.7.21\n\nOriginal notes.\n";
|
||||
const updated = cutIosReleaseChangelog(current, "2026.7.21");
|
||||
|
||||
expect(updated).toContain("## 2026.7.201\n\nRetry fix.\n\nOriginal notes.");
|
||||
expect(updated).toContain("## 2026.7.21\n\nRetry fix.\n\nOriginal notes.");
|
||||
});
|
||||
|
||||
it("preserves an existing release heading suffix", () => {
|
||||
const current =
|
||||
"# OpenClaw iOS Changelog\n\n## Unreleased\n\nRetry fix.\n\n## 2026.7.201 - 2026-07-23\n\nOriginal notes.\n";
|
||||
const updated = cutIosReleaseChangelog(current, "2026.7.201");
|
||||
"# OpenClaw iOS Changelog\n\n## Unreleased\n\nRetry fix.\n\n## 2026.7.21 - 2026-07-23\n\nOriginal notes.\n";
|
||||
const updated = cutIosReleaseChangelog(current, "2026.7.21");
|
||||
|
||||
expect(updated).toContain("## 2026.7.201 - 2026-07-23\n\nRetry fix.\n\nOriginal notes.");
|
||||
expect(updated).toContain("## 2026.7.21 - 2026-07-23\n\nRetry fix.\n\nOriginal notes.");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -113,7 +113,7 @@ describe("resolveIosVersion", () => {
|
||||
it("prints an encoded App Store version for an explicit gateway revision", () => {
|
||||
const rootDir = writeIosFixture({
|
||||
packageVersion: "2026.7.2",
|
||||
changelog: "# OpenClaw iOS Changelog\n\n## 2026.7.201\n\nRevision notes.\n",
|
||||
changelog: "# OpenClaw iOS Changelog\n\n## 2026.7.21\n\nRevision notes.\n",
|
||||
});
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
@@ -134,7 +134,7 @@ describe("resolveIosVersion", () => {
|
||||
);
|
||||
|
||||
expect(result.status).toBe(0);
|
||||
expect(result.stdout).toBe("2026.7.201\n");
|
||||
expect(result.stdout).toBe("2026.7.21\n");
|
||||
expect(result.stderr).toBe("");
|
||||
});
|
||||
|
||||
@@ -212,19 +212,19 @@ describe("resolveIosVersion", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("encodes App Store revisions into the gateway patch component", () => {
|
||||
expect(encodeIosAppStoreVersion("2026.7.2", 0)).toBe("2026.7.200");
|
||||
expect(encodeIosAppStoreVersion("2026.7.2", 1)).toBe("2026.7.201");
|
||||
expect(encodeIosAppStoreVersion("2026.7.2", 99)).toBe("2026.7.299");
|
||||
expect(encodeIosAppStoreVersion("2026.7.3", 0)).toBe("2026.7.300");
|
||||
expect(encodeIosAppStoreVersion("2026.12.33", 4)).toBe("2026.12.3304");
|
||||
it("appends one unpadded App Store revision digit to the gateway patch", () => {
|
||||
expect(encodeIosAppStoreVersion("2026.7.2", 0)).toBe("2026.7.20");
|
||||
expect(encodeIosAppStoreVersion("2026.7.2", 1)).toBe("2026.7.21");
|
||||
expect(encodeIosAppStoreVersion("2026.7.2", 9)).toBe("2026.7.29");
|
||||
expect(encodeIosAppStoreVersion("2026.7.3", 0)).toBe("2026.7.30");
|
||||
expect(encodeIosAppStoreVersion("2026.12.33", 4)).toBe("2026.12.334");
|
||||
});
|
||||
|
||||
it("rejects invalid App Store revisions", () => {
|
||||
expect(() => normalizeIosAppStoreRevision("-1")).toThrow("integer from 0 to 99");
|
||||
expect(() => normalizeIosAppStoreRevision("01")).toThrow("integer from 0 to 99");
|
||||
expect(() => normalizeIosAppStoreRevision("100")).toThrow("integer from 0 to 99");
|
||||
expect(() => normalizeIosAppStoreRevision("1.5")).toThrow("integer from 0 to 99");
|
||||
expect(() => normalizeIosAppStoreRevision("-1")).toThrow("integer from 0 to 9");
|
||||
expect(() => normalizeIosAppStoreRevision("01")).toThrow("integer from 0 to 9");
|
||||
expect(() => normalizeIosAppStoreRevision("10")).toThrow("integer from 0 to 9");
|
||||
expect(() => normalizeIosAppStoreRevision("1.5")).toThrow("integer from 0 to 9");
|
||||
});
|
||||
|
||||
it("rejects semver-only package versions", () => {
|
||||
@@ -306,7 +306,7 @@ describe("release note extraction", () => {
|
||||
|
||||
Draft notes.
|
||||
|
||||
## 2026.7.201
|
||||
## 2026.7.21
|
||||
|
||||
- App Store revision notes.
|
||||
`,
|
||||
@@ -334,7 +334,7 @@ Draft notes.
|
||||
const changelog = fs.readFileSync(path.join(rootDir, "apps", "ios", "CHANGELOG.md"), "utf8");
|
||||
|
||||
expect(() => renderIosReleaseNotes(version, changelog)).toThrow(
|
||||
"Unable to find iOS changelog notes for 2026.7.201",
|
||||
"Unable to find iOS changelog notes for 2026.7.21",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user