# How Career-Ops update-system.mjs Checks Versions and Rolls Back Updates

> Discover how Career-Ops update-system.mjs checks versions using curl and safeguards your data by rolling back updates to backup branches without touching user files.

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

---

**The `update-system.mjs` script in Career-Ops checks for updates by comparing local and remote semantic versions using curl-based HTTP requests, and rolls back changes by restoring system files from timestamped backup branches while leaving user data untouched.**

Career-Ops uses a self-updating mechanism to keep its system layer synchronized with the upstream repository without touching user data. The `update-system.mjs` file serves as the core component that manages version verification through the `check` command and disaster recovery through the `rollback` command.

## Version Checking Workflow

The `check` command determines whether the local Career-Ops installation is current by comparing the local `VERSION` file against remote sources. This process respects user preferences, handles offline scenarios, and outputs machine-readable JSON for integration with the CLI.

### Dismiss-Flag and Local Version Resolution

Before querying remote servers, the script checks for a `.update-dismissed` file in the repository root. If `existsSync(join(ROOT, '.update-dismissed'))` returns true, the script immediately emits `{"status":"dismissed"}` and exits (lines 35-40).

If no dismiss flag exists, the script reads the local version via `localVersion()`, which calls `parseVersionFile`. This helper strips any "release-please" markers from the `VERSION` file content and normalizes it to a clean semver string (lines 39-48).

### Remote Version Fetching

Because Node’s `fetch` API does not function inside the sandboxed environment, `update-system.mjs` uses a `curlGet` wrapper to perform two independent HTTP requests (lines 52-68):

- **Raw VERSION file**: Fetched from `RAW_VERSION_URL` to get the canonical version string.
- **GitHub Releases API**: Queried at `RELEASES_API` to retrieve the latest published release tag.

Both requests return `null` on network errors, ensuring the script never crashes due to connectivity issues.

### Semantic Version Parsing and Comparison

The script extracts semver strings using the regex `SEMVER_RE = /^v?(\d+\.\d+\.\d+)$/i` (lines 50-57). It parses versions from both the raw file and the release API, then selects the higher of the two remote versions as the target.

The `compareVersions` function performs a numeric component-wise comparison (lines 99-103). If the local version is greater than or equal to the remote version, the script outputs `{"status":"up-to-date"}`; otherwise, it returns `{"status":"update-available"}` along with a trimmed changelog (lines 104-109).

### Offline and Error Handling

If both `curlGet` calls return `null`, the script emits `{"status":"offline"}` (lines 78-88). If the requests succeed but no parsable version is found in either response, it returns `{"status":"no-remote-version"}`. These graceful degradation paths ensure the updater never blocks the user due to temporary network issues.

## Rollback Workflow

When an update introduces breaking changes, the `rollback` command restores the system layer to its pre-update state using Git backup branches created during the last successful `apply` operation.

### Backup Branch Selection

The script identifies candidate branches using `git('for-each-ref', …)` to list all local branches matching `backup-pre-update-*` (lines 66-88). It selects the newest branch by commit date or embedded timestamp, favoring the pattern `backup-pre-update-${version}-${stamp}`. If no backup branches exist, the script prints an error and exits with code 1 (lines 70-74).

### System Path Restoration

For each path defined in `SYSTEM_PATHS` (which excludes user data per [`DATA_CONTRACT.md`](https://github.com/santifer/career-ops/blob/main/DATA_CONTRACT.md)), the script executes `git checkout <backup> -- <path>` to restore the file state from the backup branch (lines 96-104). This targets only system-layer files, ensuring personal data remains untouched.

### Handling Missing Paths

If a path exists in the current working tree but not in the backup branch, the catch block at lines 105-122 handles the divergence. The script executes `git rm` to stage the deletion and `rmSync` to remove the file from the filesystem. This cleanup covers files that were added after the backup was created, preventing orphaned files from lingering after rollback.

### Commit and Safety Guarantees

After successfully checking out paths, the script stages them with `git add` (lines 126-127) and attempts to commit with a message indicating the rollback (lines 128-132). Commit failures are silently ignored because the working tree may already be clean if the user runs rollback twice.

The backup branch naming convention encodes both the version and timestamp, making rollbacks deterministic. Because `SYSTEM_PATHS` strictly defines the scope of restoration, user directories are never modified during this process.

## Practical Usage Examples

```bash

# Check whether a newer version exists

node update-system.mjs check

# → {"status":"update-available","local":"1.6.0","remote":"1.7.0","changelog":"…"}

# Apply the update (creates backup branch before modifying system)

node update-system.mjs apply

# Roll back to the previous state after a faulty update

node update-system.mjs rollback

# → Restores files from backup-pre-update-1.6.0-<timestamp> and commits changes

```

The `check` command is safe to run frequently and produces JSON output suitable for programmatic parsing by the Career-Ops CLI.

## Summary

- **Dismissible notifications**: Creating `.update-dismissed` suppresses update checks until the file is removed.
- **Dual-source verification**: Queries both the raw `VERSION` file and GitHub Releases API to determine the latest available version.
- **Sandbox compatibility**: Uses `curl` CLI instead of Node `fetch` to work within restricted environments.
- **Deterministic rollback**: Restores from timestamped backup branches (`backup-pre-update-*`) affecting only paths defined in `SYSTEM_PATHS`.
- **Safe cleanup**: Removes files added post-update using both `git rm` and `rmSync` to maintain repository consistency.

## Frequently Asked Questions

### What happens if Career-Ops is offline during a version check?

If both `curlGet` requests to `RAW_VERSION_URL` and `RELEASES_API` return `null` due to network failure, `update-system.mjs` outputs `{"status":"offline"}` and exits gracefully. This prevents the updater from falsely reporting that no updates exist or crashing due to connection timeouts.

### How does update-system.mjs ensure user data is never rolled back?

The script strictly operates on paths listed in the `SYSTEM_PATHS` constant, which references only system-layer files as defined in [`DATA_CONTRACT.md`](https://github.com/santifer/career-ops/blob/main/DATA_CONTRACT.md). During rollback, it runs `git checkout` exclusively against these paths from the backup branch, ensuring user directories and personal data remain untouched throughout the restoration process.

### Can I skip update notifications temporarily?

Yes. Creating an empty file named `.update-dismissed` in the repository root causes the `check` command to return `{"status":"dismissed"}` immediately without querying remote servers. This suppresses update prompts until the file is deleted, allowing users to defer updates during critical work periods.

### What is the naming convention for backup branches?

Backup branches follow the pattern `backup-pre-update-${version}-${stamp}`, where `version` is the semver being backed up from and `stamp` is a numeric timestamp. The rollback logic selects the newest branch by commit date, ensuring the system restores to the most recent stable state prior to the last update.