Files
copilotkit__copilotkit/.github/workflows/publish-vscode-extension.yml

380 lines
16 KiB
YAML

name: release / publish vscode-extension
# Mirrors the CopilotKit/aimock release pattern:
# - Contributor bumps packages/vscode-extension/package.json + prepends
# packages/vscode-extension/CHANGELOG.md, commits as
# "chore(vscode-extension): release vX.Y.Z", and merges to main.
# - On push to main this workflow self-gates by checking the VS Code
# Marketplace for the current package.json version; if already published,
# it's a no-op. Otherwise it builds, packages, dual-publishes (Marketplace
# + Open VSX with retry wrappers), tags vscode-extension-vX.Y.Z, and
# cuts a GitHub Release.
#
# No hand-cut tags. No tag-driven triggers. Pushes merged to main drive
# everything, exactly like aimock.
on:
push:
branches: [main]
paths:
- "packages/vscode-extension/**"
- ".github/workflows/publish-vscode-extension.yml"
workflow_dispatch:
inputs:
dry_run:
description: "Validate auth only (no publish)"
type: boolean
default: true
concurrency:
group: publish-vscode-extension-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: write
id-token: write
jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 20
environment: production
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: "10.13.1"
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20.x
cache: pnpm
- name: Read package.json version
id: pkg
working-directory: packages/vscode-extension
run: |
PKG_VERSION=$(node -p "require('./package.json').version")
PKG_NAME=$(node -p "require('./package.json').name")
PUBLISHER=$(node -p "require('./package.json').publisher || 'copilotkit'")
EXT_ID="${PUBLISHER}.${PKG_NAME}"
echo "version=${PKG_VERSION}" >> "$GITHUB_OUTPUT"
echo "name=${PKG_NAME}" >> "$GITHUB_OUTPUT"
echo "publisher=${PUBLISHER}" >> "$GITHUB_OUTPUT"
echo "ext_id=${EXT_ID}" >> "$GITHUB_OUTPUT"
echo "Resolved ${EXT_ID}@${PKG_VERSION}"
- name: Check if version already published to Marketplace
id: check
run: |
# Use `vsce show` to read Marketplace metadata. Unlike `npm view`
# there's no fast per-version endpoint, so we parse the full version
# list and look for an exact match. `vsce show` does not require
# auth for public extensions — no token needed.
EXT_ID="${{ steps.pkg.outputs.ext_id }}"
WANT="${{ steps.pkg.outputs.version }}"
# `vsce show` exits non-zero the very first time the extension is
# published (extension does not yet exist). Treat that as
# "not published".
set +e
OUT=$(npx --yes @vscode/vsce@^3 show "$EXT_ID" --json 2>/dev/null)
RC=$?
set -e
if [ $RC -ne 0 ] || [ -z "$OUT" ]; then
echo "vsce show returned empty or failed — treating as first-ever publish."
echo "published=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if echo "$OUT" | node -e "
let s=''; process.stdin.on('data',d=>s+=d).on('end',()=>{
try {
const j = JSON.parse(s);
const versions = (j.versions || []).map(v => v.version);
const found = versions.includes(process.argv[1]);
process.exit(found ? 0 : 1);
} catch (_) { process.exit(1); }
});
" "$WANT"; then
echo "Version $WANT already on Marketplace — nothing to do."
echo "published=true" >> "$GITHUB_OUTPUT"
else
echo "Version $WANT not yet on Marketplace — will publish."
echo "published=false" >> "$GITHUB_OUTPUT"
fi
- name: Install dependencies
if: steps.check.outputs.published == 'false'
run: pnpm install --frozen-lockfile --filter copilotkit-vscode-extension...
- name: Build
if: steps.check.outputs.published == 'false'
run: pnpm --filter copilotkit-vscode-extension build
- name: Package VSIX
if: steps.check.outputs.published == 'false'
working-directory: packages/vscode-extension
run: npx --yes @vscode/vsce@^3 package --no-dependencies --out extension.vsix
- name: Upload VSIX artifact
if: steps.check.outputs.published == 'false'
uses: actions/upload-artifact@v4
with:
name: vscode-extension-v${{ steps.pkg.outputs.version }}
path: packages/vscode-extension/extension.vsix
if-no-files-found: error
- name: Login to Azure (OIDC → Entra SP)
if: steps.check.outputs.published == 'false' || inputs.dry_run == true
uses: azure/login@v2
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
- name: Verify Marketplace credential
if: steps.check.outputs.published == 'false' || inputs.dry_run == true
working-directory: packages/vscode-extension
run: npx --yes @vscode/vsce@^3 verify-pat --azure-credential copilotkit
- name: Verify Open VSX credential
if: steps.check.outputs.published == 'false' || inputs.dry_run == true
env:
OVSX_PAT: ${{ secrets.OVSX_PAT }}
run: |
set -euo pipefail
if [ -z "${OVSX_PAT:-}" ]; then
echo "::error::OVSX_PAT secret is not configured in the 'production' environment"
exit 1
fi
echo "::group::ovsx verify-pat copilotkit"
if npx --yes ovsx verify-pat copilotkit -p "$OVSX_PAT" 2>&1; then
echo "✅ Open VSX token has publish rights for namespace 'copilotkit'"
else
rc=$?
echo "::error::ovsx verify-pat failed (exit $rc) — token is invalid or lacks namespace access"
exit $rc
fi
echo "::endgroup::"
- name: Publish to VS Code Marketplace
if: inputs.dry_run != true && steps.check.outputs.published == 'false'
id: publish_vsce
continue-on-error: true
working-directory: packages/vscode-extension
run: |
set -u
# Retry wrapper for transient registry failures (502/503/504, timeouts, DNS).
# Treats "version already exists" as idempotent success — covers the case
# where a prior attempt published but its response never reached us.
retry_publish() {
local attempts=5
local backoff=(10 20 40 60 90)
local i output rc wait
for (( i=1; i<=attempts; i++ )); do
echo "::group::vsce publish attempt $i/$attempts"
output=$("$@" 2>&1)
rc=$?
echo "$output"
echo "::endgroup::"
if [ $rc -eq 0 ]; then
echo "Published successfully on attempt $i"
return 0
fi
# Idempotent success: version already published (likely by a prior attempt).
if echo "$output" | grep -qiE 'already exists|already published|version.*(is )?already|extension.*version.*exists'; then
echo "Version already exists — treating as idempotent success (likely landed on a prior attempt)"
return 0
fi
# Non-retryable: auth, validation, manifest errors. Fail fast.
if echo "$output" | grep -qiE 'invalid.*token|invalid.*access.*token|unauthorized|forbidden|401\b|403\b|TF400813|is not authorized|bad request|422\b|validation (failed|error)|manifest'; then
echo "::error::Non-retryable error detected (auth/validation) — failing fast"
return $rc
fi
# Retryable (5xx, timeout, DNS, connection reset, generic network).
if [ $i -lt $attempts ]; then
wait=${backoff[$((i-1))]}
echo "Retryable error (rc=$rc). Sleeping ${wait}s before attempt $((i+1))/$attempts..."
sleep "$wait"
else
echo "::error::All $attempts vsce publish attempts exhausted"
return $rc
fi
done
}
retry_publish npx --yes @vscode/vsce@^3 publish \
--no-dependencies \
--packagePath extension.vsix \
--azure-credential
- name: Publish to Open VSX
if: inputs.dry_run != true && steps.check.outputs.published == 'false'
id: publish_ovsx
continue-on-error: true
working-directory: packages/vscode-extension
env:
OVSX_PAT: ${{ secrets.OVSX_PAT }}
run: |
set -u
if [ -z "${OVSX_PAT:-}" ]; then
echo "::error::OVSX_PAT secret is not configured in the 'production' environment"
exit 1
fi
# Retry wrapper for transient registry failures. Open VSX's /publish
# endpoint is known to return intermittent 502s from Eclipse Foundation
# infra; this absorbs that class of failure without bumping the version.
retry_publish() {
local attempts=5
local backoff=(10 20 40 60 90)
local i output rc wait
for (( i=1; i<=attempts; i++ )); do
echo "::group::ovsx publish attempt $i/$attempts"
output=$("$@" 2>&1)
rc=$?
echo "$output"
echo "::endgroup::"
if [ $rc -eq 0 ]; then
echo "Published successfully on attempt $i"
return 0
fi
# Idempotent success: version already published (likely by a prior attempt).
if echo "$output" | grep -qiE 'already exists|already published|version.*(is )?already|extension.*version.*exists'; then
echo "Version already exists — treating as idempotent success (likely landed on a prior attempt)"
return 0
fi
# Non-retryable: auth, validation. Fail fast.
# NOTE: ovsx emits "Invalid access token." for bad PATs — match that phrasing.
if echo "$output" | grep -qiE 'invalid.*token|invalid.*access.*token|unauthorized|forbidden|401\b|403\b|is not authorized|bad request|422\b|validation (failed|error)|namespace.*not.*found|must be verified'; then
echo "::error::Non-retryable error detected (auth/validation) — failing fast"
return $rc
fi
# Retryable (5xx, timeout, DNS, connection reset, generic network).
if [ $i -lt $attempts ]; then
wait=${backoff[$((i-1))]}
echo "Retryable error (rc=$rc). Sleeping ${wait}s before attempt $((i+1))/$attempts..."
sleep "$wait"
else
echo "::error::All $attempts ovsx publish attempts exhausted"
return $rc
fi
done
}
retry_publish npx --yes ovsx publish extension.vsix -p "$OVSX_PAT"
- name: Report publish results
if: inputs.dry_run != true && steps.check.outputs.published == 'false'
id: report
run: |
VSCE_STATUS="${{ steps.publish_vsce.outcome }}"
OVSX_STATUS="${{ steps.publish_ovsx.outcome }}"
echo "VS Code Marketplace: $VSCE_STATUS"
echo "Open VSX: $OVSX_STATUS"
# Each publish step retries transient 5xx/timeouts up to 5x with backoff
# and treats "version already exists" as idempotent success. A step
# outcome of "failure" here means the final attempt failed for a
# non-retryable reason (auth, validation) or exhausted retries on a
# persistent 5xx — either way, fail the job.
if [ "$VSCE_STATUS" != "success" ] || [ "$OVSX_STATUS" != "success" ]; then
echo "::error::One or more registry publishes failed after retries (vsce=$VSCE_STATUS ovsx=$OVSX_STATUS). Check the per-attempt logs above. Rerun the job from the Actions UI to retry — do NOT bump the version; registries treat 'already exists' as idempotent."
exit 1
fi
- name: Create git tag
if: inputs.dry_run != true && steps.check.outputs.published == 'false'
run: |
TAG="vscode-extension-v${{ steps.pkg.outputs.version }}"
if ! git rev-parse "${TAG}" >/dev/null 2>&1; then
git tag "${TAG}"
git push origin "${TAG}"
else
echo "Tag ${TAG} already exists — skipping."
fi
- name: Create GitHub Release
if: inputs.dry_run != true && steps.check.outputs.published == 'false'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="vscode-extension-v${{ steps.pkg.outputs.version }}"
if gh release view "${TAG}" >/dev/null 2>&1; then
echo "Release ${TAG} already exists — skipping."
exit 0
fi
# Prefer the CHANGELOG section for this version if present; otherwise
# let GitHub auto-generate notes from commits.
CHANGELOG="packages/vscode-extension/CHANGELOG.md"
NOTES_FILE=""
if [ -f "$CHANGELOG" ]; then
VERSION="${{ steps.pkg.outputs.version }}"
# Extract the section between "## <version>" and the next "## " heading.
NOTES_FILE=$(mktemp)
awk -v ver="$VERSION" '
$0 ~ "^## " ver "( |$)" { capture = 1; next }
capture && /^## / { exit }
capture { print }
' "$CHANGELOG" > "$NOTES_FILE"
if [ ! -s "$NOTES_FILE" ]; then
NOTES_FILE=""
fi
fi
if [ -n "$NOTES_FILE" ]; then
gh release create "${TAG}" --title "${TAG}" --verify-tag --latest --notes-file "$NOTES_FILE"
else
gh release create "${TAG}" --title "${TAG}" --verify-tag --latest --generate-notes
fi
- name: Notify Slack (published)
if: inputs.dry_run != true && steps.check.outputs.published == 'false' && env.SLACK_WEBHOOK_OSS_ALERTS != ''
env:
SLACK_WEBHOOK_OSS_ALERTS: ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
uses: slackapi/slack-github-action@v2.1.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
webhook-type: incoming-webhook
payload: |
{
"text": ":package: *copilotkit-vscode-extension v${{ steps.pkg.outputs.version }} published*\nMarketplace: https://marketplace.visualstudio.com/items?itemName=${{ steps.pkg.outputs.ext_id }}\nOpen VSX: https://open-vsx.org/extension/${{ steps.pkg.outputs.publisher }}/${{ steps.pkg.outputs.name }}\nRelease: https://github.com/${{ github.repository }}/releases/tag/vscode-extension-v${{ steps.pkg.outputs.version }}"
}
- name: Dry-run summary
if: inputs.dry_run == true
run: |
echo "Dry run complete. Auth chains validated:"
echo " - Marketplace: OIDC -> Entra SP -> vsce verify-pat --azure-credential"
echo " - Open VSX: bearer token -> /api/user"
echo ""
echo "No publish occurred. To cut a real release, add a new entry to packages/vscode-extension/CHANGELOG.md and open a PR."
- name: Notify Slack (publish failure)
if: failure() && inputs.dry_run != true && steps.check.outputs.published == 'false' && env.SLACK_WEBHOOK_OSS_ALERTS != ''
env:
SLACK_WEBHOOK_OSS_ALERTS: ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
uses: slackapi/slack-github-action@v2.1.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_OSS_ALERTS }}
webhook-type: incoming-webhook
payload: |
{
"text": ":x: *copilotkit-vscode-extension v${{ steps.pkg.outputs.version }} publish failed*\nFailed job: ${{ github.job }} | vsce=${{ steps.publish_vsce.outcome }} ovsx=${{ steps.publish_ovsx.outcome }}\n<https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}|View run>"
}