name: ext-vscode-publish-stable on: workflow_dispatch: inputs: release-type: description: "Choose release type (release or pre-release)" required: true default: "release" type: choice options: - pre-release - release auto_create_tag_from_main: description: "Auto-create and push the provided tag from the tested main commit (recommended)" required: true default: true type: boolean tag: description: "Tag to publish (required in both modes, e.g., v3.1.2)" required: true type: string permissions: contents: write packages: write checks: write pull-requests: write concurrency: group: ext-vscode-publish-stable-${{ github.event.inputs.tag }} cancel-in-progress: false jobs: test: uses: ./.github/workflows/ext-vscode-test.yml publish: needs: test name: Publish Extension runs-on: ubuntu-latest environment: publish defaults: run: working-directory: apps/vscode steps: - uses: actions/checkout@v4 with: ref: main fetch-depth: 0 fetch-tags: true lfs: true - name: Resolve Release Tag id: resolve_tag working-directory: ${{ github.workspace }} env: TAG: ${{ github.event.inputs.tag }} AUTO_CREATE: ${{ github.event.inputs.auto_create_tag_from_main }} run: | TESTED_SHA="${{ github.sha }}" WORKFLOW_REF="${{ github.ref }}" if [[ -z "$TAG" ]]; then echo "Error: tag input is required" exit 1 fi if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?$ ]]; then echo "Error: tag must match vX.Y.Z (optionally with -suffix or .suffix)" exit 1 fi TAG_REF="refs/tags/$TAG" git fetch origin main --tags if [[ "$AUTO_CREATE" == "true" ]]; then if [[ "$WORKFLOW_REF" != "refs/heads/main" ]]; then echo "Error: auto-create mode requires dispatching from main (current ref: $WORKFLOW_REF)" exit 1 fi echo "Auto-create enabled. Using tested workflow SHA: $TESTED_SHA" if ! git merge-base --is-ancestor "$TESTED_SHA" origin/main; then echo "Error: tested SHA $TESTED_SHA is not on origin/main" exit 1 fi if git show-ref --verify --quiet "$TAG_REF"; then TAG_SHA=$(git rev-list -n 1 "$TAG_REF^{commit}") if [[ "$TAG_SHA" != "$TESTED_SHA" ]]; then echo "Error: tag '$TAG' already exists at $TAG_SHA, not at tested SHA ($TESTED_SHA)" exit 1 fi echo "Tag '$TAG' already exists at tested SHA. Continuing." else git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git tag "$TAG" "$TESTED_SHA" git push origin "$TAG_REF" echo "Created and pushed tag '$TAG' from tested SHA $TESTED_SHA." fi else if ! git show-ref --verify --quiet "$TAG_REF"; then echo "Error: tag '$TAG' does not exist in the repository" exit 1 fi TAG_SHA=$(git rev-list -n 1 "$TAG_REF^{commit}") if [[ "$TAG_SHA" != "$TESTED_SHA" ]]; then echo "Error: existing tag '$TAG' points to $TAG_SHA, but this workflow tested $TESTED_SHA" echo "Dispatch from the tag ref, or from the exact main commit the tag points to." exit 1 fi echo "Using existing tag '$TAG' at tested SHA $TESTED_SHA." fi git checkout --detach "$TAG_REF^{commit}" echo "tag=$TAG" >> $GITHUB_OUTPUT echo "resolved_sha=$(git rev-parse HEAD)" >> $GITHUB_OUTPUT - name: Setup Bun uses: oven-sh/setup-bun@v2 with: bun-version: 1.3.14 # Node is still REQUIRED in the publish job (not just for install): the # publish scripts run as `node scripts/publish-*.mjs`, the version step uses # `node -p`, and `npx ovsx` needs npm. setup-bun does not provide a Node # runtime, so keep setup-node. Pinned to Node 22 because newer LTS # (Node 24 / npm 11) can make vsce's `npm list` detection fail with # ELSPROBLEMS during packaging. - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 22 # Single root install resolves the whole bun workspace at once (replaces the # per-package `npm install` steps for apps/vscode + webview-ui). - name: Install workspace dependencies working-directory: ${{ github.workspace }} run: bun install --frozen-lockfile # @cline/* are local workspace symlinks to source packages; build dist/ before # packaging/publishing the extension. - name: Build SDK packages working-directory: ${{ github.workspace }} run: bun run build:sdk - name: Assert better-sqlite3 native binary present run: | NODE_FILE="node_modules/better-sqlite3/build/Release/better_sqlite3.node" if [ ! -f "$NODE_FILE" ]; then echo "ERROR: better-sqlite3 native binary missing at apps/vscode/$NODE_FILE" echo "(bun trustedDependencies postinstall likely did not run)" exit 1 fi echo "Found better-sqlite3 native binary: $NODE_FILE" # vsce is a workspace devDependency (on node_modules/.bin), but ovsx is not # vendored and the publish script invokes it via `npx ovsx`, so install ovsx # globally (npm is available via setup-node). vsce is installed globally too # to preserve the script's existing PATH expectations. - name: Install Publishing Tools run: npm install -g @vscode/vsce ovsx - name: Get Version id: get_version run: | VERSION=$(node -p "require('./package.json').version") echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Verify Tag Matches Package Version run: | TAG="${{ steps.resolve_tag.outputs.tag }}" VERSION="v${{ steps.get_version.outputs.version }}" if [[ "$TAG" != "$VERSION" ]]; then echo "Error: tag '$TAG' does not match package version '$VERSION'" exit 1 fi echo "Tag and package version match: $TAG" - name: Verify Changelog Entry working-directory: ${{ github.workspace }} run: | EXPECTED_HEADING="## [${{ steps.get_version.outputs.version }}]" FIRST_HEADING=$(grep -m 1 '^## \[' CHANGELOG.md || true) if [[ "$FIRST_HEADING" != "$EXPECTED_HEADING" ]]; then echo "Error: CHANGELOG.md must start with '$EXPECTED_HEADING' before publishing." echo "Current first release heading: ${FIRST_HEADING:-}" exit 1 fi echo "Found changelog entry for ${{ steps.get_version.outputs.version }}" - name: Verify Marketplace Tokens env: VSCE_PAT: ${{ secrets.VSCE_PAT }} OVSX_PAT: ${{ secrets.OVSX_PAT }} run: | if [[ -z "$VSCE_PAT" ]]; then echo "Error: VSCE_PAT is required to publish the stable VS Code extension." exit 1 fi if [[ -z "$OVSX_PAT" ]]; then echo "Error: OVSX_PAT is required to publish the stable Open VSX extension." exit 1 fi echo "Marketplace publish tokens are configured." - name: Get Previous Tag id: prev_tag working-directory: ${{ github.workspace }} run: | CURRENT_TAG="${{ steps.resolve_tag.outputs.tag }}" PREV_TAG=$( git tag --merged "$CURRENT_TAG^" --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname \ | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?$' \ | head -n 1 || true ) echo "prev_tag=$PREV_TAG" >> $GITHUB_OUTPUT - name: Get Changelog Entry id: changelog working-directory: ${{ github.workspace }} run: | # Get content between the matching version heading and the next release heading. CONTENT=$(awk -v version="${{ steps.get_version.outputs.version }}" ' $0 == "## [" version "]" { found=1; next } found && /^## \[/ { exit } found { print } END { if (!found) exit 1 } ' CHANGELOG.md) echo "content<> $GITHUB_OUTPUT echo "$CONTENT" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - name: Package and Publish Extension env: VSCE_PAT: ${{ secrets.VSCE_PAT }} OVSX_PAT: ${{ secrets.OVSX_PAT }} CLINE_ENVIRONMENT: production TELEMETRY_SERVICE_API_KEY: ${{ secrets.TELEMETRY_SERVICE_API_KEY }} ERROR_SERVICE_API_KEY: ${{ secrets.ERROR_SERVICE_API_KEY }} # OpenTelemetry production defaults (can be overridden at runtime) OTEL_TELEMETRY_ENABLED: ${{ secrets.OTEL_TELEMETRY_ENABLED }} OTEL_LOGS_EXPORTER: otlp OTEL_METRICS_EXPORTER: otlp OTEL_EXPORTER_OTLP_PROTOCOL: ${{ secrets.OTEL_EXPORTER_OTLP_PROTOCOL }} OTEL_EXPORTER_OTLP_ENDPOINT: ${{ secrets.OTEL_EXPORTER_OTLP_ENDPOINT }} OTEL_EXPORTER_OTLP_HEADERS: ${{ secrets.OTEL_EXPORTER_OTLP_HEADERS }} RELEASE_TYPE: ${{ github.event.inputs.release-type }} run: | # Swap README.marketplace.md into README.md so both the GitHub # release artifact (vsce package below) and the marketplace # publish (npm run publish:marketplace below, which swaps # internally as an idempotent no-op) ship the same README. node scripts/marketplace-readme.mjs swap-in trap 'node scripts/marketplace-readme.mjs restore' EXIT # Required to generate the .vsix. --no-dependencies: the extension # is fully esbuild-bundled, and under the bun workspace the @cline/* # deps are symlinks pointing outside the package, so without this vsce # would walk them and pull the whole monorepo into the .vsix. vsce package --no-dependencies --allow-package-secrets sendgrid --out "cline-${{ steps.get_version.outputs.version }}.vsix" # These scripts run under `node scripts/publish-marketplace.mjs`; # bun run just launches them. Node + npm (for `npx ovsx`) come from # setup-node above. if [ "$RELEASE_TYPE" = "pre-release" ]; then bun run publish:marketplace:prerelease echo "Successfully published pre-release version ${{ steps.get_version.outputs.version }} to VS Code Marketplace and Open VSX Registry" else bun run publish:marketplace echo "Successfully published release version ${{ steps.get_version.outputs.version }} to VS Code Marketplace and Open VSX Registry" fi - name: Create GitHub Release uses: softprops/action-gh-release@v1 with: tag_name: ${{ steps.resolve_tag.outputs.tag }} files: "apps/vscode/*.vsix" body: | ${{ steps.changelog.outputs.content }} **Full Changelog**: https://github.com/${{ github.repository }}/compare/${{ steps.prev_tag.outputs.prev_tag }}...${{ steps.resolve_tag.outputs.tag }} prerelease: ${{ github.event.inputs.release-type == 'pre-release' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Post release to Slack uses: slackapi/slack-github-action@v3.0.1 with: method: chat.postMessage token: ${{ secrets.SLACK_RELEASE_BOT_TOKEN }} payload: | channel: "C0APVKGGZFC" text: "Cline ${{ steps.resolve_tag.outputs.tag }}" blocks: - type: "section" text: type: "mrkdwn" text: "*Cline ${{ steps.resolve_tag.outputs.tag }}*" - type: "section" text: type: "mrkdwn" text: ${{ toJSON(steps.changelog.outputs.content) }} - type: "context" elements: - type: "mrkdwn" text: "Full Changelog: https://github.com/${{ github.repository }}/compare/${{ steps.prev_tag.outputs.prev_tag }}...${{ steps.resolve_tag.outputs.tag }}"