feat: implement dynamic versioning from Git tags #42

Merged
sarman merged 5 commits from fix/version-dynamic-build into master 2026-04-15 02:10:11 +00:00
Owner
  • Add build.rs to read version from git describe --tags
  • Create update-version.mjs script to sync version across files
  • Add get_app_version() command to Rust backend
  • Update App.tsx to use custom version command
  • Run version update in CI before Rust checks
- Add build.rs to read version from git describe --tags - Create update-version.mjs script to sync version across files - Add get_app_version() command to Rust backend - Update App.tsx to use custom version command - Run version update in CI before Rust checks
sarman added 1 commit 2026-04-13 21:14:18 +00:00
feat: implement dynamic versioning from Git tags
Some checks failed
Test / rust-clippy (pull_request) Failing after 15s
Test / rust-tests (pull_request) Failing after 19s
Test / rust-fmt-check (pull_request) Successful in 55s
Test / frontend-typecheck (pull_request) Successful in 1m22s
Test / frontend-tests (pull_request) Successful in 1m26s
PR Review Automation / review (pull_request) Successful in 2m57s
9e1a9b1d34
- Add build.rs to read version from git describe --tags
- Create update-version.mjs script to sync version across files
- Add get_app_version() command to Rust backend
- Update App.tsx to use custom version command
- Run version update in CI before Rust checks
sarman reviewed 2026-04-13 21:17:14 +00:00
sarman left a comment
Author
Owner

🤖 Automated PR Review:\n\nHere's a comprehensive review of the PR "feat: implement dynamic versioning from Git tags", covering correctness, security, and best practices:


1. Summary

This PR implements dynamic version injection from Git tags into the application build, replacing hardcoded versions in multiple configuration files (Cargo.toml, package.json, tauri.conf.json) and exposing the version via a new Tauri command (get_app_version). It also cleans up outdated entries from CHANGELOG.md.

The approach is largely sound and follows common patterns (e.g., using git describe --tags), but several critical correctness and security issues must be addressed before merging.


⚠️ 2. Bugs & Correctness Issues

🔴 Critical: npm ci in CI workflow may fail if package-lock.json is outdated

- name: Update version from Git
  run: |
    npm ci --legacy-peer-deps
    node scripts/update-version.mjs    
  • Problem: npm ci requires an exact package-lock.json match. If the lockfile is not up-to-date (e.g., after merging unrelated PRs), this will fail.
  • Fix: Replace with npm install (or ensure lockfile is regenerated before this step):
    - name: Install dependencies
      run: npm install --legacy-peer-deps
    
    Or better yet: move version update before dependency installation.

🔴 Critical: git describe --tags --abbrev=0 may fail in shallow clones or detached HEAD

  • Problem: In CI workflows (especially PRs), Git may use shallow clones (fetch-depth: 1) or detached HEAD states. git describe requires full history and tags to work. If tags aren’t fetched (common in PRs from forks), this will fall back silently to 0.2.50, leading to incorrect versioning.
  • Fix:
    1. Ensure tags are fetched in the workflow:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # critical!
          ref: ${{ github.event.pull_request.head.sha || github.sha }}
      
    2. In update-version.mjs and build.rs, add robustness:
      • Check if .git/refs/tags exists before running git describe.
      • Fall back to git describe --tags --always (commit hash) if tags are missing.
      • Log why fallback was used.

🟠 Medium: Regex replacement in update-version.mjs is fragile

content.replace(/version = "([^"]+)"/, `version = "${version}"`)
  • Problem:
    • Fails if version is quoted differently (e.g., version = '0.2.50').
    • May match multiple version = "..." lines (e.g., in [package] and [dependencies.trcaa]), replacing the wrong one.
    • No validation that the new version is semver-compliant.
  • Fix:
    • Use a TOML parser (e.g., toml-rs) for Cargo.toml.
    • Use JSON.parse/JSON.stringify for package.json and tauri.conf.json.
    • Validate version with semver package:
      import semver from 'semver';
      if (!semver.valid(version)) {
        throw new Error(`Invalid version from Git: ${version}`);
      }
      

🟠 Medium: build.rs doesn’t update Cargo.lock

  • Problem: Cargo.lock still has version = "0.2.50" for trcaa, but Cargo.toml is updated to 0.2.62. This causes inconsistency and may break reproducible builds.
  • Fix: Regenerate Cargo.lock after version update in CI:
    - name: Update version from Git
      run: |
        npm install --legacy-peer-deps
        node scripts/update-version.mjs
        cargo generate-lockfile  # or `cargo update` if needed    
    

🟠 Medium: get_version_from_git() in build.rs may return invalid version

  • Problem: If git describe returns v0.2.62-abc123 (dirty tag), .trim_start_matches('v') yields 0.2.62-abc123, which is not a valid semver. Tauri/Rust may reject it.
  • Fix: Strip non-numeric suffixes or use git describe --tags --abbrev=0 only when tags exist, and fallback to 0.2.50 if not:
    let version = String::from_utf8_lossy(&output.stdout)
        .trim()
        .trim_start_matches('v')
        .split_once('-')  // strip git describe suffix (e.g., -gabc123)
        .map(|(v, _)| v)
        .unwrap_or_default()
        .to_string();
    

🛡️ 3. Security Issues

🔴 Critical: Command injection via untrusted Git tags

execSync('git describe --tags --abbrev=0', { ... })
  • Problem: If an attacker creates a malicious Git tag (e.g., v1.2.3; rm -rf /), git describe will output it. While execSync runs the command, the output is used directly in regex replacement. However, the bigger risk is in build.rs:

    let version = String::from_utf8_lossy(&output.stdout).trim()...
    

    If output.stdout contains newlines or control characters, it could corrupt APP_VERSION, but more critically:

    • If git describe is misconfigured (e.g., core.abbrev=0 + bad ref), it could output arbitrary text.
    • Worse: If the CI runner is compromised, git describe could be replaced with a malicious script that outputs shell metacharacters — but this is a broader infrastructure risk.
  • Mitigation:

    • Sanitize version output strictly:
      const version = output.trim().replace(/^v/, '').match(/^[0-9]+\.[0-9]+\.[0-9]+$/) 
        ? output.trim().replace(/^v/, '') 
        : '0.2.50';
      
    • In Rust: validate with regex or semver:
      let re = Regex::new(r"^\d+\.\d+\.\d+$").unwrap();
      if !re.is_match(&version) {
          eprintln!("Invalid version: {}", version);
          "0.2.50".to_string()
      } else {
          version
      }
      

🟠 Medium: npm ci with --legacy-peer-deps may install vulnerable packages

  • Problem: --legacy-peer-deps bypasses peer dependency resolution, potentially installing outdated/unpatched packages.
  • Fix: Prefer npm install or ensure lockfile is up-to-date. Better yet: pin npm version and avoid legacy flags.

📜 4. Best Practices & Recommendations

Good Practices

  • Clean separation: version logic in scripts/, build-time in build.rs.
  • Graceful fallback to 0.2.50 (though should be configurable).
  • Exposing version via Tauri command (better than @tauri-apps/api/app which may not reflect build-time version).
  • Updated CHANGELOG.md (removing outdated entries).

📝 Suggestions for Improvement

  1. Use git describe with --match to filter tags
    Prevent accidental matches on non-version tags:

    git describe --tags --match='v*' --abbrev=0
    
  2. Add version validation in CI
    Add a step to verify version is semver:

    - name: Validate version
      run: node -e "console.log(require('semver').valid(process.argv[1]))" "$(node scripts/update-version.mjs 2>&1 | grep 'Setting version' | awk '{print $NF}')"
    
  3. Avoid modifying Cargo.lock at runtime
    Instead of regenerating Cargo.lock in CI, consider:

    • Using a CI job that only updates version + regenerates lockfile, then commits back.
    • Or use cargo update -p trcaa to update just the affected package.
  4. Document versioning policy
    Add to CONTRIBUTING.md:

    Versions are derived from Git tags (vX.Y.Z). Tags must follow semver. CI will fail if git describe returns non-semver.

  5. Use tauri.conf.json schema
    Ensure tauri.conf.json has "version" in the correct location (e.g., package.version, not tauri.bundle.version — check Tauri docs).

  6. Add unit tests for update-version.mjs
    Mock execSync and test edge cases (no tags, invalid tags, etc.).


Final Recommendation

Do not merge as-is. Fix the critical issues first:

  1. Ensure Git tags are fetched in CI (fetch-depth: 0).
  2. Sanitize and validate Git tag output to prevent injection/malformed versions.
  3. Regenerate Cargo.lock after version update.
  4. Use proper parsers (TOML/JSON) instead of regex for config files.

Once fixed, this is a solid, production-ready feature.

Let me know if you'd like help drafting the corrected update-version.mjs or CI workflow!\n\n---\nthis is an automated review from Ollama

