Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
41c3a91
feat: add VS Code extension CI/CD infrastructure
madhur310 May 22, 2026
55e1261
test: add workflows to test vscode-extension-ci integration
madhur310 May 26, 2026
a752f30
chore: update .gitignore to exclude build artifacts
madhur310 May 26, 2026
7ee71a4
chore: remove dist/ files from git tracking
madhur310 May 26, 2026
e3b396f
fix: only run test workflows on manual dispatch
madhur310 May 26, 2026
e75c767
fix: move VS Code workflows to top level and enable push triggers
madhur310 May 26, 2026
89a6826
fix: replace complex test workflows with simple package test
madhur310 May 26, 2026
3b6317e
fix: make package tsconfig self-contained
madhur310 May 26, 2026
18c18c1
test: add integration test workflow for VS Code workflows
madhur310 May 26, 2026
6d32dd6
fix: simplify integration test workflow
madhur310 May 26, 2026
f38c9fc
fix: add push trigger to integration test workflow
madhur310 May 26, 2026
9d4a1de
fix: correct path indentation in checkout action
madhur310 May 26, 2026
ab1b9df
fix: replace require() with ES module imports
madhur310 May 26, 2026
d82fcac
fix: use correct package script name
madhur310 May 26, 2026
e6782a9
Fix nested workflow calls to use absolute paths
madhur310 May 26, 2026
db4cc93
Fix composite action references to use absolute paths
madhur310 May 26, 2026
294a519
Add prepare script to build package on install
madhur310 May 26, 2026
6021653
Add prepare script to root to build workspaces on install
madhur310 May 26, 2026
4316ec0
Use full path to CLI instead of npx
madhur310 May 26, 2026
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
34 changes: 34 additions & 0 deletions .github/actions/vscode/calculate-artifact-name/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: 'Calculate Artifact Name'
description: 'Calculate artifact name with run number and mode suffix'

inputs:
artifact-name:
description: 'Base artifact name or pre-calculated name'
required: true
dry-run:
description: 'Whether this is a dry-run mode'
required: false
default: 'false'
run-number:
description: 'GitHub run number (defaults to github.run_number)'
required: false
default: '${{ github.run_number }}'

outputs:
artifact-name:
description: 'The calculated artifact name'
value: ${{ steps.calc.outputs.artifact-name }}

runs:
using: 'composite'
steps:
- name: Calculate artifact name
id: calc
shell: bash
run: |
# Only treat as already set if artifact-name ends with -dry-run or -release
if [[ "${{ inputs.artifact-name }}" =~ -dry-run$ ]] || [[ "${{ inputs.artifact-name }}" =~ -release$ ]]; then
echo "artifact-name=${{ inputs.artifact-name }}" >> $GITHUB_OUTPUT
else
echo "artifact-name=${{ format('{0}-{1}-{2}', inputs.artifact-name, inputs.run-number, inputs.dry-run == 'true' && 'dry-run' || 'release') }}" >> $GITHUB_OUTPUT
fi
100 changes: 100 additions & 0 deletions .github/actions/vscode/check-ci-status/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: Check CI Status
description: >
Verifies that CI checks passed for a given commit SHA before promotion.
Fails if any required check did not succeed.

inputs:
commit-sha:
description: 'Commit SHA to check CI status for'
required: true
token:
description: 'GitHub token with repo read access'
required: true
required-checks:
description: >
Comma-separated list of check names that must have succeeded.
If empty, all non-skipped check-runs must have conclusion "success".
required: false
default: ''

runs:
using: composite
steps:
- name: Verify CI checks passed
shell: bash
env:
GH_TOKEN: ${{ inputs.token }}
COMMIT_SHA: ${{ inputs.commit-sha }}
REQUIRED_CHECKS: ${{ inputs.required-checks }}
REPO: ${{ github.repository }}
run: |
echo "Checking CI status for commit $COMMIT_SHA in $REPO..."

# Fetch all check-runs for the commit (paginate up to 100)
CHECK_RUNS=$(gh api \
"repos/$REPO/commits/$COMMIT_SHA/check-runs" \
--paginate \
--jq '.check_runs[] | {name: .name, status: .status, conclusion: .conclusion}' \
2>&1)

if [ -z "$CHECK_RUNS" ]; then
echo "No check-runs found for commit $COMMIT_SHA"
echo "Cannot verify CI status — failing to prevent untested promotion"
exit 1
fi

echo "Check-runs found:"
echo "$CHECK_RUNS" | jq -r '" \(.name): status=\(.status) conclusion=\(.conclusion)"'

FAILED=0

if [ -n "$REQUIRED_CHECKS" ]; then
# Only validate the specified checks
IFS=',' read -ra CHECKS <<< "$REQUIRED_CHECKS"
for CHECK in "${CHECKS[@]}"; do
CHECK=$(echo "$CHECK" | xargs) # trim whitespace
CONCLUSION=$(echo "$CHECK_RUNS" | jq -r --arg name "$CHECK" \
'select(.name == $name) | .conclusion' | head -1)
if [ "$CONCLUSION" != "success" ]; then
echo "FAIL: required check '$CHECK' has conclusion '$CONCLUSION' (expected 'success')"
FAILED=1
else
echo "PASS: required check '$CHECK' succeeded"
fi
done
else
# Validate all non-skipped check-runs
while IFS= read -r RUN; do
NAME=$(echo "$RUN" | jq -r '.name')
STATUS=$(echo "$RUN" | jq -r '.status')
CONCLUSION=$(echo "$RUN" | jq -r '.conclusion')

# Skip queued/in-progress (treat as not-yet-run, which is a failure)
if [ "$STATUS" != "completed" ]; then
echo "FAIL: check '$NAME' is not completed (status=$STATUS)"
FAILED=1
continue
fi

# Allow skipped checks (neutral conclusion)
if [ "$CONCLUSION" = "skipped" ] || [ "$CONCLUSION" = "neutral" ]; then
echo "SKIP: check '$NAME' was skipped — ignoring"
continue
fi

if [ "$CONCLUSION" != "success" ]; then
echo "FAIL: check '$NAME' has conclusion '$CONCLUSION'"
FAILED=1
fi
done < <(echo "$CHECK_RUNS" | jq -c '.')
fi

if [ "$FAILED" -eq 1 ]; then
echo ""
echo "CI quality gate FAILED for commit $COMMIT_SHA"
echo "Promotion blocked. Fix failing checks before retrying."
exit 1
fi

echo ""
echo "CI quality gate PASSED for commit $COMMIT_SHA"
59 changes: 59 additions & 0 deletions .github/actions/vscode/detect-packages/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
name: 'Detect Packages'
description: 'Dynamically discovers NPM packages and VS Code extensions in a monorepo'

inputs:
packages-root:
description: 'Root directory containing packages (default: packages)'
required: false
default: 'packages'

outputs:
npm-packages:
description: 'Comma-separated list of NPM package names'
value: ${{ steps.packages.outputs.npm-packages }}
extensions:
description: 'Comma-separated list of VS Code extension names'
value: ${{ steps.packages.outputs.extensions }}
extension-paths:
description: 'Extension package paths for publishing'
value: ${{ steps.packages.outputs.extension-paths }}

runs:
using: 'composite'
steps:
- name: Detect packages and extensions
id: packages
shell: bash
env:
PACKAGES_ROOT: ${{ inputs.packages-root }}
run: |
# Get NPM packages (packages with package.json but no publisher)
NPM_PACKAGES=""
EXTENSIONS=""
EXTENSION_PATHS=""

for pkg in $PACKAGES_ROOT/*/; do
PKG_NAME=$(basename "$pkg")
if [ -f "$pkg/package.json" ]; then
if grep -q '"publisher"' "$pkg/package.json"; then
# It's a VS Code extension
EXTENSIONS="$EXTENSIONS,$PKG_NAME"
EXTENSION_PATHS="$EXTENSION_PATHS,$pkg"
else
# It's an NPM package
NPM_PACKAGES="$NPM_PACKAGES,$PKG_NAME"
fi
fi
done

# Remove leading commas
NPM_PACKAGES=${NPM_PACKAGES#,}
EXTENSIONS=${EXTENSIONS#,}
EXTENSION_PATHS=${EXTENSION_PATHS#,}

echo "npm-packages=$NPM_PACKAGES" >> $GITHUB_OUTPUT
echo "extensions=$EXTENSIONS" >> $GITHUB_OUTPUT
echo "extension-paths=$EXTENSION_PATHS" >> $GITHUB_OUTPUT

echo "Detected NPM packages: $NPM_PACKAGES"
echo "Detected VS Code extensions: $EXTENSIONS"
30 changes: 30 additions & 0 deletions .github/actions/vscode/download-vsix-artifacts/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: 'Download VSIX Artifacts'
description: 'Downloads and finds VSIX artifacts for publishing workflows'

inputs:
artifact-name:
description: 'Name for the VSIX artifacts'
required: false
default: 'vsix-packages'
type: string

outputs:
vsix_files:
description: 'JSON array of VSIX file paths'
value: ${{ steps.find_vsix.outputs.vsix_files }}

runs:
using: composite
steps:
- name: Download VSIX artifacts
uses: actions/download-artifact@v4
with:
name: ${{ inputs.artifact-name }}
path: ./vsix-artifacts

- name: Find VSIX files
id: find_vsix
shell: bash
run: |
VSIX_FILES=$(find ./vsix-artifacts -name "*.vsix" | jq -R -s -c 'split("\n")[:-1]')
echo "vsix_files=$VSIX_FILES" >> $GITHUB_OUTPUT
16 changes: 16 additions & 0 deletions .github/actions/vscode/npm-install-with-retries/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: npm-install-with-retries
description: "wraps npm ci with retries/timeout to handle network failures"
inputs:
ignore-scripts:
default: 'false'
description: "Skip pre/post install scripts"
runs:
using: composite
steps:
- name: Set npm fetch timeout
shell: bash
run: npm config set fetch-timeout 600000
- name: npm ci
uses: salesforcecli/github-workflows/.github/actions/retry@main
with:
command: npm ci ${{ inputs.ignore-scripts == 'true' && '--ignore-scripts' || '' }}
147 changes: 147 additions & 0 deletions .github/actions/vscode/publish-vsix/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
name: "Publish VSIX"
description: "Publishes VSIX files to a marketplace with dry-run support"

inputs:
vsix-path:
description: "Path to the VSIX file to publish"
required: true
publish-tool:
description: "Publishing tool to use"
required: true
pre-release:
description: "Publish as pre-release version"
required: false
default: "false"
dry-run:
description: "Run in dry-run mode"
required: false
default: "false"

runs:
using: composite
steps:
- name: Validate inputs
shell: bash
run: |
# Validate VSIX path exists
if [ ! -f "${{ inputs.vsix-path }}" ]; then
echo "❌ Error: VSIX file not found at ${{ inputs.vsix-path }}"
exit 1
fi

# Validate VSIX file extension
if [[ ! "${{ inputs.vsix-path }}" =~ \.vsix$ ]]; then
echo "❌ Error: File must have .vsix extension"
exit 1
fi

# Validate publish tool
if [[ ! "${{ inputs.publish-tool }}" =~ ^(ovsx|vsce)$ ]]; then
echo "❌ Error: Invalid publish tool: ${{ inputs.publish-tool }}"
exit 1
fi

echo "✅ Input validation passed"

- name: Audit publish attempt
shell: bash
run: |
# Create audit log entry
AUDIT_LOG="/tmp/publish_audit.log"
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
ACTOR="${{ github.actor }}"
REPO="${{ github.repository }}"
RUN_ID="${{ github.run_id }}"
WORKFLOW="${{ github.workflow }}"

# Get file info for audit
FILE_SIZE=$(stat -c%s "${{ inputs.vsix-path }}" 2>/dev/null || stat -f%z "${{ inputs.vsix-path }}" 2>/dev/null || echo "unknown")
FILE_HASH=$(sha256sum "${{ inputs.vsix-path }}" 2>/dev/null | cut -d' ' -f1 || echo "unknown")

# Log audit information
echo "[$TIMESTAMP] PUBLISH_ATTEMPT: actor=$ACTOR, repo=$REPO, run_id=$RUN_ID, workflow=$WORKFLOW, tool=${{ inputs.publish-tool }}, file=${{ inputs.vsix-path }}, size=$FILE_SIZE, hash=$FILE_HASH, pre_release=${{ inputs.pre-release }}, dry_run=${{ inputs.dry-run }}" >> "$AUDIT_LOG"

# Also log to GitHub Actions output for visibility
echo "🔍 AUDIT: Publish attempt logged - $TIMESTAMP"
echo " Actor: $ACTOR"
echo " Repository: $REPO"
echo " Run ID: $RUN_ID"
echo " Workflow: $WORKFLOW"
echo " Tool: ${{ inputs.publish-tool }}"
echo " File: ${{ inputs.vsix-path }}"
echo " Size: $FILE_SIZE bytes"
echo " Hash: $FILE_HASH"
echo " Pre-release: ${{ inputs.pre-release }}"
echo " Dry-run: ${{ inputs.dry-run }}"

- name: Publish VSIX
shell: bash
run: |
echo "Publishing ${{ inputs.vsix-path }}"

# Calculate marketplace name based on publish tool
if [ "${{ inputs.publish-tool }}" = "ovsx" ]; then
MARKETPLACE_NAME="Open VSX Registry"
TOKEN_ENV="OVSX_PAT"
else
MARKETPLACE_NAME="Visual Studio Marketplace"
TOKEN_ENV="VSCE_PERSONAL_ACCESS_TOKEN"
fi

PRE_RELEASE_FLAG=""
if [ "${{ inputs.pre-release }}" = "true" ]; then
PRE_RELEASE_FLAG="--pre-release"
echo "Would publish as pre-release version"
fi

# Mask token in logs for security
TOKEN_MASK="***"

if [ "${{ inputs.dry-run }}" = "true" ]; then
echo "🔍 DRY RUN MODE - Would publish to $MARKETPLACE_NAME:"
echo " VSIX: ${{ inputs.vsix-path }}"
echo " Pre-release: ${{ inputs.pre-release }}"

if [ "${{ inputs.publish-tool }}" = "ovsx" ]; then
echo " Command: npx ovsx publish \"${{ inputs.vsix-path }}\" -p $TOKEN_MASK $PRE_RELEASE_FLAG"
else
echo " Command: npx @vscode/vsce publish --packagePath \"${{ inputs.vsix-path }}\" --skip-duplicate $PRE_RELEASE_FLAG"
fi
echo "✅ Dry run completed - no actual publish performed"
else
echo "Publishing VSIX: ${{ inputs.vsix-path }}"

# Verify token is available
if [ -z "${!TOKEN_ENV}" ]; then
echo "❌ Error: $TOKEN_ENV environment variable is not set"
exit 1
fi

if [ "${{ inputs.publish-tool }}" = "vsce" ]; then
export VSCE_PAT="${!TOKEN_ENV}" # ensure the expected env var is set
npx @vscode/vsce publish --packagePath "${{ inputs.vsix-path }}" --skip-duplicate $PRE_RELEASE_FLAG
else
npx ovsx publish "${{ inputs.vsix-path }}" -p "${!TOKEN_ENV}" --skip-duplicate $PRE_RELEASE_FLAG
fi

echo "✅ Successfully published to $MARKETPLACE_NAME"
fi

- name: Audit publish result
shell: bash
if: inputs.dry-run != 'true'
run: |
# Log the result of the publish attempt
AUDIT_LOG="/tmp/publish_audit.log"
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
ACTOR="${{ github.actor }}"
REPO="${{ github.repository }}"
RUN_ID="${{ github.run_id }}"

if [ $? -eq 0 ]; then
echo "[$TIMESTAMP] PUBLISH_SUCCESS: actor=$ACTOR, repo=$REPO, run_id=$RUN_ID, tool=${{ inputs.publish-tool }}, file=${{ inputs.vsix-path }}" >> "$AUDIT_LOG"
echo "✅ AUDIT: Publish successful - $TIMESTAMP"
else
echo "[$TIMESTAMP] PUBLISH_FAILURE: actor=$ACTOR, repo=$REPO, run_id=$RUN_ID, tool=${{ inputs.publish-tool }}, file=${{ inputs.vsix-path }}" >> "$AUDIT_LOG"
echo "❌ AUDIT: Publish failed - $TIMESTAMP"
fi
Loading