Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
36 changes: 36 additions & 0 deletions .github/actions/calculate-artifact-name/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: 'Calculate Artifact Name'
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:
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
115 changes: 115 additions & 0 deletions .github/actions/check-ci-status/action.yml
Original file line number Diff line number Diff line change
@@ -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"
159 changes: 159 additions & 0 deletions .github/actions/publish-vsix/action.yml
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion .github/actions/vscode/publish-vsix/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/vscode-publish-extensions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down