diff --git a/.github/actions/ctcOpen/action.yml b/.github/actions/ctcOpen/action.yml index 942c97d..2c918bf 100644 --- a/.github/actions/ctcOpen/action.yml +++ b/.github/actions/ctcOpen/action.yml @@ -21,6 +21,18 @@ inputs: description: version of node to use. It's better to specify latest, lts/* or lts/-1 than to hardcode numbers required: false default: lts/* + package-manager: + description: Package manager used by the calling repository (npm, pnpm, or yarn) + required: false + default: npm + package-manager-version: + description: pnpm version to use when package-manager is pnpm + required: false + default: '10' + cache-dependency-path: + description: Path to the calling repository's package manager lockfile + required: false + default: package-lock.json outputs: changeCaseId: @@ -32,10 +44,17 @@ runs: steps: - uses: actions/checkout@v4 + - name: Setup pnpm + if: inputs.package-manager == 'pnpm' + uses: pnpm/action-setup@v4 + with: + version: ${{ inputs.package-manager-version }} + - uses: actions/setup-node@v4 with: node-version: ${{ inputs.nodeVersion }} - cache: npm + cache: ${{ inputs.package-manager }} + cache-dependency-path: ${{ inputs.cache-dependency-path }} - run: npm install -g @salesforce/change-case-management --omit=dev shell: bash diff --git a/.github/actions/npmInstallWithRetries/action.yml b/.github/actions/npmInstallWithRetries/action.yml index 30c64cf..78e9829 100644 --- a/.github/actions/npmInstallWithRetries/action.yml +++ b/.github/actions/npmInstallWithRetries/action.yml @@ -1,5 +1,5 @@ name: npm-install-with-retries -description: 'wraps npm install with retries/timeout to handle network failures' +description: 'Wraps npm ci with retries and timeouts. New workflows should use setupNodeAndInstall for package-manager-agnostic installation.' inputs: ignore-scripts: default: 'false' diff --git a/.github/actions/setupNodeAndInstall/action.yml b/.github/actions/setupNodeAndInstall/action.yml index 184e11a..660ecee 100644 --- a/.github/actions/setupNodeAndInstall/action.yml +++ b/.github/actions/setupNodeAndInstall/action.yml @@ -63,11 +63,11 @@ runs: - name: Install npm dependencies if: inputs.package-manager == 'npm' && steps.install-command.outputs.value == 'npm ci' - uses: salesforcecli/github-workflows/.github/actions/npmInstallWithRetries@main + uses: salesforcecli/github-workflows/.github/actions/npmInstallWithRetries@ph/W-23832274-pnpm-stable-promotion - 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 + uses: salesforcecli/github-workflows/.github/actions/yarnInstallWithRetries@ph/W-23832274-pnpm-stable-promotion - 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') diff --git a/.github/actions/updateNodeLockfile/action.yml b/.github/actions/updateNodeLockfile/action.yml new file mode 100644 index 0000000..3e4ceca --- /dev/null +++ b/.github/actions/updateNodeLockfile/action.yml @@ -0,0 +1,60 @@ +name: Update Node lockfile +description: Refreshes an npm, pnpm, or Yarn lockfile with retries and without lifecycle scripts. +inputs: + package-manager: + description: 'Package manager to use: npm, pnpm, or yarn.' + required: true + package-manager-version: + description: 'pnpm version to install when package-manager is pnpm.' + required: false + default: '10' + lockfile-path: + description: 'Path to the lockfile to refresh.' + required: true +runs: + using: composite + steps: + - name: Validate package manager and lockfile + shell: bash + env: + PACKAGE_MANAGER: ${{ inputs.package-manager }} + LOCKFILE_PATH: ${{ inputs.lockfile-path }} + run: | + case "$PACKAGE_MANAGER" in + npm|pnpm|yarn) ;; + *) echo "Unsupported package manager: $PACKAGE_MANAGER"; exit 1 ;; + esac + [ -f "$LOCKFILE_PATH" ] || { echo "Lockfile not found: $LOCKFILE_PATH"; exit 1; } + + - name: Setup pnpm + if: inputs.package-manager == 'pnpm' + uses: pnpm/action-setup@v4 + with: + version: ${{ inputs.package-manager-version }} + + - name: Resolve lockfile update command + id: command + shell: bash + env: + PACKAGE_MANAGER: ${{ inputs.package-manager }} + LOCKFILE_PATH: ${{ inputs.lockfile-path }} + run: | + LOCKFILE_DIR=$(dirname "$LOCKFILE_PATH") + case "$PACKAGE_MANAGER" in + npm) COMMAND="cd '$LOCKFILE_DIR' && npm install --package-lock-only --ignore-scripts" ;; + pnpm) COMMAND="cd '$LOCKFILE_DIR' && pnpm install --lockfile-only --ignore-scripts" ;; + yarn) + YARN_MAJOR=$(yarn --version | cut -d. -f1) + if [ "$YARN_MAJOR" -ge 2 ]; then + COMMAND="cd '$LOCKFILE_DIR' && YARN_ENABLE_IMMUTABLE_INSTALLS=false yarn install --mode=skip-build" + else + COMMAND="cd '$LOCKFILE_DIR' && yarn install --ignore-scripts" + fi + ;; + esac + echo "value=$COMMAND" >> "$GITHUB_OUTPUT" + + - name: Refresh lockfile with retries + uses: salesforcecli/github-workflows/.github/actions/retry@main + with: + command: ${{ steps.command.outputs.value }} diff --git a/.github/workflows/automerge.yml b/.github/workflows/automerge.yml index 593f9b5..7620273 100644 --- a/.github/workflows/automerge.yml +++ b/.github/workflows/automerge.yml @@ -20,6 +20,21 @@ on: description: Optionally skip ci builds on merges into main type: boolean default: false + package-manager: + required: false + description: Package manager used by the calling repository (npm, pnpm, or yarn) + type: string + default: npm + package-manager-version: + required: false + description: pnpm version to use when package-manager is pnpm + type: string + default: '10' + cache-dependency-path: + required: false + description: Path to the calling repository's package manager lockfile + type: string + default: package-lock.json jobs: dependabot-automerge: @@ -29,10 +44,17 @@ jobs: with: token: ${{ secrets.SVC_CLI_BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} + - name: Setup pnpm + if: inputs.package-manager == 'pnpm' + uses: pnpm/action-setup@v4 + with: + version: ${{ inputs.package-manager-version }} + - uses: actions/setup-node@v4 with: node-version: lts/* - cache: npm + cache: ${{ inputs.package-manager }} + cache-dependency-path: ${{ inputs.cache-dependency-path }} - run: npm install -g @salesforce/plugin-release-management --omit=dev diff --git a/.github/workflows/ctcClose.yml b/.github/workflows/ctcClose.yml index 6d76f9d..cfa9581 100644 --- a/.github/workflows/ctcClose.yml +++ b/.github/workflows/ctcClose.yml @@ -15,6 +15,21 @@ on: type: string default: lts/* required: false + package-manager: + description: Package manager used by the calling repository (npm, pnpm, or yarn) + type: string + default: npm + required: false + package-manager-version: + description: pnpm version to use when package-manager is pnpm + type: string + default: '10' + required: false + cache-dependency-path: + description: Path to the calling repository's package manager lockfile + type: string + default: package-lock.json + required: false jobs: ctcClose: @@ -22,11 +37,17 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup pnpm + if: inputs.package-manager == 'pnpm' + uses: pnpm/action-setup@v4 + with: + version: ${{ inputs.package-manager-version }} + - uses: actions/setup-node@v4 with: node-version: ${{ inputs.nodeVersion }} - # No `cache: npm`: it requires an npm lockfile and so hard-fails on - # pnpm/yarn consumer repos; the CTC CLI below is a one-off global install. + cache: ${{ inputs.package-manager }} + cache-dependency-path: ${{ inputs.cache-dependency-path }} - run: npm install -g @salesforce/change-case-management --omit=dev - id: ctc run: | diff --git a/.github/workflows/ctcOpen.yml b/.github/workflows/ctcOpen.yml index 4934fe2..9babb60 100644 --- a/.github/workflows/ctcOpen.yml +++ b/.github/workflows/ctcOpen.yml @@ -9,6 +9,21 @@ on: type: string default: lts/* required: false + package-manager: + description: Package manager used by the calling repository (npm, pnpm, or yarn) + type: string + default: npm + required: false + package-manager-version: + description: pnpm version to use when package-manager is pnpm + type: string + default: '10' + required: false + cache-dependency-path: + description: Path to the calling repository's package manager lockfile + type: string + default: package-lock.json + required: false outputs: changeCaseId: description: Id for the change case created @@ -22,11 +37,17 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Setup pnpm + if: inputs.package-manager == 'pnpm' + uses: pnpm/action-setup@v4 + with: + version: ${{ inputs.package-manager-version }} + - uses: actions/setup-node@v4 with: node-version: ${{ inputs.nodeVersion }} - # No `cache: npm`: it requires an npm lockfile and so hard-fails on - # pnpm/yarn consumer repos; the CTC CLI below is a one-off global install. + cache: ${{ inputs.package-manager }} + cache-dependency-path: ${{ inputs.cache-dependency-path }} - run: npm install -g @salesforce/change-case-management --omit=dev diff --git a/.github/workflows/npmPublish.yml b/.github/workflows/npmPublish.yml index 8e39a96..5ae954d 100644 --- a/.github/workflows/npmPublish.yml +++ b/.github/workflows/npmPublish.yml @@ -52,10 +52,20 @@ on: required: true type: string packageManager: - description: the package manager to use. Defaults to yarn, but can be set to npm + description: the package manager to use. Defaults to yarn; supports npm, pnpm, or yarn required: false default: yarn type: string + packageManagerVersion: + description: pnpm version to use when packageManager is pnpm + required: false + default: '10' + type: string + cacheDependencyPath: + description: path to the package manager lockfile; defaults to yarn.lock, so npm and pnpm callers must override it + required: false + default: yarn.lock + type: string vulnerabilityCheck: description: if true, checks for known vulnerable package versions required: false @@ -81,8 +91,8 @@ jobs: ref: ${{ inputs.githubTag }} - name: Validate package manager run: | - if [[ "$INPUTS_PACKAGE_MANAGER" != "yarn" && "$INPUTS_PACKAGE_MANAGER" != "npm" ]]; then - echo "Error: packageManager must be 'yarn' or 'npm', got '$INPUTS_PACKAGE_MANAGER'" + if [[ "$INPUTS_PACKAGE_MANAGER" != "yarn" && "$INPUTS_PACKAGE_MANAGER" != "npm" && "$INPUTS_PACKAGE_MANAGER" != "pnpm" ]]; then + echo "Error: packageManager must be 'npm', 'pnpm', or 'yarn', got '$INPUTS_PACKAGE_MANAGER'" exit 1 fi - uses: actions/setup-node@v4 @@ -118,10 +128,13 @@ jobs: needs: [check-publish] # CTC will only open when publishing to 'latest' if: inputs.ctc && needs.check-publish.outputs.published == 'false' && inputs.tag == 'latest' - uses: salesforcecli/github-workflows/.github/workflows/ctcOpen.yml@main + uses: salesforcecli/github-workflows/.github/workflows/ctcOpen.yml@ph/W-23832274-pnpm-stable-promotion with: githubTag: ${{ inputs.githubTag }} nodeVersion: ${{ inputs.nodeVersion }} + package-manager: ${{ inputs.packageManager }} + package-manager-version: ${{ inputs.packageManagerVersion }} + cache-dependency-path: ${{ inputs.cacheDependencyPath }} secrets: inherit npm-publish: @@ -136,16 +149,27 @@ jobs: - uses: actions/checkout@v4 with: ref: ${{ inputs.githubTag }} + - name: Setup pnpm + if: inputs.packageManager == 'pnpm' + uses: pnpm/action-setup@v4 + with: + version: ${{ inputs.packageManagerVersion }} - uses: actions/setup-node@v4 with: node-version: ${{ inputs.nodeVersion }} cache: ${{ inputs.packageManager }} + cache-dependency-path: ${{ inputs.cacheDependencyPath }} - name: Install dependencies with yarn if: inputs.packageManager == 'yarn' - uses: salesforcecli/github-workflows/.github/actions/yarnInstallWithRetries@main + uses: salesforcecli/github-workflows/.github/actions/yarnInstallWithRetries@ph/W-23832274-pnpm-stable-promotion - name: Install dependencies with npm if: inputs.packageManager == 'npm' - uses: salesforcecli/github-workflows/.github/actions/npmInstallWithRetries@main + uses: salesforcecli/github-workflows/.github/actions/npmInstallWithRetries@ph/W-23832274-pnpm-stable-promotion + - name: Install dependencies with pnpm + if: inputs.packageManager == 'pnpm' + uses: salesforcecli/github-workflows/.github/actions/retry@main + with: + command: pnpm install --frozen-lockfile - name: Vulnerability check if: inputs.vulnerabilityCheck # Check for known vulnerable packages from the following supply chain attacks: @@ -200,18 +224,24 @@ jobs: ctcCloseSuccess: needs: [ctc-open, npm-publish] if: needs.ctc-open.result == 'success' && needs.npm-publish.result == 'success' && needs.ctc-open.outputs.changeCaseId - uses: salesforcecli/github-workflows/.github/workflows/ctcClose.yml@main + uses: salesforcecli/github-workflows/.github/workflows/ctcClose.yml@ph/W-23832274-pnpm-stable-promotion secrets: inherit with: changeCaseId: ${{needs.ctc-open.outputs.changeCaseId}} nodeVersion: ${{ inputs.nodeVersion }} + package-manager: ${{ inputs.packageManager }} + package-manager-version: ${{ inputs.packageManagerVersion }} + cache-dependency-path: ${{ inputs.cacheDependencyPath }} ctcCloseFail: needs: [ctc-open, npm-publish] if: always() && inputs.ctc && needs.ctc-open.outputs.changeCaseId && (needs.ctc-open.result != 'success' || needs.npm-publish.result != 'success') - uses: salesforcecli/github-workflows/.github/workflows/ctcClose.yml@main + uses: salesforcecli/github-workflows/.github/workflows/ctcClose.yml@ph/W-23832274-pnpm-stable-promotion secrets: inherit with: changeCaseId: ${{ needs.ctc-open.outputs.changeCaseId }} nodeVersion: ${{ inputs.nodeVersion }} status: Not Implemented + package-manager: ${{ inputs.packageManager }} + package-manager-version: ${{ inputs.packageManagerVersion }} + cache-dependency-path: ${{ inputs.cacheDependencyPath }} diff --git a/.github/workflows/vscode-manual-publish.yml b/.github/workflows/vscode-manual-publish.yml index 78ed14e..6b68291 100644 --- a/.github/workflows/vscode-manual-publish.yml +++ b/.github/workflows/vscode-manual-publish.yml @@ -14,7 +14,8 @@ name: Manual Publish VS Code Extension # vsix-name-pattern: 'apex-language-server-extension-*.vsix' # version-tag: 'apex-language-server-extension-v0.5.3-nightly.20260301' # OR source-run-id # slot: 'pre-release' # or 'stable' -# registries: 'all' # or 'vsce' or 'ovsx' +# registries: 'all' # or 'vsce', 'ovsx', or 'none' +# publish-web-vsix: 'true' # publish the web artifact to CBWeb # exclude-web-vsix: 'true' # extensions-root: 'packages' # dry-run: 'false' @@ -22,7 +23,8 @@ name: Manual Publish VS Code Extension # # Requirements - calling repository must have: # - Secrets: IDEE_GH_TOKEN, VSCE_PERSONAL_ACCESS_TOKEN, IDEE_OVSX_PAT -# - Action: ./.github/actions/npm-install-with-retries (custom npm install with retry logic) +# - For CBWeb: MARKETPLACE_DEPLOY_TOKEN and MARKETPLACE_URL variable +# - The package manager and lockfile inputs must match the caller repository. # - Action: ./.github/actions/check-ci-status (validates CI checks passed before publish) # - Action: ./.github/actions/repackage-vsix-stable (repackages pre-release VSIX as stable) # - Action: ./.github/actions/publish-vsix (publishes to VS Code Marketplace and/or Open VSX) @@ -57,10 +59,15 @@ on: required: true type: string registries: - description: "Registries to publish to (all, vsce, ovsx)" + description: "Public registries to publish to (all, vsce, ovsx, none)" required: false default: "all" type: string + publish-web-vsix: + description: "Publish the web VSIX to the CBWeb internal marketplace" + required: false + default: "false" + type: string target-stable-version: description: "Optional stable version override (e.g., 0.6.1). Must be valid semver with EVEN minor. Only applies when slot is stable. Required when using source-run-id with slot stable." required: false @@ -106,6 +113,31 @@ on: required: false 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 + lockfile-path: + description: "Single lockfile path to update and commit with the stable version" + required: false + default: "package-lock.json" + type: string + install-command: + description: "Command used to install dependencies" + required: false + default: "npm ci" + type: string workflow_dispatch: inputs: extension-name: @@ -143,6 +175,15 @@ on: - all - vsce - ovsx + - none + publish-web-vsix: + description: "Publish the web VSIX to the CBWeb internal marketplace" + required: false + default: "false" + type: choice + options: + - "false" + - "true" target-stable-version: description: "Optional stable version override" required: false @@ -187,6 +228,32 @@ on: required: false default: "22.x" type: string + package-manager: + description: "Package manager to use: npm, pnpm, or yarn" + required: false + default: "npm" + type: choice + options: [npm, pnpm, yarn] + 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 + lockfile-path: + description: "Single lockfile path to update and commit with the stable version" + required: false + default: "package-lock.json" + type: string + install-command: + description: "Command used to install dependencies" + required: false + default: "npm ci" + type: string concurrency: group: manual-publish @@ -217,6 +284,13 @@ jobs: SOURCE_RUN_ID="${{ inputs.source-run-id }}" SKIP_QC="${{ inputs.skip-quality-checks }}" BYPASS="${{ inputs.confirm-bypass }}" + REGISTRIES="${{ inputs.registries }}" + PUBLISH_WEB_VSIX="${{ inputs.publish-web-vsix }}" + + case "$REGISTRIES" in + all|vsce|ovsx|none) ;; + *) echo "ERROR: registries must be all, vsce, ovsx, or none."; exit 1 ;; + esac # Exactly one source must be provided if [ -z "$VERSION_TAG" ] && [ -z "$SOURCE_RUN_ID" ]; then @@ -228,6 +302,16 @@ jobs: exit 1 fi + if [ "$REGISTRIES" = "none" ] && [ "$PUBLISH_WEB_VSIX" != "true" ]; then + echo "ERROR: registries=none requires publish-web-vsix=true." + exit 1 + fi + + if [ "$PUBLISH_WEB_VSIX" = "true" ] && [ "${{ inputs.slot }}" != "pre-release" ]; then + echo "ERROR: publish-web-vsix currently supports only the pre-release slot." + exit 1 + fi + # Run path requires bypass (branch CI profiles differ from main) if [ -n "$SOURCE_RUN_ID" ]; then if [ "$SKIP_QC" != "true" ]; then @@ -251,20 +335,21 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 - 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@ph/W-23832274-pnpm-stable-promotion with: node-version: ${{ inputs.node-version || '22.x' }} - - - name: Install dependencies - uses: ./.github/actions/npm-install-with-retries + 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: Resolve source and compute versions id: resolve env: - GH_TOKEN: ${{ secrets.IDEE_GH_TOKEN }} + GH_TOKEN: ${{ secrets.IDEE_GH_TOKEN || github.token }} VERSION_TAG: ${{ inputs.version-tag }} SOURCE_RUN_ID: ${{ inputs.source-run-id }} SLOT: ${{ inputs.slot }} @@ -445,23 +530,37 @@ jobs: id: ci-commit if: inputs.skip-quality-checks != 'true' env: - GH_TOKEN: ${{ secrets.IDEE_GH_TOKEN }} + GH_TOKEN: ${{ secrets.IDEE_GH_TOKEN || github.token }} REPO: ${{ github.repository }} START_SHA: ${{ steps.resolve.outputs.commit-sha }} + REQUIRED_CHECKS: ${{ inputs.required-ci-checks }} run: | MAX_DEPTH=20 SHA="$START_SHA" CI_SHA="" + IFS=',' read -r -a CHECK_NAMES <<< "$REQUIRED_CHECKS" + for i in "${!CHECK_NAMES[@]}"; do + CHECK_NAMES[$i]=$(echo "${CHECK_NAMES[$i]}" | xargs) + done + for i in $(seq 1 "$MAX_DEPTH"); do - CONCLUSION=$(gh api "repos/$REPO/commits/$SHA/check-runs" --paginate \ - --jq '[.check_runs[] | select(.name == "CI Complete")] | .[0].conclusion // empty' 2>/dev/null || echo "") - if [ "$CONCLUSION" = "success" ]; then + CHECK_RUNS=$(gh api "repos/$REPO/commits/$SHA/check-runs" --paginate 2>/dev/null || echo "") + MISSING_OR_FAILED="" + for CHECK_NAME in "${CHECK_NAMES[@]}"; do + CONCLUSION=$(echo "$CHECK_RUNS" | jq -r --arg name "$CHECK_NAME" \ + '[.check_runs[] | select(.name == $name)] | .[0].conclusion // empty') + if [ "$CONCLUSION" != "success" ]; then + MISSING_OR_FAILED="${MISSING_OR_FAILED}${MISSING_OR_FAILED:+, }${CHECK_NAME} (${CONCLUSION:-none})" + fi + done + + if [ -z "$MISSING_OR_FAILED" ]; then CI_SHA="$SHA" echo "Found CI-tested ancestor at depth $((i - 1)): $CI_SHA" break fi - echo " $SHA: no successful 'CI Complete' (conclusion='${CONCLUSION:-none}') — walking to parent" + echo " $SHA: required checks not successful: $MISSING_OR_FAILED — walking to parent" PARENT=$(git rev-parse "${SHA}^" 2>/dev/null || echo "") if [ -z "$PARENT" ]; then echo "Reached root of history without a CI-tested commit." @@ -471,7 +570,7 @@ jobs: done if [ -z "$CI_SHA" ]; then - echo "ERROR: No ancestor with a successful 'CI Complete' check found within $MAX_DEPTH commits of $START_SHA." + echo "ERROR: No ancestor with successful required checks ('$REQUIRED_CHECKS') found within $MAX_DEPTH commits of $START_SHA." echo "Cannot verify CI status — failing to prevent untested promotion. Use skip-quality-checks=true to bypass." exit 1 fi @@ -583,7 +682,7 @@ jobs: uses: ./.github/actions/check-ci-status with: commit-sha: ${{ needs.prepare.outputs.ci-commit-sha }} - token: ${{ secrets.IDEE_GH_TOKEN }} + token: ${{ secrets.IDEE_GH_TOKEN || github.token }} required-checks: ${{ inputs.required-ci-checks }} # ── gate ─────────────────────────────────────────────────────────────────── @@ -612,6 +711,12 @@ jobs: with: node-version: ${{ inputs.node-version || '22.x' }} + - name: Setup pnpm + if: inputs.package-manager == 'pnpm' + uses: pnpm/action-setup@v4 + with: + version: ${{ inputs.package-manager-version }} + - name: Download source VSIX uses: actions/download-artifact@v8 with: @@ -663,7 +768,7 @@ jobs: # ── publish ──────────────────────────────────────────────────────────────── publish: needs: [prepare, repackage] - if: always() && needs.prepare.result == 'success' && needs.repackage.result == 'success' + if: always() && inputs.registries != 'none' && needs.prepare.result == 'success' && needs.repackage.result == 'success' runs-on: ubuntu-latest strategy: matrix: @@ -677,9 +782,6 @@ jobs: with: node-version: ${{ inputs.node-version || '22.x' }} - - name: Install dependencies - uses: ./.github/actions/npm-install-with-retries - - name: Download VSIX uses: actions/download-artifact@v8 with: @@ -709,6 +811,50 @@ jobs: VSCE_PERSONAL_ACCESS_TOKEN: ${{ secrets.VSCE_PERSONAL_ACCESS_TOKEN }} OVSX_PAT: ${{ secrets.IDEE_OVSX_PAT }} + publish-to-cbweb-marketplace: + name: Publish to CBWeb Internal Marketplace + needs: [prepare, gate] + if: always() && inputs.publish-web-vsix == 'true' && needs.prepare.result == 'success' && needs.gate.result == 'success' + runs-on: ubuntu-latest + steps: + - name: Download source VSIX artifacts + uses: actions/download-artifact@v8 + with: + name: source-vsix + path: ./vsix-artifacts + + - name: Find web VSIX + id: web-vsix + run: | + mapfile -t VSIX_FILES < <(find ./vsix-artifacts -type f -name '*-web-*.vsix') + if [ "${#VSIX_FILES[@]}" -ne 1 ]; then + echo "Expected exactly one web VSIX artifact, found ${#VSIX_FILES[@]}" + exit 1 + fi + echo "vsix_file=${VSIX_FILES[0]}" >> "$GITHUB_OUTPUT" + + - name: Publish web VSIX to CBWeb internal marketplace + env: + DRY_RUN: ${{ inputs.dry-run || 'false' }} + MARKETPLACE_URL: ${{ vars.MARKETPLACE_URL }} + MARKETPLACE_DEPLOY_TOKEN: ${{ secrets.MARKETPLACE_DEPLOY_TOKEN }} + VSIX_FILE: ${{ steps.web-vsix.outputs.vsix_file }} + run: | + if [ "$DRY_RUN" = "true" ]; then + echo "DRY RUN: Would publish $VSIX_FILE to CBWeb marketplace" + exit 0 + fi + + if [ -z "$MARKETPLACE_URL" ] || [ -z "$MARKETPLACE_DEPLOY_TOKEN" ]; then + echo "CBWeb marketplace credentials are required for live publishing" + exit 1 + fi + + curl --fail-with-body \ + -X POST "${MARKETPLACE_URL}/api/internal/publish" \ + -H "Authorization: Bearer ${MARKETPLACE_DEPLOY_TOKEN}" \ + -F "vsix=@${VSIX_FILE}" + # ── create-github-release ────────────────────────────────────────────────── create-github-release: needs: [prepare, repackage, publish] @@ -719,7 +865,7 @@ jobs: uses: actions/checkout@v6 with: fetch-depth: 0 - token: ${{ secrets.IDEE_GH_TOKEN }} + token: ${{ secrets.IDEE_GH_TOKEN || github.token }} - name: Download VSIX uses: actions/download-artifact@v8 @@ -729,7 +875,7 @@ jobs: - name: Create GitHub release for stable version env: - GH_TOKEN: ${{ secrets.IDEE_GH_TOKEN }} + GH_TOKEN: ${{ secrets.IDEE_GH_TOKEN || github.token }} STABLE_VERSION: ${{ needs.prepare.outputs.stable-version }} PRERELEASE_VERSION: ${{ needs.prepare.outputs.prerelease-version }} SOURCE_TYPE: ${{ needs.prepare.outputs.source-type }} @@ -783,15 +929,15 @@ jobs: # ── tag-published ────────────────────────────────────────────────────────── tag-published: - needs: [prepare, publish, create-github-release] - if: always() && needs.publish.result == 'success' && (inputs.slot == 'pre-release' || needs.create-github-release.result == 'success') + needs: [prepare, publish, publish-to-cbweb-marketplace, create-github-release] + if: always() && needs.prepare.result == 'success' && (needs.publish.result == 'success' || needs.publish.result == 'skipped') && (needs.publish-to-cbweb-marketplace.result == 'success' || needs.publish-to-cbweb-marketplace.result == 'skipped') && (inputs.slot == 'pre-release' || needs.create-github-release.result == 'success') runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - token: ${{ secrets.IDEE_GH_TOKEN }} + token: ${{ secrets.IDEE_GH_TOKEN || github.token }} - name: Create tracking tag env: @@ -800,7 +946,7 @@ jobs: STABLE_VERSION: ${{ needs.prepare.outputs.stable-version }} COMMIT_SHA: ${{ needs.prepare.outputs.commit-sha }} DRY_RUN: ${{ inputs.dry-run || 'false' }} - GH_TOKEN: ${{ secrets.IDEE_GH_TOKEN }} + GH_TOKEN: ${{ secrets.IDEE_GH_TOKEN || github.token }} EXTENSION_NAME: ${{ inputs.extension-name }} run: | if [ "$SLOT" = "stable" ]; then @@ -829,8 +975,8 @@ jobs: # ── commit-stable-version ──────────────────────────────────────────────── commit-stable-version: - needs: [prepare, publish, create-github-release, tag-published] - if: needs.publish.result == 'success' && inputs.slot == 'stable' + needs: [prepare, publish, publish-to-cbweb-marketplace, create-github-release, tag-published] + if: needs.publish.result == 'success' && needs.publish-to-cbweb-marketplace.result == 'skipped' && inputs.slot == 'stable' runs-on: ubuntu-latest steps: - name: Checkout main @@ -838,18 +984,19 @@ jobs: with: ref: main fetch-depth: 0 - token: ${{ secrets.IDEE_GH_TOKEN }} + token: ${{ secrets.IDEE_GH_TOKEN || github.token }} - name: Setup Node.js uses: actions/setup-node@v6 with: node-version: ${{ inputs.node-version || '22.x' }} - - name: Commit stable version bump to main + - name: Update stable version + id: update-version env: STABLE_VERSION: ${{ needs.prepare.outputs.stable-version }} DRY_RUN: ${{ inputs.dry-run || 'false' }} - GH_TOKEN: ${{ secrets.IDEE_GH_TOKEN }} + GH_TOKEN: ${{ secrets.IDEE_GH_TOKEN || github.token }} EXTENSION_NAME: ${{ inputs.extension-name }} EXTENSIONS_ROOT: ${{ inputs.extensions-root }} run: | @@ -876,11 +1023,13 @@ jobs: ') if [ "$IS_GT" != "yes" ]; then echo "Skipping commit-back: main ($CURRENT_VERSION) is already >= stable ($STABLE_VERSION)" + echo "should-commit=false" >> "$GITHUB_OUTPUT" exit 0 fi if [ "$DRY_RUN" = "true" ]; then echo "DRY RUN: Would set $PKG_DIR to $STABLE_VERSION and commit to main" + echo "should-commit=false" >> "$GITHUB_OUTPUT" exit 0 fi @@ -890,12 +1039,35 @@ jobs: ( cd "$PKG_DIR" && npm version "$STABLE_VERSION" --no-git-tag-version ) - npm install --package-lock-only --ignore-scripts + git add "$PKG_DIR/package.json" + if git diff --cached --quiet -- "$PKG_DIR/package.json"; then + echo "Package version was not updated: $PKG_DIR/package.json" + exit 1 + fi + echo "should-commit=true" >> "$GITHUB_OUTPUT" + + - name: Refresh lockfile with retries + if: steps.update-version.outputs.should-commit == 'true' + uses: salesforcecli/github-workflows/.github/actions/updateNodeLockfile@ph/W-23832274-pnpm-stable-promotion + with: + package-manager: ${{ inputs.package-manager }} + package-manager-version: ${{ inputs.package-manager-version }} + lockfile-path: ${{ inputs.lockfile-path }} - git add "$PKG_DIR/package.json" package-lock.json - if git diff --staged --quiet; then - echo "No version change to commit - skipping (idempotent rerun)" - exit 0 + - name: Commit stable version bump + if: steps.update-version.outputs.should-commit == 'true' + env: + STABLE_VERSION: ${{ needs.prepare.outputs.stable-version }} + EXTENSION_NAME: ${{ inputs.extension-name }} + EXTENSIONS_ROOT: ${{ inputs.extensions-root }} + LOCKFILE_PATH: ${{ inputs.lockfile-path }} + run: | + set -euo pipefail + git add "$LOCKFILE_PATH" + if [ -n "$(git diff --name-only)" ]; then + echo "Lockfile refresh produced unstaged changes:" + git diff --name-only + exit 1 fi git commit -m "chore: set stable version $STABLE_VERSION [skip ci]" diff --git a/.github/workflows/vscode-promote-prerelease.yml b/.github/workflows/vscode-promote-prerelease.yml index c101c5f..9f79a24 100644 --- a/.github/workflows/vscode-promote-prerelease.yml +++ b/.github/workflows/vscode-promote-prerelease.yml @@ -210,9 +210,6 @@ jobs: with: node-version: ${{ inputs.node-version || '22.x' }} - - name: Install dependencies - uses: salesforcecli/github-workflows/.github/actions/npmInstallWithRetries@main - - name: Download VSIX from nightly GitHub release env: GH_TOKEN: ${{ secrets.IDEE_GH_TOKEN }} diff --git a/.github/workflows/vscode-promote-stable.yml b/.github/workflows/vscode-promote-stable.yml index 50c2997..c641aae 100644 --- a/.github/workflows/vscode-promote-stable.yml +++ b/.github/workflows/vscode-promote-stable.yml @@ -22,7 +22,7 @@ name: Promote Pre-release to Stable # - Secrets: IDEE_GH_TOKEN, VSCE_PERSONAL_ACCESS_TOKEN, IDEE_OVSX_PAT # - Action: ./.github/actions/repackage-vsix-stable (repackages pre-release VSIX as stable) # - Action: ./.github/actions/publish-vsix (publishes to VS Code Marketplace and/or Open VSX) -# - Action: ./.github/actions/npm-install-with-retries (custom npm install with retry logic) +# - The package manager and lockfile inputs must match the caller repository. # # Note: This workflow uses local actions from the calling repository. For a fully self-contained # workflow that doesn't require local actions, use vscode-promote-prerelease.yml instead. @@ -63,6 +63,31 @@ on: required: false 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 + lockfile-path: + description: 'Single lockfile path to update and commit with the stable version' + required: false + default: 'package-lock.json' + type: string + install-command: + description: 'Command used to install dependencies' + required: false + default: 'npm ci' + type: string workflow_dispatch: inputs: extension-name: @@ -100,6 +125,32 @@ on: required: false default: '22.x' type: string + package-manager: + description: 'Package manager to use: npm, pnpm, or yarn' + required: false + default: 'npm' + type: choice + options: [npm, pnpm, yarn] + 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 + lockfile-path: + description: 'Single lockfile path to update and commit with the stable version' + required: false + default: 'package-lock.json' + type: string + install-command: + description: 'Command used to install dependencies' + required: false + default: 'npm ci' + type: string concurrency: group: promote-stable @@ -125,13 +176,14 @@ jobs: fetch-depth: 0 token: ${{ secrets.IDEE_GH_TOKEN }} - - name: Setup Node.js - uses: actions/setup-node@v6 + - name: Setup Node.js and install dependencies + uses: salesforcecli/github-workflows/.github/actions/setupNodeAndInstall@ph/W-23832274-pnpm-stable-promotion with: node-version: ${{ inputs.node-version || '22.x' }} - - - name: Install dependencies - uses: ./.github/actions/npm-install-with-retries + 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: Find latest pre-release candidate and compute stable version id: find @@ -292,9 +344,6 @@ jobs: with: node-version: ${{ inputs.node-version || '22.x' }} - - name: Install dependencies - uses: ./.github/actions/npm-install-with-retries - - name: Download stable VSIX artifact uses: actions/download-artifact@v8 with: @@ -459,7 +508,8 @@ jobs: with: node-version: ${{ inputs.node-version || '22.x' }} - - name: Commit stable version bump to main + - name: Update stable version + id: update-version env: STABLE_VERSION: ${{ needs.find-prerelease-candidate.outputs.stable-version }} DRY_RUN: ${{ inputs.dry-run || 'false' }} @@ -481,11 +531,13 @@ jobs: fi if [ "$IS_GT" != "yes" ]; then echo "Skipping commit-back: main ($CURRENT_VERSION) is already >= stable ($STABLE_VERSION)" + echo "should-commit=false" >> "$GITHUB_OUTPUT" exit 0 fi if [ "$DRY_RUN" = "true" ]; then echo "DRY RUN: Would set $PKG_DIR to $STABLE_VERSION and commit to main" + echo "should-commit=false" >> "$GITHUB_OUTPUT" exit 0 fi @@ -495,17 +547,35 @@ jobs: ( cd "$PKG_DIR" && npm version "$STABLE_VERSION" --no-git-tag-version ) - # Keep the root lockfile's workspace entry in sync with package.json. - # --package-lock-only rewrites package-lock.json from the manifests - # without installing node_modules, so the lockfile never drifts behind - # the committed version. This commit carries [skip ci], so nothing - # downstream re-derives the lockfile — it must be correct here. - npm install --package-lock-only --ignore-scripts + git add "$PKG_DIR/package.json" + if git diff --cached --quiet -- "$PKG_DIR/package.json"; then + echo "Package version was not updated: $PKG_DIR/package.json" + exit 1 + fi + echo "should-commit=true" >> "$GITHUB_OUTPUT" - git add "$PKG_DIR/package.json" package-lock.json - if git diff --staged --quiet; then - echo "No version change to commit - skipping (idempotent rerun)" - exit 0 + - name: Refresh lockfile with retries + if: steps.update-version.outputs.should-commit == 'true' + uses: salesforcecli/github-workflows/.github/actions/updateNodeLockfile@ph/W-23832274-pnpm-stable-promotion + with: + package-manager: ${{ inputs.package-manager }} + package-manager-version: ${{ inputs.package-manager-version }} + lockfile-path: ${{ inputs.lockfile-path }} + + - name: Commit stable version bump + if: steps.update-version.outputs.should-commit == 'true' + env: + STABLE_VERSION: ${{ needs.find-prerelease-candidate.outputs.stable-version }} + EXTENSION_NAME: ${{ inputs.extension-name }} + EXTENSIONS_ROOT: ${{ inputs.extensions-root }} + LOCKFILE_PATH: ${{ inputs.lockfile-path }} + run: | + set -euo pipefail + git add "$LOCKFILE_PATH" + if [ -n "$(git diff --name-only)" ]; then + echo "Lockfile refresh produced unstaged changes:" + git diff --name-only + exit 1 fi git commit -m "chore: set stable version $STABLE_VERSION [skip ci]" diff --git a/.github/workflows/vscode-publish-extensions.yml b/.github/workflows/vscode-publish-extensions.yml index f5e24fa..bf7a885 100644 --- a/.github/workflows/vscode-publish-extensions.yml +++ b/.github/workflows/vscode-publish-extensions.yml @@ -203,7 +203,7 @@ jobs: ref: ${{ inputs.branch || github.ref }} - name: Setup Node.js and install dependencies - uses: salesforcecli/github-workflows/.github/actions/setupNodeAndInstall@main + uses: salesforcecli/github-workflows/.github/actions/setupNodeAndInstall@ph/W-23832274-pnpm-stable-promotion with: node-version: ${{ inputs.node-version || '22.x' }} package-manager: ${{ inputs.package-manager }} @@ -326,7 +326,7 @@ jobs: fetch-depth: 0 - name: Setup Node.js and install dependencies - uses: salesforcecli/github-workflows/.github/actions/setupNodeAndInstall@main + uses: salesforcecli/github-workflows/.github/actions/setupNodeAndInstall@ph/W-23832274-pnpm-stable-promotion with: node-version: ${{ inputs.node-version || '22.x' }} package-manager: ${{ inputs.package-manager }} @@ -348,6 +348,9 @@ jobs: run: | set -e + # workflow_dispatch string inputs can retain pasted whitespace. + BRANCH=$(echo "$BRANCH" | xargs) + case "$PACKAGE_MANAGER" in pnpm) unexpected_lockfiles=(package-lock.json yarn.lock) ;; yarn) unexpected_lockfiles=(package-lock.json pnpm-lock.yaml) ;; @@ -545,7 +548,7 @@ jobs: - name: Validate GitHub authentication if: inputs.dry-run != 'true' && github.event.inputs.dry-run != 'true' env: - GH_TOKEN: ${{ secrets.IDEE_GH_TOKEN }} + GH_TOKEN: ${{ secrets.IDEE_GH_TOKEN || github.token }} run: | # Validate that required tokens are present if [ -z "$GH_TOKEN" ]; then @@ -565,13 +568,15 @@ jobs: # Manually push the missing tags: git push origin --tags - name: Commit version bumps with tags env: - # Ensure GitHub CLI has proper authentication - GITHUB_TOKEN: ${{ secrets.IDEE_GH_TOKEN }} + # Use the IDE token when configured; otherwise retain GitHub's workflow token. + GH_TOKEN: ${{ secrets.IDEE_GH_TOKEN || github.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: | + BRANCH=$(echo "$BRANCH" | xargs) + if [ "$DRY_RUN" = "true" ]; then echo "🔄 DRY RUN: Would commit and push version bumps..." echo "📋 DRY RUN: Changes that would be committed:" @@ -599,8 +604,8 @@ jobs: export GIT_COMMITTER_NAME="$GIT_USER_NAME" export GIT_COMMITTER_EMAIL="$GIT_USER_EMAIL" - # Configure git to use the PAT for authentication - git remote set-url origin https://x-access-token:${{ secrets.IDEE_GH_TOKEN }}@github.com/${{ github.repository }}.git + # Configure git to use the effective workflow token for authentication. + git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" # Add all changes # Note: git add . respects .gitignore, so ignored files won't be added @@ -673,7 +678,7 @@ jobs: package: needs: [bump-versions, calculate-artifact-name] - uses: salesforcecli/github-workflows/.github/workflows/vscode-package.yml@main + uses: salesforcecli/github-workflows/.github/workflows/vscode-package.yml@ph/W-23832274-pnpm-stable-promotion with: node-version: ${{ inputs.node-version }} branch: ${{ inputs.branch || github.ref_name }} @@ -701,7 +706,7 @@ jobs: uses: actions/checkout@v6 - name: Setup Node.js and install dependencies - uses: salesforcecli/github-workflows/.github/actions/setupNodeAndInstall@main + uses: salesforcecli/github-workflows/.github/actions/setupNodeAndInstall@ph/W-23832274-pnpm-stable-promotion with: node-version: ${{ inputs.node-version || '22.x' }} package-manager: ${{ inputs.package-manager }} @@ -948,7 +953,7 @@ jobs: token: ${{ secrets.IDEE_GH_TOKEN || github.token }} - name: Setup Node.js and install dependencies - uses: salesforcecli/github-workflows/.github/actions/setupNodeAndInstall@main + uses: salesforcecli/github-workflows/.github/actions/setupNodeAndInstall@ph/W-23832274-pnpm-stable-promotion with: node-version: ${{ inputs.node-version || '22.x' }} package-manager: ${{ inputs.package-manager }} @@ -964,7 +969,7 @@ jobs: - name: Create GitHub releases env: - GITHUB_TOKEN: ${{ secrets.IDEE_GH_TOKEN }} + GH_TOKEN: ${{ secrets.IDEE_GH_TOKEN || github.token }} GITHUB_REPOSITORY: ${{ github.repository }} SELECTED_EXTENSIONS: ${{ needs.determine-changes.outputs.selected-extensions }} IS_NIGHTLY: ${{ inputs.nightly && 'true' || 'false' }} @@ -976,6 +981,8 @@ jobs: EXTENSIONS_ROOT: ${{ inputs.extensions-root || 'packages' }} EXCLUDE_WEB: ${{ inputs.exclude-web-vsix || 'false' }} run: | + BRANCH=$(echo "$BRANCH" | xargs) + echo "Mode: $([ "$DRY_RUN" = "true" ] && echo "DRY RUN" || echo "LIVE")" echo "Creating GitHub releases..." @@ -1205,6 +1212,7 @@ jobs: if: inputs.dry-run != 'true' && github.event.inputs.dry-run != 'true' && needs.publish.result == 'success' uses: slackapi/slack-github-action@v3.0.3 with: + webhook-type: incoming-webhook payload: | { "text": "${{ inputs.slack-notification-title }}", @@ -1338,6 +1346,7 @@ jobs: if: inputs.dry-run != 'true' && github.event.inputs.dry-run != 'true' && (needs.publish.result == 'failure' || needs.bump-versions.result == 'failure' || needs.package.result == 'failure') uses: slackapi/slack-github-action@v3.0.3 with: + webhook-type: incoming-webhook payload: | { "text": "❌ VS Code Extension Release Failed!",