Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 144 additions & 14 deletions .github/workflows/release-cli.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,64 @@ on:
tags:
- "cli-v*"

# Restrict the default GITHUB_TOKEN to the minimum needed for this workflow:
# - contents: write is required to create the GitHub Release and upload assets.
# We deliberately do NOT request packages, id-token, or other write scopes.
permissions:
contents: write

jobs:
release:
name: Build and publish CLI
build:
name: Build CLI artifact
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v7
with:
fetch-depth: 0

- name: Verify tag points to current main
env:
TAG_NAME: ${{ github.ref_name }}
GITHUB_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
# Only release tags whose commit is the tip of the default branch.
# This blocks accidental releases from arbitrary branches or
# rewritten history.
default_branch="${GITHUB_BASE_REF:-main}"
tag_sha="$(git rev-parse --verify "refs/tags/${TAG_NAME}^{commit}")"
tip_sha="$(git rev-parse --verify "origin/${default_branch}")"
if [[ "$tag_sha" != "$tip_sha" ]]; then
echo "::error::Tag ${TAG_NAME} points to ${tag_sha} but origin/${default_branch} is at ${tip_sha}." >&2
echo "::error::Releases must be tagged at the current tip of ${default_branch}." >&2
exit 1
fi
echo "Tag ${TAG_NAME} matches origin/${default_branch} (${tag_sha})."

- name: Extract version from tag
id: version
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
version="${tag#cli-v}"
if [[ -z "$version" || "$version" == "$tag" ]]; then
echo "::error::Tag '$tag' must follow the cli-v<semver> pattern." >&2
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"

- name: Sync package.json version with tag
working-directory: extensions/cli
run: |
set -euo pipefail
current="$(node -p "require('./package.json').version")"
if [[ "$current" != "${{ steps.version.outputs.version }}" ]]; then
echo "::error::extensions/cli/package.json version is '$current' but tag is '${{ steps.version.outputs.version }}'." >&2
echo "::error::Update package.json before tagging, or remove this guard." >&2
exit 1
fi

- name: Set up Node.js
uses: actions/setup-node@v7
Expand All @@ -24,38 +71,121 @@ jobs:
cache: npm
cache-dependency-path: extensions/cli/package-lock.json

- name: Build local CLI dependencies and CLI bundle
- name: Build local CLI dependencies
working-directory: extensions/cli
run: |
npm run build:local-deps
npm run build
run: npm run build:local-deps

- name: Build CLI bundle
working-directory: extensions/cli
run: npm run build

- name: Verify release entrypoint
- name: Verify release entrypoint exists
working-directory: extensions/cli
run: node dist/cn.js --help >/tmp/cn-help.txt && grep -qi "continue" /tmp/cn-help.txt
run: |
set -euo pipefail
test -f dist/cn.js || { echo "dist/cn.js missing" >&2; exit 1; }
test -f dist/index.js || { echo "dist/index.js missing" >&2; exit 1; }
test -f dist/xhr-sync-worker.js || { echo "dist/xhr-sync-worker.js missing" >&2; exit 1; }

- name: Package CLI
run: |
set -euo pipefail
rm -rf release
mkdir -p release/cn
cp extensions/cli/dist/index.js release/cn/index.js
cp extensions/cli/dist/cn.js release/cn/cn.js
cp extensions/cli/dist/xhr-sync-worker.js release/cn/xhr-sync-worker.js
printf '%s\n' '{"type":"module"}' > release/cn/package.json
(
cd release/cn
node cn.js --help >/tmp/packaged-cn-help.txt
grep -qi "continue" /tmp/packaged-cn-help.txt
)
# The artifact must advertise ESM so `node cn.js` loads correctly,
# and must carry the version so `cn --version` reports the same
# semver that the GitHub Release was tagged with.
version="${{ steps.version.outputs.version }}"
node -e '
const fs = require("fs");
const pkg = JSON.parse(fs.readFileSync("extensions/cli/package.json", "utf8"));
fs.writeFileSync(
"release/cn/package.json",
JSON.stringify(
{ name: pkg.name, version: pkg.version, type: "module" },
null,
2,
) + "\n",
);
'
tar -C release -czf release/cn-node.tar.gz cn
cp extensions/cli/scripts/install-fork.sh release/install.sh
sha256sum release/cn-node.tar.gz | awk '{print $1 " cn-node.tar.gz"}' > release/cn-node.tar.gz.sha256

