Skip to main content

PLAN-101: version check — tell the user when their noclickops is stale

IMPLEMENTATION RULES: Before implementing this plan, read and follow:

Status: Completed 2026-05-28

Goal: Add a "you have v1.0.0, latest is v1.0.1 — run noclickops update" hint to the lister, plus the version.txt file the hint reads from. Adopts the DCT (devcontainer-toolbox) pattern from .devcontainer/manage/lib/version-utils.sh, with one addition: a 1-hour cache so the lister stays snappy.

Last Updated: 2026-05-28

Investigations: none — direct response to user feedback after v1 ship.

Depends on: PLAN-002 (the lister is what shows the hint), PLAN-001 (lib/ + sourcing-guards pattern).

Priority: Medium — the v1 surface works without it; this is polish.


Problem

After v1 ships, users running an old noclickops have no way to know they're behind. There's no announce channel, no auto-update, no version awareness. The only way to know is to read GitHub or notice that noclickops update produces commits.

DCT solved this for its dev-* commands. Adopting the pattern keeps noclickops feeling alive without becoming nagware.


What it delivers

version.txt at repo root

One line, no newline tricks. The maintainer bumps it on each release (semver). Initial value: 1.0.0 (the v1 we just merged).

lib/version.{sh,ps1} — the version helpers

nco_load_version # → sets NCO_VERSION from version.txt
nco_check_remote # → sets NCO_REMOTE_VERSION if cache says newer
nco_show_update_hint # → prints '⬆ Update available: vX' if applicable

