Skip to content

fix(cli): make the fork release pipeline ship a working, verified artifact - #9

Merged
continued-agent merged 6 commits into
mainfrom
fix/cli-release-hardening
Aug 26, 2026
Merged

fix(cli): make the fork release pipeline ship a working, verified artifact#9
continued-agent merged 6 commits into
mainfrom
fix/cli-release-hardening

Conversation

@kilo-code-bot

@kilo-code-bot kilo-code-bot Bot commented Aug 26, 2026

Copy link
Copy Markdown

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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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):

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.

  6. 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.

…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.
@continued-agent
continued-agent merged commit b07b0f8 into main Aug 26, 2026
34 of 37 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant