Skip to content

Make basecamp upgrade a real upgrade: native, verified self-update - #615

Merged
jeremy merged 1 commit into
mainfrom
native-self-update
Aug 4, 2026
Merged

Make basecamp upgrade a real upgrade: native, verified self-update#615
jeremy merged 1 commit into
mainfrom
native-self-update

Conversation

@jeremy

@jeremy jeremy commented Aug 4, 2026

Copy link
Copy Markdown
Member

Problem

basecamp upgrade was a dispatcher, not an installer. For every install method except Homebrew/Scoop — including the front-page curl | bash install — it printed a release URL with "ok": true and exit 0, which users (and agents) read as a successful upgrade. The brew/scoop paths reported "Upgraded" purely on child exit code (brew upgrade exits 0 on no-ops). Closes the loop on the #589 lineage.

What this does

Native self-update for installer-script/tarball installs under the user's home:

  • Downloads the platform release asset; verifies authenticity with sigstore-go v1.3.0 (certificate identity pinned to release.yml@refs/tags/v<ver> + GitHub OIDC issuer, SCT + transparency-log + observer-timestamp thresholds, artifact-bound to the actual checksums.txt bytes; trusted root via TUF) and integrity via SHA-256.
  • Swaps the executable transactionally: on unix the target path is continuously occupied (hard-link/copy backup + single rename-over); Windows uses the rename shuffle with rollback. Post-install probe confirms the installed binary reports the new version; on mismatch the previous binary is restored. A double-rename catastrophe (Windows) is reported distinctly, naming the preserved backup — never a false "left in place" claim.
  • An exclusive .upgrade.lock (gofrs/flock) held across staging/replace/verify/rollback serializes concurrent upgrades and gates the sidecar cleanup that runs on every ordinary invocation.
  • Fail-closed path policy: root, /nix/store, outside-home (filepath.Rel, no prefix matching), unwritable dir, and go install provenance (ldflags vs debug.ReadBuildInfo) all refuse with method-specific hints.

Success/exit contract — exit 0 only for up_to_date/dev or applied-and-confirmed upgrades. Everything else is ok:false + nonzero: upgrade_required, upgrade_incomplete, upgrade_unverified, upgrade_failed. Legacy cask/manifest migration is now an error, not a success.

Brew/Scoop delegation stays but post-verifies via manager-derived paths (brew --prefix, scoop prefix) — never os.Executable.

Installer cosign compatibility: current releases publish the protobuf (v0.3+json) bundle, which cosign v2.x fails to verify bare. Both installers now gate on the cosign version: v3 bare, v2.6–v2.x adds --new-bundle-format=true, older (or broken/unparseable) warns and skips — never aborts the install. The floor is empirical: v2.4 fails on the bundle's PKIX_ED25519 tlog key. goreleaser pins --new-bundle-format=true so the published format no longer depends on the CI cosign default.

Testing

  • The real published v0.8.1 checksums.txt + bundle + TUF trusted root are vendored as fixtures: hermetic tests prove the exact verification policy against reality (and that tampered artifacts, wrong identities, and garbage bundles fail).
  • Unit coverage for the exit-contract table, path policy (sibling-prefix and symlink home escapes included), swap/rollback/catastrophe, lock exclusion (second upgrade refused before any asset download or filesystem mutation — the read-only metadata check runs unlocked; cleanup skips while held), extraction hardening (links, duplicates, nesting, decompression bomb), checksum parsing, and brew/scoop post-verification.
  • installer.bats drives the cosign gate for install.sh and — via AST extraction under pwsh — install.ps1, including the broken-cosign abort regression.
  • New upgrade-smoke.yml: 3-OS matrix builds a stale 0.0.1 binary under the runner's home and runs a real upgrade against the latest release (real download, real sigstore verification, real Windows locked-exe shuffle), plus install.sh/ps1 legs with digest-pinned cosign v3.0.5 / v2.6.0 / v2.4.0.
  • Live macOS smoke: 0.8.0 → 0.8.1 against the real release, exit 0, no leftover staging or backup files (the upgrade lock file remains until a later invocation reaps it).
  • bin/ci green end-to-end.

Note: the release-shape (stripped) binary grows 25.6 MB → 35.3 MB (+9.7 MB / 38%); gzip-compressed binary 8.3 MB → 11.6 MB (+3.2 MB / 39%, a proxy for archive size) — from the sigstore-go/TUF dependency tree. Accepted; a release size budget (38 MiB stripped binary / 13 MiB gzipped binary, increases need explicit review) is recorded in RELEASING.md.


Summary by cubic