Behaviour:

  • Local version read from $NOCLICKOPS_DIR/version.txt. Falls back to "unknown" if missing (so a malformed install doesn't crash the lister).
  • Remote check hits https://raw.githubusercontent.com/<user>/<repo>/main/version.txt — where <user>/<repo> is derived from $NOCLICKOPS_DIR's origin remote, the same way lib/azdo.sh derives target-repo identity. A fork at alice/noclickops checks alice's main, not the original maintainer's. No hardcoded identity in any noclickops code. Curl with --connect-timeout 2 --max-time 4. Failure (network down, no origin remote, non-GitHub upstream) is silent — no warning, no hint.
  • Cache at $NOCLICKOPS_DIR/.version-cache: one line, <unix-ts> <version>. TTL 1 hour. Eliminates the 2s-on-every-call latency DCT pays.
  • Cache invalidation: bin/update.sh rm -fs the cache after pulling, so the next noclickops call sees fresh state.
  • Env override NCO_VERSION_CHECK_URL lets tests point at a local file:// URL instead of github.com.

Lister change

bin/noclickops.sh:

noclickops v1.0.0 — portable script suite for developers
Install: /Users/.../.noclickops

Meta
...

If a newer version is detected, an extra line at the end:

Run 'noclickops <cmd> --help' for usage details, e.g.:
noclickops update --help

⬆ Update available: v1.0.1 — run 'noclickops update'

The hint goes at the bottom (most visible — last thing scrolled past). The version in the header confirms the current state regardless of update status.

.gitignore

.version-cache is per-install state; never committed. Add a .gitignore at the repo root (currently absent).


Phases

  1. version.txt (= 1.0.0).
  2. .gitignore.
  3. lib/version.{sh,ps1} with the three helpers + cache logic.
  4. bin/noclickops.{sh,ps1} — source version.sh, emit version in header, emit hint at bottom.
  5. bin/update.{sh,ps1} — clear .version-cache after pull.
  6. tests/test-PLAN-101-version-check.sh — uses NCO_VERSION_CHECK_URL=file://… to stub the remote check.

Validation criteria

  • noclickops (no args) shows noclickops vX.Y.Z in the header line.
  • With a stubbed remote URL pointing at a version.txt containing a different version, the lister shows the ⬆ Update available hint at the bottom.
  • With a stubbed remote that matches the local version, no hint.
  • Cache file is written after a fetch; subsequent calls within 1h skip the network (verifiable by stubbing a URL that doesn't exist — the second call still shows the cached hint).
  • bin/update.sh removes the cache file after a successful pull.
  • Network failure path: lister still runs; no hint, no warning.
  • Portability grep stays clean.

Out of scope

  • Auto-update. The hint nudges; doesn't auto-pull. User-initiated update keeps things explicit.
  • Background check. No daemon, no &. Synchronous fetch with a 2s timeout + 4s max; cache makes the amortised cost negligible.
  • Multiple-channel (stable / beta / nightly). One channel: main. Bumping version.txt is a release.
  • Notification on update command. update already does what's needed; the hint shows during the next lister call.

Completion notes (2026-05-28)

Six-phase ship on feat/version-check.

Teststests/test-PLAN-101-version-check.sh, 18 tests:

GroupCountHighlights
nco_load_version2Reads version.txt; falls back to "unknown"
nco_check_remote with stubbed URL7newer-than-local → set; cache written; cache hit within TTL; cache cleared + URL gone → empty; equal → empty; expired cache → refetch; malformed cache → falls through
nco_show_update_hint3Prints when set; tells user the command; silent when unset
Lister + update.sh integration2Header shows vX.Y.Z; update.sh has cache-busting rm
_nco_derive_check_url (fork-friendly URL derivation)3https origin → raw URL; ssh origin → raw URL; non-GitHub origin → non-zero (silent skip)
Anti-regression sweep1No literal terchris/noclickops in lib/version.{sh,ps1}

tests/test-portability.sh expanded to guard against both kinds of identity leak, not just target-tenant. Same shape, two greps now:

✓ no target-tenant identity (ExampleOrg/FrontendPlatform/JKL900X016) in bin|lib|templates|shell
✓ no hardcoded noclickops-own-repo identity (terchris/noclickops) in bin|lib|templates|shell

Aggregate: tests/run-all.sh is now 285 tests, 0 failed, 0 skipped (was 266 after PLAN-010).

Bugs caught mid-test (good ones):

  1. User-flagged: hardcoded terchris/noclickops in lib/version.{sh,ps1}. The first draft had NCO_VERSION_CHECK_URL default to that URL — same bad pattern that PLAN-003 fixed for target-tenant identity, repeated for noclickops's OWN identity. Replaced with _nco_derive_check_url that parses git -C $NOCLICKOPS_DIR remote get-url origin. Forks now self-check correctly: alice/noclickops checks alice's main, not the original maintainer's.

  2. templates/welcome.txt had the same leakSource: https://github.com/terchris/noclickops. The expanded portability test caught it. Reworded to Source: see git -C ~/.noclickops remote get-url origin so the user discovers their own upstream from their own install.

  3. Test infrastructure bug: my first attempt at the derivation tests did export NOCLICKOPS_DIR=<fixture> BEFORE sourcing paths.sh, but paths.sh resolves NOCLICKOPS_DIR from BASH_SOURCE and overrides any pre-set value (by design — bin/ scripts need a fixed answer). Fix: assign NOCLICKOPS_DIR AFTER sourcing paths.sh; version.sh's _NCO_PATHS_LOADED guard prevents re-sourcing and clobbering.

Visual confirmation:

noclickops v1.0.0 — portable script suite for developers
Install: /Users/.../.noclickops
...

When a stub remote URL points at a different version, an extra trailing line:

⬆ Update available: v1.0.5 — run 'noclickops update'

version.txt = 1.0.0 — declares the just-merged v1 as the baseline. Future bumps from here: patch → 1.0.1, feature → 1.1.0.

PowerShell port unverified on Mac as usual; the derivation logic mirrors bash via PowerShell regex matching.

Pending separate PR: fix/install-sh-silent-prompt (the 2>/dev/null swallowing the prompt during curl | bash). Not bundled with this PR — user chose to keep them separate.