From 28fd974c2662a4cb4864630a7d18aff1c144dd3d Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Wed, 29 Jul 2026 15:45:20 +0000 Subject: [PATCH 1/3] ci: harden GitHub Actions workflows - Add least-privilege permissions blocks at workflow and job level so jobs only receive the scopes they need (contents: read by default, write only where releases are created). - Set persist-credentials: false on all actions/checkout steps that do not push, so the repo-scoped token is not left on disk for later steps and node_modules scripts. - pre-release: verify the release tag commit is an ancestor of origin/main before building, preventing releases from tags pointing at unreviewed commits. - publish-extension: gate marketplace publish jobs behind a 'publish' environment, pin vsce and ovsx to exact versions instead of installing latest at run time, and pass marketplace PATs via env vars instead of argv (argv is world-readable via /proc on the runner). --- .github/workflows/ci.yaml | 13 +++++++++ .github/workflows/pre-release.yaml | 18 ++++++++++-- .github/workflows/publish-extension.yaml | 37 ++++++++++++++++++++---- .github/workflows/release.yaml | 9 ++++-- 4 files changed, 65 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6917ff8021..631358c42f 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -9,6 +9,9 @@ on: workflow_dispatch: +permissions: + contents: read + jobs: lint: name: Lint @@ -16,6 +19,8 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false - name: Setup pnpm, Node.js, and dependencies uses: ./.github/actions/setup @@ -48,6 +53,8 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false - name: Setup pnpm, Node.js, and dependencies uses: ./.github/actions/setup @@ -71,6 +78,8 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false - name: Setup pnpm, Node.js, and dependencies uses: ./.github/actions/setup @@ -88,6 +97,8 @@ jobs: if: github.repository_owner == 'coder' && github.actor != 'dependabot[bot]' steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false - name: Setup pnpm, Node.js, and dependencies uses: ./.github/actions/setup @@ -107,6 +118,8 @@ jobs: runs-on: ubuntu-24.04 steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false - name: Setup pnpm, Node.js, and dependencies uses: ./.github/actions/setup diff --git a/.github/workflows/pre-release.yaml b/.github/workflows/pre-release.yaml index 81ab098207..bfc6321de9 100644 --- a/.github/workflows/pre-release.yaml +++ b/.github/workflows/pre-release.yaml @@ -5,9 +5,7 @@ on: - "v*-pre" permissions: - # Required to publish a release - contents: write - pull-requests: read + contents: read jobs: package: @@ -17,6 +15,16 @@ jobs: version: ${{ steps.version.outputs.version }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Verify tag is on main + run: | + if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then + echo "Error: Pre-release tags must be pushed from the main branch" + exit 1 + fi - name: Setup pnpm, Node.js, and dependencies uses: ./.github/actions/setup @@ -65,6 +73,10 @@ jobs: publish: name: Publish Extension and Create Pre-Release needs: package + permissions: + # Required to create the GitHub release + contents: write + pull-requests: read uses: ./.github/workflows/publish-extension.yaml with: version: ${{ needs.package.outputs.version }} diff --git a/.github/workflows/publish-extension.yaml b/.github/workflows/publish-extension.yaml index 60758fbf00..a3798d6b11 100644 --- a/.github/workflows/publish-extension.yaml +++ b/.github/workflows/publish-extension.yaml @@ -22,12 +22,16 @@ jobs: setup: name: Setup runs-on: ubuntu-24.04 + permissions: + contents: read outputs: packageName: ${{ steps.package.outputs.packageName }} hasVscePat: ${{ steps.check-secrets.outputs.hasVscePat }} hasOvsxPat: ${{ steps.check-secrets.outputs.hasOvsxPat }} steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: @@ -58,6 +62,11 @@ jobs: name: Publish to VS Marketplace needs: setup runs-on: ubuntu-24.04 + # Protected environment: configure required reviewers and (ideally) move + # VSCE_PAT/OVSX_PAT into it so publishing needs an explicit approval. + environment: publish + permissions: + contents: read if: ${{ needs.setup.outputs.hasVscePat == 'true' }} steps: - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 @@ -65,25 +74,33 @@ jobs: node-version: "22" - name: Install vsce - run: npm install -g @vscode/vsce + # Pinned to the version locked in pnpm-lock.yaml; keep in sync. + run: npm install -g @vscode/vsce@3.9.2 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: extension-${{ inputs.version }} - name: Publish to VS Marketplace + env: + # vsce reads VSCE_PAT from the environment; never pass tokens as + # command-line arguments (visible in process listings). + VSCE_PAT: ${{ secrets.VSCE_PAT }} run: | echo "Publishing version ${{ inputs.version }} to VS Marketplace" if [ "${{ inputs.isPreRelease }}" = "true" ]; then - vsce publish --pre-release --packagePath "./${{ needs.setup.outputs.packageName }}" -p ${{ secrets.VSCE_PAT }} + vsce publish --pre-release --packagePath "./${{ needs.setup.outputs.packageName }}" else - vsce publish --packagePath "./${{ needs.setup.outputs.packageName }}" -p ${{ secrets.VSCE_PAT }} + vsce publish --packagePath "./${{ needs.setup.outputs.packageName }}" fi publishOVSX: name: Publish to Open VSX needs: setup runs-on: ubuntu-24.04 + environment: publish + permissions: + contents: read if: ${{ needs.setup.outputs.hasOvsxPat == 'true' }} steps: - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 @@ -91,25 +108,33 @@ jobs: node-version: "22" - name: Install ovsx - run: npm install -g ovsx + run: npm install -g ovsx@1.0.2 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: extension-${{ inputs.version }} - name: Publish to Open VSX + env: + # ovsx reads OVSX_PAT from the environment; never pass tokens as + # command-line arguments (visible in process listings). + OVSX_PAT: ${{ secrets.OVSX_PAT }} run: | echo "Publishing version ${{ inputs.version }} to Open VSX" if [ "${{ inputs.isPreRelease }}" = "true" ]; then - ovsx publish "./${{ needs.setup.outputs.packageName }}" --pre-release -p ${{ secrets.OVSX_PAT }} + ovsx publish "./${{ needs.setup.outputs.packageName }}" --pre-release else - ovsx publish "./${{ needs.setup.outputs.packageName }}" -p ${{ secrets.OVSX_PAT }} + ovsx publish "./${{ needs.setup.outputs.packageName }}" fi publishGH: name: Create GitHub ${{ inputs.isPreRelease && 'Pre-' || '' }}Release needs: setup runs-on: ubuntu-24.04 + permissions: + # Required to create the release and generate release notes + contents: write + pull-requests: read steps: - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d5d0a5597f..6de07d0097 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -6,9 +6,7 @@ on: - "!v*-pre" permissions: - # Required to publish a release - contents: write - pull-requests: read + contents: read jobs: package: @@ -20,6 +18,7 @@ jobs: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: fetch-depth: 0 + persist-credentials: false - name: Verify tag is on main run: | @@ -74,6 +73,10 @@ jobs: publish: name: Publish Extension and Create Release needs: package + permissions: + # Required to create the GitHub release + contents: write + pull-requests: read uses: ./.github/workflows/publish-extension.yaml with: version: ${{ needs.package.outputs.version }} From f6ec3b7dc9562cf247c008607c26227e11a813ad Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Wed, 29 Jul 2026 15:59:33 +0000 Subject: [PATCH 2/3] ci: allow pre-release tags off main Pre-release tags are intentionally used to test builds without merging everything to main first, so drop the ancestor check and full fetch; stable release.yaml keeps its existing tag-on-main verification. --- .github/workflows/pre-release.yaml | 8 -------- 1 file changed, 8 deletions(-) diff --git a/.github/workflows/pre-release.yaml b/.github/workflows/pre-release.yaml index bfc6321de9..eb9361f7ed 100644 --- a/.github/workflows/pre-release.yaml +++ b/.github/workflows/pre-release.yaml @@ -16,16 +16,8 @@ jobs: steps: - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: - fetch-depth: 0 persist-credentials: false - - name: Verify tag is on main - run: | - if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then - echo "Error: Pre-release tags must be pushed from the main branch" - exit 1 - fi - - name: Setup pnpm, Node.js, and dependencies uses: ./.github/actions/setup From 8b25074f3c82a34d4a3c4d475f411ad19a84e6a4 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Wed, 29 Jul 2026 21:52:59 +0000 Subject: [PATCH 3/3] ci: simplify vsce version extraction from lockfile The setup job extracts the locked @vscode/vsce version from pnpm-lock.yaml (fails closed on a miss via pipefail) and the Marketplace publish job installs exactly that version, so packaging and publishing always run the same tool version. --- .github/workflows/publish-extension.yaml | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/.github/workflows/publish-extension.yaml b/.github/workflows/publish-extension.yaml index a3798d6b11..e9a36cf5a3 100644 --- a/.github/workflows/publish-extension.yaml +++ b/.github/workflows/publish-extension.yaml @@ -26,6 +26,7 @@ jobs: contents: read outputs: packageName: ${{ steps.package.outputs.packageName }} + vsceVersion: ${{ steps.versions.outputs.vsceVersion }} hasVscePat: ${{ steps.check-secrets.outputs.hasVscePat }} hasOvsxPat: ${{ steps.check-secrets.outputs.hasOvsxPat }} steps: @@ -49,6 +50,15 @@ jobs: echo "packageName=$PACKAGE_NAME" >> $GITHUB_OUTPUT echo "Package name: $PACKAGE_NAME" + - name: Determine vsce version from lockfile + id: versions + # Explicit bash enables pipefail: an extraction miss fails the job + # instead of silently installing latest. + shell: bash + run: | + VSCE_VERSION="$(grep -m 1 -o "@vscode/vsce@[0-9][^'(]*" pnpm-lock.yaml | cut -d @ -f 3)" + echo "vsceVersion=$VSCE_VERSION" >> "$GITHUB_OUTPUT" + - name: Check secrets id: check-secrets env: @@ -62,8 +72,6 @@ jobs: name: Publish to VS Marketplace needs: setup runs-on: ubuntu-24.04 - # Protected environment: configure required reviewers and (ideally) move - # VSCE_PAT/OVSX_PAT into it so publishing needs an explicit approval. environment: publish permissions: contents: read @@ -74,8 +82,9 @@ jobs: node-version: "22" - name: Install vsce - # Pinned to the version locked in pnpm-lock.yaml; keep in sync. - run: npm install -g @vscode/vsce@3.9.2 + env: + VSCE_VERSION: ${{ needs.setup.outputs.vsceVersion }} + run: npm install -g "@vscode/vsce@${VSCE_VERSION}" - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: @@ -83,8 +92,6 @@ jobs: - name: Publish to VS Marketplace env: - # vsce reads VSCE_PAT from the environment; never pass tokens as - # command-line arguments (visible in process listings). VSCE_PAT: ${{ secrets.VSCE_PAT }} run: | echo "Publishing version ${{ inputs.version }} to VS Marketplace" @@ -116,8 +123,6 @@ jobs: - name: Publish to Open VSX env: - # ovsx reads OVSX_PAT from the environment; never pass tokens as - # command-line arguments (visible in process listings). OVSX_PAT: ${{ secrets.OVSX_PAT }} run: | echo "Publishing version ${{ inputs.version }} to Open VSX"