diff --git a/.github/actions/setupNodeAndInstall/action.yml b/.github/actions/setupNodeAndInstall/action.yml new file mode 100644 index 0000000..184e11a --- /dev/null +++ b/.github/actions/setupNodeAndInstall/action.yml @@ -0,0 +1,76 @@ +name: Setup Node and install dependencies +description: Configures npm, pnpm, or Yarn, restores the matching dependency cache, and installs dependencies. +inputs: + node-version: + description: Node.js version to use. + required: true + package-manager: + description: 'Package manager to use: npm, pnpm, or yarn.' + required: false + default: npm + package-manager-version: + description: 'pnpm version to install when package-manager is pnpm.' + required: false + default: '10' + cache-dependency-path: + description: 'Path to the package manager lockfile.' + required: false + default: package-lock.json + install-command: + description: 'Command used to install repository dependencies.' + required: false + default: npm ci +runs: + using: composite + steps: + - name: Validate package manager + shell: bash + env: + PACKAGE_MANAGER: ${{ inputs.package-manager }} + run: | + if [ "$PACKAGE_MANAGER" != "npm" ] && [ "$PACKAGE_MANAGER" != "pnpm" ] && [ "$PACKAGE_MANAGER" != "yarn" ]; then + echo "Unsupported package manager: $PACKAGE_MANAGER. Expected npm, pnpm, or yarn." + exit 1 + fi + + - name: Setup pnpm + if: inputs.package-manager == 'pnpm' + uses: pnpm/action-setup@v4 + with: + version: ${{ inputs.package-manager-version }} + + - name: Setup Node.js and restore dependency cache + uses: actions/setup-node@v6 + with: + node-version: ${{ inputs.node-version }} + cache: ${{ inputs.package-manager }} + cache-dependency-path: ${{ inputs.cache-dependency-path }} + + - name: Resolve install command + id: install-command + shell: bash + env: + INSTALL_COMMAND: ${{ inputs.install-command }} + PACKAGE_MANAGER: ${{ inputs.package-manager }} + run: | + if [ "$INSTALL_COMMAND" = "npm ci" ]; then + case "$PACKAGE_MANAGER" in + pnpm) INSTALL_COMMAND="pnpm install --frozen-lockfile" ;; + yarn) INSTALL_COMMAND="yarn install --network-timeout 600000" ;; + esac + fi + echo "value=$INSTALL_COMMAND" >> "$GITHUB_OUTPUT" + + - name: Install npm dependencies + if: inputs.package-manager == 'npm' && steps.install-command.outputs.value == 'npm ci' + uses: salesforcecli/github-workflows/.github/actions/npmInstallWithRetries@main + + - name: Install Yarn dependencies + if: inputs.package-manager == 'yarn' && steps.install-command.outputs.value == 'yarn install --network-timeout 600000' + uses: salesforcecli/github-workflows/.github/actions/yarnInstallWithRetries@main + + - name: Install custom or pnpm dependencies + if: (inputs.package-manager != 'npm' || steps.install-command.outputs.value != 'npm ci') && (inputs.package-manager != 'yarn' || steps.install-command.outputs.value != 'yarn install --network-timeout 600000') + uses: salesforcecli/github-workflows/.github/actions/retry@main + with: + command: ${{ steps.install-command.outputs.value }} diff --git a/.github/workflows/vscode-ci-template.yml b/.github/workflows/vscode-ci-template.yml index 9905942..e057d8d 100644 --- a/.github/workflows/vscode-ci-template.yml +++ b/.github/workflows/vscode-ci-template.yml @@ -7,10 +7,12 @@ name: CI # ci: # uses: salesforcecli/github-workflows/.github/workflows/vscode/ci-template.yml@main # with: -# lint-command: 'npm run lint' -# compile-command: 'npm run compile' -# test-command: 'npm run test' -# test-coverage-command: 'npm run test:coverage' +# package-manager: npm +# install-command: npm ci +# lint-command: npm run lint +# build-command: npm run build +# test-command: npm run test +# coverage-command: npm run test:coverage # # Features: # - Tests across multiple OS (Ubuntu, Windows) @@ -22,37 +24,167 @@ name: CI on: workflow_call: inputs: + package-manager: + description: "Package manager to use: npm, pnpm, or yarn" + required: false + default: "npm" + type: string + package-manager-version: + description: "pnpm version to use when package-manager is pnpm" + required: false + default: "10" + type: string + cache-dependency-path: + description: "Path to the package manager lockfile" + required: false + default: "package-lock.json" + type: string + install-command: + description: "Command to install dependencies" + required: false + default: "npm ci" + type: string lint-command: - description: 'Command to run linting' + description: "Command to run linting" required: false - default: 'npm run lint' + default: "npm run lint" type: string compile-command: - description: 'Command to compile' + description: "Deprecated compatibility command used when build-command is empty" required: false - default: 'npm run compile' + default: "" + type: string + build-command: + description: "Command to build or compile the project; leave empty to use compile-command" + required: false + default: "npm run compile" type: string test-command: - description: 'Command to run tests (without coverage)' + description: "Command to run tests (without coverage)" required: false - default: 'npm run test' + default: "npm run test" type: string test-coverage-command: - description: 'Command to run tests with coverage' + description: "Deprecated compatibility command used when coverage-command is empty" + required: false + default: "" + type: string + coverage-command: + description: "Command to run tests with coverage; leave empty to use test-coverage-command" required: false - default: 'npm run test:coverage' + default: "npm run test:coverage" type: string coverage-report-command: - description: 'Command to merge coverage reports' + description: "Command to merge coverage reports; set empty to skip" + required: false + default: "npm run test:coverage:report" + type: string + quality-command: + description: "Command to run additional quality checks; set empty to skip" + required: false + default: "npm run test:quality" + type: string + extensions-root: + description: "Root directory containing VS Code extensions" required: false - default: 'npm run test:coverage:report' + default: "packages" + type: string + package-command: + description: "Command to create stable VSIX artifacts" + required: false + default: "npm run package:packages" + type: string + prerelease-package-command: + description: "Command to create prerelease VSIX artifacts" + required: false + default: "npm run package:packages:prerelease" + type: string + artifact-glob: + description: "Glob for VSIX artifacts relative to the repository root" + required: false + default: "packages/**/*.vsix" type: string workflow_dispatch: inputs: + package-manager: + description: "Package manager to use: npm, pnpm, or yarn" + required: false + default: "npm" + type: string + package-manager-version: + description: "pnpm version to use when package-manager is pnpm" + required: false + default: "10" + type: string + cache-dependency-path: + description: "Path to the package manager lockfile" + required: false + default: "package-lock.json" + type: string + install-command: + description: "Command to install dependencies" + required: false + default: "npm ci" + type: string lint-command: - description: 'Command to run linting' + description: "Command to run linting" + required: false + default: "npm run lint" + type: string + compile-command: + description: "Deprecated compatibility command used when build-command is empty" required: false - default: 'npm run lint' + default: "" + type: string + build-command: + description: "Command to build or compile the project; leave empty to use compile-command" + required: false + default: "npm run compile" + type: string + test-command: + description: "Command to run tests without coverage" + required: false + default: "npm run test" + type: string + test-coverage-command: + description: "Deprecated compatibility command used when coverage-command is empty" + required: false + default: "" + type: string + coverage-command: + description: "Command to run tests with coverage; leave empty to use test-coverage-command" + required: false + default: "npm run test:coverage" + type: string + coverage-report-command: + description: "Command to merge coverage reports; set empty to skip" + required: false + default: "npm run test:coverage:report" + type: string + quality-command: + description: "Command to run additional quality checks; set empty to skip" + required: false + default: "npm run test:quality" + type: string + extensions-root: + description: "Root directory containing VS Code extensions" + required: false + default: "packages" + type: string + package-command: + description: "Command to create stable VSIX artifacts" + required: false + default: "npm run package:packages" + type: string + prerelease-package-command: + description: "Command to create prerelease VSIX artifacts" + required: false + default: "npm run package:packages:prerelease" + type: string + artifact-glob: + description: "Glob for VSIX artifacts relative to the repository root" + required: false + default: "packages/**/*.vsix" type: string # Add explicit permissions for security @@ -67,7 +199,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest] - node-version: ['lts/-1', 'lts/*', 'current'] + node-version: ["lts/-1", "lts/*", "current"] fail-fast: false runs-on: ${{ matrix.os }} @@ -78,24 +210,35 @@ jobs: persist-credentials: false submodules: false - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v6 + - name: Setup Node.js and install dependencies + uses: salesforcecli/github-workflows/.github/actions/setupNodeAndInstall@main with: node-version: ${{ matrix.node-version }} - cache: 'npm' - - - name: Install dependencies - uses: salesforcecli/github-workflows/.github/actions/npmInstallWithRetries@main + package-manager: ${{ inputs.package-manager }} + package-manager-version: ${{ inputs.package-manager-version }} + cache-dependency-path: ${{ inputs.cache-dependency-path }} + install-command: ${{ inputs.install-command }} - name: Run linting - run: ${{ inputs.lint-command }} + env: + LINT_COMMAND: ${{ inputs.lint-command }} + run: bash -c "$LINT_COMMAND" - - name: Compile project - run: ${{ inputs.compile-command }} + - name: Build project + env: + BUILD_COMMAND: ${{ inputs.build-command || inputs.compile-command }} + run: | + if [ -z "$BUILD_COMMAND" ]; then + echo "Error: build-command and compile-command cannot both be empty" + exit 1 + fi + bash -c "$BUILD_COMMAND" - name: Run tests with coverage (lts/current) - if: ${{ matrix.node-version != 'lts/-1' }} - run: ${{ inputs.test-coverage-command }} + if: ${{ matrix.node-version != 'lts/-1' && (inputs.coverage-command || inputs.test-coverage-command) != '' }} + env: + COVERAGE_COMMAND: ${{ inputs.coverage-command || inputs.test-coverage-command }} + run: bash -c "$COVERAGE_COMMAND" - name: Run tests (lts/-1, no coverage) if: ${{ matrix.node-version == 'lts/-1' }} @@ -103,11 +246,14 @@ jobs: # Old-LTS defaults to ~4 GB old-space, which is too low for heavy stdlib suites. # Keep this scoped to lts/-1 and non-coverage runs only. NODE_OPTIONS: --max-old-space-size=6144 - run: ${{ inputs.test-command }} + TEST_COMMAND: ${{ inputs.test-command }} + run: bash -c "$TEST_COMMAND" - name: Merge coverage reports - if: ${{ matrix.node-version != 'lts/-1' }} - run: ${{ inputs.coverage-report-command }} + if: ${{ matrix.node-version != 'lts/-1' && inputs.coverage-report-command != '' }} + env: + COVERAGE_REPORT_COMMAND: ${{ inputs.coverage-report-command }} + run: bash -c "$COVERAGE_REPORT_COMMAND" - name: Determine Node Label id: node-label @@ -126,7 +272,7 @@ jobs: fi - name: Upload coverage report - if: ${{ matrix.node-version != 'lts/-1' }} + if: ${{ matrix.node-version != 'lts/-1' && (inputs.coverage-command || inputs.test-coverage-command) != '' }} uses: actions/upload-artifact@v7 with: name: coverage-report-${{ matrix.os }}-${{ steps.node-label.outputs.value }} @@ -135,10 +281,11 @@ jobs: test-quality: name: Test Quality needs: test + if: inputs.quality-command != '' strategy: matrix: os: [ubuntu-latest] - node-version: ['lts/*'] + node-version: ["lts/*"] fail-fast: false runs-on: ${{ matrix.os }} @@ -149,17 +296,19 @@ jobs: persist-credentials: false submodules: false - - name: Setup Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v6 + - name: Setup Node.js and install dependencies + uses: salesforcecli/github-workflows/.github/actions/setupNodeAndInstall@main with: node-version: ${{ matrix.node-version }} - cache: 'npm' - - - name: Install dependencies - uses: salesforcecli/github-workflows/.github/actions/npmInstallWithRetries@main + package-manager: ${{ inputs.package-manager }} + package-manager-version: ${{ inputs.package-manager-version }} + cache-dependency-path: ${{ inputs.cache-dependency-path }} + install-command: ${{ inputs.install-command }} - name: Run quality tests - run: npm run test:quality + env: + QUALITY_COMMAND: ${{ inputs.quality-command }} + run: bash -c "$QUALITY_COMMAND" package: name: Package @@ -170,6 +319,14 @@ jobs: branch: ${{ github.head_ref || github.ref_name }} artifact-name: vsix-packages dry-run: false + extensions-root: ${{ inputs.extensions-root }} + package-manager: ${{ inputs.package-manager }} + package-manager-version: ${{ inputs.package-manager-version }} + cache-dependency-path: ${{ inputs.cache-dependency-path }} + install-command: ${{ inputs.install-command }} + package-command: ${{ inputs.package-command }} + prerelease-package-command: ${{ inputs.prerelease-package-command }} + artifact-glob: ${{ inputs.artifact-glob }} ci-complete: name: CI Complete diff --git a/.github/workflows/vscode-package.yml b/.github/workflows/vscode-package.yml index b2adf57..cbca4f7 100644 --- a/.github/workflows/vscode-package.yml +++ b/.github/workflows/vscode-package.yml @@ -14,60 +14,155 @@ on: workflow_call: inputs: node-version: - description: 'Node.js version to use' + description: "Node.js version to use" required: false - default: '22.x' + default: "22.x" type: string branch: - description: 'Branch to package from' + description: "Branch to package from" required: false - default: 'main' + default: "main" type: string artifact-name: - description: 'Name for the VSIX artifacts (base name or pre-calculated: vsix-packages-{run_number}-{mode})' + description: "Name for the VSIX artifacts (base name or pre-calculated: vsix-packages-{run_number}-{mode})" required: false - default: 'vsix-packages' + default: "vsix-packages" type: string dry-run: - description: 'Run in dry-run mode' + description: "Run in dry-run mode" required: false - default: 'false' + default: "false" type: string pre-release: - description: 'Indicates if this is a pre-release version' + description: "Indicates if this is a pre-release version" required: false - default: 'false' + default: "false" type: string extensions-root: - description: 'Root directory for extensions (default: packages)' + description: "Root directory for extensions (default: packages)" required: false - default: 'packages' + default: "packages" + type: string + package-manager: + description: "Package manager to use: npm, pnpm, or yarn" + required: false + default: "npm" + type: string + package-manager-version: + description: "pnpm version to use when package-manager is pnpm" + required: false + default: "10" + type: string + cache-dependency-path: + description: "Path to the package manager lockfile" + required: false + default: "package-lock.json" + type: string + install-command: + description: "Command used to install dependencies" + required: false + default: "npm ci" + type: string + package-command: + description: "Command used to create VSIX artifacts" + required: false + default: "npm run package:packages" + type: string + prerelease-package-command: + description: "Command used to create prerelease VSIX artifacts" + required: false + default: "npm run package:packages:prerelease" + type: string + web-package-command: + description: "Optional command used to create a web-target VSIX artifact" + required: false + default: "" + type: string + web-prerelease-package-command: + description: "Optional command used to create a prerelease web-target VSIX artifact" + required: false + default: "" + type: string + artifact-glob: + description: "Glob for VSIX artifacts relative to the repository root" + required: false + default: "packages/**/*.vsix" type: string outputs: artifact-name: - description: 'The calculated artifact name' + description: "The calculated artifact name" value: ${{ jobs.package.outputs.artifact-name }} workflow_dispatch: inputs: node-version: - description: 'Node.js version to use' + description: "Node.js version to use" required: false - default: '22.x' + default: "22.x" type: string branch: - description: 'Branch to package from' + description: "Branch to package from" required: false - default: 'main' + default: "main" type: string dry-run: - description: 'Run in dry-run mode' + description: "Run in dry-run mode" required: false - default: 'false' + default: "false" type: string pre-release: - description: 'Indicates if this is a pre-release version' + description: "Indicates if this is a pre-release version" + required: false + default: "false" + type: string + extensions-root: + description: "Root directory for extensions (default: packages)" + required: false + default: "packages" + type: string + package-manager: + description: "Package manager to use: npm, pnpm, or yarn" required: false - default: 'false' + default: "npm" + type: string + package-manager-version: + description: "pnpm version to use when package-manager is pnpm" + required: false + default: "10" + type: string + cache-dependency-path: + description: "Path to the package manager lockfile" + required: false + default: "package-lock.json" + type: string + install-command: + description: "Command used to install dependencies" + required: false + default: "npm ci" + type: string + package-command: + description: "Command used to create VSIX artifacts" + required: false + default: "npm run package:packages" + type: string + prerelease-package-command: + description: "Command used to create prerelease VSIX artifacts" + required: false + default: "npm run package:packages:prerelease" + type: string + web-package-command: + description: "Optional command used to create a web-target VSIX artifact" + required: false + default: "" + type: string + web-prerelease-package-command: + description: "Optional command used to create a prerelease web-target VSIX artifact" + required: false + default: "" + type: string + artifact-glob: + description: "Glob for VSIX artifacts relative to the repository root" + required: false + default: "packages/**/*.vsix" type: string # Add explicit permissions for security @@ -87,23 +182,34 @@ jobs: with: ref: ${{ inputs.branch || github.head_ref || github.ref }} - - name: Setup Node.js ${{ inputs.node-version || '22.x' }} - uses: actions/setup-node@v6 + - name: Setup Node.js and install dependencies + uses: salesforcecli/github-workflows/.github/actions/setupNodeAndInstall@main with: node-version: ${{ inputs.node-version || '22.x' }} - - - name: Install dependencies - uses: salesforcecli/github-workflows/.github/actions/npmInstallWithRetries@main + package-manager: ${{ inputs.package-manager }} + package-manager-version: ${{ inputs.package-manager-version }} + cache-dependency-path: ${{ inputs.cache-dependency-path }} + install-command: ${{ inputs.install-command }} # Universal + web-target VSIXs are defined in packages/apex-lsp-vscode-extension (Wireit: package + package-web). - - name: Package packages + - name: Package VSIX artifacts env: EXTENSIONS_ROOT: ${{ inputs.extensions-root || 'packages' }} + PACKAGE_COMMAND: ${{ inputs.package-command }} + PRERELEASE_PACKAGE_COMMAND: ${{ inputs.prerelease-package-command }} + WEB_PACKAGE_COMMAND: ${{ inputs.web-package-command }} + WEB_PRERELEASE_PACKAGE_COMMAND: ${{ inputs.web-prerelease-package-command }} run: | if [ "${{ inputs.pre-release }}" = "true" ]; then - npm run package:packages:prerelease + bash -c "$PRERELEASE_PACKAGE_COMMAND" + if [ -n "$WEB_PRERELEASE_PACKAGE_COMMAND" ]; then + bash -c "$WEB_PRERELEASE_PACKAGE_COMMAND" + fi else - npm run package:packages + bash -c "$PACKAGE_COMMAND" + if [ -n "$WEB_PACKAGE_COMMAND" ]; then + bash -c "$WEB_PACKAGE_COMMAND" + fi fi - name: Generate MD5 checksums @@ -115,20 +221,20 @@ jobs: # Universal + web-target VSIX under packages/ VSIX_FILES=$(find "$EXTENSIONS_ROOT" -name "*.vsix" -type f) - + if [ -z "$VSIX_FILES" ]; then echo "No VSIX files found to generate checksums for" echo "checksums_generated=false" >> $GITHUB_OUTPUT exit 0 fi - + # Create checksums directory structure CHECKSUMS_FILE="checksums.md5" CHECKSUMS_JSON_FILE="checksums.json" > "$CHECKSUMS_FILE" # Create/clear checksums file > "$CHECKSUMS_JSON_FILE" # Create/clear JSON file echo "[" > "$CHECKSUMS_JSON_FILE" - + FIRST=true # Generate MD5 checksums for each VSIX file while IFS= read -r vsix_file; do @@ -162,12 +268,14 @@ jobs: echo " Size: $FILE_SIZE bytes" fi done <<< "$VSIX_FILES" - + echo "]" >> "$CHECKSUMS_JSON_FILE" - # Move combined checksums files to packages root for artifact upload - mv "$CHECKSUMS_FILE" "$EXTENSIONS_ROOT/checksums.md5" - mv "$CHECKSUMS_JSON_FILE" "$EXTENSIONS_ROOT/checksums.json" + # The repository root already contains the generated checksum files. + if [ "$EXTENSIONS_ROOT" != "." ]; then + mv "$CHECKSUMS_FILE" "$EXTENSIONS_ROOT/checksums.md5" + mv "$CHECKSUMS_JSON_FILE" "$EXTENSIONS_ROOT/checksums.json" + fi echo "checksums_generated=true" >> $GITHUB_OUTPUT echo "checksums_file=$EXTENSIONS_ROOT/checksums.json" >> $GITHUB_OUTPUT @@ -212,11 +320,11 @@ jobs: - name: Calculate artifact name id: calc-artifact-name + env: + BASE_NAME: ${{ inputs.artifact-name }} + RUN_NUMBER: ${{ github.run_number }} + IS_DRY_RUN: ${{ inputs.dry-run }} run: | - BASE_NAME="${{ inputs.artifact-name }}" - RUN_NUMBER="${{ github.run_number }}" - IS_DRY_RUN="${{ inputs.dry-run }}" - # Check if already suffixed if [[ "$BASE_NAME" =~ -dry-run$ ]] || [[ "$BASE_NAME" =~ -release$ ]]; then ARTIFACT_NAME="$BASE_NAME" @@ -231,15 +339,25 @@ jobs: echo "artifact-name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT echo "Artifact name: $ARTIFACT_NAME" + - name: Validate VSIX artifact glob + env: + ARTIFACT_GLOB: ${{ inputs.artifact-glob }} + run: | + shopt -s globstar nullglob + artifacts=( $ARTIFACT_GLOB ) + if [ ${#artifacts[@]} -eq 0 ]; then + echo "No VSIX artifacts matched artifact-glob: $ARTIFACT_GLOB" + echo "Set artifact-glob to match extensions-root when using a non-default extension directory." + exit 1 + fi + - name: Upload VSIX artifacts id: upload uses: actions/upload-artifact@v7 - env: - EXTENSIONS_ROOT: ${{ inputs.extensions-root || 'packages' }} with: name: ${{ steps.calc-artifact-name.outputs.artifact-name }} path: | - ${{ inputs.extensions-root || 'packages' }}/**/*.vsix + ${{ inputs.artifact-glob }} ${{ inputs.extensions-root || 'packages' }}/**/*.vsix.md5 ${{ inputs.extensions-root || 'packages' }}/checksums.md5 ${{ inputs.extensions-root || 'packages' }}/checksums.json @@ -261,10 +379,10 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "| Extension | MD5 Checksum | Size |" >> $GITHUB_STEP_SUMMARY echo "|-----------|-------------|------|" >> $GITHUB_STEP_SUMMARY - + # Read checksums from JSON file and format table CHECKSUMS_FILE="${{ steps.md5-checksums.outputs.checksums_file }}" - + if [ -f "$CHECKSUMS_FILE" ]; then # Use node to parse JSON and format table node -e " @@ -281,7 +399,7 @@ jobs: else echo "| No checksums available | - | - |" >> $GITHUB_STEP_SUMMARY fi - + echo "" >> $GITHUB_STEP_SUMMARY echo "**Note:** Individual \`.md5\` files are available alongside each VSIX file in the artifacts." >> $GITHUB_STEP_SUMMARY echo "A combined \`checksums.md5\` file and \`checksums.json\` file are also included in the artifacts." >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/vscode-publish-extensions.yml b/.github/workflows/vscode-publish-extensions.yml index babcf88..f5e24fa 100644 --- a/.github/workflows/vscode-publish-extensions.yml +++ b/.github/workflows/vscode-publish-extensions.yml @@ -49,70 +49,141 @@ on: workflow_call: inputs: branch: - description: 'Branch to release from' + description: "Branch to release from" required: false - default: 'main' + default: "main" type: string extensions: - description: 'Extensions to release (all, changed, or comma-separated extension names)' + description: "Extensions to release (all, changed, or comma-separated extension names)" required: false - default: 'changed' + default: "changed" type: string registries: - description: 'Registries to publish to (all, vsce, ovsx)' + description: "Registries to publish to (all, vsce, ovsx)" required: false - default: 'all' + default: "all" type: string available-extensions: - description: 'Available VS Code extensions' + description: "Available VS Code extensions" required: false type: string dry-run: - description: 'Run in dry-run mode (no actual publishing)' + description: "Run in dry-run mode (no actual publishing)" required: false - default: 'false' + default: "false" type: string pre-release: - description: 'Publish as pre-release version' + description: "Publish as pre-release version" required: false - default: 'true' + default: "true" type: string + nightly: + description: "Use nightly-release versioning and skip public marketplace publishing" + required: false + default: true + type: boolean version-bump: - description: 'Version bump type (auto, patch, minor, major)' + description: "Version bump type (auto, patch, minor, major)" required: false - default: 'auto' + default: "auto" type: string extensions-root: - description: 'Root directory for extensions (default: packages)' + description: "Root directory for extensions (default: packages)" required: false - default: 'packages' + default: "packages" type: string exclude-web-vsix: - description: 'Exclude *-web-* VSIX files from publishing and releases' + description: "Exclude *-web-* VSIX files from publishing and releases" required: false - default: 'false' + default: "false" type: string slack-notification-title: - description: 'Title for Slack success notifications' + description: "Title for Slack success notifications" required: false - default: '🎉 Extensions Released Successfully!' + default: "🎉 Extensions Released Successfully!" type: string node-version: - description: 'Node.js version to use' + description: "Node.js version to use" required: false - default: '22.x' + default: "22.x" type: string git-user-name: - description: 'Git user name for version bump commits' + description: "Git user name for version bump commits" required: false - default: 'GitHub Action' + default: "GitHub Action" type: string git-user-email: - description: 'Git user email for version bump commits' + description: "Git user email for version bump commits" + required: false + default: "action@github.com" + type: string + package-manager: + description: "Package manager to use: npm, pnpm, or yarn" + required: false + default: "npm" + type: string + package-manager-version: + description: "pnpm version to use when package-manager is pnpm" + required: false + default: "10" + type: string + cache-dependency-path: + description: "Path to the package manager lockfile" + required: false + default: "package-lock.json" + type: string + install-command: + description: "Command used to install dependencies" + required: false + default: "npm ci" + type: string + package-command: + description: "Command used to create VSIX artifacts" required: false - default: 'action@github.com' + default: "npm run package:packages" type: string + prerelease-package-command: + description: "Command used to create prerelease VSIX artifacts" + required: false + default: "npm run package:packages:prerelease" + type: string + web-package-command: + description: "Optional command used to create a web-target VSIX artifact" + required: false + default: "" + type: string + web-prerelease-package-command: + description: "Optional command used to create a prerelease web-target VSIX artifact" + required: false + default: "" + type: string + artifact-glob: + description: "Glob for VSIX artifacts relative to the repository root" + required: false + default: "packages/**/*.vsix" + type: string + publish-web-vsix: + description: "Require and publish a web-target VSIX to the CBWeb internal marketplace" + required: false + default: false + type: boolean + secrets: + IDEE_GH_TOKEN: + description: "GitHub token used for release commits and tags" + required: false + VSCE_PERSONAL_ACCESS_TOKEN: + description: "VS Code Marketplace publishing token" + required: false + IDEE_OVSX_PAT: + description: "Open VSX publishing token" + required: false + MARKETPLACE_DEPLOY_TOKEN: + description: "CBWeb internal marketplace publishing token" + required: false + IDEE_MAIN_SLACK_WEBHOOK: + description: "Slack notification webhook" + required: false # Add explicit permissions for security permissions: @@ -131,22 +202,23 @@ jobs: with: ref: ${{ inputs.branch || github.ref }} - - name: Setup Node.js - uses: actions/setup-node@v6 + - name: Setup Node.js and install dependencies + uses: salesforcecli/github-workflows/.github/actions/setupNodeAndInstall@main with: node-version: ${{ inputs.node-version || '22.x' }} - - - name: Install dependencies - uses: salesforcecli/github-workflows/.github/actions/npmInstallWithRetries@main + package-manager: ${{ inputs.package-manager }} + package-manager-version: ${{ inputs.package-manager-version }} + cache-dependency-path: ${{ inputs.cache-dependency-path }} + install-command: ${{ inputs.install-command }} - name: Display Extension Release Plan env: BRANCH: ${{ inputs.branch || github.ref_name }} BUILD_TYPE: ${{ github.event_name }} - IS_NIGHTLY: 'true' + IS_NIGHTLY: ${{ inputs.nightly && 'true' || 'false' }} VERSION_BUMP: ${{ needs.determine-changes.outputs.version-bumps }} REGISTRIES: ${{ inputs.registries }} - PRE_RELEASE: 'true' + PRE_RELEASE: ${{ inputs.pre-release || github.event.inputs.pre-release || 'false' }} SELECTED_EXTENSIONS: ${{ needs.determine-changes.outputs.selected-extensions }} EXTENSIONS_ROOT: ${{ inputs.extensions-root || 'packages' }} run: | @@ -209,7 +281,7 @@ jobs: - name: Checkout uses: actions/checkout@v6 with: - fetch-depth: 0 # Need full history for git diff against tags + fetch-depth: 0 # Need full history for git diff against tags - name: Setup Node.js uses: actions/setup-node@v6 @@ -219,10 +291,10 @@ jobs: - name: Detect changes and version bumps id: changes env: - IS_NIGHTLY: 'true' + IS_NIGHTLY: ${{ inputs.nightly && 'true' || 'false' }} VERSION_BUMP: ${{ inputs.version-bump }} PRE_RELEASE: ${{ inputs.pre-release || 'true' }} - IS_PROMOTION: 'false' + IS_PROMOTION: "false" SELECTED_EXTENSIONS: ${{ inputs.extensions }} EXTENSIONS_ROOT: ${{ inputs.extensions-root || 'packages' }} run: | @@ -236,7 +308,7 @@ jobs: curl -sS "$SCRIPTS_URL/utils.ts" -o .github/scripts/utils.ts curl -sS "$SCRIPTS_URL/index.ts" -o .github/scripts/index.ts - echo "Installing dependencies..." + echo "Installing change detection dependencies..." npm install --no-save simple-git@3.27.0 chalk@5.3.0 semver@7.6.3 zod@3.24.1 tsx@4.19.2 echo "Running change detection..." @@ -250,29 +322,42 @@ jobs: - name: Checkout uses: actions/checkout@v6 with: - token: ${{ secrets.IDEE_GH_TOKEN }} + token: ${{ secrets.IDEE_GH_TOKEN || github.token }} fetch-depth: 0 - - name: Setup Node.js - uses: actions/setup-node@v6 + - name: Setup Node.js and install dependencies + uses: salesforcecli/github-workflows/.github/actions/setupNodeAndInstall@main with: node-version: ${{ inputs.node-version || '22.x' }} - - - name: Install dependencies - uses: salesforcecli/github-workflows/.github/actions/npmInstallWithRetries@main + package-manager: ${{ inputs.package-manager }} + package-manager-version: ${{ inputs.package-manager-version }} + cache-dependency-path: ${{ inputs.cache-dependency-path }} + install-command: ${{ inputs.install-command }} - name: Bump versions and tag for selected extensions env: VERSION_BUMP: ${{ needs.determine-changes.outputs.version-bumps }} + PACKAGE_MANAGER: ${{ inputs.package-manager }} + DRY_RUN: ${{ inputs.dry-run || github.event.inputs.dry-run || 'false' }} SELECTED_EXTENSIONS: ${{ needs.determine-changes.outputs.selected-extensions }} PRE_RELEASE: ${{ inputs.pre-release || github.event.inputs.pre-release || 'false' }} - IS_NIGHTLY: 'true' - IS_PROMOTION: 'false' + IS_NIGHTLY: ${{ inputs.nightly && 'true' || 'false' }} + IS_PROMOTION: "false" BRANCH: ${{ inputs.branch || github.ref_name }} EXTENSIONS_ROOT: ${{ inputs.extensions-root || 'packages' }} run: | set -e + case "$PACKAGE_MANAGER" in + pnpm) unexpected_lockfiles=(package-lock.json yarn.lock) ;; + yarn) unexpected_lockfiles=(package-lock.json pnpm-lock.yaml) ;; + npm) unexpected_lockfiles=(pnpm-lock.yaml yarn.lock) ;; + esac + declare -A unexpected_lockfile_hashes + for lockfile in "${unexpected_lockfiles[@]}"; do + unexpected_lockfile_hashes["$lockfile"]=$(git hash-object "$lockfile" 2>/dev/null || echo missing) + done + # Function to parse semantic version parse_version() { local version=$1 @@ -366,6 +451,11 @@ jobs: local is_nightly=$4 local tag_name + if [ "$DRY_RUN" = "true" ]; then + echo "DRY RUN: Would create tag for $package_name $version" + return 0 + fi + if [ "$is_nightly" = "true" ]; then # Nightly format: v{version}-nightly[.branch].{date} local nightly_date=$(date -u +%Y%m%d) @@ -434,6 +524,22 @@ jobs: echo "" done + if [ "$PACKAGE_MANAGER" = "pnpm" ]; then + pnpm install --lockfile-only --ignore-scripts + elif [ "$PACKAGE_MANAGER" = "yarn" ]; then + yarn install --ignore-scripts + else + npm install --package-lock-only --ignore-scripts + fi + + for lockfile in "${unexpected_lockfiles[@]}"; do + current_hash=$(git hash-object "$lockfile" 2>/dev/null || echo missing) + if [ "${unexpected_lockfile_hashes[$lockfile]}" != "$current_hash" ]; then + echo "$PACKAGE_MANAGER version bumps must not create or modify $lockfile" + exit 1 + fi + done + echo "✅ Version bumping complete" - name: Validate GitHub authentication @@ -462,6 +568,9 @@ jobs: # Ensure GitHub CLI has proper authentication GITHUB_TOKEN: ${{ secrets.IDEE_GH_TOKEN }} DRY_RUN: ${{ inputs.dry-run || github.event.inputs.dry-run || 'false' }} + GIT_USER_NAME: ${{ inputs.git-user-name }} + GIT_USER_EMAIL: ${{ inputs.git-user-email }} + BRANCH: ${{ inputs.branch || github.ref_name }} run: | if [ "$DRY_RUN" = "true" ]; then echo "🔄 DRY RUN: Would commit and push version bumps..." @@ -474,9 +583,6 @@ jobs: echo "🔄 Committing version bumps..." # Validate git identity inputs (prevent empty strings from overriding defaults) - GIT_USER_NAME="${{ inputs.git-user-name }}" - GIT_USER_EMAIL="${{ inputs.git-user-email }}" - if [ -z "$GIT_USER_NAME" ]; then echo "âš ī¸ git-user-name is empty, using default: GitHub Action" GIT_USER_NAME="GitHub Action" @@ -504,8 +610,8 @@ jobs: if git diff --staged --quiet; then # Nothing to stage — check whether the bump was already committed to remote # (idempotent rerun: version bumper ran, committed, pushed, then a later step failed) - git fetch origin ${{ inputs.branch || github.ref_name }} - REMOTE_MSG=$(git log -1 --format='%s' origin/${{ inputs.branch || github.ref_name }}) + git fetch origin "$BRANCH" + REMOTE_MSG=$(git log -1 --format='%s' "origin/$BRANCH") if echo "$REMOTE_MSG" | grep -q "chore: bump versions for release"; then echo "â­ī¸ Version bump already committed to remote — skipping commit (idempotent rerun)" else @@ -517,12 +623,12 @@ jobs: git commit -m "chore: bump versions for release [skip ci]" # Push version bumps — retry once with rebase on non-fast-forward - echo "Pushing version bumps to ${{ inputs.branch || github.ref_name }}..." - if ! git push origin HEAD:${{ inputs.branch || github.ref_name }}; then + echo "Pushing version bumps to $BRANCH..." + if ! git push origin "HEAD:$BRANCH"; then echo "âš ī¸ Push failed, attempting fetch+rebase and retry..." git fetch origin - git rebase origin/${{ inputs.branch || github.ref_name }} - if ! git push origin HEAD:${{ inputs.branch || github.ref_name }}; then + git rebase "origin/$BRANCH" + if ! git push origin "HEAD:$BRANCH"; then echo "❌ Error: Push failed after rebase. Check branch protection rules." exit 1 fi @@ -545,10 +651,11 @@ jobs: steps: - name: Calculate artifact name id: calc + env: + RUN_NUMBER: ${{ github.run_number }} + IS_DRY_RUN: ${{ inputs.dry-run || github.event.inputs.dry-run || 'false' }} run: | BASE_NAME="vsix-packages" - RUN_NUMBER="${{ github.run_number }}" - IS_DRY_RUN="${{ inputs.dry-run || github.event.inputs.dry-run || 'false' }}" # Check if already suffixed if [[ "$BASE_NAME" =~ -dry-run$ ]] || [[ "$BASE_NAME" =~ -release$ ]]; then @@ -568,11 +675,21 @@ jobs: needs: [bump-versions, calculate-artifact-name] uses: salesforcecli/github-workflows/.github/workflows/vscode-package.yml@main with: + node-version: ${{ inputs.node-version }} branch: ${{ inputs.branch || github.ref_name }} artifact-name: ${{ needs.calculate-artifact-name.outputs.artifact-name }} dry-run: ${{ inputs.dry-run || github.event.inputs.dry-run || 'false' }} pre-release: ${{ inputs.pre-release || github.event.inputs.pre-release || 'false' }} extensions-root: ${{ inputs.extensions-root || 'packages' }} + package-manager: ${{ inputs.package-manager }} + package-manager-version: ${{ inputs.package-manager-version }} + cache-dependency-path: ${{ inputs.cache-dependency-path }} + install-command: ${{ inputs.install-command }} + package-command: ${{ inputs.package-command }} + prerelease-package-command: ${{ inputs.prerelease-package-command }} + web-package-command: ${{ inputs.web-package-command }} + web-prerelease-package-command: ${{ inputs.web-prerelease-package-command }} + artifact-glob: ${{ inputs.artifact-glob }} determine-publish-matrix: needs: [determine-changes, calculate-artifact-name] @@ -583,20 +700,21 @@ jobs: - name: Checkout uses: actions/checkout@v6 - - name: Setup Node.js - uses: actions/setup-node@v6 + - name: Setup Node.js and install dependencies + uses: salesforcecli/github-workflows/.github/actions/setupNodeAndInstall@main with: node-version: ${{ inputs.node-version || '22.x' }} - - - name: Install dependencies - uses: salesforcecli/github-workflows/.github/actions/npmInstallWithRetries@main + package-manager: ${{ inputs.package-manager }} + package-manager-version: ${{ inputs.package-manager-version }} + cache-dependency-path: ${{ inputs.cache-dependency-path }} + install-command: ${{ inputs.install-command }} - name: Determine publish matrix id: matrix env: REGISTRIES: ${{ inputs.registries }} SELECTED_EXTENSIONS: ${{ needs.determine-changes.outputs.selected-extensions }} - IS_NIGHTLY: 'true' + IS_NIGHTLY: ${{ inputs.nightly && 'true' || 'false' }} EXTENSIONS_ROOT: ${{ inputs.extensions-root || 'packages' }} run: | # Skip marketplace publishing for nightly builds @@ -652,7 +770,7 @@ jobs: if [ ${#matrix_entries[@]} -eq 0 ]; then matrix_json='{"include":[]}' else - matrix_json=$(printf '%s\n' "${matrix_entries[@]}" | jq -s '{include: .}') + matrix_json=$(printf '%s\n' "${matrix_entries[@]}" | jq -sc '{include: .}') fi echo "matrix=$matrix_json" >> $GITHUB_OUTPUT @@ -672,11 +790,14 @@ jobs: if: needs.determine-publish-matrix.outputs.matrix == '[]' || needs.determine-publish-matrix.outputs.matrix == '' steps: - name: Log skipped publish + env: + SELECTED_EXTENSIONS: ${{ needs.determine-changes.outputs.selected-extensions }} + REGISTRIES: ${{ inputs.registries }} run: | echo "â„šī¸ Marketplace publishing skipped for this build type" echo " Reason: Nightly builds only create GitHub releases, not marketplace publishes" - echo " Extensions would be published: ${{ needs.determine-changes.outputs.selected-extensions }}" - echo " Registries configured: ${{ inputs.registries }}" + echo " Extensions would be published: $SELECTED_EXTENSIONS" + echo " Registries configured: $REGISTRIES" publish: needs: @@ -689,23 +810,25 @@ jobs: runs-on: ubuntu-latest if: needs.determine-publish-matrix.outputs.matrix != '[]' && needs.determine-publish-matrix.outputs.matrix != '' strategy: - matrix: - include: ${{ fromJson(needs.determine-publish-matrix.outputs.matrix) }} + matrix: ${{ fromJson(needs.determine-publish-matrix.outputs.matrix) }} steps: - name: Audit release attempt shell: bash + env: + ACTOR: ${{ github.actor }} + REPO: ${{ github.repository }} + RUN_ID: ${{ github.run_id }} + WORKFLOW: ${{ github.workflow }} + BRANCH: ${{ inputs.branch || github.ref_name }} + REGISTRY: ${{ matrix.registry }} + MARKETPLACE: ${{ matrix.marketplace }} + DRY_RUN: ${{ inputs.dry-run || github.event.inputs.dry-run || 'false' }} run: | # Create audit log entry for release attempt AUDIT_LOG="/tmp/release_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 }}" - BRANCH="${{ inputs.branch || github.ref_name }}" - # Log audit information - echo "[$TIMESTAMP] RELEASE_ATTEMPT: actor=$ACTOR, repo=$REPO, run_id=$RUN_ID, workflow=$WORKFLOW, branch=$BRANCH, registry=${{ matrix.registry }}, marketplace=${{ matrix.marketplace }}, dry_run=${{ inputs.dry-run || github.event.inputs.dry-run || 'false' }}" >> "$AUDIT_LOG" + echo "[$TIMESTAMP] RELEASE_ATTEMPT: actor=$ACTOR, repo=$REPO, run_id=$RUN_ID, workflow=$WORKFLOW, branch=$BRANCH, registry=$REGISTRY, marketplace=$MARKETPLACE, dry_run=$DRY_RUN" >> "$AUDIT_LOG" # Also log to GitHub Actions output for visibility echo "🔍 AUDIT: Release attempt logged - $TIMESTAMP" @@ -714,14 +837,14 @@ jobs: echo " Run ID: $RUN_ID" echo " Workflow: $WORKFLOW" echo " Branch: $BRANCH" - echo " Registry: ${{ matrix.registry }}" - echo " Marketplace: ${{ matrix.marketplace }}" - echo " Dry-run: ${{ inputs.dry-run || github.event.inputs.dry-run || 'false' }}" + echo " Registry: $REGISTRY" + echo " Marketplace: $MARKETPLACE" + echo " Dry-run: $DRY_RUN" - name: Checkout uses: actions/checkout@v6 with: - token: ${{ secrets.IDEE_GH_TOKEN }} + token: ${{ secrets.IDEE_GH_TOKEN || github.token }} ref: ${{ inputs.branch || github.ref }} - name: Download VSIX artifacts @@ -756,9 +879,9 @@ jobs: id: find_vsix env: EXCLUDE_WEB: ${{ inputs.exclude-web-vsix || 'false' }} + VSIX_PATTERN: ${{ matrix.vsix_pattern }} run: | ARTIFACTS_DIR="./vsix-artifacts" - VSIX_PATTERN="${{ matrix.vsix_pattern }}" # Find VSIX, optionally excluding *-web-* files if [ "$EXCLUDE_WEB" = "true" ]; then @@ -793,20 +916,22 @@ jobs: - name: Audit release result shell: bash if: inputs.dry-run != 'true' && github.event.inputs.dry-run != 'true' + env: + ACTOR: ${{ github.actor }} + REPO: ${{ github.repository }} + RUN_ID: ${{ github.run_id }} + BRANCH: ${{ inputs.branch || github.ref_name }} + REGISTRY: ${{ matrix.registry }} + MARKETPLACE: ${{ matrix.marketplace }} run: | # Log the result of the release attempt AUDIT_LOG="/tmp/release_audit.log" TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ") - ACTOR="${{ github.actor }}" - REPO="${{ github.repository }}" - RUN_ID="${{ github.run_id }}" - BRANCH="${{ inputs.branch || github.ref_name }}" - if [ $? -eq 0 ]; then - echo "[$TIMESTAMP] RELEASE_SUCCESS: actor=$ACTOR, repo=$REPO, run_id=$RUN_ID, branch=$BRANCH, registry=${{ matrix.registry }}, marketplace=${{ matrix.marketplace }}" >> "$AUDIT_LOG" + echo "[$TIMESTAMP] RELEASE_SUCCESS: actor=$ACTOR, repo=$REPO, run_id=$RUN_ID, branch=$BRANCH, registry=$REGISTRY, marketplace=$MARKETPLACE" >> "$AUDIT_LOG" echo "✅ AUDIT: Release successful - $TIMESTAMP" else - echo "[$TIMESTAMP] RELEASE_FAILURE: actor=$ACTOR, repo=$REPO, run_id=$RUN_ID, branch=$BRANCH, registry=${{ matrix.registry }}, marketplace=${{ matrix.marketplace }}" >> "$AUDIT_LOG" + echo "[$TIMESTAMP] RELEASE_FAILURE: actor=$ACTOR, repo=$REPO, run_id=$RUN_ID, branch=$BRANCH, registry=$REGISTRY, marketplace=$MARKETPLACE" >> "$AUDIT_LOG" echo "❌ AUDIT: Release failed - $TIMESTAMP" fi @@ -820,15 +945,16 @@ jobs: uses: actions/checkout@v6 with: ref: ${{ inputs.branch || github.ref }} - token: ${{ secrets.IDEE_GH_TOKEN }} + token: ${{ secrets.IDEE_GH_TOKEN || github.token }} - - name: Setup Node.js - uses: actions/setup-node@v6 + - name: Setup Node.js and install dependencies + uses: salesforcecli/github-workflows/.github/actions/setupNodeAndInstall@main with: node-version: ${{ inputs.node-version || '22.x' }} - - - name: Install dependencies - uses: salesforcecli/github-workflows/.github/actions/npmInstallWithRetries@main + package-manager: ${{ inputs.package-manager }} + package-manager-version: ${{ inputs.package-manager-version }} + cache-dependency-path: ${{ inputs.cache-dependency-path }} + install-command: ${{ inputs.install-command }} - name: Download VSIX artifacts uses: actions/download-artifact@v8 @@ -841,8 +967,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.IDEE_GH_TOKEN }} GITHUB_REPOSITORY: ${{ github.repository }} SELECTED_EXTENSIONS: ${{ needs.determine-changes.outputs.selected-extensions }} - IS_NIGHTLY: 'true' - PRE_RELEASE: 'true' + IS_NIGHTLY: ${{ inputs.nightly && 'true' || 'false' }} + PRE_RELEASE: ${{ inputs.pre-release || github.event.inputs.pre-release || 'false' }} VERSION_BUMP: ${{ inputs.version-bump }} DRY_RUN: ${{ inputs.dry-run || github.event.inputs.dry-run || 'false' }} BRANCH: ${{ inputs.branch || github.ref_name }} @@ -940,8 +1066,7 @@ jobs: name: Publish to CBWeb Internal Marketplace needs: [package, create-github-releases, calculate-artifact-name] runs-on: ubuntu-latest - continue-on-error: true - if: needs.package.result == 'success' + if: needs.package.result == 'success' && inputs.publish-web-vsix steps: - name: Download VSIX artifacts uses: actions/download-artifact@v8 @@ -954,11 +1079,8 @@ jobs: run: | VSIX_FILE=$(find ./vsix-artifacts -type f -name "*-web-*.vsix" | head -1) if [ -z "$VSIX_FILE" ]; then - echo "â„šī¸ No web-target VSIX found in artifacts" - echo " This is expected for repos without web extensions" - echo " Skipping CBWeb marketplace publish" - echo "has_web_vsix=false" >> $GITHUB_OUTPUT - exit 0 + echo "Required web-target VSIX was not found in artifacts" + exit 1 fi FILE_SIZE=$(stat -c%s "$VSIX_FILE" 2>/dev/null || stat -f%z "$VSIX_FILE" 2>/dev/null || echo "unknown") @@ -969,6 +1091,11 @@ jobs: - name: Publish web VSIX to CBWeb internal marketplace if: steps.find-web-vsix.outputs.has_web_vsix == 'true' && inputs.dry-run != 'true' && github.event.inputs.dry-run != 'true' run: | + if [ -z "$MARKETPLACE_URL" ] || [ -z "$MARKETPLACE_DEPLOY_TOKEN" ]; then + echo "CBWeb marketplace credentials are required for live publishing" + exit 1 + fi + echo "Publishing $VSIX_FILE to CBWeb marketplace..." HTTP_CODE=$(curl -s -o response.json -w '%{http_code}' \ @@ -983,7 +1110,7 @@ jobs: if [ "$HTTP_CODE" -ge 200 ] && [ "$HTTP_CODE" -lt 300 ]; then echo "Successfully published to CBWeb marketplace" else - echo "::warning::Failed to publish to CBWeb marketplace (HTTP $HTTP_CODE)" + echo "Failed to publish to CBWeb marketplace (HTTP $HTTP_CODE)" exit 1 fi env: @@ -999,7 +1126,13 @@ jobs: slack-notify: name: Slack Notification needs: - [determine-changes, bump-versions, package, publish, publish-skipped-notice] + [ + determine-changes, + bump-versions, + package, + publish, + publish-skipped-notice, + ] runs-on: ubuntu-latest if: always() && (needs.publish.result == 'success' || needs.publish.result == 'skipped') steps: @@ -1012,11 +1145,13 @@ jobs: id: extension-details env: EXTENSIONS_ROOT: ${{ inputs.extensions-root || 'packages' }} + SELECTED_EXTENSIONS: ${{ needs.determine-changes.outputs.selected-extensions }} + VERSION_BUMP_INPUT: ${{ inputs.version-bump }} + PRE_RELEASE_INPUT: ${{ inputs.pre-release || github.event.inputs.pre-release || 'false' }} run: | # Get selected extensions and their details - SELECTED_EXTENSIONS="${{ needs.determine-changes.outputs.selected-extensions }}" - VERSION_BUMP="${{ inputs.version-bump }}" - PRE_RELEASE="true" + VERSION_BUMP="$VERSION_BUMP_INPUT" + PRE_RELEASE="$PRE_RELEASE_INPUT" # Initialize arrays for extension details EXTENSION_NAMES="" @@ -1125,7 +1260,13 @@ jobs: slack-notify-failure: name: Slack Failure Notification needs: - [determine-changes, bump-versions, package, publish, publish-skipped-notice] + [ + determine-changes, + bump-versions, + package, + publish, + publish-skipped-notice, + ] runs-on: ubuntu-latest if: always() && (needs.publish.result == 'failure' || needs.bump-versions.result == 'failure' || needs.package.result == 'failure') steps: @@ -1138,11 +1279,13 @@ jobs: id: extension-details env: EXTENSIONS_ROOT: ${{ inputs.extensions-root || 'packages' }} + SELECTED_EXTENSIONS: ${{ needs.determine-changes.outputs.selected-extensions }} + VERSION_BUMP_INPUT: ${{ inputs.version-bump }} + PRE_RELEASE_INPUT: ${{ inputs.pre-release || github.event.inputs.pre-release || 'false' }} run: | # Get selected extensions and their details - SELECTED_EXTENSIONS="${{ needs.determine-changes.outputs.selected-extensions }}" - VERSION_BUMP="${{ inputs.version-bump }}" - PRE_RELEASE="true" + VERSION_BUMP="$VERSION_BUMP_INPUT" + PRE_RELEASE="$PRE_RELEASE_INPUT" # Initialize arrays for extension details EXTENSION_NAMES="" diff --git a/.github/workflows/vscode-release-explicit.yml b/.github/workflows/vscode-release-explicit.yml index adb1aef..944538f 100644 --- a/.github/workflows/vscode-release-explicit.yml +++ b/.github/workflows/vscode-release-explicit.yml @@ -8,22 +8,22 @@ on: required: true type: string registries: - description: 'Where to publish: marketplace | openvsx | all' + description: "Where to publish: marketplace | openvsx | all" required: false type: string - default: 'all' + default: "all" pre-release: - description: 'Mark as pre-release' + description: "Mark as pre-release" required: false type: boolean default: true version-bump: - description: 'Version bump strategy: auto | major | minor | patch' + description: "Version bump strategy: auto | major | minor | patch" required: false type: string - default: 'auto' + default: "auto" dry-run: - description: 'Skip actual publishing (for testing)' + description: "Skip actual publishing (for testing)" required: false type: boolean default: false @@ -31,23 +31,43 @@ on: description: 'Command to build VSIX packages (e.g., "npm run vscode:package" or "vsce package")' required: false type: string - default: 'vsce package' + default: "vsce package" bundle-command: description: 'Command to bundle extension code (e.g., "npm run vscode:bundle"). Set to empty string to skip bundling.' required: false type: string - default: 'npm run vscode:bundle' + default: "npm run vscode:bundle" node-version: - description: 'Node.js version to use' + description: "Node.js version to use" required: false - default: '22.x' + default: "22.x" + type: string + package-manager: + description: "Package manager to use: npm, pnpm, or yarn" + required: false + default: "npm" + type: string + package-manager-version: + description: "pnpm version to use when package-manager is pnpm" + required: false + default: "10" + type: string + cache-dependency-path: + description: "Path to the package manager lockfile" + required: false + default: "package-lock.json" + type: string + install-command: + description: "Command to install dependencies" + required: false + default: "npm ci" type: string secrets: VSCE_PAT: - description: 'VS Code Marketplace Personal Access Token' + description: "VS Code Marketplace Personal Access Token" required: false OVSX_PAT: - description: 'Open VSX Personal Access Token' + description: "Open VSX Personal Access Token" required: false permissions: @@ -65,36 +85,46 @@ jobs: - name: Checkout uses: actions/checkout@v6 - - name: Setup Node.js - uses: actions/setup-node@v6 + - name: Setup Node.js and install dependencies + uses: salesforcecli/github-workflows/.github/actions/setupNodeAndInstall@main with: node-version: ${{ inputs.node-version || '22.x' }} - - - name: Install dependencies - run: npm ci + package-manager: ${{ inputs.package-manager }} + package-manager-version: ${{ inputs.package-manager-version }} + cache-dependency-path: ${{ inputs.cache-dependency-path }} + install-command: ${{ inputs.install-command }} - name: Bundle extension if: inputs.bundle-command != '' + env: + BUNDLE_COMMAND: ${{ inputs.bundle-command }} + EXTENSION: ${{ matrix.extension }} run: | - cd ${{ matrix.extension }} - # Check if the bundle script exists in package.json - if npm run | grep -q "vscode:bundle"; then - ${{ inputs.bundle-command }} + cd "$EXTENSION" + if jq -e '.scripts["vscode:bundle"]' package.json > /dev/null; then + bash -c "$BUNDLE_COMMAND" else - echo "â­ī¸ Skipping bundle step - vscode:bundle script not found" + echo "Skipping bundle step: vscode:bundle script not found" fi - name: Build extension + env: + EXTENSION: ${{ matrix.extension }} + PACKAGE_COMMAND: ${{ inputs.package-command }} run: | - cd ${{ matrix.extension }} - ${{ inputs.package-command }} + cd "$EXTENSION" + bash -c "$PACKAGE_COMMAND" - name: Publish (dry-run) if: inputs.dry-run + env: + EXTENSION: ${{ matrix.extension }} + REGISTRIES: ${{ inputs.registries }} + PRE_RELEASE: ${{ inputs.pre-release }} run: | - echo "🔍 DRY RUN: Would publish ${{ matrix.extension }}" - echo " Registry: ${{ inputs.registries }}" - echo " Pre-release: ${{ inputs.pre-release }}" + echo "🔍 DRY RUN: Would publish $EXTENSION" + echo " Registry: $REGISTRIES" + echo " Pre-release: $PRE_RELEASE" - name: Publish to VS Code Marketplace if: | @@ -102,9 +132,11 @@ jobs: (inputs.registries == 'marketplace' || inputs.registries == 'all') env: VSCE_PAT: ${{ secrets.VSCE_PAT }} + EXTENSION: ${{ matrix.extension }} + PRE_RELEASE_FLAG: ${{ inputs.pre-release && '--pre-release' || '' }} run: | - cd ${{ matrix.extension }} - npx vsce publish ${{ inputs.pre-release && '--pre-release' || '' }} + cd "$EXTENSION" + npx vsce publish $PRE_RELEASE_FLAG - name: Publish to Open VSX if: | @@ -112,9 +144,11 @@ jobs: (inputs.registries == 'openvsx' || inputs.registries == 'all') env: OVSX_PAT: ${{ secrets.OVSX_PAT }} + EXTENSION: ${{ matrix.extension }} + PRE_RELEASE_FLAG: ${{ inputs.pre-release && '--pre-release' || '' }} run: | - cd ${{ matrix.extension }} - npx ovsx publish ${{ inputs.pre-release && '--pre-release' || '' }} -p $OVSX_PAT + cd "$EXTENSION" + npx ovsx publish $PRE_RELEASE_FLAG -p "$OVSX_PAT" - name: Prepare artifact name id: artifact diff --git a/.gitignore b/.gitignore index b512c09..8351add 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -node_modules \ No newline at end of file +node_modules +.fastcontext/ diff --git a/README.md b/README.md index 8e06cbe..b3db4c9 100644 --- a/README.md +++ b/README.md @@ -432,11 +432,13 @@ These workflows use an **odd/even minor version convention** to distinguish rele - **Stable releases**: Even minor versions (e.g., `0.6.0`) **Benefits:** + - Clear visual distinction between release channels - Prevents accidental overwrites - Predictable version progression: nightly `0.5.x` → pre-release `0.5.y` → stable `0.6.0` **Version bump types:** + - `major`: Breaking change → next major with first odd minor (e.g., `0.5.3` → `1.1.0`) - `minor`: New feature → next odd minor (e.g., `0.5.3` → `0.7.0`) - `patch`: Bug fix → increment patch, maintain odd minor (e.g., `0.5.3` → `0.5.4`) @@ -451,6 +453,32 @@ Most workflows share these common inputs: - `dry-run` (optional) - Run without publishing/tagging (default: `false`) - `pre-release` (optional) - Mark as pre-release version (default: `true`) - `registries` (optional) - Where to publish: `all`, `vsce`, or `ovsx` (default: `all`) +- `package-manager` (optional) - `npm`, `pnpm`, or `yarn` (default: `npm`) +- `package-manager-version` (optional) - pnpm version to install +- `cache-dependency-path` (optional) - Package-manager lockfile path (default: `package-lock.json`) +- `install-command` (optional) - Dependency installation command (default: `npm ci`) +- `package-command` / `prerelease-package-command` (optional) - Commands that create stable and prerelease VSIX artifacts +- `artifact-glob` (optional) - Glob for the produced VSIX artifacts (default: `packages/**/*.vsix`) +- `publish-web-vsix` (optional) - Publish a web-target VSIX to the CBWeb internal marketplace (default: `false`) + +### Node Lifecycle Contract + +The workflows execute Node-module lifecycle activities; they do not require a specific `package.json` script name. New consumers should expose the conventional scripts below, while existing consumers can map their current scripts or direct commands through workflow inputs. + +| Activity | Recommended script | Workflow input | +| --------------------------- | ------------------------------------------ | ---------------------------- | +| Install dependencies | manager-native frozen or immutable install | `install-command` | +| Lint | `lint` | `lint-command` | +| Build | `build` | `build-command` | +| Test | `test` | `test-command` | +| CI test variant | `test:ci` | `test-command` | +| Coverage | `test:coverage` | `coverage-command` | +| Merge coverage | `test:coverage:report` | `coverage-report-command` | +| Additional quality checks | `test:quality` | `quality-command` | +| Package stable artifact | `package` | `package-command` | +| Package prerelease artifact | `package:prerelease` | `prerelease-package-command` | + +The existing npm defaults remain compatible with VSE: `npm run compile`, `npm run package:packages`, and `npm run package:packages:prerelease`. Direct commands are valid when a repository does not use scripts, such as `cd lana && pnpm exec vsce package --no-dependencies`. --- @@ -472,10 +500,10 @@ jobs: nightly: uses: salesforcecli/github-workflows/.github/workflows/vscode-release-explicit.yml@main with: - extensions: '["packages/ext1", "packages/ext2"]' # JSON array of paths - registries: all # all | marketplace | openvsx + extensions: '["packages/ext1", "packages/ext2"]' # JSON array of paths + registries: all # all | marketplace | openvsx pre-release: true - version-bump: auto # auto | major | minor | patch + version-bump: auto # auto | major | minor | patch package-command: 'npx vsce package --no-dependencies' dry-run: false secrets: @@ -484,6 +512,7 @@ jobs: ``` **Inputs:** + - `extensions` (required) - JSON array of extension directory paths - `registries` (optional) - Where to publish: `all`, `marketplace`, or `openvsx` (default: `all`) - `pre-release` (optional) - Mark as pre-release version (default: `true`) @@ -493,6 +522,7 @@ jobs: - `dry-run` (optional) - Skip actual publishing for testing (default: `false`) **Required Secrets:** + - `VSCE_PAT` - VS Code Marketplace Personal Access Token - `OVSX_PAT` - Open VSX Personal Access Token @@ -524,10 +554,16 @@ jobs: ci: uses: salesforcecli/github-workflows/.github/workflows/vscode-ci-template.yml@main with: + package-manager: npm + install-command: npm ci lint-command: 'npm run lint' - compile-command: 'npm run compile' + build-command: 'npm run build' test-command: 'npm run test' - test-coverage-command: 'npm run test:coverage' + coverage-command: 'npm run test:coverage' + coverage-report-command: 'npm run test:coverage:report' + quality-command: 'npm run test:quality' + package-command: 'npm run package' + prerelease-package-command: 'npm run package:prerelease' ``` ### vscode-publish-extensions @@ -542,19 +578,57 @@ jobs: uses: salesforcecli/github-workflows/.github/workflows/vscode-publish-extensions.yml@main with: branch: main - extensions: changed # or 'all' or 'ext1,ext2' - registries: all # all | vsce | ovsx + extensions: changed # or 'all' or 'ext1,ext2' + registries: all # all | vsce | ovsx pre-release: true - version-bump: auto # auto | major | minor | patch - extensions-root: packages # for monorepos + version-bump: auto # auto | major | minor | patch + extensions-root: packages # for monorepos exclude-web-vsix: 'false' + publish-web-vsix: false # set true only for repos with a CBWeb web VSIX slack-notification-title: '🎉 Extensions Released Successfully!' node-version: '22.x' dry-run: false secrets: inherit ``` +**pnpm caller example:** + +```yaml +jobs: + publish: + uses: salesforcecli/github-workflows/.github/workflows/vscode-publish-extensions.yml@main + with: + branch: main + extensions: lana + extensions-root: . + package-manager: pnpm + package-manager-version: '10' + cache-dependency-path: pnpm-lock.yaml + install-command: pnpm run ci:install + package-command: cd lana && pnpm exec vsce package --no-dependencies + prerelease-package-command: cd lana && pnpm exec vsce package --pre-release --no-dependencies + artifact-glob: lana/*.vsix + dry-run: true + secrets: inherit +``` + +**Yarn caller example:** + +```yaml +jobs: + publish: + uses: salesforcecli/github-workflows/.github/workflows/vscode-publish-extensions.yml@main + with: + package-manager: yarn + cache-dependency-path: yarn.lock + install-command: yarn install --network-timeout 600000 + package-command: yarn package:packages + prerelease-package-command: yarn package:packages:prerelease + secrets: inherit +``` + **Key Features:** + - Auto-detects changed extensions in monorepos - Smart version bumping using odd/even convention - Conventional commit analysis @@ -563,6 +637,7 @@ jobs: - Slack notifications on success **Inputs:** + - `extensions` (optional) - Extensions to release: `changed`, `all`, or comma-separated names (default: `changed`) - `version-bump` (optional) - Version bump type: `auto`, `patch`, `minor`, `major` (default: `auto`) - `slack-notification-title` (optional) - Slack notification title (default: `🎉 Extensions Released Successfully!`) @@ -578,20 +653,22 @@ jobs: promote: uses: salesforcecli/github-workflows/.github/workflows/vscode-promote-prerelease.yml@main with: - extension-name: 'my-extension' # Used for tracking tags - min-tag-age-days: '7' # Nightly must be at least 7 days old - vsix-name-pattern: 'my-extension-*.vsix' # Pattern to match VSIX files - exclude-web-vsix: 'true' # Exclude *-web-* VSIX files + extension-name: 'my-extension' # Used for tracking tags + min-tag-age-days: '7' # Nightly must be at least 7 days old + vsix-name-pattern: 'my-extension-*.vsix' # Pattern to match VSIX files + exclude-web-vsix: 'true' # Exclude *-web-* VSIX files dry-run: 'false' secrets: inherit ``` **Requirements:** + - Nightly tags matching `v{version}-nightly.*` pattern (e.g., `v1.2.3-nightly.20260709`) - GitHub releases for each nightly tag with VSIX files attached - Passing CI checks on nightly commits **How it works:** + 1. Finds oldest nightly tag â‰Ĩ min-tag-age-days that hasn't been promoted 2. Verifies CI checks passed for that commit 3. Downloads VSIX from nightly GitHub release @@ -618,6 +695,7 @@ jobs: ``` **Requirements:** + - `marketplace-prerelease-*` tracking tags from previous promotions - Local actions in calling repository (see Prerequisites section above) @@ -626,6 +704,7 @@ jobs: ### vscode-manual-publish Manually publish a specific nightly or CI build to the marketplace. Supports two source paths: + 1. **Tag path**: Publish from a nightly GitHub Release 2. **Run path**: Publish from a CI build artifact (with quality check bypass) @@ -638,8 +717,8 @@ jobs: with: extension-name: 'my-extension' vsix-name-pattern: 'my-extension-*.vsix' - version-tag: 'v0.5.3-nightly.20260301' # OR use source-run-id - slot: 'pre-release' # or 'stable' + version-tag: 'v0.5.3-nightly.20260301' # OR use source-run-id + slot: 'pre-release' # or 'stable' registries: 'all' exclude-web-vsix: 'true' extensions-root: 'packages' @@ -648,6 +727,7 @@ jobs: ``` **Requirements:** + - Local actions in calling repository (see Prerequisites section above) - Environment: `manual-publish-gate` (with required reviewers for approval)