Skip to content

Commit ff2334d

Browse files
committed
feat(one): automate version sync with @objectstack/cli
1 parent a351ddc commit ff2334d

8 files changed

Lines changed: 372 additions & 7 deletions

File tree

.github/workflows/one.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,27 @@ jobs:
7575
- name: Install workspace deps
7676
run: pnpm install --frozen-lockfile
7777

78+
- name: Sync app version to @objectstack/cli
79+
run: pnpm --filter @objectos/one sync-version
80+
81+
- name: Verify tag matches @objectstack/cli version
82+
if: startsWith(github.ref, 'refs/tags/one-v')
83+
shell: bash
84+
run: |
85+
set -euo pipefail
86+
# Tag form: one-v<X.Y.Z>; must equal the resolved cli version that
87+
# sync-version wrote into the bundle, otherwise the updater's
88+
# latest.json (built from the tag) will not match the installed
89+
# app version and auto-update will silently fail.
90+
TAG_VERSION="${GITHUB_REF_NAME#one-v}"
91+
PKG_VERSION=$(node -p "require('./apps/objectos-one/package.json').version")
92+
echo "tag version : $TAG_VERSION"
93+
echo "pkg version : $PKG_VERSION"
94+
if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
95+
echo "::error::tag $GITHUB_REF_NAME does not match @objectstack/cli version $PKG_VERSION. Re-tag as one-v$PKG_VERSION."
96+
exit 1
97+
fi
98+
7899
- name: Stage Node runtime
79100
env:
80101
TARGET_ARCH: ${{ matrix.target-arch }}

.github/workflows/release-one.yml

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
name: release-one
2+
3+
# Manual release trigger for ObjectOS One.
4+
#
5+
# What it does:
6+
# 1. Resolves the @objectstack/cli version (single source of truth).
7+
# 2. Runs sync-version to write that version into package.json,
8+
# tauri.conf.json and Cargo.toml.
9+
# 3. If anything changed, commits the bump on the chosen branch.
10+
# 4. Creates and pushes the matching `one-v<X.Y.Z>` tag.
11+
# 5. The tag push automatically triggers `one.yml`, which builds all
12+
# four platforms and drafts the GitHub Release.
13+
#
14+
# Use this instead of tagging by hand — it guarantees the tag matches
15+
# whatever cli version `pnpm install` resolves on a clean tree.
16+
17+
on:
18+
workflow_dispatch:
19+
inputs:
20+
ref:
21+
description: "Branch to release from (default: main)"
22+
required: false
23+
default: "main"
24+
dry_run:
25+
description: "Resolve + sync but do NOT commit or push the tag"
26+
type: boolean
27+
default: false
28+
force:
29+
description: "Re-create the tag if it already exists (deletes the old tag)"
30+
type: boolean
31+
default: false
32+
33+
permissions:
34+
contents: write
35+
36+
concurrency:
37+
group: release-one
38+
cancel-in-progress: false
39+
40+
jobs:
41+
tag:
42+
name: Resolve version & push tag
43+
runs-on: ubuntu-22.04
44+
timeout-minutes: 10
45+
steps:
46+
- uses: actions/checkout@v4
47+
with:
48+
ref: ${{ inputs.ref }}
49+
# We need full history so `git push` of the bump commit works
50+
# against the branch tip.
51+
fetch-depth: 0
52+
53+
- uses: pnpm/action-setup@v4
54+
with:
55+
version: 10.28.2
56+
57+
- uses: actions/setup-node@v4
58+
with:
59+
node-version: 20
60+
cache: pnpm
61+
62+
- name: Install workspace deps
63+
# Required so sync-version sees the resolved @objectstack/cli
64+
# version under node_modules (the most reliable source).
65+
run: pnpm install --frozen-lockfile
66+
67+
- name: Sync app version to @objectstack/cli
68+
run: pnpm --filter @objectos/one sync-version
69+
70+
- name: Resolve target version
71+
id: ver
72+
run: |
73+
set -euo pipefail
74+
VERSION=$(node -p "require('./apps/objectos-one/package.json').version")
75+
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.-]+)?$ ]]; then
76+
echo "::error::resolved version '$VERSION' is not valid semver"
77+
exit 1
78+
fi
79+
TAG="one-v$VERSION"
80+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
81+
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
82+
echo "Releasing $TAG"
83+
84+
- name: Check tag uniqueness
85+
env:
86+
TAG: ${{ steps.ver.outputs.tag }}
87+
FORCE: ${{ inputs.force }}
88+
run: |
89+
set -euo pipefail
90+
if git ls-remote --exit-code --tags origin "$TAG" >/dev/null 2>&1; then
91+
if [ "$FORCE" = "true" ]; then
92+
echo "Tag $TAG exists — will overwrite (force=true)."
93+
else
94+
echo "::error::tag $TAG already exists on origin. Bump @objectstack/cli or rerun with force=true."
95+
exit 1
96+
fi
97+
else
98+
echo "Tag $TAG is free."
99+
fi
100+
101+
- name: Commit version bump (if needed)
102+
if: ${{ !inputs.dry_run }}
103+
env:
104+
VERSION: ${{ steps.ver.outputs.version }}
105+
run: |
106+
set -euo pipefail
107+
git config user.name "github-actions[bot]"
108+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
109+
110+
git add \
111+
apps/objectos-one/package.json \
112+
apps/objectos-one/src-tauri/tauri.conf.json \
113+
apps/objectos-one/src-tauri/Cargo.toml
114+
115+
if git diff --cached --quiet; then
116+
echo "Versions already at $VERSION — nothing to commit."
117+
else
118+
git commit -m "chore(one): release v$VERSION"
119+
git push origin "HEAD:${{ inputs.ref }}"
120+
fi
121+
122+
- name: Create & push tag
123+
if: ${{ !inputs.dry_run }}
124+
env:
125+
TAG: ${{ steps.ver.outputs.tag }}
126+
VERSION: ${{ steps.ver.outputs.version }}
127+
FORCE: ${{ inputs.force }}
128+
run: |
129+
set -euo pipefail
130+
if [ "$FORCE" = "true" ]; then
131+
# Delete remote tag first so the new one points at the latest commit.
132+
git push origin ":refs/tags/$TAG" || true
133+
git tag -d "$TAG" 2>/dev/null || true
134+
fi
135+
git tag -a "$TAG" -m "ObjectOS One v$VERSION"
136+
git push origin "$TAG"
137+
echo "Pushed $TAG — the 'one' workflow will now build and draft the release."
138+
echo "Watch it at: https://github.com/${GITHUB_REPOSITORY}/actions/workflows/one.yml"
139+
140+
- name: Dry-run summary
141+
if: ${{ inputs.dry_run }}
142+
env:
143+
TAG: ${{ steps.ver.outputs.tag }}
144+
run: |
145+
echo "::notice::Dry run complete. Would have pushed tag $TAG to trigger one.yml. No commit or push performed."

apps/objectos-one/README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,36 @@ Output lands in `src-tauri/target/release/bundle/`:
103103
| Windows | `nsis/ObjectOS_<v>_x64-setup.exe` |
104104
| Linux | `deb/objectos_<v>_amd64.deb`, AppImage |
105105

106+
## Versioning
107+
108+
ObjectOS One ships the bundled `@objectos/server` runtime, which in turn
109+
depends on `@objectstack/cli`. We pin the app version to the cli version so
110+
the installed app always advertises the underlying engine release.
111+
112+
`apps/objectos-one/scripts/sync-version.mjs` reads the cli version (from
113+
`node_modules/@objectstack/cli` after install, falling back to the declared
114+
range in `apps/objectos/package.json`) and writes it into:
115+
116+
- `apps/objectos-one/package.json`
117+
- `apps/objectos-one/src-tauri/tauri.conf.json`
118+
- `apps/objectos-one/src-tauri/Cargo.toml`
119+
120+
`pnpm dev` / `pnpm build` run it automatically. To release:
121+
122+
```bash
123+
pnpm install # resolve the exact cli version
124+
pnpm --filter @objectos/one sync-version # writes the three files
125+
git commit -am "chore(one): bump to $(node -p \
126+
"require('./apps/objectos-one/package.json').version")"
127+
git tag "one-v$(node -p \
128+
"require('./apps/objectos-one/package.json').version")"
129+
git push --follow-tags
130+
```
131+
132+
CI rejects the build if the `one-v<X.Y.Z>` tag does not equal the cli
133+
version sync-version computed, so a wrong tag fails fast instead of
134+
shipping a broken updater manifest.
135+
106136
## CI
107137

108138
`.github/workflows/one.yml` builds all four platforms in parallel

apps/objectos-one/package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
{
22
"name": "@objectos/one",
3-
"version": "6.0.6",
3+
"version": "6.5.1",
44
"private": true,
55
"license": "AGPL-3.0",
66
"description": "ObjectOS One — all-in-one local distribution (Tauri shell + bundled Node runtime + DB).",
77
"type": "module",
88
"scripts": {
9+
"sync-version": "node scripts/sync-version.mjs",
10+
"sync-version:check": "node scripts/sync-version.mjs --check",
911
"stage": "node scripts/stage-runtime.mjs",
1012
"tauri": "tauri",
11-
"dev": "pnpm run stage && tauri dev",
12-
"build": "pnpm run stage && tauri build"
13+
"dev": "pnpm run sync-version && pnpm run stage && tauri dev",
14+
"build": "pnpm run sync-version && pnpm run stage && tauri build"
1315
},
1416
"devDependencies": {
1517
"@tauri-apps/cli": "^2.1.0"
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
#!/usr/bin/env node
2+
// Sync the ObjectOS One release version to the @objectstack/cli version that
3+
// the bundled server depends on.
4+
//
5+
// Source of truth, in order of preference:
6+
// 1. node_modules/@objectstack/cli/package.json (resolved/installed version)
7+
// 2. apps/objectos/package.json `dependencies["@objectstack/cli"]`
8+
// with any leading ^/~/= stripped
9+
//
10+
// Targets updated (skipped if already up to date):
11+
// - apps/objectos-one/package.json "version"
12+
// - apps/objectos-one/src-tauri/tauri.conf.json "version"
13+
// - apps/objectos-one/src-tauri/Cargo.toml [package].version
14+
//
15+
// Flags:
16+
// --check Exit non-zero if any file would change. Doesn't write.
17+
// --quiet Suppress informational logs.
18+
//
19+
// Used by CI (.github/workflows/one.yml) and locally before tagging.
20+
21+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
22+
import { dirname, join, resolve } from "node:path";
23+
import { fileURLToPath } from "node:url";
24+
25+
const __dirname = dirname(fileURLToPath(import.meta.url));
26+
const REPO_ROOT = resolve(__dirname, "..", "..", "..");
27+
28+
const argv = process.argv.slice(2);
29+
const CHECK_ONLY = argv.includes("--check");
30+
const QUIET = argv.includes("--quiet");
31+
32+
const log = (...a) => {
33+
if (!QUIET) console.log("[sync-version]", ...a);
34+
};
35+
const warn = (...a) => console.warn("[sync-version]", ...a);
36+
37+
function readJson(path) {
38+
return JSON.parse(readFileSync(path, "utf8"));
39+
}
40+
41+
function resolveCliVersion() {
42+
// Prefer the actually-resolved version from node_modules — what the bundle
43+
// will ship — but fall back to the declared dependency range if the
44+
// workspace hasn't been installed yet (e.g. fresh CI before pnpm install).
45+
const resolved = join(
46+
REPO_ROOT,
47+
"node_modules/@objectstack/cli/package.json"
48+
);
49+
if (existsSync(resolved)) {
50+
const v = readJson(resolved).version;
51+
log(`resolved @objectstack/cli@${v} from node_modules`);
52+
return v;
53+
}
54+
55+
const serverPkgPath = join(REPO_ROOT, "apps/objectos/package.json");
56+
const serverPkg = readJson(serverPkgPath);
57+
const range =
58+
serverPkg.dependencies?.["@objectstack/cli"] ??
59+
serverPkg.devDependencies?.["@objectstack/cli"];
60+
if (!range) {
61+
throw new Error(
62+
`@objectstack/cli not found in ${serverPkgPath}; cannot derive version`
63+
);
64+
}
65+
const v = range.replace(/^[\^~=v]+/, "").trim();
66+
if (!/^\d+\.\d+\.\d+/.test(v)) {
67+
throw new Error(
68+
`cannot extract a concrete version from "${range}"; run pnpm install first`
69+
);
70+
}
71+
log(`derived @objectstack/cli@${v} from ${serverPkgPath} (not installed)`);
72+
return v;
73+
}
74+
75+
function updateJson(path, mutate) {
76+
const before = readFileSync(path, "utf8");
77+
const data = JSON.parse(before);
78+
mutate(data);
79+
// Keep 2-space indent + trailing newline to match the rest of the repo
80+
// and avoid noisy diffs.
81+
const after = JSON.stringify(data, null, 2) + "\n";
82+
if (after === before) return false;
83+
if (!CHECK_ONLY) writeFileSync(path, after);
84+
return true;
85+
}
86+
87+
function updateCargoTomlVersion(path, version) {
88+
const before = readFileSync(path, "utf8");
89+
// Only replace the version line inside the first `[package]` table.
90+
// We don't pull in a TOML parser to avoid an extra dep for this one field.
91+
const pkgIdx = before.indexOf("[package]");
92+
if (pkgIdx < 0) {
93+
throw new Error(`no [package] section in ${path}`);
94+
}
95+
const before_pkg = before.slice(0, pkgIdx);
96+
const rest = before.slice(pkgIdx);
97+
// Replace only the FIRST version = "..." after [package] (before the next [table]).
98+
const nextTableIdx = rest.indexOf("\n[", 1);
99+
const head = nextTableIdx < 0 ? rest : rest.slice(0, nextTableIdx);
100+
const tail = nextTableIdx < 0 ? "" : rest.slice(nextTableIdx);
101+
102+
const versionRe = /^(version\s*=\s*)"([^"]*)"/m;
103+
const match = head.match(versionRe);
104+
if (!match) {
105+
throw new Error(`could not find version line under [package] in ${path}`);
106+
}
107+
if (match[2] === version) {
108+
return false;
109+
}
110+
const replaced = head.replace(versionRe, `$1"${version}"`);
111+
const after = before_pkg + replaced + tail;
112+
if (!CHECK_ONLY) writeFileSync(path, after);
113+
return true;
114+
}
115+
116+
function main() {
117+
const version = resolveCliVersion();
118+
119+
const targets = [
120+
{
121+
path: join(REPO_ROOT, "apps/objectos-one/package.json"),
122+
label: "apps/objectos-one/package.json",
123+
apply: (p) =>
124+
updateJson(p, (data) => {
125+
data.version = version;
126+
}),
127+
},
128+
{
129+
path: join(REPO_ROOT, "apps/objectos-one/src-tauri/tauri.conf.json"),
130+
label: "apps/objectos-one/src-tauri/tauri.conf.json",
131+
apply: (p) =>
132+
updateJson(p, (data) => {
133+
data.version = version;
134+
}),
135+
},
136+
{
137+
path: join(REPO_ROOT, "apps/objectos-one/src-tauri/Cargo.toml"),
138+
label: "apps/objectos-one/src-tauri/Cargo.toml",
139+
apply: (p) => updateCargoTomlVersion(p, version),
140+
},
141+
];
142+
143+
let changedAny = false;
144+
for (const t of targets) {
145+
if (!existsSync(t.path)) {
146+
warn(`skip: ${t.label} (not found)`);
147+
continue;
148+
}
149+
const changed = t.apply(t.path);
150+
if (changed) {
151+
changedAny = true;
152+
log(`${CHECK_ONLY ? "would update" : "updated"} ${t.label}${version}`);
153+
} else {
154+
log(`unchanged ${t.label} (already ${version})`);
155+
}
156+
}
157+
158+
if (CHECK_ONLY && changedAny) {
159+
console.error(
160+
`[sync-version] FAIL: versions are out of sync with @objectstack/cli@${version}. ` +
161+
`Run \`pnpm --filter @objectos/one sync-version\` and commit the result.`
162+
);
163+
process.exit(1);
164+
}
165+
}
166+
167+
main();

0 commit comments

Comments
 (0)