# How Career-Ops Update Checker Works: Safe Version Migration and Rollback

> Discover how Career-Ops update checker ensures safe version migration and rollback. Understand the transactional update process and isolation of system files from user data.

- Repository: [Santiago Fernández de Valderrama/career-ops](https://github.com/santifer/career-ops)
- Tags: internals
- Published: 2026-07-03

---

**Career-Ops uses a self-contained Node.js script (`update-system.mjs`) that checks upstream GitHub releases against a local `VERSION` file, then applies updates transactionally while strictly isolating system files from user data like CVs and profiles.**

The `santifer/career-ops` repository implements a robust update checker mechanism that keeps the system layer synchronized with upstream releases without risking user customizations. This version migration system operates through `update-system.mjs`, a command-line tool that handles everything from availability checks to atomic rollbacks while enforcing a strict boundary between system-owned scripts and user-owned content.

## Update Check Workflow

The update checker follows a deterministic seven-step process to determine if a version migration is available. This workflow is implemented in `update-system.mjs` and consumed by both the CLI and the `doctor.mjs` health-check script.

### Dismiss Flag and Local Version Detection

The process begins by checking for a `.update-dismissed` hidden file in the project root. If present, the script immediately returns `{"status":"dismissed"}` and skips all network operations, allowing users to silence update notifications temporarily.

If not dismissed, the script reads the current installation state via `localVersion()`, which parses the project-root `VERSION` file to extract the local semantic version. This file serves as the ground truth for the installed system layer.

### Remote Version Fetching

To determine if a newer release exists, the script queries two upstream sources in parallel using `Promise.all` and `curl`:

- **RAW_VERSION_URL**: The raw `VERSION` file from the upstream repository
- **RELEASES_API**: GitHub's latest release API endpoint returning JSON metadata

Both sources are fetched simultaneously to minimize network latency. This dual-source approach ensures the update checker remains functional even if one endpoint is temporarily unavailable.

### Semantic Version Parsing and Comparison

The raw response from `RAW_VERSION_URL` is processed by `parseVersionFile()`, which applies the `SEMVER_RE` regex (`/v?(\d+\.\d+\.\d+)/`) to extract valid version strings. Similarly, the release API response is parsed to extract `release.tag_name` using the same regex pattern.

The system then determines the authoritative remote version using a fallback strategy: if one source fails, the other is used; if both succeed, the higher semantic version wins. This handles cases where the raw `VERSION` file lags behind an official GitHub release. Finally, `compareVersions()` evaluates whether the remote version exceeds the local installation, returning JSON statuses including `"up-to-date"`, `"update-available"` (with changelog), `"offline"`, or `"no-remote-version"`.

## Transactional Update Application

When users run `node update-system.mjs apply`, the version migration executes as an atomic, multi-stage transaction with built-in recovery mechanisms.

### Backup and Safety Staging

Before modifying any files, the updater creates a temporary backup branch (`update-backup-<date>`) and a WIP stash reference. This ensures any uncommitted work can be recovered even if the process interrupts unexpectedly. The backup is created *before* any file overwrites occur, following the principle that recovery assets must exist prior to risk exposure.

### Self-Re-execution Pattern

The updater implements a defensive self-re-execution strategy to handle module changes across versions. Initially, it checks out only the files necessary for the updater itself to run via `resolveReexecCheckout()`, then spawns a second Node process with `CAREER_OPS_UPDATE_REEXEC=1` set. This isolation prevents crashes that would occur if the current updater tried to import modules that exist only in the target version.

### System File Checkout

Once re-executed, the updater reads the target version's `SYSTEM_PATHS` array by calling `extractArrayFromSource()` on the new `update-system.mjs`. It then checks out every path in this array from `FETCH_HEAD`, merging historic `BOOTSTRAP_PATHS` for backward compatibility with very old installations. This selective checkout ensures only system-layer files (scripts, modes, templates) are modified, while user-layer files remain untouched.

### Materializing Skill Entrypoints

For filesystems that cannot store symlinks (common in certain sandboxed environments), the updater calls `ensureSkillEntrypoints()` to write full skill files (e.g., `.claude/skills/…`) directly to disk. This guarantees CLI functionality continues regardless of underlying filesystem limitations.

### Safety Validation

After checkout, the script performs a strict safety validation by diffing `git status` against the initial clean state. Any modification detected outside `SYSTEM_PATHS` but within `USER_PATHS` prefixes triggers a **SAFETY VIOLATION** error, causing immediate abort. This boundary enforcement protects user files like [`cv.md`](https://github.com/santifer/career-ops/blob/main/cv.md), [`config/profile.yml`](https://github.com/santifer/career-ops/blob/main/config/profile.yml), and [`modes/_profile.md`](https://github.com/santifer/career-ops/blob/main/modes/_profile.md) from accidental overwrites.

## Rollback and Recovery

If any stage fails or the user runs `node update-system.mjs rollback`, the system executes a complete reversal:

1. Checks out the backup branch created at the start of `apply`
2. Removes any files that did not exist in the backup (cleaning up newly added system files)
3. Restores user files from the backup if the safety violation check detected unintentional touches

This atomic rollback guarantees the repository returns to the exact pre-update state, making version migrations fully reversible.

## System vs. User Data Boundaries

The update checker mechanism respects a strict data contract defined in [`DATA_CONTRACT.md`](https://github.com/santifer/career-ops/blob/main/DATA_CONTRACT.md) and enforced through path constants:

- **System Paths**: Includes `update-system.mjs` itself, scripts, modes, templates, and dashboards
- **User Paths**: Includes CV files, profile configurations, and custom modes

The updater itself is part of `SYSTEM_PATHS`, allowing future releases to safely add, rename, or remove system files without breaking older installations. The semantic version parser tolerates both plain semver (`1.2.3`) and prefixed tags (`career-ops-v1.2.3`), ensuring flexibility in release naming conventions.

## Summary

- **Availability Detection**: Compares local `VERSION` file against GitHub's raw content and releases API using parallel `curl` requests
- **Transactional Safety**: Creates git backups and stashes before any modifications, with atomic rollback capability
- **Self-Re-execution**: Isolates version jumps by re-spawning the updater process to handle new module imports safely
- **Path Enforcement**: Strictly validates that changes remain within `SYSTEM_PATHS` and never touch `USER_PATHS` like CVs or profiles
- **Symlink Handling**: Automatically materializes skill entrypoints on filesystems without symlink support

## Frequently Asked Questions

### How does Career-Ops handle network outages during update checks?

If both `curl` calls to `RAW_VERSION_URL` and `RELEASES_API` fail, the update checker returns `{"status":"offline"}` and preserves the current installation state. The system treats this as a non-fatal condition, allowing the user to continue working with the existing version until connectivity resumes.

### Can I skip update notifications without applying the update?

Yes. Running `node update-system.mjs dismiss` creates a `.update-dismissed` file that suppresses future check notifications. The system will remain silent across all sessions until you manually remove this flag or apply the update, at which point the dismissal is automatically cleared.

### What happens if the updater modifies my personal CV or profile data?

The updater strictly enforces a safety boundary between system and user data. If `git status` detects any modification to files within `USER_PATHS` (such as [`cv.md`](https://github.com/santifer/career-ops/blob/main/cv.md) or [`config/profile.yml`](https://github.com/santifer/career-ops/blob/main/config/profile.yml)) during the update process, the script immediately aborts with a **SAFETY VIOLATION** error and triggers a rollback to the pre-update backup. This design ensures your personal content is never overwritten during version migrations.

### Why does the updater restart itself during the apply process?

The self-re-execution pattern prevents import errors when upgrading across major versions. When the target version contains new dependencies or renamed modules that the current updater doesn't recognize, the script first checks out its own new source code, then spawns a fresh Node process with `CAREER_OPS_UPDATE_REEXEC=1`. This ensures the migration logic runs with the target version's code rather than the outdated local version.