mirror of
https://github.com/CopilotKit/CopilotKit.git
synced 2026-09-14 16:26:20 +08:00
380 lines
16 KiB
YAML
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>"
|
|
}
|