Skip to content

Add post-deploy installer smoke canary - #569

Merged
jeremy merged 2 commits into
mainfrom
fix/installer-canary
Jul 27, 2026
Merged

Add post-deploy installer smoke canary#569
jeremy merged 2 commits into
mainfrom
fix/installer-canary

Conversation

@jeremy

@jeremy jeremy commented Jul 27, 2026

Copy link
Copy Markdown
Member

What

A post-deploy canary for the published installer, plus two installer-scoped comment fixes from the #568 incident review.

The install endpoint serves main, so merging a change to scripts/install.sh deploys it immediately. #558 shipped a sourcing guard that broke the documented curl … | bash path under set -u; #568 fixed it and added a bats regression test that gates pre-merge. Nothing verified the published installer post-merge — this closes that gap.

The canary (.github/workflows/installer-smoke.yml)

  • Triggers: nightly schedule, workflow_dispatch, and push to main touching scripts/install.sh.
  • Endpoint: the canonical Quick Start command from the README, cache-busted with the commit SHA (?sha=${GITHUB_SHA}) — raw.githubusercontent.com serves max-age=300, so a push-triggered run could otherwise fetch the pre-push installer and falsely pass. The canonical endpoint preserves the query on redirect, making each commit's request cache-distinct.
  • Legs: ubuntu-latest (modern bash) and macos-latest via stock /bin/bash, which asserts exactly Bash 3.2 through the same interpreter that runs the install — the coverage can't silently degrade if a future runner image changes /bin/bash.
  • Isolation: scratch HOME + explicit BASECAMP_BIN_DIR contain the installer's writes (it edits shell config and installs the baseline skill even with BASECAMP_SKIP_SETUP=1 / BASECAMP_SETUP_AGENT=none). Asserts the deterministic installed path, not ambient PATH.
  • Hygiene: permissions: {}, timeout-minutes: 10, concurrency group; no actions used, so nothing to pin. actionlint and zizmor clean.

Comment fixes

  • e2e/installer.bats: the piped-execution test's PATH comment said "Keep PATH empty"; it actually points at a binary-free tmpdir.
  • scripts/install.sh: document why the guard's :-$0 fallback is required (bash from stdin or -c leaves BASH_SOURCE unset, aborting under set -u) so a future cleanup doesn't re-break piping.

Verification

  • bats e2e/installer.bats — 12/12 pass.
  • bin/ci green.
  • Will workflow_dispatch the canary after merge to confirm both legs.

Refs #558, #568


Summary by cubic

Adds a post-deploy smoke canary that installs the published CLI via the Quick Start on Linux and macOS. Also updates installer comments and sets explicit workflow concurrency behavior.

  • New Features

    • Adds .github/workflows/installer-smoke.yml: runs nightly, on demand, and when scripts/install.sh changes; permissions: {}, timeout-minutes: 10, concurrency group with cancel-in-progress: false.
    • Installs from https://basecamp.com/install-cli?sha=${GITHUB_SHA} to bust CDN cache; verifies the binary at the deterministic install path.
    • Runs on ubuntu-latest and macos-latest; macOS uses /bin/bash and asserts Bash 3.2; uses scratch HOME and BASECAMP_BIN_DIR to isolate writes.
  • Bug Fixes

    • Corrects PATH comment in e2e/installer.bats to note a binary-free tmpdir.
    • Documents why ${BASH_SOURCE[0]:-$0} is required in scripts/install.sh to keep curl | bash working with set -u.

Written for commit 48bd75c. Summary will update on new commits.

Review in cubic

The install endpoint serves main, so merging a change to
scripts/install.sh deploys it immediately. #558 shipped a sourcing guard
that broke the documented `curl | bash` path under `set -u`; the bats
suite now gates that regression pre-merge (#568), but nothing checked
the published installer post-merge.

Add .github/workflows/installer-smoke.yml: nightly + workflow_dispatch +
push-to-main (scripts/install.sh) runs of the canonical Quick Start
command against the published endpoint, cache-busted with the commit SHA
since raw.githubusercontent.com serves max-age=300. Two legs: Linux
(modern bash) and macOS via stock /bin/bash, which asserts exactly Bash
3.2 through the same interpreter so the coverage can't silently degrade.
Installs into a scratch HOME with explicit BASECAMP_BIN_DIR and asserts
the deterministic installed path.

Also two installer-scoped comment fixes from the incident review:
correct the "Keep PATH empty" description in e2e/installer.bats (PATH is
a binary-free tmpdir, not empty), and document why the guard's `:-$0`
fallback is required so a future cleanup doesn't re-break piping.

Refs #558, #568
Copilot AI review requested due to automatic review settings July 27, 2026 18:37
@github-actions github-actions Bot added tests Tests (unit and e2e) ci CI/CD workflows labels Jul 27, 2026
@github-actions

Copy link
Copy Markdown

Sensitive Change Detection (shadow mode)

This PR modifies control-plane files:

  • .github/workflows/installer-smoke.yml

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 the enhancement New feature or request label Jul 27, 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.

Pull request overview

Adds a post-deploy GitHub Actions canary to validate the published installer (the documented curl … | bash path) after changes to scripts/install.sh are merged, plus clarifying comment updates from the #568 incident review to reduce risk of regressions.

Tip

If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.

Changes:

  • Add a new installer-smoke workflow that installs via the public endpoint on Linux and macOS, using an isolated HOME/BASECAMP_BIN_DIR and verifying the deterministic installed binary path.
  • Clarify the installer’s sourcing guard comment to document why the :-$0 fallback is required under set -u when Bash runs from stdin / -c.
  • Clarify the e2e installer test comment to accurately describe the PATH setup used to stop before downloading.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
.github/workflows/installer-smoke.yml New post-deploy canary that runs `curl …
scripts/install.sh Comment-only clarification explaining the stdin/-c + set -u behavior behind the BASH_SOURCE[0]:-$0 guard.
e2e/installer.bats Comment-only fix to accurately describe the test’s PATH isolation behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

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

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

No issues found across 3 files

Re-trigger cubic

Copilot AI review requested due to automatic review settings July 27, 2026 18:49

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 3 out of 3 changed files in this pull request and generated no new comments.

@jeremy
jeremy merged commit e2b5103 into main Jul 27, 2026
27 checks passed
@jeremy
jeremy deleted the fix/installer-canary branch July 27, 2026 18:55
jeremy added a commit that referenced this pull request Jul 27, 2026
The canary's first macOS run hung for its full 10-minute timeout after
"Skipping setup wizard": the post-install skill step reaches for the OS
keyring, and on a headless runner the `security` tool blocks forever on
the locked keychain (the cancelled job's orphan processes were
`basecamp` and `security`). Set BASECAMP_NO_KEYRING=1 workflow-wide,
the same escape hatch the Test workflow uses.

Refs #569
jeremy added a commit that referenced this pull request Jul 27, 2026
* ci: harden installer canary and add pre-merge Bash 3.2 gate

Canary (installer-smoke.yml):
- Trigger on scripts/install.ps1 pushes too — the raw URL serves main, so
  merge == deploy for the PowerShell installer as well.
- New Windows job running the documented irm | iex shape under both
  Windows PowerShell 5.1 and pwsh 7 (matrix), against scratch
  USERPROFILE/BASECAMP_BIN_DIR under RUNNER_TEMP.
- New notify job: on any leg's failure, file or update an
  "Installer canary failure" issue (exact-title dedupe, best-effort gh
  calls, ::error:: annotation). Skipped entirely on green runs.

Pre-merge (test.yml): new installer-bash32 job on macos-latest — plain
steps, not bats (bats 1.11 execs `env bash` at every layer, so the
interpreter is not pinnable through it). Asserts /bin/bash is exactly
3.2, syntax-checks install.sh, and runs it piped with curl off PATH,
asserting the guard's clean failure message and the absence of
unbound-variable errors (the #558 regression class). Path-filtered to
installer files plus test.yml itself so the job proves itself on the PR
that introduces or edits it; skipped filters still report success, so
the job is safe to mark required.

Refs #558 #568 #569

* TEMP: notify drill (revert me)

* Revert "TEMP: notify drill (revert me)"

This reverts commit 37cf5f7.

* ci: run a full offline install under Bash 3.2, not just the curl guard

The piped-execution step stops at the curl prerequisite, and `bash -n`
only checks grammar — neither catches Bash-4-only constructs in the
download, checksum, extract, or post-install code. Add a step that runs
one complete install path under real /bin/bash 3.2 using a fixture curl
serving a local release (stub binary, real sha256 checksums) with PATH
restricted to fixture + system dirs, then asserts the installed stub
runs. Review feedback on #575.

* ci: grant the 3.2 gate pull-requests read for paths-filter

dorny/paths-filter enumerates changed PR files via the REST API and
documents a pull-requests: read requirement; the job-level permissions
block zeroes everything unlisted. Review feedback on #575.
jeremy added a commit that referenced this pull request Jul 27, 2026
Every CLI command runs root PersistentPreRunE -> appctx.NewApp ->
auth.NewManager -> auth.NewStore, and credstore.NewStore eagerly probes
OS keyring availability with a write. On macOS that probe shells out to
an uncancellable `security` child; a locked keychain with no TTY or GUI
blocks it forever — on *every* command, including credential-free ones
like `setup agents` and `skill install`. That is how the installer hung
headless (#568, canary discovery #569/#571 -> #574 drill lineage).

Go fix (reaches users at the next release): Store now records the
fallback dir at construction and builds the credstore lazily behind
sync.Once on first credential operation. Credential-free commands never
touch the keyring; credential-touching commands probe on first use
exactly as before, with the documented BASECAMP_NO_KEYRING=1 escape
hatch. No timeout was added deliberately: an abandoned goroutine cannot
kill the hung `security` child, so a timeout would hide the hang while
keeping the leak. Bounded fallback needs cancellation support upstream
in credstore/go-keyring first.

Installer belt (deploys on merge, covers released binaries <= v0.7.2
which still probe eagerly): post_install_setup in install.sh prefixes
each best-effort child with per-command BASECAMP_NO_KEYRING=1;
Invoke-PostInstallSetup in install.ps1 sets it for the duration of setup
and restores the caller's value on exit. These children never touch
credentials — the var only skips the old binaries' startup probe.

Tests: keyring_test.go pins constructor laziness through an injectable
credstore seam. installer.bats gains belt tests that explicitly unset
the suite-wide BASECAMP_NO_KEYRING and assert both directions (real
calls carry it, the sh capability probe does not), plus a pwsh test that
extracts Invoke-PostInstallSetup from install.ps1's AST (no test hooks
in the production script), runs it against a logging stub, and asserts
set + restore; it fails closed when pwsh is missing in CI.

The canary's workflow-level BASECAMP_NO_KEYRING is removed with no
step-scoped replacement: the macOS leg now genuinely exercises the
deployed belt (a missing belt = hung security child = timeout = notify),
and the direct `basecamp version` asserts skip PersistentPreRunE
entirely.

Refs #558 #568 #569
jeremy added a commit that referenced this pull request Jul 27, 2026
* Defer keyring probe to first credential use; belt the installers

Every CLI command runs root PersistentPreRunE -> appctx.NewApp ->
auth.NewManager -> auth.NewStore, and credstore.NewStore eagerly probes
OS keyring availability with a write. On macOS that probe shells out to
an uncancellable `security` child; a locked keychain with no TTY or GUI
blocks it forever — on *every* command, including credential-free ones
like `setup agents` and `skill install`. That is how the installer hung
headless (#568, canary discovery #569/#571 -> #574 drill lineage).

Go fix (reaches users at the next release): Store now records the
fallback dir at construction and builds the credstore lazily behind
sync.Once on first credential operation. Credential-free commands never
touch the keyring; credential-touching commands probe on first use
exactly as before, with the documented BASECAMP_NO_KEYRING=1 escape
hatch. No timeout was added deliberately: an abandoned goroutine cannot
kill the hung `security` child, so a timeout would hide the hang while
keeping the leak. Bounded fallback needs cancellation support upstream
in credstore/go-keyring first.

Installer belt (deploys on merge, covers released binaries <= v0.7.2
which still probe eagerly): post_install_setup in install.sh prefixes
each best-effort child with per-command BASECAMP_NO_KEYRING=1;
Invoke-PostInstallSetup in install.ps1 sets it for the duration of setup
and restores the caller's value on exit. These children never touch
credentials — the var only skips the old binaries' startup probe.

Tests: keyring_test.go pins constructor laziness through an injectable
credstore seam. installer.bats gains belt tests that explicitly unset
the suite-wide BASECAMP_NO_KEYRING and assert both directions (real
calls carry it, the sh capability probe does not), plus a pwsh test that
extracts Invoke-PostInstallSetup from install.ps1's AST (no test hooks
in the production script), runs it against a logging stub, and asserts
set + restore; it fails closed when pwsh is missing in CI.

The canary's workflow-level BASECAMP_NO_KEYRING is removed with no
step-scoped replacement: the macOS leg now genuinely exercises the
deployed belt (a missing belt = hung security child = timeout = notify),
and the direct `basecamp version` asserts skip PersistentPreRunE
entirely.

Refs #558 #568 #569

* test(installer): pin restore of a caller-set BASECAMP_NO_KEYRING

The pwsh belt test proved restore-to-unset only; a regression that
overwrites an existing caller value would have passed. Second driver
pass sets the var to 0 before Invoke-PostInstallSetup and asserts it
survives. Review feedback on #578.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci CI/CD workflows enhancement New feature or request tests Tests (unit and e2e)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants