From ff605fa858ac98022cb20ffd0e0eed30dc22ddb0 Mon Sep 17 00:00:00 2001 From: madhur310 Date: Thu, 16 Jul 2026 10:34:18 -0700 Subject: [PATCH 1/3] feat(actions): add shared VS Code extension CI actions Add three reusable composite actions for VS Code extension workflows: - check-ci-status: Validates CI checks passed before promotion - calculate-artifact-name: Generates unique artifact names with run isolation - publish-vsix: Publishes VSIX to VS Code Marketplace and Open VSX These actions are shared across apex-language-support and salesforcedx-vscode to reduce duplication and prevent version drift. Source: salesforcedx-vscode @ ms/prerelease (commit 05aedfed9) --- .../calculate-artifact-name/action.yml | 34 ++++ .github/actions/check-ci-status/action.yml | 115 +++++++++++++ .github/actions/publish-vsix/action.yml | 159 ++++++++++++++++++ 3 files changed, 308 insertions(+) create mode 100644 .github/actions/calculate-artifact-name/action.yml create mode 100644 .github/actions/check-ci-status/action.yml create mode 100644 .github/actions/publish-vsix/action.yml diff --git a/.github/actions/calculate-artifact-name/action.yml b/.github/actions/calculate-artifact-name/action.yml new file mode 100644 index 0000000..e13ed32 --- /dev/null +++ b/.github/actions/calculate-artifact-name/action.yml @@ -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 \ No newline at end of file diff --git a/.github/actions/check-ci-status/action.yml b/.github/actions/check-ci-status/action.yml new file mode 100644 index 0000000..709ba6e --- /dev/null +++ b/.github/actions/check-ci-status/action.yml @@ -0,0 +1,115 @@ +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 + + # Grab the latest run for this name (check-runs are newest-first per name). + MATCH=$(echo "$CHECK_RUNS" | jq -c --arg name "$CHECK" \ + 'select(.name == $name)' | head -1) + + if [ -z "$MATCH" ]; then + echo "FAIL: required check '$CHECK' is absent on this commit (never ran)" + FAILED=1 + continue + fi + + STATUS=$(echo "$MATCH" | jq -r '.status') + CONCLUSION=$(echo "$MATCH" | jq -r '.conclusion') + + if [ "$STATUS" != "completed" ]; then + echo "FAIL: required check '$CHECK' is not completed (status=$STATUS)" + FAILED=1 + elif [ "$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" diff --git a/.github/actions/publish-vsix/action.yml b/.github/actions/publish-vsix/action.yml new file mode 100644 index 0000000..be6c5d1 --- /dev/null +++ b/.github/actions/publish-vsix/action.yml @@ -0,0 +1,159 @@ +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 + id: publish + 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" + echo "result=success" >> $GITHUB_OUTPUT + 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" + echo "result=failure" >> $GITHUB_OUTPUT + 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 + + PUBLISH_EXIT_CODE=$? + if [ $PUBLISH_EXIT_CODE -eq 0 ]; then + echo "✅ Successfully published to $MARKETPLACE_NAME" + echo "result=success" >> $GITHUB_OUTPUT + else + echo "❌ Failed to publish to $MARKETPLACE_NAME" + echo "result=failure" >> $GITHUB_OUTPUT + exit $PUBLISH_EXIT_CODE + fi + fi + + - name: Audit publish result + shell: bash + if: always() && 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 }}" + PUBLISH_RESULT="${{ steps.publish.outputs.result }}" + + if [ "$PUBLISH_RESULT" = "success" ]; 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 From 833681b502025c178dd9a79789296cfd0f46464c Mon Sep 17 00:00:00 2001 From: madhur310 Date: Fri, 17 Jul 2026 12:08:22 -0700 Subject: [PATCH 2/3] fix: skip GitHub auth validation in dry-run mode - Authentication not needed for dry-run since it doesn't push commits/tags - Allows testing in repos without IDEE_GH_TOKEN secret configured - Changed GITHUB_TOKEN to GH_TOKEN for proper GitHub CLI authentication --- .github/workflows/vscode-publish-extensions.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/vscode-publish-extensions.yml b/.github/workflows/vscode-publish-extensions.yml index be81e79..668731e 100644 --- a/.github/workflows/vscode-publish-extensions.yml +++ b/.github/workflows/vscode-publish-extensions.yml @@ -427,12 +427,13 @@ jobs: echo "✅ Version bumping complete" - name: Validate GitHub authentication + if: inputs.dry-run != 'true' && github.event.inputs.dry-run != 'true' env: - GITHUB_TOKEN: ${{ secrets.IDEE_GH_TOKEN }} + GH_TOKEN: ${{ secrets.IDEE_GH_TOKEN }} run: | # Validate that required tokens are present - if [ -z "$GITHUB_TOKEN" ]; then - echo "❌ Error: GITHUB_TOKEN is not set" + if [ -z "$GH_TOKEN" ]; then + echo "❌ Error: GH_TOKEN is not set" exit 1 fi From dbd1e8d0bb7f9d2ddc03155e204d04a201894741 Mon Sep 17 00:00:00 2001 From: madhur310 Date: Fri, 17 Jul 2026 14:27:25 -0700 Subject: [PATCH 3/3] fix: prfb --- .github/actions/calculate-artifact-name/action.yml | 6 ++++-- .github/actions/vscode/publish-vsix/action.yml | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/actions/calculate-artifact-name/action.yml b/.github/actions/calculate-artifact-name/action.yml index e13ed32..906885c 100644 --- a/.github/actions/calculate-artifact-name/action.yml +++ b/.github/actions/calculate-artifact-name/action.yml @@ -1,5 +1,7 @@ name: 'Calculate Artifact Name' -description: 'Calculate artifact name with run number and mode suffix' +description: > + Generates a unique artifact name by appending run number and mode suffix (dry-run/release). + Used to ensure artifact names are unique per workflow run and distinguishable by mode. inputs: artifact-name: @@ -31,4 +33,4 @@ runs: 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 \ No newline at end of file + fi diff --git a/.github/actions/vscode/publish-vsix/action.yml b/.github/actions/vscode/publish-vsix/action.yml index 9ac4c80..b969601 100644 --- a/.github/actions/vscode/publish-vsix/action.yml +++ b/.github/actions/vscode/publish-vsix/action.yml @@ -103,7 +103,7 @@ runs: 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" + echo " Command: npx ovsx publish \"${{ inputs.vsix-path }}\" -p $TOKEN_MASK --skip-duplicate $PRE_RELEASE_FLAG" else echo " Command: npx @vscode/vsce publish --packagePath \"${{ inputs.vsix-path }}\" --skip-duplicate $PRE_RELEASE_FLAG" fi