Skip to content

Commit 1f62625

Browse files
Use Entra ID + GitHub OIDC for Marketplace publish (drop stored PAT) (#9)
Replaces the long-lived VSCE_PAT secret with Microsoft Entra ID workload identity federation: GitHub mints a short-lived OIDC token per run and exchanges it for an Entra access token via `az login --federated-token`, then `vsce publish --azure-credential`. No publishing credential is ever stored in the repo. - marketplace-publish.yml: id-token: write; OIDC token-exchange flow using the preinstalled Azure CLI (no third-party action to SHA-pin); gated on AZURE_CLIENT_ID / AZURE_TENANT_ID repo variables; Open VSX still uses OVSX_PAT since it has no Entra equivalent - docs/PUBLISHING.adoc: document the one-time Entra app-registration / federated-credential setup; PAT demoted to a local-only fallback - ROADMAP.adoc: updated to reflect OIDC https://claude.ai/code/session_0111iLmV7VFMTFGigST1M1cb Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0293d8a commit 1f62625

3 files changed

Lines changed: 129 additions & 47 deletions

File tree

.github/workflows/marketplace-publish.yml

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,26 @@
44
# Marketplace publication — triggered by version tags (v*).
55
# Packages the extension into a .vsix and publishes to the VS Code
66
# Marketplace and the Open VSX Registry. Each publish step is gated on
7-
# the presence of its credential, so a missing token skips (not fails)
8-
# that registry.
7+
# the presence of its credential, so a missing credential skips (not
8+
# fails) that registry.
99
#
10-
# Required repository secrets (Settings -> Secrets and variables -> Actions):
11-
# VSCE_PAT Azure DevOps PAT for the `hyperpolymath` Marketplace publisher
12-
# https://dev.azure.com -> User settings -> Personal access tokens
13-
# Organization: All accessible | Scope: Marketplace > Manage
14-
# OVSX_PAT Open VSX Registry access token (optional, open-source registry)
15-
# https://open-vsx.org -> Settings -> Access Tokens
10+
# VS Code Marketplace auth: Microsoft Entra ID via GitHub OIDC workload
11+
# identity federation. No long-lived secret is stored — GitHub mints a
12+
# short-lived token per run and exchanges it for an Entra access token.
13+
#
14+
# Required repository configuration
15+
# (Settings -> Secrets and variables -> Actions):
16+
#
17+
# Variables (not sensitive):
18+
# AZURE_CLIENT_ID App registration (service principal) client ID,
19+
# with a federated credential trusting
20+
# repo:hyperpolymath/vscode-a2ml, and the Marketplace
21+
# publisher role granted to that identity.
22+
# AZURE_TENANT_ID Entra tenant ID that owns the publisher.
23+
#
24+
# Secrets:
25+
# OVSX_PAT Open VSX Registry token (optional, open-source
26+
# registry; Open VSX has no Entra/OIDC equivalent).
1627
#
1728
# Manual one-off alternative (no CI): see docs/PUBLISHING.adoc
1829
name: Marketplace Publish
@@ -32,6 +43,7 @@ jobs:
3243
runs-on: ubuntu-latest
3344
permissions:
3445
contents: read
46+
id-token: write # required to mint the GitHub OIDC token for Entra
3547
steps:
3648
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
3749

@@ -63,15 +75,36 @@ jobs:
6375
path: ./*.vsix
6476
retention-days: 30
6577

66-
- name: Publish to VS Code Marketplace
78+
- name: Publish to VS Code Marketplace (Entra OIDC)
6779
env:
68-
VSCE_PAT: ${{ secrets.VSCE_PAT }}
80+
AZURE_CLIENT_ID: ${{ vars.AZURE_CLIENT_ID }}
81+
AZURE_TENANT_ID: ${{ vars.AZURE_TENANT_ID }}
6982
run: |
70-
if [ -z "${VSCE_PAT:-}" ]; then
71-
echo "::warning::VSCE_PAT secret not set — skipping VS Code Marketplace publish."
83+
set -euo pipefail
84+
if [ -z "${AZURE_CLIENT_ID:-}" ] || [ -z "${AZURE_TENANT_ID:-}" ]; then
85+
echo "::warning::AZURE_CLIENT_ID / AZURE_TENANT_ID not set — skipping VS Code Marketplace publish."
7286
exit 0
7387
fi
74-
deno run -A --node-modules-dir=auto npm:@vscode/vsce@3.2.1 publish --packagePath ./*.vsix --pat "$VSCE_PAT"
88+
89+
# Mint a GitHub OIDC token scoped to the Entra token-exchange audience.
90+
# az preinstalled on ubuntu-latest; no third-party action needed.
91+
OIDC_TOKEN="$(curl -sS \
92+
-H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \
93+
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=api://AzureADTokenExchange" \
94+
| jq -r '.value')"
95+
96+
az login --service-principal \
97+
--username "$AZURE_CLIENT_ID" \
98+
--tenant "$AZURE_TENANT_ID" \
99+
--federated-token "$OIDC_TOKEN" \
100+
--allow-no-subscriptions >/dev/null
101+
102+
# vsce --azure-credential uses DefaultAzureCredential, which picks
103+
# up the az CLI session above. No PAT is ever stored or printed.
104+
deno run -A --node-modules-dir=auto npm:@vscode/vsce@3.2.1 \
105+
publish --azure-credential --packagePath ./*.vsix
106+
107+
az logout >/dev/null 2>&1 || true
75108
76109
- name: Publish to Open VSX Registry
77110
env:

ROADMAP.adoc

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@ Extension work is in pre-publication state.
1616
* [x] PNG icon generated (Marketplace rejects SVG icons)
1717
* [x] Automated `Marketplace Publish` workflow (tag-triggered, VS Code + Open VSX)
1818
* [x] Publishing runbook: link:docs/PUBLISHING.adoc[docs/PUBLISHING.adoc]
19-
* [ ] Acquire Azure DevOps Marketplace PAT for publisher account (human step)
20-
* [ ] Add `VSCE_PAT` repository secret, then push a `v*` tag to publish
19+
* [x] Entra ID + GitHub OIDC auth (no stored PAT for Marketplace)
20+
* [ ] One-time Entra setup: app registration + federated credential +
21+
grant publisher rights (human step)
22+
* [ ] Set `AZURE_CLIENT_ID` / `AZURE_TENANT_ID` repo variables, then push
23+
a `v*` tag to publish
2124

2225
=== v1.0.0 - Stable Release
2326
* [ ] Full feature set

docs/PUBLISHING.adoc

Lines changed: 78 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,59 +5,105 @@
55

66
How this extension gets "online" — i.e. installable from a marketplace.
77

8-
== The one step only a human can do
9-
10-
Publishing requires an *Azure DevOps Personal Access Token (PAT)* bound to
11-
the `hyperpolymath` Marketplace publisher. This is a credential that must be
12-
generated interactively while signed into the Microsoft/Azure account that
13-
owns the publisher. No automation (CI or otherwise) can create it. This is
14-
the single blocker that has kept issue #1 open.
15-
16-
=== Create the PAT (5 minutes, once)
17-
18-
. Sign in at https://dev.azure.com with the account that owns the
19-
`hyperpolymath` Marketplace publisher.
20-
. Top-right avatar → *Personal access tokens* → *New Token*.
21-
. Organization: *All accessible organizations*.
22-
. Scopes: *Custom defined* → expand *Marketplace* → tick *Manage*.
23-
. Create, then *copy the token now* (it is shown only once).
8+
== Authentication: Entra ID via OIDC (no stored secret)
9+
10+
The VS Code Marketplace publish uses *Microsoft Entra ID* through *GitHub
11+
OIDC workload identity federation*. No long-lived Personal Access Token is
12+
stored anywhere: GitHub mints a short-lived token for each workflow run and
13+
exchanges it for an Entra access token. This removes the leak / theft /
14+
rotation risk inherent to PATs.
15+
16+
The one-time setup below must be done by a human with admin rights in the
17+
Entra tenant that owns the `hyperpolymath` publisher. No automation can
18+
perform it.
19+
20+
=== One-time Entra setup
21+
22+
. *App registration* — in the Entra tenant that owns the publisher, create
23+
an app registration (service principal). Note its *Application (client)
24+
ID* and the *Directory (tenant) ID*.
25+
. *Federated credential* — on that app registration:
26+
*Certificates & secrets → Federated credentials → Add credential*.
27+
Scenario: *GitHub Actions deploying Azure resources*.
28+
+
29+
[cols="1,2"]
30+
|===
31+
|Field |Value
32+
33+
|Organization |`hyperpolymath`
34+
|Repository |`vscode-a2ml`
35+
|Entity type |`Branch` (e.g. `main`) or `Tag` (e.g. `v*`) or `Environment`
36+
|Audience |`api://AzureADTokenExchange` (default)
37+
|===
38+
. *Grant publisher rights* — sign in to
39+
https://marketplace.visualstudio.com/manage/publishers/ as the publisher
40+
owner and add the service principal as a member of the `hyperpolymath`
41+
publisher (Manage rights). If the publisher does not exist yet, create it
42+
there once; the publisher ID must equal the `publisher` field in
43+
`package.json`.
44+
. *Expose the IDs to CI* — in GitHub:
45+
*Settings → Secrets and variables → Actions → Variables → New repository
46+
variable*:
47+
+
48+
[cols="1,2"]
49+
|===
50+
|Variable |Value
2451

25-
If the `hyperpolymath` publisher does not yet exist, create it once at
26-
https://marketplace.visualstudio.com/manage/publishers/ (publisher ID must
27-
equal the `publisher` field in `package.json`).
52+
|`AZURE_CLIENT_ID` |Application (client) ID from step 1
53+
|`AZURE_TENANT_ID` |Directory (tenant) ID from step 1
54+
|===
55+
+
56+
These are *variables*, not secrets — they are identifiers, not
57+
credentials, and confer no access without the federated trust.
58+
. (Optional, open-source registry) Open VSX has no Entra/OIDC equivalent,
59+
so it still uses a token. Add secret `OVSX_PAT` from
60+
https://open-vsx.org → Settings → Access Tokens. Keep it short-expiry
61+
and rotate routinely; omit it entirely to skip Open VSX.
2862

2963
== Automated path (recommended)
3064

31-
. In GitHub: *Settings → Secrets and variables → Actions → New repository
32-
secret*.
33-
. Add `VSCE_PAT` = the token from above.
34-
. (Optional, open-source registry) add `OVSX_PAT` from
35-
https://open-vsx.org → Settings → Access Tokens.
36-
. Bump `version` in `package.json`, commit, then tag and push:
37-
+
65+
After the one-time setup, publishing is a tag push:
66+
3867
[source,bash]
3968
----
69+
# bump "version" in package.json first, then:
70+
git commit -am "Release v0.1.0"
4071
git tag v0.1.0
4172
git push origin v0.1.0
4273
----
4374

44-
The `Marketplace Publish` workflow packages a `.vsix` and publishes to the
45-
VS Code Marketplace (and Open VSX if `OVSX_PAT` is set). Registries without
46-
a configured token are skipped, not failed. The workflow can also be run
47-
manually via *Actions → Marketplace Publish → Run workflow*.
75+
The `Marketplace Publish` workflow packages a `.vsix`, authenticates to the
76+
VS Code Marketplace via Entra OIDC, and publishes (plus Open VSX if
77+
`OVSX_PAT` is set). Registries without configured credentials are skipped,
78+
not failed. The workflow can also be run manually via
79+
*Actions → Marketplace Publish → Run workflow*.
4880

4981
== Manual one-off path (no CI)
5082

51-
Requires Deno (the repo's standard toolchain; npm/bun are blocked).
83+
OIDC federation only works from GitHub Actions. For a local one-off publish
84+
you must use an Azure DevOps PAT. Treat this as a fallback, not the norm:
85+
scope it to *Marketplace → Manage* only, set a short expiry, and never
86+
commit or persist it.
5287

5388
[source,bash]
5489
----
55-
export VSCE_PAT=... # the Azure DevOps PAT
90+
# Create at https://dev.azure.com -> Personal access tokens
91+
# Organization: All accessible | Scope: Marketplace > Manage
92+
export VSCE_PAT=...
5693
deno run -A --node-modules-dir=auto npm:@vscode/vsce@3.2.1 package
5794
deno run -A --node-modules-dir=auto npm:@vscode/vsce@3.2.1 \
5895
publish --packagePath ./*.vsix --pat "$VSCE_PAT"
5996
----
6097

98+
Alternatively, if you have the Azure CLI logged in to the publisher's
99+
tenant (`az login`), you can skip the PAT locally too:
100+
101+
[source,bash]
102+
----
103+
deno run -A --node-modules-dir=auto npm:@vscode/vsce@3.2.1 \
104+
publish --azure-credential --packagePath ./*.vsix
105+
----
106+
61107
== Verifying it is online
62108

63109
* VS Code Marketplace:

0 commit comments

Comments
 (0)