Make basecamp upgrade a real, verified self-update that only exits 0 after the new version is installed and confirmed. Adds Sigstore verification, transactional swaps across OSes, startup cleanup of upgrade sidecars, and secure GitHub release lookups with strict token handling.

  • New Features

    • Native self-update for installer/tarball installs under home: Sigstore bundle + SHA-256 verification via github.com/sigstore/sigstore-go (identity pinned to the release workflow/tag; trusted via TUF). Release lookups attach a GitHub token only for api.github.com, with GH_TOKEN preferred over GITHUB_TOKEN.
    • Transactional replace with rollback and post-install version probe; concurrency-safe with an upgrade lock; leftover sidecars are cleaned on CLI startup.
    • Clear exit contract and fail-closed policy (system paths, outside-home, unwritable, and go install builds refuse with guidance). Homebrew/Scoop are delegated then verified via manager-derived prefixes (brew --prefix, scoop prefix).
    • Installer cosign compatibility gate (v3 bare, v2.6+ adds --new-bundle-format=true, older warn/skip). New 3-OS upgrade-smoke.yml and hermetic fixtures validate the full flow. README and install docs explain upgrade behavior.
  • Dependencies

    • Add github.com/sigstore/sigstore-go v1.3.0; pin --new-bundle-format=true in goreleaser.
    • Binary grows due to Sigstore/TUF; a release size budget is documented in RELEASING.md.

Written for commit 4df2e48. Summary will update on new commits.

Review in cubic

Copilot AI balanced review requested due to automatic review settings August 4, 2026 05:44
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown

Sensitive Change Detection (shadow mode)

This PR modifies control-plane files:

  • .github/workflows/upgrade-smoke.yml
  • .goreleaser.yaml

Shadow mode — this check is informational only. When activated, changes to these paths will require approval from a maintainer.

@github-actions github-actions Bot added commands CLI command implementations tests Tests (unit and e2e) ci CI/CD workflows docs deps labels Aug 4, 2026

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI review requested due to automatic review settings August 4, 2026 05:48
@jeremy
jeremy force-pushed the native-self-update branch from cd479d9 to 27e616e Compare August 4, 2026 05:48

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@jeremy
jeremy force-pushed the native-self-update branch from 27e616e to d0a39c3 Compare August 4, 2026 05:54
Copilot AI review requested due to automatic review settings August 4, 2026 05:54
@jeremy
jeremy force-pushed the native-self-update branch from d0a39c3 to ecb7e83 Compare August 4, 2026 05:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ecb7e8356f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/commands/upgrade.go Outdated
Comment thread internal/commands/upgrade_selfupdate.go Outdated
@jeremy
jeremy force-pushed the native-self-update branch from ecb7e83 to f3a039a Compare August 4, 2026 06:43
Copilot AI review requested due to automatic review settings August 4, 2026 06:43

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f3a039a2b5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread internal/commands/upgrade_selfupdate.go Outdated
Comment thread internal/commands/upgrade.go Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 23 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings August 4, 2026 07:10
@jeremy
jeremy force-pushed the native-self-update branch from f3a039a to 93bd049 Compare August 4, 2026 07:10

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 93bd04993a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread .github/workflows/upgrade-smoke.yml

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 23 changed files in this pull request and generated no new comments.

basecamp upgrade previously printed a release URL with ok:true for every
install method it couldn't handle, and reported brew/scoop success on
child exit code alone. Now:

- Installer-script/tarball installs under the user's home self-update:
  download the platform asset, verify authenticity via sigstore-go
  (identity pinned to the release workflow at the version tag, SCT +
  tlog + observer thresholds, artifact-bound) and integrity via sha256,
  swap the executable transactionally (target path continuously
  occupied on unix; rename shuffle with rollback on Windows), and
  confirm the installed binary reports the new version. Backup restored
  on post-verify failure; sidecars reaped on every CLI invocation.
- Fail-closed path policy: root, /nix/store, outside-home (Rel-based,
  no prefix matching), unwritable dir, and go-install provenance
  (ldflags vs debug.ReadBuildInfo) all refuse with method-specific
  hints.
- Exit contract: exit 0 only for up_to_date/dev or applied-and-
  confirmed upgrades. upgrade_required / upgrade_incomplete /
  upgrade_unverified / upgrade_failed are ok:false and nonzero;
  legacy cask/manifest migration is now an error, not a success.
- Brew/scoop delegation post-verifies via manager-derived paths
  (brew --prefix, scoop prefix), never os.Executable.
- Installer cosign capability gate: v3 bare, v2.6+ needs
  --new-bundle-format=true, older warns and skips (checksums still
  verified). goreleaser pins the published bundle format explicitly.
- Real published v0.8.1 checksums + protobuf bundle + TUF trusted root
  vendored as hermetic verification fixtures.
- New 3-OS upgrade-smoke workflow exercises the full flow against the
  real latest release, plus install.sh/ps1 cosign tier legs.
Copilot AI review requested due to automatic review settings August 4, 2026 07:30
@jeremy
jeremy force-pushed the native-self-update branch from 93bd049 to 4df2e48 Compare August 4, 2026 07:30

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 22 out of 23 changed files in this pull request and generated no new comments.

@jeremy
jeremy merged commit e980c27 into main Aug 4, 2026
35 checks passed
@jeremy
jeremy deleted the native-self-update branch August 4, 2026 07:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci CI/CD workflows commands CLI command implementations deps docs tests Tests (unit and e2e)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants