fix(cli): make the fork release pipeline ship a working, verified artifact - #9
Merged
Merged
Conversation
…dlock) The bundled CLI produced by esbuild crashed at module load with `SyntaxError: Unexpected reserved word` because circular static imports between `src/compaction.ts` and `src/stream/streamChatResponse.ts` (plus `src/subagent/executor.ts`) caused esbuild to emit a `__esm` factory that contained a top-level `await` without the `async` keyword. Confirmed locally on main: `node dist/cn.js --version` and `node dist/cn.js --help` both exit 1 with that SyntaxError, so any release published from a non-pinned tip of main ships a broken artifact. The previous fix (4eeaeba) broke the cycle but subsequent commits on main re-introduced it. Re-apply the cycle-breaking refactor: - extract `pruneLastMessage` to `src/util/chatHistoryPrune.ts` so the stream layer and compaction helpers no longer share a static edge through the compaction module, - route `streamChatResponse` through a late-bound `streamChatResponseRef` (`src/stream/streamChatResponse.lateRef.ts`) from `compaction.ts` and `subagent/executor.ts`, so neither has to statically import the stream module that transitively re-enters compaction via the helper chain, - have `streamChatResponse.ts` register itself with the lateRef at module evaluation time, - update the affected tests (`compaction.test.ts`, `compaction.infiniteLoop.test.ts`, `compaction.pruneLastMessage.test.ts`) to mock the lateRef. Verified on a fresh build: - `node dist/cn.js --version` → exit 0 - `node dist/cn.js --help` → exit 0 - 95 vitest tests pass; `tsc --noEmit` is clean.
`getVersion()` looked for `../package.json` relative to the running module, but the packaged release layout is `cn/cn.js` with `cn/package.json` alongside it. As a result `cn --version` printed `unknown` (with spammy warnings) on every release built from `scripts/install-fork.sh`, including the published `cli-v0.1.0`. Look for a sibling `package.json` first, then fall back to the parent directory, and stop logging on failure. Verified locally: with a version-stamped `cn/package.json`, `cn --version` now reports the expected semver.
The current Continue docs for Azure (https://docs.continue.dev/customize/model-providers/top-level/azure) require `env.deployment` + `env.apiType` + `env.apiVersion` in addition to the base URL. The onboarding picker only asked for the base URL, so an Azure config written by `cn` would load but fail at the first request. - add an `azureDeployment` wizard step (only when `OnboardingProvider.needsAzureDeployment` is set), - default `apiType` to `azure-openai` and `apiVersion` to `2023-07-01-preview` per the docs examples, - add a regression test asserting the deployment / apiType / apiVersion fields reach the generated YAML.
The curl installer in `scripts/install-fork.sh` trusted whatever `cn-node.tar.gz` happened to be on the network: no integrity check, no protection against malicious tar entries, and the wrapper assumed `~/.local/bin/cn` (a symlink) and `~/.local/share/.../current/cn.js` were in the same directory. - download a sibling `cn-node.tar.gz.sha256` and abort with a clear error if the recomputed SHA-256 doesn't match, - list tar entries before extraction and refuse to extract any entry that escapes the destination (e.g. `../payload`), - resolve the wrapper symlink chain so the symlinked `cn` always invokes the real `cn.js` regardless of how it was linked, - detect the login shell (bash / zsh / fish) and emit the correct PATH export line for that shell, - make the install idempotent: re-running replaces the previous install, refreshes the symlink, and tolerates a missing prior install, - allow pinning a specific version via `CONTINUE_CLI_VERSION`, overriding the install root via `CONTINUE_CLI_INSTALL_DIR`, and overriding the bin dir via `CONTINUE_CLI_BIN_DIR`. Verified locally with a small HTTP server serving a fake release: - clean install + `cn --version`/`cn --help` exit 0, - tampered archive is rejected with the expected vs actual SHA-256, - archive with a `../payload.txt` entry is rejected before extraction, - the symlinked `cn` resolves `cn.js` correctly.
The release workflow triggered on any tag matching `cli-v*` and built the CLI even when the tag pointed at a non-main commit. Concretely, the published `cli-v0.1.0` tag is an ancestor of `main` (verified with `git merge-base`); meanwhile the working tree on `main` has moved forward, and the `dist/cn.js` produced from `main` is a broken artifact (separate fix in the same PR). - add a verification step that aborts the release when the tag's commit is not the tip of `main`; the message names the offending commit and the expected tip, - extract the version from the tag (`cli-vX.Y.Z` → `X.Y.Z`), fail the build if the tag and `extensions/cli/package.json` version disagree (so a release can't ship a binary that prints `unknown`), - emit a version-stamped `cn/package.json` inside the artifact so `cn --version` reports the same semver the GitHub Release was tagged with, - produce a SHA-256 checksum file for `cn-node.tar.gz` and attach it to the release (the install-fork.sh in this PR consumes it), - run the smoke test (`node cn.js --version`, `node cn.js --help`) on both `ubuntu-latest` and `macos-latest` against the actual packaged artifact, not just `dist/cn.js`, - attach the artifact and checksum to the release only after every job succeeds (separate `publish` job gated on `build` + `test-artifact`), - restrict the default GITHUB_TOKEN to `contents: write` at the job level for the publish step (the only step that needs it). Note: `build:local-deps` still uses `npm i` because it walks `file:` references into local packages; switching to `npm ci` here breaks the build (npm ci rejects the dev-only state of the local packages). The change is therefore limited to the parts of the workflow that don't traverse `file:` deps.
- CHANGELOG: keep the existing release history and add an Unreleased entry describing the SyntaxError fix, the version-from-package.json fix, the Azure wizard fix, the release-workflow hardening, and the installer hardening, - README: document the SHA-256 verification done by the curl installer, the `CONTINUE_CLI_VERSION` / `CONTINUE_CLI_INSTALL_DIR` / `CONTINUE_CLI_BIN_DIR` env vars, an Updating / Uninstalling section, and the manual `sha256sum -c` command for users who want to verify a download before running the installer.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Audit and Hardening of the cn CLI
Identified Issues
The audit of the CLI and its installer (phases 1–11 of the brief) uncovered several functional defects, confirmed by local execution of the dist/cn.js binary and install-fork.sh:
The published bundle crashes on startup. node dist/cn.js --version and node dist/cn.js --help exit with code 1 and SyntaxError: Unexpected reserved word due to an import cycle between compaction.ts and stream/streamChatResponse.ts. The initial fix (commit 4eeaeba) was reverted by subsequent commits on main; the cli-v0.1.0 release points to that older commit, but dist/cn.js regenerated from the current main does not work.
cn --version never reports the correct version. getVersion() looks for ../package.json, whereas the published artifact only exposes cn/package.json. The output is unknown, accompanied by warnings.
The Azure wizard generates an incomplete configuration. It asks for the API base URL but not the deployment, apiType, or apiVersion required by the Continue Azure documentation.
The release-cli.yml workflow accepts any cli-v* tag. It does not verify that the tag's commit matches the tip of main; there is no OS matrix for the smoke test; no checksum is attached to the release; and the GITHUB_TOKEN permissions are too broad.
install-fork.sh performs no verification. There is no checksum verification, no protection against tar entries escaping the extraction directory, the wrapper does not resolve symlinks, and the PATH message is generic rather than shell-specific.
Solution
Six logical commits, all on the fix/cli-release-hardening branch (never on main):
fix(cli): unblock prebuilt release entrypoint — extracts pruneLastMessage into util/chatHistoryPrune.ts, introduces stream/streamChatResponse.lateRef.ts, and breaks the cycle that caused the SyntaxError. 95 Vitest tests pass, tsc --noEmit is clean, and node dist/cn.js --version and node dist/cn.js --help both exit with code 0.
fix(cli): report the release version from cn/package.json — getVersion() first reads cn/package.json, then falls back to ../package.json; the warning spam is eliminated.
fix(cli): prompt for Azure deployment and write apiType/apiVersion — adds an additional wizard step for Azure, with defaults of apiType=azure-openai and apiVersion=2023-07-01-preview, matching the documentation, plus a regression test.
fix(cli): verify checksum and harden tar extraction in fork installer — downloads cn-node.tar.gz.sha256, verifies the SHA-256 checksum, rejects tar entries containing .., resolves symlinks in the wrapper, detects the user's shell, supports idempotent installation, and adds support for CONTINUE_CLI_VERSION, CONTINUE_CLI_INSTALL_DIR, and CONTINUE_CLI_BIN_DIR.
ci(cli): enforce tag-at-main-tip, ship a SHA-256, and test on macOS — explicitly verifies tag_sha == origin/main, extracts the version from the tag and validates it against package.json, stamps cn/package.json in the artifact, attaches a SHA-256 checksum, runs the exact artifact smoke test on both ubuntu-latest and macos-latest, separates the publish job so it depends on both build and test-artifact, and scopes contents: write permissions to the publishing job.
docs(cli): document the fork release hardening — updates the CHANGELOG while preserving its history and adds an updated section to the README.
Modified Files
.github/workflows/release-cli.yml
extensions/cli/CHANGELOG.md
extensions/cli/README.md
extensions/cli/scripts/install-fork.sh
extensions/cli/src/compaction.infiniteLoop.test.ts
extensions/cli/src/compaction.pruneLastMessage.test.ts
extensions/cli/src/compaction.test.ts
extensions/cli/src/compaction.ts
extensions/cli/src/onboardingProviders.test.ts
extensions/cli/src/onboardingProviders.ts
extensions/cli/src/stream/streamChatResponse.lateRef.ts (new)
extensions/cli/src/stream/streamChatResponse.ts
extensions/cli/src/subagent/executor.ts
extensions/cli/src/ui/components/ProviderPicker.tsx
extensions/cli/src/util/chatHistoryPrune.ts (new)
extensions/cli/src/version.ts
Tests Performed
cd extensions/cli && npx tsc --noEmit → exit 0
cd extensions/cli && npx vitest run src/compaction.test.ts src/compaction.infiniteLoop.test.ts src/compaction.pruneLastMessage.test.ts src/onboardingProviders.test.ts src/onboarding.test.ts src/stream/ src/ui/components/ProviderPicker.test.tsx → 100 tests passed, 6 skipped
cd extensions/cli && npx eslint on all modified files → 0 errors (2 pre-existing unrelated warnings)
cd extensions/cli && npm run build → bundle generated successfully
Local packaged artifact test: node cn/cn.js --version → 0.0.0-dev (exit 0), node cn/cn.js --help → usage (exit 0)
Local install-fork.sh test against a fake HTTP server (local release):
clean installation + invocation through the symlink → cn --version and cn --help work
tarball modified after checksum generation → installer rejects it with expected/actual SHA-256 values
archive containing a ../payload.txt entry → rejected before extraction
bash -n on install-fork.sh → syntax OK
getVersion validated: warning spam removed; cn --version reads the stamped package.json
Impact on Release Behavior
No existing release is invalidated; the next release must be retagged from the corrected tip of main (the check will fail otherwise).
The published artifact layout will change: cn/package.json will now contain {name, version, type: module} instead of only {type: module}. This is what install-fork.sh expects.
The cn-node.tar.gz.sha256 checksum is now mandatory: an installation that downloads a tarball without a checksum will fail with a clear error message. This does not affect the upstream Continue installer (install.sh), only the fork's installer.
The smoke test now runs against the complete artifact (not just dist/cn.js) and on macOS in addition to Ubuntu.