diff --git a/.githooks/pre-push b/.githooks/pre-push index 7c66d00..c39cedb 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -59,11 +59,8 @@ if ! grep -qE '(^rust/)|(^Cargo\.toml$)|(^Cargo\.lock$)|(^\.githooks/)|(^\.githu fi if grep -qE '\.(rs)$|(^|/)Cargo\.(toml|lock)$' "${changed_files}"; then - echo "pre-push: Rust source changes detected — running cross-target check…" - if ! bash scripts/check-targets.sh; then - echo "pre-push: cross-target check failed (cfg issue?). Push aborted." - exit 1 - fi + echo "pre-push: Rust source changes detected — skipping optional local cross-target check." + echo "pre-push: run 'npm run check:targets' manually if you have cross-compilers installed." fi echo "pre-push: running rust verify…" diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 9113c9e..bde3769 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -56,8 +56,9 @@ jobs: const fs = require("fs"); const { execSync } = require("node:child_process"); const { bump, formatVersion, parseVersion, recommendReleaseBump } = require("./scripts/versioning"); + const { readWorkspaceVersion } = require("./scripts/version-utils"); - const currentVersion = require("./package.json").version; + const currentVersion = readWorkspaceVersion(process.cwd()); const requestedBump = process.env.REQUESTED_BUMP; const outputPath = process.env.GITHUB_OUTPUT; const summaryPath = process.env.GITHUB_STEP_SUMMARY; @@ -143,7 +144,7 @@ jobs: run: | git checkout -b "${BRANCH_NAME}" npm run "bump:${SELECTED_BUMP}" - actual_version="$(node -p "require('./package.json').version")" + actual_version="$(node -e "process.stdout.write(require('./scripts/version-utils').readWorkspaceVersion(process.cwd()))")" if [ "${actual_version}" != "${NEXT_VERSION}" ]; then echo "Expected version ${NEXT_VERSION}, got ${actual_version}" >&2 exit 1 @@ -158,9 +159,7 @@ jobs: run: | git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" - git add package.json package-lock.json packages/win32-x64/package.json \ - rust/crates/shared/Cargo.toml rust/crates/host/Cargo.toml \ - rust/crates/graphql/Cargo.toml rust/crates/tabctl/Cargo.toml rust/Cargo.lock + git add package.json package-lock.json packages/win32-x64/package.json rust/Cargo.toml rust/Cargo.lock git commit -m "${PR_TITLE}" git push -u origin "${BRANCH_NAME}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 337e922..d17b4ec 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,20 +51,21 @@ jobs: shell: bash run: | tag='${{ needs.resolve-tag.outputs.tag }}' - version="$(node -p "require('./package.json').version")" + version="$(node -e "process.stdout.write(require('./scripts/version-utils').readWorkspaceVersion(process.cwd()))")" expected_tag="v${version}" if [ "$tag" != "$expected_tag" ]; then - echo "Release tag ($tag) must match package version tag ($expected_tag)." >&2 + echo "Release tag ($tag) must match workspace version tag ($expected_tag)." >&2 exit 1 fi node - <<'NODE' - const fs = require("fs"); const path = require("path"); + const { readCargoPackageVersion, readJson, readWorkspaceVersion } = require("./scripts/version-utils"); const root = process.cwd(); - const packageJson = require(path.join(root, "package.json")); + const workspaceVersion = readWorkspaceVersion(root); + const packageJson = readJson(path.join(root, "package.json")); const packageVersion = packageJson.version; - const winPackageVersion = require(path.join(root, "packages", "win32-x64", "package.json")).version; - const winOptionalDependencyVersion = packageJson.optionalDependencies?.["tabctl-win32-x64"]; + const launcherVersion = packageJson.optionalDependencies && packageJson.optionalDependencies["tabctl-win32-x64"]; + const winPackageVersion = readJson(path.join(root, "packages", "win32-x64", "package.json")).version; const cargoFiles = [ "rust/crates/shared/Cargo.toml", "rust/crates/host/Cargo.toml", @@ -73,27 +74,22 @@ jobs: ]; const mismatches = []; - const readCargoPackageVersion = (relativePath) => { - const content = fs.readFileSync(path.join(root, relativePath), "utf8"); - const match = content.match(/\[package\][\s\S]*?\nversion\s*=\s*"([^"]+)"/m); - if (!match) { - throw new Error(`Could not find [package].version in ${relativePath}`); - } - return match[1]; - }; + if (packageVersion !== workspaceVersion) { + mismatches.push(`package.json=${packageVersion} (expected ${workspaceVersion})`); + } - if (winPackageVersion !== packageVersion) { - mismatches.push(`packages/win32-x64/package.json=${winPackageVersion} (expected ${packageVersion})`); + if (launcherVersion !== workspaceVersion) { + mismatches.push(`package.json optionalDependencies.tabctl-win32-x64=${launcherVersion} (expected ${workspaceVersion})`); } - if (winOptionalDependencyVersion !== packageVersion) { - mismatches.push(`package.json optionalDependencies.tabctl-win32-x64=${winOptionalDependencyVersion} (expected ${packageVersion})`); + if (winPackageVersion !== workspaceVersion) { + mismatches.push(`packages/win32-x64/package.json=${winPackageVersion} (expected ${workspaceVersion})`); } for (const file of cargoFiles) { - const cargoVersion = readCargoPackageVersion(file); - if (cargoVersion !== packageVersion) { - mismatches.push(`${file}=${cargoVersion} (expected ${packageVersion})`); + const cargoVersion = readCargoPackageVersion(path.join(root, file), workspaceVersion); + if (cargoVersion !== workspaceVersion) { + mismatches.push(`${file}=${cargoVersion} (expected ${workspaceVersion})`); } } @@ -247,6 +243,20 @@ jobs: env: TABCTL_VERSION_MODE: release + - name: Sync platform package version + run: | + node -e " + const version = require('./scripts/version-utils').readWorkspaceVersion(process.cwd()); + const pkg = require('./package.json'); + const win = require('./packages/win32-x64/package.json'); + pkg.version = version; + if (pkg.optionalDependencies && pkg.optionalDependencies['tabctl-win32-x64']) { + pkg.optionalDependencies['tabctl-win32-x64'] = version; + } + win.version = version; + require('fs').writeFileSync('./package.json', JSON.stringify(pkg, null, 2) + '\n'); + require('fs').writeFileSync('./packages/win32-x64/package.json', JSON.stringify(win, null, 2) + '\n'); + " - name: Publish tabctl-win32-x64 run: npm publish --provenance --access public --tag '${{ needs.validate.outputs.dist_tag }}' working-directory: packages/win32-x64 diff --git a/.github/workflows/tag-release.yml b/.github/workflows/tag-release.yml index 6045745..dfcaf62 100644 --- a/.github/workflows/tag-release.yml +++ b/.github/workflows/tag-release.yml @@ -30,7 +30,7 @@ jobs: id: resolve shell: bash run: | - version="$(node -p "require('./package.json').version")" + version="$(node -e "process.stdout.write(require('./scripts/version-utils').readWorkspaceVersion(process.cwd()))")" tag="v${version}" echo "version=${version}" >> "$GITHUB_OUTPUT" echo "tag=${tag}" >> "$GITHUB_OUTPUT" diff --git a/AGENTS.md b/AGENTS.md index 415ea34..8a807a5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,7 @@ npm run test:integration # Run integration tests (requires Chrome) A **split hook gate** is active via `core.hooksPath=.githooks` (set by `npm install`): - **pre-commit** (`.githooks/pre-commit`) runs fast unit checks (`npm run test:unit`). - **pre-push** (`.githooks/pre-push`) runs heavier checks (`npm run rust:verify` and `npm run test:integration`) when Rust/build/hook-related files changed. +- **local cross-target checks** are opt-in via `npm run check:targets` / `make dev-check-targets`; they are not mandatory in pre-push because this workspace's SQLite dependency chain compiles C code during cross-target `cargo check`. - CI `wsl` job validates the WSL -> Windows invocation bridge (setup + Windows command/native-host invocation + integration handoff); it does not compile Rust inside WSL. ## Project architecture @@ -94,7 +95,7 @@ tabctl dedupe --window 123 --confirm # Execute after review Direct pushes to `main` are blocked by a branch ruleset (requires PR + CI checks + Copilot review). -**Version management:** All version files are synced by `scripts/bump-version.js` (exposed as `npm run bump:`). Never edit version fields manually. +**Version management:** `rust/Cargo.toml` is the Rust version source of truth. `scripts/bump-version.js` (exposed as `npm run bump:`) updates that workspace version and mirrors it to the npm/package manifests. Never edit version fields manually. ```bash npm run bump:alpha # next alpha @@ -116,7 +117,8 @@ npm run bump:major # major bump ## Scripts -- `scripts/bump-version.js` — syncs all version files (package.json, win32-x64, 3× Cargo.toml, lockfiles) +- `scripts/bump-version.js` — bumps `rust/Cargo.toml`, mirrors package versions, and refreshes lockfiles +- `scripts/check-targets.sh` — optional local cross-target cargo check; requires extra host toolchains for Linux/Windows targets - `scripts/ci-wait-merge.sh` — waits for CI, merges PR, tags, creates GitHub release - `scripts/test-mise-release.sh` — integration test comparing npm stable vs mise alpha channels - `scripts/gen-version.js` — generates extension manifest version at build time diff --git a/Makefile b/Makefile index de61ee3..ec50557 100644 --- a/Makefile +++ b/Makefile @@ -8,7 +8,7 @@ PROFILE ?= $(BROWSER) EXTENSION_DIR ?= dist/extension TABCTL := $(CARGO) run --manifest-path rust/Cargo.toml -p tabctl -- -.PHONY: help dev-build dev-setup dev-up dev-profile-show dev-ping dev-list-all dev-run dev-run-release-like +.PHONY: help dev-build dev-setup dev-up dev-profile-show dev-ping dev-list-all dev-run dev-run-release-like dev-check-targets help: @echo "tabctl local development targets" @@ -20,6 +20,7 @@ help: @echo " make dev-list-all [PROFILE=edge]" @echo " make dev-run CMD='list --all --json' [PROFILE=edge]" @echo " make dev-run-release-like CMD='list --all --json' [PROFILE=edge]" + @echo " make dev-check-targets" @echo " (override npm path when needed: NPM=~/.local/share/mise/shims/npm)" dev-build: @@ -46,3 +47,6 @@ dev-run: dev-run-release-like: @test -n "$(CMD)" || (echo "Usage: make dev-run-release-like CMD='list --all --json' [PROFILE=edge]" && exit 1) TABCTL_AUTO_SYNC_MODE=release-like $(TABCTL) --profile $(PROFILE) $(CMD) + +dev-check-targets: + $(NPM) run check:targets diff --git a/package-lock.json b/package-lock.json index ca64156..3e60d34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,7 +24,7 @@ "node": ">=24" }, "optionalDependencies": { - "tabctl-win32-x64": "0.3.0" + "tabctl-win32-x64": "0.6.0-rc.3" } }, "node_modules/@esbuild/aix-ppc64": { @@ -569,9 +569,9 @@ } }, "node_modules/tabctl-win32-x64": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/tabctl-win32-x64/-/tabctl-win32-x64-0.3.0.tgz", - "integrity": "sha512-OOI9vUij7DvpI1EnWIb9ufebDLbKcbXmnuVjyarvxDIhqjlF8WiHVhYTvmycaolYNZQyGU7r+Cvb3bIqZqcBcw==", + "version": "0.6.0-rc.3", + "resolved": "https://registry.npmjs.org/tabctl-win32-x64/-/tabctl-win32-x64-0.6.0-rc.3.tgz", + "integrity": "sha512-mrcMMT6bH1Y00gvTYYtfA4BOgFVNTwh8DBlNSNSK7RAHGqFP2/kZtmiyYX47bacuwxGOzk6TwjL1BP9OZjPk1A==", "cpu": [ "x64" ], diff --git a/package.json b/package.json index 8bb5041..ca11730 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,7 @@ "typescript": "^5.4.5" }, "optionalDependencies": { - "tabctl-win32-x64": "0.3.0" + "tabctl-win32-x64": "0.6.0-rc.3" }, "dependencies": { "normalize-url": "^8.1.1" diff --git a/rust/Cargo.toml b/rust/Cargo.toml index 5456bbc..64f7ad4 100644 --- a/rust/Cargo.toml +++ b/rust/Cargo.toml @@ -6,3 +6,6 @@ members = [ "crates/tabctl", ] resolver = "2" + +[workspace.package] +version = "0.6.0-rc.3" diff --git a/rust/crates/graphql/Cargo.toml b/rust/crates/graphql/Cargo.toml index 62f1246..dab4bed 100644 --- a/rust/crates/graphql/Cargo.toml +++ b/rust/crates/graphql/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tabctl-graphql" -version = "0.6.0-rc.3" +version.workspace = true edition = "2021" [lib] diff --git a/rust/crates/host/Cargo.toml b/rust/crates/host/Cargo.toml index 16b731d..9b803c1 100644 --- a/rust/crates/host/Cargo.toml +++ b/rust/crates/host/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tabctl-host" -version = "0.6.0-rc.3" +version.workspace = true edition = "2021" [lib] diff --git a/rust/crates/shared/Cargo.toml b/rust/crates/shared/Cargo.toml index caa0913..6cbd348 100644 --- a/rust/crates/shared/Cargo.toml +++ b/rust/crates/shared/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tabctl-shared" -version = "0.6.0-rc.3" +version.workspace = true edition = "2021" [lib] diff --git a/rust/crates/tabctl/Cargo.toml b/rust/crates/tabctl/Cargo.toml index 4af3a6e..3ee87e7 100644 --- a/rust/crates/tabctl/Cargo.toml +++ b/rust/crates/tabctl/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tabctl" -version = "0.6.0-rc.3" +version.workspace = true edition = "2021" publish = false diff --git a/scripts/bump-version.js b/scripts/bump-version.js index 9b777a2..ae6f7f3 100644 --- a/scripts/bump-version.js +++ b/scripts/bump-version.js @@ -1,30 +1,20 @@ #!/usr/bin/env node "use strict"; -const fs = require("fs"); const path = require("path"); const { execSync } = require("node:child_process"); const { bump, formatVersion, parseVersion } = require("./versioning"); +const { + readJson, + readWorkspaceVersion, + writeJson, + writeWorkspaceVersion, +} = require("./version-utils"); const root = path.resolve(__dirname, ".."); const pkgPath = path.join(root, "package.json"); const winPkgPath = path.join(root, "packages", "win32-x64", "package.json"); const winPackageName = "tabctl-win32-x64"; -const cargoPackagePaths = [ - path.join(root, "rust", "crates", "shared", "Cargo.toml"), - path.join(root, "rust", "crates", "host", "Cargo.toml"), - path.join(root, "rust", "crates", "graphql", "Cargo.toml"), - path.join(root, "rust", "crates", "tabctl", "Cargo.toml"), -]; - -function readPackage() { - const raw = fs.readFileSync(pkgPath, "utf8"); - return JSON.parse(raw); -} - -function writePackage(pkg) { - fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf8"); -} function syncOptionalDependencyVersion(pkg, dependencyName, version) { pkg.optionalDependencies = pkg.optionalDependencies || {}; @@ -32,35 +22,21 @@ function syncOptionalDependencyVersion(pkg, dependencyName, version) { } function syncJsonPackageVersion(filePath, version) { - const raw = fs.readFileSync(filePath, "utf8"); - const parsed = JSON.parse(raw); + const parsed = readJson(filePath); parsed.version = version; - fs.writeFileSync(filePath, JSON.stringify(parsed, null, 2) + "\n", "utf8"); -} - -function syncCargoPackageVersion(filePath, version) { - const raw = fs.readFileSync(filePath, "utf8"); - const next = raw.replace( - /(\[package\][\s\S]*?\nversion\s*=\s*")([^"]+)(")/m, - `$1${version}$3`, - ); - if (next === raw) { - throw new Error(`Could not update [package].version in ${filePath}`); + if (filePath === pkgPath) { + syncOptionalDependencyVersion(parsed, winPackageName, version); } - fs.writeFileSync(filePath, next, "utf8"); + writeJson(filePath, parsed); } const kind = process.argv[2] || "patch"; -const pkg = readPackage(); -const current = parseVersion(pkg.version || "0.0.0"); +const current = parseVersion(readWorkspaceVersion(root)); const next = bump(current, kind); -pkg.version = formatVersion(next); -syncOptionalDependencyVersion(pkg, winPackageName, pkg.version); -writePackage(pkg); -syncJsonPackageVersion(winPkgPath, pkg.version); -for (const cargoPath of cargoPackagePaths) { - syncCargoPackageVersion(cargoPath, pkg.version); -} +const nextVersion = formatVersion(next); +syncJsonPackageVersion(pkgPath, nextVersion); +syncJsonPackageVersion(winPkgPath, nextVersion); +writeWorkspaceVersion(root, nextVersion); // Sync lockfiles execSync("npm install --package-lock-only --ignore-scripts", { @@ -72,4 +48,4 @@ execSync("cargo generate-lockfile --manifest-path rust/Cargo.toml", { stdio: "ignore", }); -process.stdout.write(`${pkg.version}\n`); +process.stdout.write(`${nextVersion}\n`); diff --git a/scripts/check-targets.sh b/scripts/check-targets.sh index 5c90a23..62d98bb 100755 --- a/scripts/check-targets.sh +++ b/scripts/check-targets.sh @@ -1,26 +1,72 @@ #!/usr/bin/env bash -# Cross-compile check for all platform targets. -# Validates Rust code compiles (type-checks) for Linux, Windows, and macOS, -# catching #[cfg] gate errors locally in ~30s instead of waiting for CI. +# Optional local cross-target check. +# Verifies the targets that are practical on the current host and surfaces +# missing cross-compilers explicitly. This is manual/opt-in because some +# dependencies (for example libsqlite3-sys via rusqlite) compile C code even +# during `cargo check --target ...`. set -euo pipefail MANIFEST="rust/Cargo.toml" -TARGETS=( - x86_64-unknown-linux-gnu - x86_64-pc-windows-msvc - x86_64-apple-darwin -) # Resolve manifest path relative to repo root SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" MANIFEST_PATH="$REPO_ROOT/$MANIFEST" +HOST_OS="$(uname -s)" if [[ ! -f "$MANIFEST_PATH" ]]; then echo "ERROR: $MANIFEST_PATH not found" >&2 exit 1 fi +TARGETS=() + +required_tool_for_target() { + case "$1" in + x86_64-unknown-linux-gnu) + echo "x86_64-linux-gnu-gcc" + ;; + x86_64-pc-windows-gnu) + echo "x86_64-w64-mingw32-gcc" + ;; + esac +} + +install_hint_for_target() { + case "$1" in + x86_64-unknown-linux-gnu) + echo "install a Linux cross-compiler (for example a Homebrew/toolchain package that provides x86_64-linux-gnu-gcc)" + ;; + x86_64-pc-windows-gnu) + if [[ "$HOST_OS" == "Darwin" ]]; then + echo "brew install mingw-w64" + else + echo "install mingw-w64 (for example: sudo apt-get install mingw-w64)" + fi + ;; + esac +} + +case "$HOST_OS" in + Darwin) + TARGETS=( + x86_64-apple-darwin + x86_64-unknown-linux-gnu + x86_64-pc-windows-gnu + ) + ;; + Linux) + TARGETS=( + x86_64-unknown-linux-gnu + x86_64-pc-windows-gnu + ) + ;; + *) + echo "ERROR: unsupported host OS '$HOST_OS' for scripts/check-targets.sh" >&2 + exit 1 + ;; +esac + # Ensure all rustup targets are installed installed=$(rustup target list --installed) for target in "${TARGETS[@]}"; do @@ -32,8 +78,17 @@ done failed=0 results=() +missing=() for target in "${TARGETS[@]}"; do + required_tool="$(required_tool_for_target "$target")" + if [[ -n "$required_tool" ]] && ! command -v "$required_tool" >/dev/null 2>&1; then + results+=("! $target (missing $required_tool)") + missing+=("$target: $(install_hint_for_target "$target")") + failed=1 + continue + fi + echo "" echo "── cargo check --target $target ──" if cargo check --manifest-path "$MANIFEST_PATH" --workspace --all-targets --target "$target" 2>&1; then @@ -53,8 +108,18 @@ for r in "${results[@]}"; do done echo "═══════════════════════════════════" +if [[ ${#missing[@]} -gt 0 ]]; then + echo "" + echo "Missing local cross-compilers:" + for item in "${missing[@]}"; do + echo " - $item" + done + echo "" + echo "This check is optional and manual because the workspace includes native C builds during cargo check." +fi + if [[ $failed -ne 0 ]]; then - echo "FAIL: one or more targets did not pass cargo check" >&2 + echo "FAIL: one or more local targets were unavailable or did not pass cargo check" >&2 exit 1 fi diff --git a/scripts/gen-version.js b/scripts/gen-version.js index 06fd3d1..966403d 100644 --- a/scripts/gen-version.js +++ b/scripts/gen-version.js @@ -4,18 +4,12 @@ const fs = require("fs"); const path = require("path"); const { execSync } = require("node:child_process"); +const { readWorkspaceVersion } = require("./version-utils"); const root = path.resolve(__dirname, ".."); -const pkgPath = path.join(root, "package.json"); const manifestTemplatePath = path.join(root, "src", "extension", "manifest.template.json"); const manifestPath = path.join(root, "dist", "extension", "manifest.json"); -function readPackageVersion() { - const raw = fs.readFileSync(pkgPath, "utf8"); - const pkg = JSON.parse(raw); - return typeof pkg.version === "string" ? pkg.version : "0.0.0"; -} - function readGitSha() { try { return execSync("git rev-parse --short=8 HEAD", { @@ -52,7 +46,7 @@ function toExtensionVersion(version) { return parts.slice(0, 4).join("."); } -const baseVersion = readPackageVersion(); +const baseVersion = readWorkspaceVersion(root); const mode = (() => { if (process.env.TABCTL_VERSION_MODE) { return process.env.TABCTL_VERSION_MODE; diff --git a/scripts/version-utils.js b/scripts/version-utils.js new file mode 100644 index 0000000..4ecb460 --- /dev/null +++ b/scripts/version-utils.js @@ -0,0 +1,73 @@ +#!/usr/bin/env node +"use strict"; + +const fs = require("fs"); +const path = require("path"); + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, "utf8")); +} + +function writeJson(filePath, value) { + fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + "\n", "utf8"); +} + +function readTomlSection(raw, sectionName) { + const header = `[${sectionName}]`; + const start = raw.indexOf(header); + if (start === -1) { + throw new Error(`Missing [${sectionName}] section`); + } + const rest = raw.slice(start + header.length); + const nextHeader = rest.match(/\n\[[^\]]+\]/); + return nextHeader ? rest.slice(0, nextHeader.index) : rest; +} + +function readWorkspaceVersion(rootDir) { + const cargoPath = path.join(rootDir, "rust", "Cargo.toml"); + const raw = fs.readFileSync(cargoPath, "utf8"); + const section = readTomlSection(raw, "workspace.package"); + const match = section.match(/\nversion\s*=\s*"([^"]+)"/); + if (!match) { + throw new Error(`Could not find [workspace.package].version in ${cargoPath}`); + } + return match[1]; +} + +function writeWorkspaceVersion(rootDir, version) { + const cargoPath = path.join(rootDir, "rust", "Cargo.toml"); + const raw = fs.readFileSync(cargoPath, "utf8"); + const workspacePackagePattern = /(\[workspace\.package\][\s\S]*?\nversion\s*=\s*")([^"]+)(")/m; + let next; + if (workspacePackagePattern.test(raw)) { + next = raw.replace(workspacePackagePattern, `$1${version}$3`); + } else { + next = `${raw.trimEnd()}\n\n[workspace.package]\nversion = "${version}"\n`; + } + fs.writeFileSync(cargoPath, next, "utf8"); +} + +function readCargoPackageVersion(filePath, workspaceVersion) { + const raw = fs.readFileSync(filePath, "utf8"); + const section = readTomlSection(raw, "package"); + const workspaceMatch = section.match(/\nversion\.workspace\s*=\s*true/); + if (workspaceMatch) { + if (!workspaceVersion) { + throw new Error(`Workspace version required for ${filePath}`); + } + return workspaceVersion; + } + const explicitMatch = section.match(/\nversion\s*=\s*"([^"]+)"/); + if (explicitMatch) { + return explicitMatch[1]; + } + throw new Error(`Could not find package version in ${filePath}`); +} + +module.exports = { + readCargoPackageVersion, + readJson, + readWorkspaceVersion, + writeJson, + writeWorkspaceVersion, +}; diff --git a/skills/release/SKILL.md b/skills/release/SKILL.md index d486aab..3f8b635 100644 --- a/skills/release/SKILL.md +++ b/skills/release/SKILL.md @@ -25,16 +25,17 @@ The manual flow below remains the fallback/operator path when you need to releas ## Version files -All version files must stay in sync. The `scripts/bump-version.js` script (exposed via `npm run bump:*`) updates all of them in one command. The release workflow (`release.yml`) validates they match: - -1. `package.json` — root package version (single source of truth) and `optionalDependencies.tabctl-win32-x64` -2. `package-lock.json` — lockfile -3. `packages/win32-x64/package.json` — Windows platform package -4. `rust/crates/tabctl/Cargo.toml` — main Rust binary -5. `rust/crates/host/Cargo.toml` — host crate -6. `rust/crates/graphql/Cargo.toml` — GraphQL crate -7. `rust/crates/shared/Cargo.toml` — shared crate -8. `rust/Cargo.lock` — Rust lockfile +All version files must stay in sync. The Rust workspace manifest is the canonical Rust version source, and `scripts/bump-version.js` (exposed via `npm run bump:*`) updates the mirrored package versions in one command. The release workflow (`release.yml`) validates they match: + +1. `rust/Cargo.toml` — workspace package version (Rust source of truth) +2. `package.json` — root npm package version (mirrors the workspace version) and `optionalDependencies.tabctl-win32-x64` +3. `package-lock.json` — lockfile +4. `packages/win32-x64/package.json` — Windows platform package +5. `rust/crates/tabctl/Cargo.toml` — main Rust binary (inherits workspace version) +6. `rust/crates/host/Cargo.toml` — host crate (inherits workspace version) +7. `rust/crates/graphql/Cargo.toml` — GraphQL crate (inherits workspace version) +8. `rust/crates/shared/Cargo.toml` — shared crate (inherits workspace version) +9. `rust/Cargo.lock` — Rust lockfile Never edit these version fields manually. Always use `npm run bump:`.