diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c73b28f631..4062524555 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,6 +74,7 @@ jobs: id: params env: EVENT_NAME: ${{ github.event_name }} + WORKFLOW_REF: ${{ github.ref }} PUSH_SHA: ${{ github.sha }} INPUT_CHANNEL: ${{ inputs.channel }} INPUT_REF: ${{ inputs.ref }} @@ -82,6 +83,10 @@ jobs: CHANNEL=insider REF="$PUSH_SHA" else + if [ "$WORKFLOW_REF" != "refs/heads/main" ]; then + echo "Refusing to release from workflow ref '$WORKFLOW_REF'. Marketplace authentication is federated to main." >&2 + exit 1 + fi CHANNEL="$INPUT_CHANNEL" REF="${INPUT_REF:-main}" case "$REF" in @@ -260,6 +265,9 @@ jobs: needs: [plan, release] runs-on: ubuntu-latest timeout-minutes: 10 + permissions: + contents: read + id-token: write # Required by azure/login and vsce --azure-credential. env: ASSET_FILE: ${{ needs.plan.outputs.asset_name }}-${{ needs.plan.outputs.version }}.vsix steps: @@ -280,10 +288,15 @@ jobs: uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: vsix + - name: Log in to Azure + uses: azure/login@7ddb5af1ef8758cf1353cf3b42f940aee27ba21c # v3.0.2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + allow-no-subscriptions: true - name: Publish vsix to marketplace - env: - VSCE_PAT: ${{ secrets.AZURE_DEVOPS_PERSONAL_ACCESS_TOKEN }} - run: cd packages/vscode && npx vsce publish --packagePath "$GITHUB_WORKSPACE/$ASSET_FILE" + run: >- + cd packages/vscode && npx vsce publish --azure-credential --packagePath "$GITHUB_WORKSPACE/$ASSET_FILE" publish-open-vsx: name: Publish to open-vsx diff --git a/docs/ci-cd.md b/docs/ci-cd.md index b276007622..c642a0bec0 100644 --- a/docs/ci-cd.md +++ b/docs/ci-cd.md @@ -54,14 +54,83 @@ graph TD ### Permissions -The workflow default is `contents: read`. Only two jobs are granted more: -`release` (`contents: write`, to create the tag and GitHub release) and -`publish-language-server` (`id-token: write`, for npm Trusted Publishers). -Every checkout sets `persist-credentials: false`, so no job has a git +The workflow default is `contents: read`. Three jobs are granted additional +permissions: `release` (`contents: write`, to create the tag and GitHub +release), `publish-language-server` (`id-token: write`, for npm Trusted +Publishers), and `publish-marketplace` (`id-token: write`, to authenticate to +Azure). Every checkout sets `persist-credentials: false`, so no job has a git credential in its config while running dependency code. Releases only run from `main` or an `x.y.x` patch branch; `plan` rejects any -other `ref`. +other source `ref`. The release workflow itself must be launched from `main` +so its OIDC subject matches the Azure federated credential. A patch release +can still select an `x.y.x` source branch with the `ref` input. + +### VS Code Marketplace authentication + +`publish-marketplace` uses GitHub OIDC to sign in as a user-assigned Azure +managed identity, then runs `vsce publish --azure-credential`. It has no +Personal Access Token fallback: an OIDC, Azure login, or Marketplace +permissions failure stops the job. Open VSX is separate and continues to use +`OPEN_VSX_ACCESS_TOKEN`. + +The Azure and GitHub setup is external to this repository: + +1. Create a user-assigned managed identity in the Azure tenant used for + Marketplace publishing. The workflow authenticates with + `allow-no-subscriptions: true`, so the identity does not need an Azure RBAC + role assignment. +2. Add a federated credential to that identity with these exact values: + + | Field | Value | + | -------- | ------------------------------------------------ | + | Issuer | `https://token.actions.githubusercontent.com` | + | Subject | `repo:prisma/language-tools:ref:refs/heads/main` | + | Audience | `api://AzureADTokenExchange` | + + For example: + + ```bash + az identity federated-credential create \ + --name language-tools-github-main \ + --identity-name \ + --resource-group \ + --issuer https://token.actions.githubusercontent.com \ + --subject repo:prisma/language-tools:ref:refs/heads/main \ + --audiences api://AzureADTokenExchange + ``` + +3. Authenticate as the managed identity and query its Azure DevOps profile to + obtain the Marketplace resource ID (the `id` field, which is not the Azure + client ID or object ID): + + ```bash + az rest \ + --url 'https://app.vssps.visualstudio.com/_apis/profile/profiles/me?api-version=7.1' \ + --resource 499b84ac-1321-427f-aa17-267ca6975798 + ``` + + One way to run this command is a temporary GitHub Actions step immediately + after the `azure/login` step. Do not print or retain access tokens. + +4. On the [Visual Studio Marketplace publisher management + page](https://marketplace.visualstudio.com/manage), add that resource ID as + a member of the Prisma publisher and assign the **Contributor** role. +5. In **GitHub → prisma/language-tools → Settings → Secrets and variables → + Actions → Secrets**, create both values as repository secrets: + + | Name | Storage | Value | + | ----------------- | ----------------- | -------------------------- | + | `AZURE_CLIENT_ID` | Repository secret | Managed identity client ID | + | `AZURE_TENANT_ID` | Repository secret | Microsoft Entra tenant ID | + +6. Launch `release.yml` from `main` and verify an insider release. After it + publishes successfully, delete the obsolete + `AZURE_DEVOPS_PERSONAL_ACCESS_TOKEN` Actions secret. + +Microsoft's [VS Code extension publishing +instructions](https://code.visualstudio.com/api/working-with-extensions/publishing-extension#secure-automated-publishing-to-visual-studio-marketplace) +describe the managed-identity Marketplace authorization flow. ### Channels diff --git a/packages/vscode/.env_sample b/packages/vscode/.env_sample index 5ed3b0b1ae..9d74d8752f 100644 --- a/packages/vscode/.env_sample +++ b/packages/vscode/.env_sample @@ -1,2 +1 @@ ENVIRONMENT=<1> # Default to dry-run, to actually publish, use ENVIRONMENT=PRODUCTION -AZURE_DEVOPS_PERSONAL_ACCESS_TOKEN= # https://code.visualstudio.com/api/working-with-extensions/publishing-extension#get-a-personal-access-token \ No newline at end of file