- name: Smoke-test packaged artifact (Linux)
working-directory: release/cn
run: |
set -euo pipefail
node cn.js --version
node cn.js --help > /tmp/cn-help.txt
grep -qi "continue" /tmp/cn-help.txt

- name: Upload release artifact
uses: actions/upload-artifact@v7
with:
name: cli-release-${{ github.ref_name }}
path: release/

test-artifact:
name: Smoke-test packaged artifact on ${{ matrix.os }}
needs: build
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os:
- ubuntu-latest
- macos-latest

steps:
- name: Download artifact
uses: actions/download-artifact@v7
with:
name: cli-release-${{ github.ref_name }}
path: artifact

- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 22

- name: Run packaged cn entrypoint
working-directory: artifact/cn
run: |
set -euo pipefail
test -f cn.js
test -f index.js
test -f package.json
# --version and --help must succeed on the exact files we ship.
node cn.js --version
node cn.js --help > /tmp/cn-help.txt
grep -qi "continue" /tmp/cn-help.txt

publish:
name: Publish GitHub release
needs: [build, test-artifact]
runs-on: ubuntu-latest
permissions:
contents: write

steps:
- name: Download artifact
uses: actions/download-artifact@v7
with:
name: cli-release-${{ github.ref_name }}
path: release

- name: Create GitHub release
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
gh release create "${GITHUB_REF_NAME}" \
release/cn-node.tar.gz \
release/cn-node.tar.gz.sha256 \
release/install.sh \
--title "Continue CLI ${GITHUB_REF_NAME#cli-v}" \
--generate-notes
8 changes: 8 additions & 0 deletions extensions/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,14 @@
### Bug Fixes

- add one column of horizontal padding around the TUI chat and input area so message bullets, assistant responses, tool output, and status content share the same left edge; Ink now wraps long text within the padded area ([extensions/cli/src/ui/TUIChat.tsx](extensions/cli/src/ui/TUIChat.tsx))
- break the esbuild/ESM import cycle between `compaction.ts` and `stream/streamChatResponse.ts` by extracting `pruneLastMessage` to `util/chatHistoryPrune.ts` and routing `compactChatHistory` / `subagent/executor.ts` through a late-bound `streamChatResponseRef` so the bundled CLI no longer crashes with `SyntaxError: Unexpected reserved word` at module load ([extensions/cli/src/compaction.ts](extensions/cli/src/compaction.ts), [extensions/cli/src/util/chatHistoryPrune.ts](extensions/cli/src/util/chatHistoryPrune.ts), [extensions/cli/src/stream/streamChatResponse.lateRef.ts](extensions/cli/src/stream/streamChatResponse.lateRef.ts))
- teach `getVersion` to read the `package.json` shipped next to `cn.js` in the release artifact so `cn --version` reports the same semver the GitHub Release was tagged with, instead of silently falling back to `unknown` ([extensions/cli/src/version.ts](extensions/cli/src/version.ts))

### Hardening