🤖 Automated PR Review:\n\nHere's a comprehensive review of the PR **"feat: implement dynamic versioning from Git tags"**, covering correctness, security, and best practices: --- ### ✅ **1. Summary** This PR implements dynamic version injection from Git tags into the application build, replacing hardcoded versions in multiple configuration files (`Cargo.toml`, `package.json`, `tauri.conf.json`) and exposing the version via a new Tauri command (`get_app_version`). It also cleans up outdated entries from `CHANGELOG.md`. The approach is largely sound and follows common patterns (e.g., using `git describe --tags`), but **several critical correctness and security issues** must be addressed before merging. --- ### ⚠️ **2. Bugs & Correctness Issues** #### 🔴 **Critical: `npm ci` in CI workflow may fail if `package-lock.json` is outdated** ```yaml - name: Update version from Git run: | npm ci --legacy-peer-deps node scripts/update-version.mjs ``` - **Problem**: `npm ci` requires an exact `package-lock.json` match. If the lockfile is not up-to-date (e.g., after merging unrelated PRs), this will fail. - **Fix**: Replace with `npm install` (or ensure lockfile is regenerated before this step): ```yaml - name: Install dependencies run: npm install --legacy-peer-deps ``` Or better yet: move version update *before* dependency installation. #### 🔴 **Critical: `git describe --tags --abbrev=0` may fail in shallow clones or detached HEAD** - **Problem**: In CI workflows (especially PRs), Git may use shallow clones (`fetch-depth: 1`) or detached HEAD states. `git describe` requires full history and tags to work. If tags aren’t fetched (common in PRs from forks), this will fall back silently to `0.2.50`, leading to incorrect versioning. - **Fix**: 1. Ensure tags are fetched in the workflow: ```yaml - uses: actions/checkout@v4 with: fetch-depth: 0 # critical! ref: ${{ github.event.pull_request.head.sha || github.sha }} ``` 2. In `update-version.mjs` and `build.rs`, add robustness: - Check if `.git/refs/tags` exists before running `git describe`. - Fall back to `git describe --tags --always` (commit hash) if tags are missing. - Log *why* fallback was used. #### 🟠 **Medium: Regex replacement in `update-version.mjs` is fragile** ```js content.replace(/version = "([^"]+)"/, `version = "${version}"`) ``` - **Problem**: - Fails if version is quoted differently (e.g., `version = '0.2.50'`). - May match *multiple* `version = "..."` lines (e.g., in `[package]` and `[dependencies.trcaa]`), replacing the wrong one. - No validation that the new version is semver-compliant. - **Fix**: - Use a TOML parser (e.g., [`toml-rs`](https://crates.io/crates/toml)) for `Cargo.toml`. - Use `JSON.parse`/`JSON.stringify` for `package.json` and `tauri.conf.json`. - Validate version with `semver` package: ```js import semver from 'semver'; if (!semver.valid(version)) { throw new Error(`Invalid version from Git: ${version}`); } ``` #### 🟠 **Medium: `build.rs` doesn’t update `Cargo.lock`** - **Problem**: `Cargo.lock` still has `version = "0.2.50"` for `trcaa`, but `Cargo.toml` is updated to `0.2.62`. This causes inconsistency and may break reproducible builds. - **Fix**: Regenerate `Cargo.lock` after version update in CI: ```yaml - name: Update version from Git run: | npm install --legacy-peer-deps node scripts/update-version.mjs cargo generate-lockfile # or `cargo update` if needed ``` #### 🟠 **Medium: `get_version_from_git()` in `build.rs` may return invalid version** - **Problem**: If `git describe` returns `v0.2.62-abc123` (dirty tag), `.trim_start_matches('v')` yields `0.2.62-abc123`, which is *not* a valid semver. Tauri/Rust may reject it. - **Fix**: Strip non-numeric suffixes or use `git describe --tags --abbrev=0` *only* when tags exist, and fallback to `0.2.50` if not: ```rust let version = String::from_utf8_lossy(&output.stdout) .trim() .trim_start_matches('v') .split_once('-') // strip git describe suffix (e.g., -gabc123) .map(|(v, _)| v) .unwrap_or_default() .to_string(); ``` --- ### 🛡️ **3. Security Issues** #### 🔴 **Critical: Command injection via untrusted Git tags** ```js execSync('git describe --tags --abbrev=0', { ... }) ``` - **Problem**: If an attacker creates a malicious Git tag (e.g., `v1.2.3; rm -rf /`), `git describe` will output it. While `execSync` runs the *command*, the *output* is used directly in regex replacement. However, **the bigger risk is in `build.rs`**: ```rust let version = String::from_utf8_lossy(&output.stdout).trim()... ``` If `output.stdout` contains newlines or control characters, it could corrupt `APP_VERSION`, but more critically: - If `git describe` is misconfigured (e.g., `core.abbrev=0` + bad ref), it could output arbitrary text. - **Worse**: If the CI runner is compromised, `git describe` could be replaced with a malicious script that outputs shell metacharacters — but this is a broader infrastructure risk. - **Mitigation**: - Sanitize version output strictly: ```js const version = output.trim().replace(/^v/, '').match(/^[0-9]+\.[0-9]+\.[0-9]+$/) ? output.trim().replace(/^v/, '') : '0.2.50'; ``` - In Rust: validate with regex or `semver`: ```rust let re = Regex::new(r"^\d+\.\d+\.\d+$").unwrap(); if !re.is_match(&version) { eprintln!("Invalid version: {}", version); "0.2.50".to_string() } else { version } ``` #### 🟠 **Medium: `npm ci` with `--legacy-peer-deps` may install vulnerable packages** - **Problem**: `--legacy-peer-deps` bypasses peer dependency resolution, potentially installing outdated/unpatched packages. - **Fix**: Prefer `npm install` or ensure lockfile is up-to-date. Better yet: pin `npm` version and avoid legacy flags. --- ### 📜 **4. Best Practices & Recommendations** #### ✅ **Good Practices** - ✅ Clean separation: version logic in `scripts/`, build-time in `build.rs`. - ✅ Graceful fallback to `0.2.50` (though should be configurable). - ✅ Exposing version via Tauri command (better than `@tauri-apps/api/app` which may not reflect build-time version). - ✅ Updated `CHANGELOG.md` (removing outdated entries). #### 📝 **Suggestions for Improvement** 1. **Use `git describe` with `--match` to filter tags** Prevent accidental matches on non-version tags: ```bash git describe --tags --match='v*' --abbrev=0 ``` 2. **Add version validation in CI** Add a step to verify version is semver: ```yaml - name: Validate version run: node -e "console.log(require('semver').valid(process.argv[1]))" "$(node scripts/update-version.mjs 2>&1 | grep 'Setting version' | awk '{print $NF}')" ``` 3. **Avoid modifying `Cargo.lock` at runtime** Instead of regenerating `Cargo.lock` in CI, consider: - Using a CI job that *only* updates version + regenerates lockfile, then commits back. - Or use `cargo update -p trcaa` to update just the affected package. 4. **Document versioning policy** Add to `CONTRIBUTING.md`: > Versions are derived from Git tags (`vX.Y.Z`). Tags must follow semver. CI will fail if `git describe` returns non-semver. 5. **Use `tauri.conf.json` schema** Ensure `tauri.conf.json` has `"version"` in the correct location (e.g., `package.version`, not `tauri.bundle.version` — check [Tauri docs](https://tauri.app/references/config/#packageversion)). 6. **Add unit tests for `update-version.mjs`** Mock `execSync` and test edge cases (no tags, invalid tags, etc.). --- ### ✅ **Final Recommendation** **Do not merge as-is.** Fix the critical issues first: 1. **Ensure Git tags are fetched in CI** (`fetch-depth: 0`). 2. **Sanitize and validate Git tag output** to prevent injection/malformed versions. 3. **Regenerate `Cargo.lock`** after version update. 4. **Use proper parsers (TOML/JSON)** instead of regex for config files. Once fixed, this is a solid, production-ready feature. Let me know if you'd like help drafting the corrected `update-version.mjs` or CI workflow!\n\n---\n*this is an automated review from Ollama*
sarman added 1 commit 2026-04-13 21:35:06 +00:00
chore: fix version update implementation
All checks were successful
PR Review Automation / review (pull_request) Successful in 2m18s
007d0ee9d5
- Replace npm ci with npm install in CI
- Remove --locked flag from cargo clippy/test
- Add cargo generate-lockfile after version update
- Update update-version.mjs with semver validation
- Add build.rs for Rust-level version injection
sarman reviewed 2026-04-13 21:37:23 +00:00
sarman left a comment
Author
Owner

🤖 Automated PR Review:\n\n### Code Review: Dynamic Versioning from Git Tags


1. Summary

This PR implements a robust, Git-tag-driven versioning system across the frontend (Node.js), Rust backend (Cargo), and build pipeline (CI). Key changes include:

  • A new scripts/update-version.mjs script that reads Git tags, validates semver, and updates package.json, Cargo.toml, tauri.conf.json, and Cargo.lock.
  • A Rust build.rs script that sets APP_VERSION at compile time (fallback to "0.2.50").
  • A new Tauri command get_app_version() for runtime version retrieval.
  • CI updates to run version update before formatting/clippy, and removal of --locked flags (⚠️ concern).
  • Version bump in package.json and Cargo files to 0.2.62.

The implementation is largely well-structured and follows best practices for version extraction and propagation. However, several critical and high-severity issues must be addressed before merging.


2. Bugs / Errors

Critical: --locked removed from cargo fmt, clippy, test
-      - run: cargo clippy --locked --manifest-path src-tauri/Cargo.toml -- -D warnings
+      - run: cargo clippy  --manifest-path src-tauri/Cargo.toml -- -D warnings

Problem: Removing --locked means cargo will re-resolve dependencies, potentially pulling newer versions than those in Cargo.lock. This defeats the purpose of having a lockfile and can lead to:

  • Non-reproducible builds.
  • CI passing but production failing (or vice versa).
  • Unintended dependency upgrades (e.g., breaking changes in patch versions).

Fix: Re-add --locked to all cargo commands in CI:

- run: cargo fmt --locked --manifest-path src-tauri/Cargo.toml --check
- run: cargo clippy --locked --manifest-path src-tauri/Cargo.toml -- -D warnings
- run: cargo test --locked --manifest-path src-tauri/Cargo.toml -- --test-threads=1

🔍 Note: cargo generate-lockfile is added before fmt, which is fine — but --locked must still be used in subsequent commands.

⚠️ Medium: Cargo.lock update logic in update-version.mjs is fragile

The updateCargoLock() function uses regex-based line-by-line parsing:

if (inTrcaaPackage && line.match(/^version\s*=\s*"/)) {
  output.push(`version = "${version}"`);
  inTrcaaPackage = false;
}

Problems:

  • Fragile to formatting changes (e.g., comments, whitespace, multiple version fields in same package).
  • Only updates trcaa — but what if other packages need version sync? (e.g., tauri-build may embed version).
  • Does not update Cargo.toml’s version field — but updateTOML() is called separately for Cargo.toml, so this is redundant and confusing.

Fix:

  • Prefer using cargo metadata --format-version=1 + jq or a TOML parser (e.g., toml-edit) instead of regex.
  • Or: Remove updateCargoLock() entirelycargo generate-lockfile already regenerates Cargo.lock with correct versions after Cargo.toml is updated. Since Cargo.lock is regenerated after update-version.mjs, the lockfile will reflect the new version automatically.

Recommendation: Remove updateCargoLock() and rely on cargo generate-lockfile (already in CI). This is safer and less error-prone.

⚠️ Medium: Git tag parsing assumes git describe --tags works in CI
const output = execSync('git describe --tags --abbrev=0', { ... });

Problem: In shallow clones (default in many CI systems), git describe may fail or return incorrect tags unless --tags and --abbrev=0 are combined with --always or depth is increased.

Fix: In CI workflow, add:

- uses: actions/checkout@v4
  with:
    fetch-depth: 0  # fetch all tags and history

Or in Gitea Actions (which this PR uses), ensure the checkout action fetches tags:

- uses: actions/checkout@v4
  with:
    fetch-depth: 0  # required for git describe
⚠️ Low: Fallback version 0.2.50 is hardcoded in multiple places
  • scripts/update-version.mjs fallback: '0.2.50'
  • build.rs fallback: "0.2.50".to_string()
  • package.json was 0.2.50 before this PR.

Risk: If versioning fails (no tags, shallow clone, detached HEAD), all components fall back to 0.2.50, which may be outdated or mismatched with the actual state.

Fix:

  • Use a single source of truth for fallback (e.g., package.json’s version field).
  • Or: Fail fast in CI if version can’t be determined (e.g., || exit 1 in update-version.mjs).

3. Security Issues

Critical: execSync in update-version.mjs lacks input sanitization
const output = execSync('git describe --tags --abbrev=0', { ... });

Problem: While git describe output is trusted, execSync is used with cwd: projectRoot. If projectRoot is compromised (e.g., symlinked to /tmp), malicious binaries could be executed.

Mitigation:

  • Use shell: false (default in Node.js execSync) — already done.
  • Validate projectRoot is a Git repo:
    if (!existsSync(resolve(projectRoot, '.git'))) {
      throw new Error('Not a Git repository');
    }
    
⚠️ Medium: tauri.conf.json beforeBuildCommand runs npm run version:update
"beforeBuildCommand": "npm run version:update && npm run build"

Risk: If version:update modifies Cargo.toml, and the build runs in a different environment (e.g., Docker, cross-platform), the version may be inconsistent.

Fix:

  • Ensure version:update is idempotent and deterministic.
  • Consider moving versioning to build.rs only (no JS script), but current dual approach is acceptable if CI enforces Git tags.

4. Best Practices

Good: Semver validation
function isValidSemver(version) {
  return /^[0-9]+\.[0-9]+\.[0-9]+$/.test(version);
}

Validates major.minor.patch only — avoids -beta, +build, etc.

Good: Rust build.rs sets APP_VERSION at compile time

This avoids runtime git calls in production binaries (safer, faster).

Good: Tauri command fallbacks to CARGO_PKG_VERSION
env::var("APP_VERSION")
  .or_else(|_| env::var("CARGO_PKG_VERSION"))

Graceful fallback if APP_VERSION isn’t set.

Missing: Version bump script should not commit Cargo.lock changes

The PR updates Cargo.lock in the script, but Cargo.lock should not be manually edited — it’s auto-generated. While cargo generate-lockfile is run in CI, the script’s direct edit of Cargo.lock is unnecessary and risky.

Fix: Remove updateCargoLock() and rely on cargo generate-lockfile.

Good: Frontend uses Tauri command instead of deprecated @tauri-apps/api/app
- import { getVersion } from "@tauri-apps/api/app";
+ import { getAppVersionCmd } from "@/lib/tauriCommands";

@tauri-apps/api/app is deprecated; using custom command is future-proof.

Missing: No tests for scripts/update-version.mjs

No unit/integration tests for version parsing, fallbacks, or TOML/lockfile updates.

Fix: Add tests (e.g., with vitest or node:test) for:

  • Valid/invalid tag formats.
  • Missing .git directory.
  • Missing files.
Good: tauri.conf.json updated to run version:update before build

Ensures version is set before frontend/Rust compilation.


  1. Re-add --locked to all cargo CI commands (critical).
  2. Remove updateCargoLock() — rely on cargo generate-lockfile.
  3. Add fetch-depth: 0 to checkout action (critical for git describe).
  4. Add .git directory existence check in update-version.mjs.
  5. Add tests for version parsing and fallback logic.
  6. Consider removing hardcoded fallback 0.2.50 — use dynamic fallback (e.g., package.json version).
  7. Add newline at end of Cargo.toml/Cargo.lock (already done — good!).

Final Verdict

Ready for merge?No — fix the --locked omission and Git tag handling first.
After fixes? → Strong candidate for merge. The implementation is thoughtful, well-structured, and aligns with modern CI/CD best practices.

Let me know if you'd like a revised patch with the recommended fixes!\n\n---\nthis is an automated review from Ollama

🤖 Automated PR Review:\n\n### Code Review: Dynamic Versioning from Git Tags --- #### **1. Summary** This PR implements a robust, Git-tag-driven versioning system across the frontend (Node.js), Rust backend (Cargo), and build pipeline (CI). Key changes include: - A new `scripts/update-version.mjs` script that reads Git tags, validates semver, and updates `package.json`, `Cargo.toml`, `tauri.conf.json`, and `Cargo.lock`. - A Rust `build.rs` script that sets `APP_VERSION` at compile time (fallback to `"0.2.50"`). - A new Tauri command `get_app_version()` for runtime version retrieval. - CI updates to run version update before formatting/clippy, and removal of `--locked` flags (⚠️ **concern**). - Version bump in `package.json` and Cargo files to `0.2.62`. The implementation is largely well-structured and follows best practices for version extraction and propagation. However, several **critical and high-severity issues** must be addressed before merging. --- #### **2. Bugs / Errors** ##### **❌ Critical: `--locked` removed from `cargo fmt`, `clippy`, `test`** ```diff - - run: cargo clippy --locked --manifest-path src-tauri/Cargo.toml -- -D warnings + - run: cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings ``` **Problem**: Removing `--locked` means `cargo` will **re-resolve dependencies**, potentially pulling newer versions than those in `Cargo.lock`. This defeats the purpose of having a lockfile and can lead to: - Non-reproducible builds. - CI passing but production failing (or vice versa). - Unintended dependency upgrades (e.g., breaking changes in patch versions). **Fix**: Re-add `--locked` to all `cargo` commands in CI: ```yaml - run: cargo fmt --locked --manifest-path src-tauri/Cargo.toml --check - run: cargo clippy --locked --manifest-path src-tauri/Cargo.toml -- -D warnings - run: cargo test --locked --manifest-path src-tauri/Cargo.toml -- --test-threads=1 ``` > 🔍 **Note**: `cargo generate-lockfile` is added *before* `fmt`, which is fine — but `--locked` must still be used in subsequent commands. ##### **⚠️ Medium: `Cargo.lock` update logic in `update-version.mjs` is fragile** The `updateCargoLock()` function uses regex-based line-by-line parsing: ```js if (inTrcaaPackage && line.match(/^version\s*=\s*"/)) { output.push(`version = "${version}"`); inTrcaaPackage = false; } ``` **Problems**: - Fragile to formatting changes (e.g., comments, whitespace, multiple `version` fields in same package). - Only updates `trcaa` — but what if other packages need version sync? (e.g., `tauri-build` may embed version). - Does **not** update `Cargo.toml`’s `version` field — but `updateTOML()` is called separately for `Cargo.toml`, so this is *redundant* and confusing. **Fix**: - Prefer using `cargo metadata --format-version=1` + `jq` or a TOML parser (e.g., `toml-edit`) instead of regex. - Or: **Remove `updateCargoLock()` entirely** — `cargo generate-lockfile` already regenerates `Cargo.lock` with correct versions *after* `Cargo.toml` is updated. Since `Cargo.lock` is regenerated *after* `update-version.mjs`, the lockfile will reflect the new version automatically. > ✅ **Recommendation**: Remove `updateCargoLock()` and rely on `cargo generate-lockfile` (already in CI). This is safer and less error-prone. ##### **⚠️ Medium: Git tag parsing assumes `git describe --tags` works in CI** ```js const output = execSync('git describe --tags --abbrev=0', { ... }); ``` **Problem**: In shallow clones (default in many CI systems), `git describe` may fail or return incorrect tags unless `--tags` and `--abbrev=0` are combined with `--always` or depth is increased. **Fix**: In CI workflow, add: ```yaml - uses: actions/checkout@v4 with: fetch-depth: 0 # fetch all tags and history ``` Or in Gitea Actions (which this PR uses), ensure the checkout action fetches tags: ```yaml - uses: actions/checkout@v4 with: fetch-depth: 0 # required for git describe ``` ##### **⚠️ Low: Fallback version `0.2.50` is hardcoded in multiple places** - `scripts/update-version.mjs` fallback: `'0.2.50'` - `build.rs` fallback: `"0.2.50".to_string()` - `package.json` was `0.2.50` before this PR. **Risk**: If versioning fails (no tags, shallow clone, detached HEAD), all components fall back to `0.2.50`, which may be outdated or mismatched with the actual state. **Fix**: - Use a *single source of truth* for fallback (e.g., `package.json`’s `version` field). - Or: Fail fast in CI if version can’t be determined (e.g., `|| exit 1` in `update-version.mjs`). --- #### **3. Security Issues** ##### **❌ Critical: `execSync` in `update-version.mjs` lacks input sanitization** ```js const output = execSync('git describe --tags --abbrev=0', { ... }); ``` **Problem**: While `git describe` output is trusted, `execSync` is used with `cwd: projectRoot`. If `projectRoot` is compromised (e.g., symlinked to `/tmp`), malicious binaries could be executed. **Mitigation**: - Use `shell: false` (default in Node.js `execSync`) — ✅ already done. - Validate `projectRoot` is a Git repo: ```js if (!existsSync(resolve(projectRoot, '.git'))) { throw new Error('Not a Git repository'); } ``` ##### **⚠️ Medium: `tauri.conf.json` `beforeBuildCommand` runs `npm run version:update`** ```json "beforeBuildCommand": "npm run version:update && npm run build" ``` **Risk**: If `version:update` modifies `Cargo.toml`, and the build runs in a different environment (e.g., Docker, cross-platform), the version may be inconsistent. **Fix**: - Ensure `version:update` is idempotent and deterministic. - Consider moving versioning to `build.rs` only (no JS script), but current dual approach is acceptable if CI enforces Git tags. --- #### **4. Best Practices** ##### ✅ **Good: Semver validation** ```js function isValidSemver(version) { return /^[0-9]+\.[0-9]+\.[0-9]+$/.test(version); } ``` ✅ Validates major.minor.patch only — avoids `-beta`, `+build`, etc. ##### ✅ **Good: Rust `build.rs` sets `APP_VERSION` at compile time** This avoids runtime `git` calls in production binaries (safer, faster). ##### ✅ **Good: Tauri command fallbacks to `CARGO_PKG_VERSION`** ```rust env::var("APP_VERSION") .or_else(|_| env::var("CARGO_PKG_VERSION")) ``` ✅ Graceful fallback if `APP_VERSION` isn’t set. ##### ❌ **Missing: Version bump script should not commit `Cargo.lock` changes** The PR updates `Cargo.lock` in the script, but `Cargo.lock` should **not** be manually edited — it’s auto-generated. While `cargo generate-lockfile` is run in CI, the script’s direct edit of `Cargo.lock` is unnecessary and risky. **Fix**: Remove `updateCargoLock()` and rely on `cargo generate-lockfile`. ##### ✅ **Good: Frontend uses Tauri command instead of deprecated `@tauri-apps/api/app`** ```diff - import { getVersion } from "@tauri-apps/api/app"; + import { getAppVersionCmd } from "@/lib/tauriCommands"; ``` ✅ `@tauri-apps/api/app` is deprecated; using custom command is future-proof. ##### ❌ **Missing: No tests for `scripts/update-version.mjs`** No unit/integration tests for version parsing, fallbacks, or TOML/lockfile updates. **Fix**: Add tests (e.g., with `vitest` or `node:test`) for: - Valid/invalid tag formats. - Missing `.git` directory. - Missing files. ##### ✅ **Good: `tauri.conf.json` updated to run `version:update` before build** Ensures version is set before frontend/Rust compilation. --- ### 🔧 **Recommended Fixes (Prioritized)** 1. **Re-add `--locked` to all `cargo` CI commands** (critical). 2. **Remove `updateCargoLock()`** — rely on `cargo generate-lockfile`. 3. **Add `fetch-depth: 0` to checkout action** (critical for `git describe`). 4. **Add `.git` directory existence check** in `update-version.mjs`. 5. **Add tests** for version parsing and fallback logic. 6. **Consider removing hardcoded fallback `0.2.50`** — use dynamic fallback (e.g., `package.json` version). 7. **Add newline at end of `Cargo.toml`/`Cargo.lock`** (already done — good!). --- ### ✅ **Final Verdict** **Ready for merge?** → **No** — fix the `--locked` omission and Git tag handling first. **After fixes?** → Strong candidate for merge. The implementation is thoughtful, well-structured, and aligns with modern CI/CD best practices. Let me know if you'd like a revised patch with the recommended fixes!\n\n---\n*this is an automated review from Ollama*
sarman added 1 commit 2026-04-13 22:54:23 +00:00
fix: add --locked to cargo commands and improve version update script
Some checks failed
Test / rust-fmt-check (pull_request) Failing after 1m11s
Test / frontend-typecheck (pull_request) Successful in 1m18s
Test / frontend-tests (pull_request) Successful in 1m21s
Test / rust-clippy (pull_request) Failing after 3m25s
PR Review Automation / review (pull_request) Successful in 3m37s
Test / rust-tests (pull_request) Successful in 5m9s
9248811076
- Add --locked to fmt, clippy, and test commands in CI
- Remove updateCargoLock() and rely on cargo generate-lockfile
- Add .git directory existence check in update-version.mjs
- Use package.json as dynamic fallback instead of hardcoded 0.2.50
- Ensure execSync uses shell: false explicitly
sarman reviewed 2026-04-13 22:57:59 +00:00
sarman left a comment
Author
Owner

🤖 Automated PR Review:\n\n### Code Review: Dynamic Versioning from Git Tags


1. Summary

This PR introduces dynamic versioning derived from Git tags, replacing hardcoded versions in package.json, Cargo.toml, and tauri.conf.json with a script (scripts/update-version.mjs) that reads the latest Git tag (stripping v prefix), validates it as SemVer, and falls back to package.json if unavailable. It also adds a Rust-side fallback via build.rs, and exposes the version via a Tauri command (get_app_version) for frontend use.

Positive aspects:

  • Good separation of concerns: version extraction logic centralized in one script.
  • Fallback mechanisms improve robustness (e.g., non-tag builds, CI without tags).
  • Rust build-time environment variable (APP_VERSION) ensures version is available at runtime even if script fails.
  • Frontend now uses a custom command instead of @tauri-apps/api/app.getVersion() (which is deprecated in Tauri v2).

⚠️ However, several critical correctness, security, and maintainability issues exist.


2. Bugs & Errors

🔴 Critical: Cargo.lock is modified at build time — violates reproducibility & breaks builds
  • Cargo.lock is updated (e.g., version = "0.2.50" → "0.2.62") by the script, but cargo generate-lockfile is run after update-version.mjs.
  • Problem: cargo generate-lockfile regenerates Cargo.lock from scratch (ignoring prior state), and cargo fmt --locked expects a stable lockfile. If Cargo.lock is modified before generate-lockfile, the lockfile may be inconsistent or regenerated with wrong versions.
  • Worse: If update-version.mjs runs after cargo generate-lockfile (e.g., in local dev), it changes Cargo.lock without regenerating it → non-deterministic builds and potential CI failures.

Fix:

  • Move cargo generate-lockfile before version update, or better:
  • Never modify Cargo.lock in CI or pre-build scripts. Instead:
    • Keep Cargo.lock versioned and only update it via cargo update in controlled workflows.
    • Let cargo build regenerate it only if needed (e.g., in clean builds).
    • Alternatively, use cargo lockfile --locked in CI to enforce consistency.

📌 Recommendation: Remove cargo generate-lockfile from CI. Use cargo build --locked instead to ensure deterministic builds.

🔴 Critical: tauri.conf.json is TOML — but it's actually JSON! Script parses it as TOML
  • scripts/update-version.mjs has updateTOML('src-tauri/tauri.conf.json', version), but tauri.conf.json is a JSON file, not TOML!
  • The script uses a TOML-style regex (^\s*version\s*=\s*"), which will never match JSON syntax ("version": "...").
  • Result: tauri.conf.json is not updated — version remains hardcoded (e.g., "version": "0.2.50"), causing version mismatch between frontend/backend.

Fix:

  • Rename updateTOMLupdateJSON and fix the regex:
    function updateJSON(path, version) {
      const fullPath = resolve(projectRoot, path);
      if (!existsSync(fullPath)) throw new Error(`File not found: ${fullPath}`);
      const content = readFileSync(fullPath, 'utf-8');
      const json = JSON.parse(content);
      json.package.version = version; // Tauri v2 uses `package.version`
      writeFileSync(fullPath, JSON.stringify(json, null, 2) + '\n', 'utf-8');
      console.log(`✓ Updated ${path} to ${version}`);
    }
    
  • Also, update call: updateJSON('src-tauri/tauri.conf.json', version).
🟠 Minor: build.rs fallback version is hardcoded
  • build.rs returns "0.2.50" as fallback — should match current package.json version to avoid drift.
  • If package.json is updated but build.rs fallback isn’t, builds may fail or use stale version.

Fix:

  • Read fallback from package.json in build.rs, like the JS script does:
    fn get_version_from_git() -> String {
        if let Ok(output) = Command::new("git").arg("describe").arg("--tags").arg("--abbrev=0").output() {
            if output.status.success() {
                let version = String::from_utf8_lossy(&output.stdout).trim().trim_start_matches('v').to_string();
                if !version.is_empty() {
                    return version;
                }
            }
        }
        // Read fallback from package.json
        let pkg_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../package.json");
        if let Ok(content) = fs::read_to_string(pkg_path) {
            if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) {
                if let Some(v) = json.get("version").and_then(|v| v.as_str()) {
                    return v.to_string();
                }
            }
        }
        "0.2.50".to_string() // last-resort fallback
    }
    
🟠 Minor: Cargo.toml has extra blank lines added
  • 4 blank lines added at end of Cargo.toml — unnecessary and inconsistent with formatting.

Fix: Remove extra newlines.


3. Security Issues

🔴 Critical: execSync in Node.js script uses shell: false but no input sanitization
  • execSync('git describe --tags --abbrev=0', { shell: false }) is safe, but if projectRoot is user-controllable, an attacker could inject malicious paths (e.g., projectRoot = "/tmp; malicious_command").
  • While shell: false prevents shell injection, path traversal or symlink attacks are still possible if projectRoot is not validated.

Fix:

  • Validate projectRoot is an absolute path and exists:
    const projectRoot = resolve(__dirname, '..');
    if (!existsSync(projectRoot)) {
      throw new Error(`Project root does not exist: ${projectRoot}`);
    }
    // Optional: resolve to canonical path to prevent symlinks
    const canonicalRoot = fs.realpathSync(projectRoot);
    
  • Also, avoid resolve(__dirname, '..') in production — use process.cwd() or a config.
🟠 Medium: update-version.mjs writes to package.json without atomic write
  • writeFileSync is not atomic — if interrupted (e.g., power loss), package.json may be corrupted (partial write).

Fix:

  • Use atomic write (e.g., fs.promises.writeFile + temp file + rename):
    import { writeFile, rename, mkdtemp } from 'fs/promises';
    import { tmpdir } from 'os';
    import { join } from 'path';
    
    async function atomicWriteFile(path, data) {
      const dir = await mkdtemp(join(tmpdir(), 'tmp-'));
      const tmpPath = join(dir, 'tmp.json');
      await writeFile(tmpPath, data, 'utf-8');
      await rename(tmpPath, path);
    }
    
🟠 Medium: No validation of Git tag format beyond regex
  • isValidSemver only checks X.Y.Z, but Git tags may include pre-release (-alpha.1) or build metadata (+build.123).
  • While the script removes v, it doesn’t handle v1.2.3-beta"1.2.3-beta" fails isValidSemver, falls back to package.json. This is intentional, but the warning is misleading.

Fix:

  • Clarify in logs:
    console.warn(Git tag "${version}" is not pure SemVer (X.Y.Z); using fallback. Full tag: ${output.trim()})
  • Consider supporting SemVer with prerelease (e.g., /^0\.\d+\.\d+(?:-(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))?$/).

4. Best Practices

Good: Centralized version logic
  • One source of truth: Git tags + fallback to package.json.
Good: Rust build.rs sets APP_VERSION
  • Ensures version is available at runtime without runtime Git dependency.
⚠️ Missing: CI workflow should validate version consistency
  • The workflow runs update-version.mjs, but no check ensures package.json, Cargo.toml, and tauri.conf.json versions match.
  • If one update fails (e.g., due to JSON parsing error), CI may proceed with mismatched versions.

Fix:

  • Add a CI step to validate version consistency:
    - name: Validate version consistency
      run: |
        PKG_VER=$(node -p "require('./package.json').version")
        CARGO_VER=$(grep '^version' src-tauri/Cargo.toml | sed 's/.*"\(.*\)".*/\1/')
        TAURI_VER=$(node -p "require('./src-tauri/tauri.conf.json').package.version")
        [ "$PKG_VER" = "$CARGO_VER" ] && [ "$PKG_VER" = "$TAURI_VER" ] || { echo "Version mismatch: pkg=$PKG_VER, cargo=$CARGO_VER, tauri=$TAURI_VER"; exit 1; }    
    
⚠️ Missing: Script should be idempotent
  • Running npm run version:update multiple times should not cause side effects (e.g., re-updating Cargo.lock unnecessarily).

Fix:

  • Compare current version in files before writing:
    const currentVersion = json.version;
    if (currentVersion === version) {
      console.log(`✓ Version already up-to-date: ${version}`);
      return;
    }
    
⚠️ Missing: Documentation
  • No README or comment explaining:
    • How to create a Git tag for release (git tag v0.2.62 && git push --tags)
    • How to test locally (e.g., git tag -d v0.2.62 && git tag v0.2.62)

Fix:

  • Add to CONTRIBUTING.md:

    To release:

    1. Ensure package.json version is correct (e.g., 0.2.62).
    2. Tag and push: git tag v0.2.62 && git push origin v0.2.62.
    3. CI will auto-detect the tag and update all version fields.
⚠️ Minor: CHANGELOG.md truncation
  • The diff removes 20+ lines of changelog entries (e.g., ## [0.2.55], ## [0.2.54]).
  • This is likely accidental — unless the changelog is auto-generated and old entries are archived elsewhere.

Fix: Verify this is intentional (e.g., using git-cliff to archive old releases). If not, restore entries.


Final Recommendations

Priority Issue Action
🔴 Critical Cargo.lock modified in CI Remove cargo generate-lockfile; use --locked in CI
🔴 Critical tauri.conf.json parsed as TOML Fix updateJSON() for JSON
🔴 Critical build.rs hardcoded fallback Read fallback from package.json
🟠 High Atomic file writes Use temp file + rename
🟠 High Version consistency check in CI Add validation step
🟠 Medium Git tag validation Support SemVer with prerelease
🟡 Low Extra blank lines in Cargo.toml Remove
🟡 Low Idempotency Skip if version unchanged
🟡 Low Documentation Add release instructions

Approve with Conditions

Do not merge until the critical bugs (TOML/JSON mismatch, Cargo.lock handling) are fixed. Once those are resolved, this is a solid, well-structured feature.\n\n---\nthis is an automated review from Ollama

🤖 Automated PR Review:\n\n### Code Review: Dynamic Versioning from Git Tags --- #### **1. Summary** This PR introduces **dynamic versioning derived from Git tags**, replacing hardcoded versions in `package.json`, `Cargo.toml`, and `tauri.conf.json` with a script (`scripts/update-version.mjs`) that reads the latest Git tag (stripping `v` prefix), validates it as SemVer, and falls back to `package.json` if unavailable. It also adds a Rust-side fallback via `build.rs`, and exposes the version via a Tauri command (`get_app_version`) for frontend use. ✅ **Positive aspects**: - Good separation of concerns: version extraction logic centralized in one script. - Fallback mechanisms improve robustness (e.g., non-tag builds, CI without tags). - Rust build-time environment variable (`APP_VERSION`) ensures version is available at runtime even if script fails. - Frontend now uses a custom command instead of `@tauri-apps/api/app.getVersion()` (which is deprecated in Tauri v2). ⚠️ **However**, several **critical correctness, security, and maintainability issues** exist. --- #### **2. Bugs & Errors** ##### 🔴 **Critical: `Cargo.lock` is modified at build time — violates reproducibility & breaks builds** - `Cargo.lock` is updated (e.g., `version = "0.2.50" → "0.2.62"`) **by the script**, but `cargo generate-lockfile` is run *after* `update-version.mjs`. - **Problem**: `cargo generate-lockfile` regenerates `Cargo.lock` from scratch (ignoring prior state), and `cargo fmt --locked` expects a *stable* lockfile. If `Cargo.lock` is modified *before* `generate-lockfile`, the lockfile may be inconsistent or regenerated with wrong versions. - Worse: If `update-version.mjs` runs *after* `cargo generate-lockfile` (e.g., in local dev), it changes `Cargo.lock` *without* regenerating it → **non-deterministic builds** and potential CI failures. ✅ **Fix**: - Move `cargo generate-lockfile` *before* version update, or better: - **Never modify `Cargo.lock` in CI or pre-build scripts**. Instead: - Keep `Cargo.lock` versioned and only update it via `cargo update` in controlled workflows. - Let `cargo build` regenerate it *only* if needed (e.g., in clean builds). - Alternatively, use `cargo lockfile --locked` in CI to enforce consistency. > 📌 **Recommendation**: Remove `cargo generate-lockfile` from CI. Use `cargo build --locked` instead to ensure deterministic builds. ##### 🔴 **Critical: `tauri.conf.json` is TOML — but it's actually JSON! Script parses it as TOML** - `scripts/update-version.mjs` has `updateTOML('src-tauri/tauri.conf.json', version)`, but `tauri.conf.json` is a **JSON file**, not TOML! - The script uses a *TOML-style* regex (`^\s*version\s*=\s*"`), which **will never match** JSON syntax (`"version": "..."`). - **Result**: `tauri.conf.json` is *not* updated — version remains hardcoded (e.g., `"version": "0.2.50"`), causing version mismatch between frontend/backend. ✅ **Fix**: - Rename `updateTOML` → `updateJSON` and fix the regex: ```js function updateJSON(path, version) { const fullPath = resolve(projectRoot, path); if (!existsSync(fullPath)) throw new Error(`File not found: ${fullPath}`); const content = readFileSync(fullPath, 'utf-8'); const json = JSON.parse(content); json.package.version = version; // Tauri v2 uses `package.version` writeFileSync(fullPath, JSON.stringify(json, null, 2) + '\n', 'utf-8'); console.log(`✓ Updated ${path} to ${version}`); } ``` - Also, update call: `updateJSON('src-tauri/tauri.conf.json', version)`. ##### 🟠 **Minor: `build.rs` fallback version is hardcoded** - `build.rs` returns `"0.2.50"` as fallback — should match current `package.json` version to avoid drift. - If `package.json` is updated but `build.rs` fallback isn’t, builds may fail or use stale version. ✅ **Fix**: - Read fallback from `package.json` in `build.rs`, like the JS script does: ```rust fn get_version_from_git() -> String { if let Ok(output) = Command::new("git").arg("describe").arg("--tags").arg("--abbrev=0").output() { if output.status.success() { let version = String::from_utf8_lossy(&output.stdout).trim().trim_start_matches('v').to_string(); if !version.is_empty() { return version; } } } // Read fallback from package.json let pkg_path = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../package.json"); if let Ok(content) = fs::read_to_string(pkg_path) { if let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) { if let Some(v) = json.get("version").and_then(|v| v.as_str()) { return v.to_string(); } } } "0.2.50".to_string() // last-resort fallback } ``` ##### 🟠 **Minor: `Cargo.toml` has extra blank lines added** - 4 blank lines added at end of `Cargo.toml` — unnecessary and inconsistent with formatting. ✅ **Fix**: Remove extra newlines. --- #### **3. Security Issues** ##### 🔴 **Critical: `execSync` in Node.js script uses `shell: false` but no input sanitization** - `execSync('git describe --tags --abbrev=0', { shell: false })` is safe, but **if `projectRoot` is user-controllable**, an attacker could inject malicious paths (e.g., `projectRoot = "/tmp; malicious_command"`). - While `shell: false` prevents shell injection, **path traversal or symlink attacks** are still possible if `projectRoot` is not validated. ✅ **Fix**: - Validate `projectRoot` is an absolute path and exists: ```js const projectRoot = resolve(__dirname, '..'); if (!existsSync(projectRoot)) { throw new Error(`Project root does not exist: ${projectRoot}`); } // Optional: resolve to canonical path to prevent symlinks const canonicalRoot = fs.realpathSync(projectRoot); ``` - Also, avoid `resolve(__dirname, '..')` in production — use `process.cwd()` or a config. ##### 🟠 **Medium: `update-version.mjs` writes to `package.json` without atomic write** - `writeFileSync` is not atomic — if interrupted (e.g., power loss), `package.json` may be corrupted (partial write). ✅ **Fix**: - Use atomic write (e.g., `fs.promises.writeFile` + temp file + `rename`): ```js import { writeFile, rename, mkdtemp } from 'fs/promises'; import { tmpdir } from 'os'; import { join } from 'path'; async function atomicWriteFile(path, data) { const dir = await mkdtemp(join(tmpdir(), 'tmp-')); const tmpPath = join(dir, 'tmp.json'); await writeFile(tmpPath, data, 'utf-8'); await rename(tmpPath, path); } ``` ##### 🟠 **Medium: No validation of Git tag format beyond regex** - `isValidSemver` only checks `X.Y.Z`, but Git tags may include pre-release (`-alpha.1`) or build metadata (`+build.123`). - While the script *removes* `v`, it doesn’t handle `v1.2.3-beta` → `"1.2.3-beta"` fails `isValidSemver`, falls back to `package.json`. This is *intentional*, but the warning is misleading. ✅ **Fix**: - Clarify in logs: `console.warn(`Git tag "${version}" is not pure SemVer (X.Y.Z); using fallback. Full tag: ${output.trim()}`)` - Consider supporting SemVer *with* prerelease (e.g., `/^0\.\d+\.\d+(?:-(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*))?$/`). --- #### **4. Best Practices** ##### ✅ **Good: Centralized version logic** - One source of truth: Git tags + fallback to `package.json`. ##### ✅ **Good: Rust `build.rs` sets `APP_VERSION`** - Ensures version is available at runtime without runtime Git dependency. ##### ⚠️ **Missing: CI workflow should validate version consistency** - The workflow runs `update-version.mjs`, but **no check ensures `package.json`, `Cargo.toml`, and `tauri.conf.json` versions match**. - If one update fails (e.g., due to JSON parsing error), CI may proceed with mismatched versions. ✅ **Fix**: - Add a CI step to validate version consistency: ```yaml - name: Validate version consistency run: | PKG_VER=$(node -p "require('./package.json').version") CARGO_VER=$(grep '^version' src-tauri/Cargo.toml | sed 's/.*"\(.*\)".*/\1/') TAURI_VER=$(node -p "require('./src-tauri/tauri.conf.json').package.version") [ "$PKG_VER" = "$CARGO_VER" ] && [ "$PKG_VER" = "$TAURI_VER" ] || { echo "Version mismatch: pkg=$PKG_VER, cargo=$CARGO_VER, tauri=$TAURI_VER"; exit 1; } ``` ##### ⚠️ **Missing: Script should be idempotent** - Running `npm run version:update` multiple times should not cause side effects (e.g., re-updating `Cargo.lock` unnecessarily). ✅ **Fix**: - Compare current version in files before writing: ```js const currentVersion = json.version; if (currentVersion === version) { console.log(`✓ Version already up-to-date: ${version}`); return; } ``` ##### ⚠️ **Missing: Documentation** - No README or comment explaining: - How to create a Git tag for release (`git tag v0.2.62 && git push --tags`) - How to test locally (e.g., `git tag -d v0.2.62 && git tag v0.2.62`) ✅ **Fix**: - Add to `CONTRIBUTING.md`: > To release: > 1. Ensure `package.json` version is correct (e.g., `0.2.62`). > 2. Tag and push: `git tag v0.2.62 && git push origin v0.2.62`. > 3. CI will auto-detect the tag and update all version fields. ##### ⚠️ **Minor: `CHANGELOG.md` truncation** - The diff removes 20+ lines of changelog entries (e.g., `## [0.2.55]`, `## [0.2.54]`). - **This is likely accidental** — unless the changelog is auto-generated and old entries are archived elsewhere. ✅ **Fix**: Verify this is intentional (e.g., using `git-cliff` to archive old releases). If not, restore entries. --- ### ✅ **Final Recommendations** | Priority | Issue | Action | |--------|-------|--------| | 🔴 Critical | `Cargo.lock` modified in CI | Remove `cargo generate-lockfile`; use `--locked` in CI | | 🔴 Critical | `tauri.conf.json` parsed as TOML | Fix `updateJSON()` for JSON | | 🔴 Critical | `build.rs` hardcoded fallback | Read fallback from `package.json` | | 🟠 High | Atomic file writes | Use temp file + rename | | 🟠 High | Version consistency check in CI | Add validation step | | 🟠 Medium | Git tag validation | Support SemVer with prerelease | | 🟡 Low | Extra blank lines in `Cargo.toml` | Remove | | 🟡 Low | Idempotency | Skip if version unchanged | | 🟡 Low | Documentation | Add release instructions | --- ### ✅ **Approve with Conditions** **Do not merge** until the **critical bugs** (TOML/JSON mismatch, `Cargo.lock` handling) are fixed. Once those are resolved, this is a solid, well-structured feature.\n\n---\n*this is an automated review from Ollama*
sarman added 1 commit 2026-04-15 01:50:55 +00:00
fix: remove invalid --locked flag from cargo commands and fix format string
All checks were successful
Test / rust-fmt-check (pull_request) Successful in 1m3s
PR Review Automation / review (pull_request) Successful in 2m54s
Test / frontend-typecheck (pull_request) Successful in 1m14s
Test / frontend-tests (pull_request) Successful in 1m25s
Test / rust-clippy (pull_request) Successful in 8m1s
Test / rust-tests (pull_request) Successful in 10m11s
1055841b6f
- Remove --locked flag from cargo fmt, clippy, and test commands in CI
- Update build.rs to use Rust 2021 direct variable interpolation in format strings
sarman added this to the Release Candidit milestone 2026-04-15 01:53:39 +00:00
sarman added this to the Initial Release project 2026-04-15 01:53:44 +00:00
sarman reviewed 2026-04-15 01:53:47 +00:00
sarman left a comment
Author
Owner

🤖 Automated PR Review:\n\n### Code Review Summary

This PR implements dynamic versioning from Git tags, updating version fields in package.json, Cargo.toml, and tauri.conf.json via a Node.js script and a Rust build script. It also removes the --locked flag from cargo commands, removes unused code from azuredevops_search.rs, and adds a get_app_version() Tauri command.

Below is a detailed review covering correctness, security, and best practices.


1. Summary of Changes

  • New versioning system:
    • scripts/update-version.mjs: Node.js script that reads Git tags, validates semver, and updates package.json, Cargo.toml, and tauri.conf.json.
    • src-tauri/build.rs: Rust build script sets APP_VERSION env var at compile time (fallback to "0.2.50").
    • src-tauri/src/commands/system.rs: Exposes get_app_version() to frontend via Tauri.
  • CI changes:
    • Removes --locked from cargo clippy/test (⚠️ concern).
    • Adds npm install + node scripts/update-version.mjs before Rust checks.
  • Code cleanup:
    • Removed unused imports, HTML sanitization, query expansion logic from azuredevops_search.rs.
    • Removed url dependency from Cargo.toml.
  • Version bump: 0.2.500.2.62 in package.json, Cargo.toml, and Cargo.lock.

⚠️ 2. Critical Bugs & Correctness Issues

🔴 A. --locked removed from cargo commands

  • Problem:
    cargo clippy --locked and cargo test --locked ensure reproducible builds by using exact versions from Cargo.lock. Removing --locked risks:

    • Unintended dependency upgrades during CI.
    • Flaky tests or clippy failures due to transitive dependency changes.
    • Potential runtime bugs if dev dependencies (e.g., mockito) behave differently across versions.
  • Fix:

    -      - run: cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings
    +      - run: cargo clippy --locked --manifest-path src-tauri/Cargo.toml -- -D warnings
    
    -      - run: cargo test --manifest-path src-tauri/Cargo.toml -- --test-threads=1
    +      - run: cargo test --locked --manifest-path src-tauri/Cargo.toml -- --test-threads=1
    

🔴 B. tauri.conf.json is not a TOML file

  • Problem:
    scripts/update-version.mjs calls updateTOML('src-tauri/tauri.conf.json', version), but tauri.conf.json is JSON, not TOML. The current code assumes TOML syntax (version = "...") and will corrupt the JSON file.

    Example corruption:

    // Before
    { "version": "0.2.50" }
    
    // After (WRONG)
    version = "0.2.62"
    
  • Fix:

    • Add a separate updateJson() function for tauri.conf.json.
    • Or better: use Tauri’s built-in versioning (see Best Practices below).

🟠 C. Cargo.lock dependency removal (url) is unexplained

  • Problem:
    url = "2.5.8" was removed from Cargo.toml, and Cargo.lock updated. But:

    • No evidence of url being unused in code (unless urlencoding fully replaced it).
    • If url was used elsewhere (e.g., parsing URLs), this may cause runtime panics.
  • Fix:

    • Verify url is truly unused (e.g., cargo tree -i url).
    • If used, replace with urlencoding or url’s serde-compatible features.

🟠 D. build.rs fallback version is hardcoded

  • Problem:
    build.rs falls back to "0.2.50" instead of reading Cargo.toml or package.json. This defeats the purpose of dynamic versioning during local dev builds.

  • Fix:

    fn get_version_from_git() -> String {
        // ... git logic ...
        "0.2.50".to_string() // ❌ hardcoded
    }
    

    → Prefer reading Cargo.toml first:

    use std::fs;
    let cargo_toml = fs::read_to_string("Cargo.toml").unwrap_or_default();
    let version = toml::from_str::<toml::Value>(&cargo_toml)
        .ok()
        .and_then(|v| v.get("package")?.get("version")?.as_str())
        .unwrap_or("0.2.50");
    

🔒 3. Security Issues

🟡 A. execSync in Node.js script lacks input sanitization

  • Problem:
    execSync('git describe --tags --abbrev=0') is safe if projectRoot is trusted. But:

    • If projectRoot is user-controlled (e.g., via symlink attacks), could execute arbitrary commands.
    • shell: false is correct, but cwd: projectRoot could be unsafe if projectRoot is derived from untrusted input.
  • Mitigation:

    • Ensure projectRoot is resolved from import.meta.url (already done ).
    • Add a check to prevent path traversal:
      const realRoot = fs.realpathSync(projectRoot);
      if (!realRoot.startsWith(process.cwd())) {
        throw new Error('Refusing to operate outside current directory');
      }
      

🟡 B. JSON.parse without error boundary in getFallbackVersion()

  • Problem:
    JSON.parse(content) may throw if package.json is corrupted. The catch returns '0.2.50', but logging is missing.

  • Fix:

    } catch (e) {
      console.error(`Failed to parse package.json: ${e.message}`);
      return '0.2.50';
    }
    

🛠️ 4. Best Practices & Recommendations

A. Prefer Tauri’s built-in versioning

  • Tauri v2+ supports tauri.conf.json "version" field, and tauri build automatically injects it into APP_VERSION.
  • Recommendation:
    Remove update-version.mjs and build.rs version logic. Instead:
    • Use git describe to tag releases.
    • Run npm version <tag> (which updates package.json and commits).
    • Tauri will pick up the version from package.jsontauri.conf.jsonCargo.toml (if synced via tauri.conf.json).

B. Validate Git tags more strictly

  • git describe --tags may return non-semver tags (e.g., v0.2.62-3-gabc123).

  • Current code only checks git describe --tags --abbrev=0, which returns the latest tag, but:

    • If the latest tag is not semver, it falls back to package.json.
    • If package.json is stale, version drift occurs.
  • Better approach:

    • Use git describe --tags --dirty to detect uncommitted changes.
    • Consider git describe --tags --match "v*" to filter tags.
    • Add a CI check: git tag --points-at HEAD to ensure the commit is tagged.

C. Avoid manual TOML/JSON editing

  • Problem:
    updateTOML() uses regex-like string replacement, which:

    • Breaks on comments or multi-line values.
    • Loses formatting.
    • Risks overwriting adjacent lines.
  • Fix:

    • Use toml-rs (Rust) or toml (Node.js) for safe TOML editing.
    • For tauri.conf.json, use tauri.conf.json’s "version" field and rely on Tauri’s tooling.

D. Add tests for update-version.mjs

  • No tests for:
    • Valid/invalid semver tags.
    • Missing .git directory.
    • Corrupted package.json.
  • Suggestion: Add unit tests with jest or node:test.

📌 Final Recommendations

Issue Type Priority Action
--locked removal 🔴 Critical Restore --locked
tauri.conf.json handling 🔴 Critical Fix JSON parsing
Hardcoded fallback 🟠 High Read from Cargo.toml
Dependency removal 🟠 High Verify url usage
Security: path traversal 🟡 Medium Add realpathSync check
Versioning strategy 🟢 Low Adopt Tauri-native versioning

Conclusion

The PR introduces a useful dynamic versioning feature but has critical correctness issues (--locked removal, JSON/TOML confusion) and security risks (unvalidated exec). With fixes to the top issues, this would be a solid improvement. I recommend blocking merge until the critical bugs are resolved.

Let me know if you'd like me to draft a PR review comment or suggest a revised update-version.mjs implementation.\n\n---\nthis is an automated review from Ollama

🤖 Automated PR Review:\n\n### Code Review Summary This PR implements **dynamic versioning from Git tags**, updating version fields in `package.json`, `Cargo.toml`, and `tauri.conf.json` via a Node.js script and a Rust build script. It also removes the `--locked` flag from `cargo` commands, removes unused code from `azuredevops_search.rs`, and adds a `get_app_version()` Tauri command. Below is a detailed review covering correctness, security, and best practices. --- ## ✅ 1. **Summary of Changes** - **New versioning system**: - `scripts/update-version.mjs`: Node.js script that reads Git tags, validates semver, and updates `package.json`, `Cargo.toml`, and `tauri.conf.json`. - `src-tauri/build.rs`: Rust build script sets `APP_VERSION` env var at compile time (fallback to `"0.2.50"`). - `src-tauri/src/commands/system.rs`: Exposes `get_app_version()` to frontend via Tauri. - **CI changes**: - Removes `--locked` from `cargo clippy`/`test` (⚠️ **concern**). - Adds `npm install` + `node scripts/update-version.mjs` before Rust checks. - **Code cleanup**: - Removed unused imports, HTML sanitization, query expansion logic from `azuredevops_search.rs`. - Removed `url` dependency from `Cargo.toml`. - **Version bump**: `0.2.50` → `0.2.62` in `package.json`, `Cargo.toml`, and `Cargo.lock`. --- ## ⚠️ 2. **Critical Bugs & Correctness Issues** ### 🔴 **A. `--locked` removed from `cargo` commands** - **Problem**: `cargo clippy --locked` and `cargo test --locked` ensure reproducible builds by using **exact versions from `Cargo.lock`**. Removing `--locked` risks: - Unintended dependency upgrades during CI. - Flaky tests or clippy failures due to transitive dependency changes. - Potential runtime bugs if dev dependencies (e.g., `mockito`) behave differently across versions. - **Fix**: ```diff - - run: cargo clippy --manifest-path src-tauri/Cargo.toml -- -D warnings + - run: cargo clippy --locked --manifest-path src-tauri/Cargo.toml -- -D warnings ``` ```diff - - run: cargo test --manifest-path src-tauri/Cargo.toml -- --test-threads=1 + - run: cargo test --locked --manifest-path src-tauri/Cargo.toml -- --test-threads=1 ``` ### 🔴 **B. `tauri.conf.json` is not a TOML file** - **Problem**: `scripts/update-version.mjs` calls `updateTOML('src-tauri/tauri.conf.json', version)`, but `tauri.conf.json` is **JSON**, not TOML. The current code assumes TOML syntax (`version = "..."`) and will corrupt the JSON file. Example corruption: ```json // Before { "version": "0.2.50" } // After (WRONG) version = "0.2.62" ``` - **Fix**: - Add a separate `updateJson()` function for `tauri.conf.json`. - Or better: **use Tauri’s built-in versioning** (see *Best Practices* below). ### 🟠 **C. `Cargo.lock` dependency removal (`url`) is unexplained** - **Problem**: `url = "2.5.8"` was removed from `Cargo.toml`, and `Cargo.lock` updated. But: - No evidence of `url` being unused in code (unless `urlencoding` fully replaced it). - If `url` was used elsewhere (e.g., parsing URLs), this may cause runtime panics. - **Fix**: - Verify `url` is truly unused (e.g., `cargo tree -i url`). - If used, replace with `urlencoding` or `url`’s `serde`-compatible features. ### 🟠 **D. `build.rs` fallback version is hardcoded** - **Problem**: `build.rs` falls back to `"0.2.50"` instead of reading `Cargo.toml` or `package.json`. This defeats the purpose of dynamic versioning during local dev builds. - **Fix**: ```rust fn get_version_from_git() -> String { // ... git logic ... "0.2.50".to_string() // ❌ hardcoded } ``` → Prefer reading `Cargo.toml` first: ```rust use std::fs; let cargo_toml = fs::read_to_string("Cargo.toml").unwrap_or_default(); let version = toml::from_str::<toml::Value>(&cargo_toml) .ok() .and_then(|v| v.get("package")?.get("version")?.as_str()) .unwrap_or("0.2.50"); ``` --- ## 🔒 3. **Security Issues** ### 🟡 **A. `execSync` in Node.js script lacks input sanitization** - **Problem**: `execSync('git describe --tags --abbrev=0')` is safe *if* `projectRoot` is trusted. But: - If `projectRoot` is user-controlled (e.g., via symlink attacks), could execute arbitrary commands. - `shell: false` is correct, but `cwd: projectRoot` could be unsafe if `projectRoot` is derived from untrusted input. - **Mitigation**: - Ensure `projectRoot` is resolved from `import.meta.url` (already done ✅). - Add a check to prevent path traversal: ```js const realRoot = fs.realpathSync(projectRoot); if (!realRoot.startsWith(process.cwd())) { throw new Error('Refusing to operate outside current directory'); } ``` ### 🟡 **B. `JSON.parse` without error boundary in `getFallbackVersion()`** - **Problem**: `JSON.parse(content)` may throw if `package.json` is corrupted. The `catch` returns `'0.2.50'`, but logging is missing. - **Fix**: ```js } catch (e) { console.error(`Failed to parse package.json: ${e.message}`); return '0.2.50'; } ``` --- ## 🛠️ 4. **Best Practices & Recommendations** ### ✅ **A. Prefer Tauri’s built-in versioning** - Tauri v2+ supports `tauri.conf.json` `"version"` field, and `tauri build` automatically injects it into `APP_VERSION`. - **Recommendation**: Remove `update-version.mjs` and `build.rs` version logic. Instead: - Use `git describe` to tag releases. - Run `npm version <tag>` (which updates `package.json` and commits). - Tauri will pick up the version from `package.json` → `tauri.conf.json` → `Cargo.toml` (if synced via `tauri.conf.json`). ### ✅ **B. Validate Git tags more strictly** - `git describe --tags` may return non-semver tags (e.g., `v0.2.62-3-gabc123`). - **Current code** only checks `git describe --tags --abbrev=0`, which returns the *latest* tag, but: - If the latest tag is not semver, it falls back to `package.json`. - If `package.json` is stale, version drift occurs. - **Better approach**: - Use `git describe --tags --dirty` to detect uncommitted changes. - Consider `git describe --tags --match "v*"` to filter tags. - Add a CI check: `git tag --points-at HEAD` to ensure the commit is tagged. ### ✅ **C. Avoid manual TOML/JSON editing** - **Problem**: `updateTOML()` uses regex-like string replacement, which: - Breaks on comments or multi-line values. - Loses formatting. - Risks overwriting adjacent lines. - **Fix**: - Use `toml-rs` (Rust) or `toml` (Node.js) for safe TOML editing. - For `tauri.conf.json`, use `tauri.conf.json`’s `"version"` field and rely on Tauri’s tooling. ### ✅ **D. Add tests for `update-version.mjs`** - No tests for: - Valid/invalid semver tags. - Missing `.git` directory. - Corrupted `package.json`. - **Suggestion**: Add unit tests with `jest` or `node:test`. --- ## 📌 Final Recommendations | Issue Type | Priority | Action | |------------|----------|--------| | `--locked` removal | 🔴 Critical | Restore `--locked` | | `tauri.conf.json` handling | 🔴 Critical | Fix JSON parsing | | Hardcoded fallback | 🟠 High | Read from `Cargo.toml` | | Dependency removal | 🟠 High | Verify `url` usage | | Security: path traversal | 🟡 Medium | Add `realpathSync` check | | Versioning strategy | 🟢 Low | Adopt Tauri-native versioning | --- ## ✅ **Conclusion** The PR introduces a useful dynamic versioning feature but has **critical correctness issues** (`--locked` removal, JSON/TOML confusion) and **security risks** (unvalidated exec). With fixes to the top issues, this would be a solid improvement. I recommend **blocking merge** until the critical bugs are resolved. Let me know if you'd like me to draft a PR review comment or suggest a revised `update-version.mjs` implementation.\n\n---\n*this is an automated review from Ollama*
sarman self-assigned this 2026-04-15 01:53:49 +00:00
sarman added 1 commit 2026-04-15 02:09:27 +00:00
Merge branch 'master' into fix/version-dynamic-build
Some checks failed
Test / rust-fmt-check (pull_request) Successful in 1m8s
Test / frontend-typecheck (pull_request) Successful in 1m17s
Test / frontend-tests (pull_request) Successful in 1m23s
PR Review Automation / review (pull_request) Failing after 2m11s
Test / rust-clippy (pull_request) Successful in 6m11s
Test / rust-tests (pull_request) Successful in 9m7s
8c35e91aef
sarman merged commit ca56b583c5 into master 2026-04-15 02:10:11 +00:00
sarman deleted branch fix/version-dynamic-build 2026-04-15 02:10:21 +00:00
Sign in to join this conversation.
No reviewers
No Milestone
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: sarman/tftsr-devops_investigation#42
No description provided.