mirror of
https://github.com/cline/cline.git
synced 2026-09-01 15:11:04 +08:00
ci(vscode): publish combined stable VSIX to Open VSX and harden ab-package gates (#12805)
* ci(vscode): publish combined stable VSIX to Open VSX and harden ab-package gates * ci(vscode): address greptile review — env-routed version input, bookkeeping survives Open VSX failure
This commit is contained in:
@@ -5,6 +5,11 @@ name: ext-vscode-ab-package
|
||||
# `legacy/` from the legacy-extension branch. Cohort selection happens at
|
||||
# runtime via PostHog flags; see apps/vscode-rollout/README.md for the design
|
||||
# and the rollout runbook.
|
||||
#
|
||||
# Job layout: cheap input gates (preflight) and the two bundle test suites run
|
||||
# ungated; the build job packages the VSIX with no environment attached, so
|
||||
# publish=false rehearsals complete without any approval; only the publish job
|
||||
# — Marketplace + Open VSX + bookkeeping — waits on the `publish` environment.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
@@ -24,7 +29,7 @@ on:
|
||||
default: "legacy-extension"
|
||||
type: string
|
||||
publish:
|
||||
description: "Publish to the VS Code Marketplace (unchecked: just build the .vsix artifact)"
|
||||
description: "Publish to the VS Code Marketplace and Open VSX (unchecked: just build the .vsix artifact)"
|
||||
required: true
|
||||
default: false
|
||||
type: boolean
|
||||
@@ -37,18 +42,80 @@ concurrency:
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Input gates that need no checkout: fail in seconds — before the test
|
||||
# suites, the ~20-minute build, and the environment approval — instead of
|
||||
# at publish time.
|
||||
preflight:
|
||||
name: Validate inputs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# The input reaches the shell ONLY via env here (never inline
|
||||
# expression interpolation, which is evaluated before bash runs and
|
||||
# would allow script injection from the dispatch form). Because
|
||||
# every later job `needs` preflight, passing this regex is what
|
||||
# makes the plain-string `${{ inputs.version }}` interpolations
|
||||
# downstream safe.
|
||||
- name: Validate version format
|
||||
env:
|
||||
VERSION: ${{ github.event.inputs.version }}
|
||||
run: |
|
||||
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "Error: version must be plain X.Y.Z with no leading 'v' and no suffix (got '$VERSION')."
|
||||
echo "It is stamped verbatim into the union manifest and both bundle manifests."
|
||||
exit 1
|
||||
fi
|
||||
echo "Version format ok: $VERSION"
|
||||
|
||||
# The reusable bun suite tests the dispatch revision (main), so
|
||||
# publishing any other next-ref would ship an untested bundle.
|
||||
# Build-only runs (publish=false) may still use arbitrary next-refs
|
||||
# for artifact rehearsals.
|
||||
- name: Refuse to publish an untested next-ref
|
||||
if: ${{ github.event.inputs.publish == 'true' && github.event.inputs.next-ref != 'main' }}
|
||||
run: |
|
||||
echo "Error: publish=true requires next-ref=main — the test gate only covers main."
|
||||
exit 1
|
||||
|
||||
# Marketplace versions are monotonic and cannot be unpublished:
|
||||
# every publish must exceed the highest version ever published to
|
||||
# the claude-dev listing FROM ANY BRANCH (combined stable or legacy
|
||||
# hotfix). The publish job re-checks right before publishing — the
|
||||
# environment-approval wait can last days and a legacy hotfix can
|
||||
# land in between. Keep both copies of this check in sync.
|
||||
- name: Verify version exceeds the live Marketplace version
|
||||
if: ${{ github.event.inputs.publish == 'true' }}
|
||||
env:
|
||||
VERSION: ${{ github.event.inputs.version }}
|
||||
run: |
|
||||
LIVE=$(curl -sf --retry 3 -X POST "https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery" \
|
||||
-H "Content-Type: application/json" -H "Accept: application/json;api-version=3.0-preview.1" \
|
||||
--data '{"filters":[{"criteria":[{"filterType":7,"value":"saoudrizwan.claude-dev"}]}],"flags":16}' \
|
||||
| node -e 'let d="";process.stdin.on("data",c=>d+=c);process.stdin.on("end",()=>{process.stdout.write(JSON.parse(d).results[0].extensions[0].versions[0].version)})')
|
||||
if [[ -z "$LIVE" ]]; then
|
||||
echo "Error: could not resolve the live Marketplace version for saoudrizwan.claude-dev."
|
||||
exit 1
|
||||
fi
|
||||
node -e '
|
||||
const [next, live] = process.argv.slice(1).map((v) => v.split(".").map(Number));
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (next[i] > live[i]) process.exit(0);
|
||||
if (next[i] < live[i]) break;
|
||||
}
|
||||
console.error(`Error: version ${process.argv[1]} does not exceed the live Marketplace version ${process.argv[2]}.`);
|
||||
process.exit(1);
|
||||
' "$VERSION" "$LIVE"
|
||||
echo "Version ok: $VERSION exceeds live Marketplace version $LIVE"
|
||||
|
||||
# Gate the build/publish on BOTH bundles' own test suites, mirroring the two
|
||||
# standalone publish paths (nightly gates on the bun suite via the same
|
||||
# reusable workflow; the legacy publish inlines the npm suite). The gated
|
||||
# `package` job requests its `publish` environment approval only after both
|
||||
# suites pass.
|
||||
# reusable workflow; the legacy publish inlines the npm suite).
|
||||
#
|
||||
# Caveat (shared with the nightly workflow): the reusable bun suite tests the
|
||||
# DISPATCH revision — main's tip at dispatch, since this workflow is only
|
||||
# dispatched from main — not `next-ref`. The package job therefore pins the
|
||||
# dispatched from main — not `next-ref`. The build job therefore pins the
|
||||
# default next-ref checkout to that same revision (tested == built) and
|
||||
# refuses publish=true for any other next-ref; build-only artifact runs may
|
||||
# still build untested refs.
|
||||
# preflight refuses publish=true for any other next-ref; build-only artifact
|
||||
# runs may still build untested refs.
|
||||
test-next:
|
||||
name: Test next (SDK) bundle
|
||||
permissions:
|
||||
@@ -62,11 +129,10 @@ jobs:
|
||||
test-legacy:
|
||||
name: Test legacy bundle
|
||||
runs-on: ubuntu-latest
|
||||
# The tested revision, exported so the package job builds EXACTLY what
|
||||
# The tested revision, exported so the build job builds EXACTLY what
|
||||
# this suite ran against. legacy-ref is a mutable branch name and the
|
||||
# package job starts much later (test phase + environment-approval wait,
|
||||
# potentially days) — re-resolving the name there could pick up commits
|
||||
# this gate never saw.
|
||||
# build job starts later — re-resolving the name there could pick up
|
||||
# commits this gate never saw.
|
||||
outputs:
|
||||
tested-sha: ${{ steps.rev.outputs.sha }}
|
||||
defaults:
|
||||
@@ -121,27 +187,11 @@ jobs:
|
||||
cd webview-ui
|
||||
npm run test:coverage
|
||||
|
||||
package:
|
||||
build:
|
||||
name: Build combined (legacy + next) VSIX
|
||||
needs: [test-next, test-legacy]
|
||||
needs: [preflight, test-next, test-legacy]
|
||||
runs-on: ubuntu-latest
|
||||
environment: publish
|
||||
# contents: write is required by the post-publish bookkeeping (tag +
|
||||
# GitHub Release), mirroring the standalone publish workflows.
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
# Refuse to publish a next bundle the test-next gate did not cover.
|
||||
# The reusable suite tests the dispatch revision (main), so publishing
|
||||
# any other next-ref would ship an untested bundle. Build-only runs
|
||||
# (publish=false) may still use arbitrary next-refs for artifact
|
||||
# rehearsals.
|
||||
- name: Refuse to publish an untested next-ref
|
||||
if: ${{ github.event.inputs.publish == 'true' && github.event.inputs.next-ref != 'main' }}
|
||||
run: |
|
||||
echo "Error: publish=true requires next-ref=main — the test gate only covers main."
|
||||
exit 1
|
||||
|
||||
# For the default next-ref (main), pin the checkout to the exact
|
||||
# revision the test-next gate ran against: a moving branch name could
|
||||
# otherwise drift past the tested commit during the test phase.
|
||||
@@ -185,9 +235,12 @@ jobs:
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
# --frozen-lockfile so the built bundle resolves the exact
|
||||
# dependency set the test-next gate ran against (the reusable suite
|
||||
# installs frozen too) — a bare install could silently re-resolve.
|
||||
- name: Install next workspace dependencies
|
||||
working-directory: next-src
|
||||
run: bun install
|
||||
run: bun install --frozen-lockfile
|
||||
|
||||
# @cline/* are local workspace symlinks to source packages; apps/vscode's
|
||||
# `package` script does NOT build them, so without this the esbuild step
|
||||
@@ -196,6 +249,17 @@ jobs:
|
||||
working-directory: next-src
|
||||
run: bun run build:sdk
|
||||
|
||||
- name: Assert better-sqlite3 native binary present
|
||||
working-directory: next-src/apps/vscode
|
||||
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"
|
||||
|
||||
# Stamp the combined version into each bundle's package.json AFTER
|
||||
# install and BEFORE its build: the About tab and telemetry
|
||||
# extension_version read the bundle's own manifest, so without this
|
||||
@@ -312,32 +376,111 @@ jobs:
|
||||
path: staging/claude-dev-${{ github.event.inputs.version }}.vsix
|
||||
if-no-files-found: error
|
||||
|
||||
publish:
|
||||
name: Publish to Marketplace and Open VSX
|
||||
needs: build
|
||||
if: ${{ github.event.inputs.publish == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
environment: publish
|
||||
# contents: write is required by the post-publish bookkeeping (tag +
|
||||
# GitHub Release), mirroring the standalone publish workflows.
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
# The built next revision: preflight refused publish=true for any
|
||||
# next-ref other than main, and the build job pinned main to the
|
||||
# dispatch SHA — so github.sha IS the published commit. Used for the
|
||||
# changelog, the release tag, and the previous-tag lookup.
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Download VSIX artifact
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: claude-dev-${{ github.event.inputs.version }}
|
||||
path: staging
|
||||
|
||||
- name: Install Publishing Tools
|
||||
run: npm install -g @vscode/vsce ovsx
|
||||
|
||||
# Re-check monotonicity at the last moment: the environment-approval
|
||||
# wait can last days, and a legacy hotfix published in the meantime
|
||||
# would otherwise be silently superseded by this older code line.
|
||||
# Keep in sync with the preflight copy of this check.
|
||||
- name: Re-verify version exceeds the live Marketplace version
|
||||
env:
|
||||
VERSION: ${{ github.event.inputs.version }}
|
||||
run: |
|
||||
LIVE=$(curl -sf --retry 3 -X POST "https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery" \
|
||||
-H "Content-Type: application/json" -H "Accept: application/json;api-version=3.0-preview.1" \
|
||||
--data '{"filters":[{"criteria":[{"filterType":7,"value":"saoudrizwan.claude-dev"}]}],"flags":16}' \
|
||||
| node -e 'let d="";process.stdin.on("data",c=>d+=c);process.stdin.on("end",()=>{process.stdout.write(JSON.parse(d).results[0].extensions[0].versions[0].version)})')
|
||||
if [[ -z "$LIVE" ]]; then
|
||||
echo "Error: could not resolve the live Marketplace version for saoudrizwan.claude-dev."
|
||||
exit 1
|
||||
fi
|
||||
node -e '
|
||||
const [next, live] = process.argv.slice(1).map((v) => v.split(".").map(Number));
|
||||
for (let i = 0; i < 3; i++) {
|
||||
if (next[i] > live[i]) process.exit(0);
|
||||
if (next[i] < live[i]) break;
|
||||
}
|
||||
console.error(`Error: version ${process.argv[1]} does not exceed the live Marketplace version ${process.argv[2]}.`);
|
||||
process.exit(1);
|
||||
' "$VERSION" "$LIVE"
|
||||
echo "Version ok: $VERSION exceeds live Marketplace version $LIVE"
|
||||
|
||||
# Both PATs are verified BEFORE the first irreversible publish so a
|
||||
# missing Open VSX token can't strand us half-published. The two
|
||||
# registries are separate steps: if Open VSX fails after the
|
||||
# Marketplace accepted the VSIX, the run goes red (so the operator
|
||||
# notices Open VSX lagged) but the bookkeeping below still runs —
|
||||
# it is keyed off the Marketplace outcome, which is what "shipped"
|
||||
# means for this listing.
|
||||
- name: Publish to Marketplace
|
||||
if: ${{ github.event.inputs.publish == 'true' }}
|
||||
id: publish_marketplace
|
||||
working-directory: staging
|
||||
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."
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "$OVSX_PAT" ]]; then
|
||||
echo "Error: OVSX_PAT is required to publish to Open VSX."
|
||||
exit 1
|
||||
fi
|
||||
vsce publish --no-dependencies --packagePath "claude-dev-${{ github.event.inputs.version }}.vsix"
|
||||
|
||||
- name: Publish to Open VSX
|
||||
working-directory: staging
|
||||
env:
|
||||
OVSX_PAT: ${{ secrets.OVSX_PAT }}
|
||||
run: npx ovsx publish --packagePath "claude-dev-${{ github.event.inputs.version }}.vsix" --pat "$OVSX_PAT"
|
||||
|
||||
# ---- Post-publish bookkeeping (tag / GitHub Release / Slack) ----
|
||||
# Mirrors the standalone publish workflows. Every step here is
|
||||
# continue-on-error: the Marketplace publish above already happened,
|
||||
# and a red run after a successful publish is exactly the confusion
|
||||
# the nightly workflow taught us to avoid (tag pushes fail whenever
|
||||
# the built commit touches .github/workflows/** — no grantable
|
||||
# permission fixes that; push the tag manually in that case, see
|
||||
# the publish-extension skill).
|
||||
# continue-on-error, and gated on the MARKETPLACE outcome rather
|
||||
# than plain step ordering: the Marketplace publish already
|
||||
# happened, so bookkeeping must still run when only the Open VSX
|
||||
# step failed, and a red run after a successful publish is exactly
|
||||
# the confusion the nightly workflow taught us to avoid (tag pushes
|
||||
# fail whenever the built commit touches .github/workflows/** — no
|
||||
# grantable permission fixes that; push the tag manually in that
|
||||
# case, see the publish-extension skill).
|
||||
|
||||
- name: Extract changelog entry
|
||||
id: changelog
|
||||
if: ${{ github.event.inputs.publish == 'true' }}
|
||||
if: ${{ !cancelled() && steps.publish_marketplace.outcome == 'success' }}
|
||||
continue-on-error: true
|
||||
working-directory: next-src
|
||||
run: |
|
||||
CONTENT=$(awk '/^## \[/{if(found) exit; found=1; next} found{print}' CHANGELOG.md)
|
||||
{
|
||||
@@ -348,9 +491,8 @@ jobs:
|
||||
|
||||
- name: Resolve previous release tag
|
||||
id: prev_tag
|
||||
if: ${{ github.event.inputs.publish == 'true' }}
|
||||
if: ${{ !cancelled() && steps.publish_marketplace.outcome == 'success' }}
|
||||
continue-on-error: true
|
||||
working-directory: next-src
|
||||
run: |
|
||||
# ls-remote needs no local tag objects; take the highest v* tag
|
||||
# below the one being released.
|
||||
@@ -362,9 +504,8 @@ jobs:
|
||||
echo "prev_tag=$PREV" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Create and push release tag
|
||||
if: ${{ github.event.inputs.publish == 'true' }}
|
||||
if: ${{ !cancelled() && steps.publish_marketplace.outcome == 'success' }}
|
||||
continue-on-error: true
|
||||
working-directory: next-src
|
||||
run: |
|
||||
TAG="v${{ github.event.inputs.version }}"
|
||||
git tag "$TAG" HEAD
|
||||
@@ -372,7 +513,7 @@ jobs:
|
||||
echo "Pushed $TAG at $(git rev-parse HEAD)"
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: ${{ github.event.inputs.publish == 'true' }}
|
||||
if: ${{ !cancelled() && steps.publish_marketplace.outcome == 'success' }}
|
||||
continue-on-error: true
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
@@ -386,7 +527,7 @@ jobs:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Post release to Slack
|
||||
if: ${{ github.event.inputs.publish == 'true' }}
|
||||
if: ${{ !cancelled() && steps.publish_marketplace.outcome == 'success' }}
|
||||
continue-on-error: true
|
||||
uses: slackapi/slack-github-action@v3.0.1
|
||||
with:
|
||||
|
||||
Reference in New Issue
Block a user