- the GitHub release workflow now refuses to publish a `cli-v*` tag whose commit is not the tip of `main`, fails the build if the tag and `extensions/cli/package.json` version disagree, writes a SHA-256 checksum file for the tarball, ships a version-stamped `package.json` inside the artifact, and runs the packaged-artifact smoke test on both `ubuntu-latest` and `macos-latest` before any release is created ([.github/workflows/release-cli.yml](.github/workflows/release-cli.yml))
- the curl installer verifies a SHA-256 checksum published alongside the tarball and refuses to install on mismatch, refuses to extract tar entries that escape the destination directory, resolves symlinks in the wrapper so the symlinked `cn` always invokes the real `cn.js`, and is fully idempotent ([extensions/cli/scripts/install-fork.sh](extensions/cli/scripts/install-fork.sh))
- the onboarding provider picker now asks Azure users for the deployment name and writes the `deployment` / `apiType` / `apiVersion` triple required by the current Continue Azure docs ([extensions/cli/src/onboardingProviders.ts](extensions/cli/src/onboardingProviders.ts), [extensions/cli/src/ui/components/ProviderPicker.tsx](extensions/cli/src/ui/components/ProviderPicker.tsx))

## [1.4.2](https://github.com/continuedev/cli/compare/v1.4.1...v1.4.2) (2025-07-17)

Expand Down
26 changes: 26 additions & 0 deletions extensions/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ curl -fsSL https://github.com/continued-agent/continued/releases/latest/download

This downloads the prebuilt CLI from the latest GitHub Release. It does not clone the repository or build the CLI locally. The installer places `cn` in `~/.local/bin` and the release files in `~/.local/share/continue-cli`.

The installer downloads `cn-node.tar.gz` and a sibling `cn-node.tar.gz.sha256` checksum file from the GitHub Release, recomputes the SHA-256 of the archive, and aborts if the two don't match. It also refuses to extract any tar entry that would escape the destination directory.

If you prefer to pin a specific version, set `CONTINUE_CLI_VERSION` before running the script (e.g. `CONTINUE_CLI_VERSION=cli-v0.1.0`). The wrapper script installs into `~/.local/share/continue-cli/current`; if `~/.local/bin` is not already on your `PATH`, the script detects your login shell and prints the exact line to add.

**Windows (PowerShell):**

```powershell
Expand Down Expand Up @@ -106,8 +110,30 @@ cn ls --json

- `CONTINUE_CLI_DISABLE_COMMIT_SIGNATURE`: Disable adding the Continue commit signature to generated commit messages
- `FORCE_NO_TTY`: Force TTY-less mode, prevents stdin reading (useful for testing and automation)
- `CONTINUE_CLI_VERSION`: Pin the install script to a specific release tag (e.g. `cli-v0.1.0`); defaults to `latest`
- `CONTINUE_CLI_INSTALL_DIR`: Override the install root (default `~/.local/share/continue-cli`)
- `CONTINUE_CLI_BIN_DIR`: Override the symlink directory (default `~/.local/bin`)
- Provider-specific API key variables are referenced from `config.yaml` as `${{ secrets.NAME }}`; common examples are `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`, `XAI_API_KEY`, `MISTRAL_API_KEY`, `DEEPSEEK_API_KEY`, `OPENROUTER_API_KEY`, `AZURE_API_KEY`, `BEDROCK_API_KEY`, `NVIDIA_API_KEY`, and `HF_TOKEN`

## Updating and Uninstalling

The installer is idempotent: re-running it replaces the current install in place and refreshes the `~/.local/bin/cn` symlink. To pin a specific version while updating, set `CONTINUE_CLI_VERSION` as shown above.

To uninstall, remove the symlink and the install root:

```bash
rm -f ~/.local/bin/cn
rm -rf ~/.local/share/continue-cli
```

To verify a downloaded archive manually before installing:

```bash
curl -fsSL https://github.com/continued-agent/continued/releases/latest/download/cn-node.tar.gz -o cn-node.tar.gz
curl -fsSL https://github.com/continued-agent/continued/releases/latest/download/cn-node.tar.gz.sha256 -o cn-node.tar.gz.sha256
sha256sum -c cn-node.tar.gz.sha256
```

## Commands

- `cn`: Start an interactive chat session
Expand Down
Loading
Loading