- Publisher:
CurateLabsAI - Extension name:
graphforge - Fully qualified id:
CurateLabsAI.graphforge - Marketplace: Visual Studio Marketplace and Open VSX (for VS Code-compatible editors that can't use the Marketplace, e.g. VSCodium, Cursor before it added Marketplace support in some configurations).
An external documentation site (for example a GraphForge docs-site build) must
only consume docs/published/, never the rest of docs/.
docs/published/— user-facing pages: install/setup (Node default, Python-prefer in Python-first repos,uvneverpip), the command map, agent interop, and a marketplace overview. Kept in sync with the rootREADME.mdandpackage.json, which remain the source of truth for command IDs and settings.- Everything else under
docs/— contributor living docs (PRODUCT.md,DESIGN.md,experience/,strategy/, requirements, engineering ADRs). Readable in this public repo, but not Marketplace copy and not for docs-site ingestion. - If a docs-site build ingests this repo, sync only
docs/published/*.md(allowlist), not the full tree. - This repo does not host its own documentation site; publishing here means
Marketplace/Open VSX packaging (below) plus keeping
docs/published/accurate.
- Ensure the
CurateLabsAIMarketplace publisher exists (Publisher management). Marketplace publication is performed only by Azure Pipelines through thegraphforge-marketplace-publishingworkload-identity service connection. Its user-assigned managed identity must be a Contributor of the publisher. - Ensure a matching Open VSX namespace (
CurateLabsAI) and access token exist (open-vsx.org namespace docs) asOVSX_PAT, likewise stored in the ESC environment. - Restrict the Marketplace service connection to the approved
mainrelease branch via Azure DevOps branch control. That check is enforced outside YAML, so a selected branch cannot bypass the publishing gate.
The Visual Studio Marketplace path intentionally has no PAT. It uses a user-assigned Azure managed identity, an Azure Resource Manager service connection, and workload identity federation. These names are the durable configuration contract:
| Surface | Value |
|---|---|
| Azure DevOps organization/project | CurateLabs / GraphForge |
| Azure pipeline | CurateLabs.graphforge-vscode |
| Service connection | graphforge-marketplace-publishing |
| Azure resource group | graphforge-publishing |
| User-assigned managed identity | graphforge-marketplace-publishing-mi |
| Federated credential | graphforge-marketplace-ado |
| Azure region | West US 2 |
| Marketplace publisher | CurateLabsAI |
Tenant, subscription, client, object, service-connection, issuer, subject, and Marketplace resource IDs are not secrets, but they are deliberately not copied into this public runbook. Read them from the live Azure and Azure DevOps resources so the runbook cannot silently drift from reality.
The one-time setup sequence is:
-
In Azure DevOps, open Project settings → Service connections, create an Azure Resource Manager connection using Workload Identity Federation (manual), name it
graphforge-marketplace-publishing, and save it as a draft. Record the generated issuer and subject identifier. -
In Azure, create resource group
graphforge-publishingand user-assigned managed identitygraphforge-marketplace-publishing-miinWest US 2. Grant the identity Reader at the subscription scope. Reader is sufficient for the Azure CLI login; Marketplace authorization is granted separately. -
On that managed identity, create federated credential
graphforge-marketplace-adousing the Other scenario. Copy the issuer and subject from the draft service connection exactly. -
Return to Azure DevOps and fill the service connection with the live subscription name/ID, tenant ID, and managed-identity client ID. Select Verify and save.
-
Open Security for the service connection. Keep Open access disabled and authorize only the
CurateLabs.graphforge-vscodeYAML pipeline. Keep the service connection shared only with the currentGraphForgeproject. -
Run the Azure pipeline with its
publishparameter left at the defaultfalse. It installs, type-checks, tests, builds, packages a dependency-free VSIX, and runs this identity lookup:az rest --method get \ --url https://app.vssps.visualstudio.com/_apis/profile/profiles/me \ --resource 499b84ac-1321-427f-aa17-267ca6975798 \ --query id --output tsv
The output is the managed identity's Visual Studio Marketplace resource ID. It is different from the Azure client ID and Azure object ID.
-
In Marketplace publisher management, open
CurateLabsAI→ Members, add that Marketplace resource ID, and assign Contributor. The resulting member may render as<tenant-id>\\<managed-identity-object-id>. -
On the service connection's Approvals and checks tab, add Branch control with allowed branch
refs/heads/main. Do not require a manual approval click for Marketplace publishes — the gate ispublish: trueon an approvedmainrun plus branch control.maindoes not currently have a GitHub branch-protection rule, so Verify branch protection is intentionally off. Enable it only after GitHub protection is configured, or all releases will be blocked.
The setup was proven with Azure run 20260803.2: checks, unit tests, build, VSIX packaging, and
identity lookup passed, while the Marketplace publish task was skipped because publish=false.
An earlier InvalidAccessException with “The requested operation is not allowed” proved that the
Azure login was working but the managed identity had not yet been added as a publisher Contributor.
For recovery or rotation, recreate the managed identity/service-connection federation, rerun the default bootstrap flow to obtain the new Marketplace resource ID, replace the publisher member, then verify pipeline-only access and branch control before enabling a publish.
npm ci
npm run check && npm run compile && npm test
npx vsce packageThis produces graphforge-<version>.vsix at the repo root. Run npm run verify:package before
packaging. It fails when required runtime or sample files are absent, the file/size budget is
exceeded, or the package contains test, source, documentation, agent, or internal review paths.
Never publish a locally built VSIX that has not passed this contract.
.github/workflows/ci.yml's package job already runs npx vsce package --no-dependencies as
a packaging gate on every PR/push to main (see TESTING.md) and uploads the .vsix as a
build artifact — it does not publish. Marketplace and Open VSX publishing are deliberately
separate: Azure Pipelines publishes the Marketplace from main with federated identity, while
the GitHub release workflow publishes Open VSX from a release tag or manual dispatch.
# Visual Studio Marketplace
# Run the Azure DevOps "CurateLabs.graphforge-vscode" pipeline from approved main.
# The pipeline uses the graphforge-marketplace-publishing service connection.
npx vsce package --no-dependencies --out graphforge-marketplace.vsix
npx vsce publish --packagePath graphforge-marketplace.vsix --azure-credential
# Open VSX
npx ovsx publish graphforge-<version>.vsix --pat "$OVSX_PAT"Bump version in package.json (semver) before each publish; vsce/ovsx both reject
republishing an existing version.
- Verify the listing on both the Marketplace and Open VSX — icon, README rendering, categories/keywords, repository link.
- Install from the Marketplace into a clean VS Code profile and run
GraphForge: Check Environmentto confirm activation and command registration work outside the dev tree. - Confirm the release tag (
vX.Y.Z) resolves to the same commit that passed CI, and note the verified published version in the tracking issue.
azure-pipelines.yml owns Visual Studio Marketplace publishing. It has no push or PR triggers;
run it manually from approved main after the PR is merged and the version has been bumped. It
defaults to a package-only bootstrap run, which prints the managed identity's Marketplace resource
ID; add that identity as a publisher Contributor once. Select publish: true only for an approved
release. The service connection's branch-control check is the authorization boundary.
.github/workflows/publish.yml owns Open VSX publishing. It has two jobs:
build— always runs onpushof av*tag or manualworkflow_dispatch. Runsnpm ci, type checks, unit tests, the Extension Development Host suite, the package-content contract, andvsce package, then uploads the.vsixas a workflow artifact. A tag build also fails unlessvX.Y.Zexactly matchespackage.json.publish— runs afterbuildwhen triggered by av*tag push, or byworkflow_dispatchwithdry_run: false. Uses the GitHubproductionenvironment for optional required-reviewer protection and publishes only to Open VSX.
Both jobs run on Blacksmith's blacksmith-4vcpu-ubuntu-2404 runner label, matching the other
CurateLabs repos (startops-nextjs, etc.). Blacksmith has no separate "linked publishing"
product — linking is just installing the Blacksmith GitHub App for the CurateLabs org at
app.blacksmith.sh so runs-on: blacksmith-* labels resolve to
Blacksmith-hosted runners instead of GitHub-hosted ones. Once an org is linked, every repo in it
can use Blacksmith labels; there is no additional per-repo "enable publishing" toggle to click.
If the org is not yet linked, publish.yml will simply queue and fail to find a runner —
link the org first (one-time, David/admin only).
Following the pattern used by startops-nextjs (see
docs/engineering/pulumi-esc-vercel-clerk-convex.md in that repo) and the
use-pulumi-for-platform-iac ADR used across CurateLabs repos, the Open VSX token lives in a
Pulumi ESC environment, not as a long-lived GitHub Actions secret:
- ESC environment:
curatelabs/graphforge-vscode/production - Values it must define:
OVSX_PAT(Open VSX access token), markedfn::secret, exported asenvironmentVariablesso opening the environment injects them into the job environment. - No literal GitHub Actions secret is required. The
publishjob authenticates to Pulumi Cloud with GitHub OIDC: the job haspermissions: id-token: write, andpulumi/auth-actionsexchanges the job's short-lived GitHub identity token for a short-lived Pulumi Cloud organization access token.pulumi/esc-actionthen openscuratelabs/graphforge-vscode/productionand injectsOVSX_PATinto the job environment (values are masked in logs). There is nothing long-lived to rotate or leak, and the trust can be scoped in Pulumi Cloud to this repo. The earlier iteration of this workflow used a long-livedPULUMI_ACCESS_TOKENrepo secret withpulumi env run; that was replaced per the hardening recommendation on issue #20 — if aPULUMI_ACCESS_TOKENsecret still exists on the repo, it is stale and should be deleted.
If the OIDC trust isn't configured yet, the ESC environment doesn't exist, or it doesn't define
OVSX_PAT, the publish job logs a warning and skips the Open VSX publish instead of failing —
the build job's artifact is still produced. This lets the workflow merge and run safely before
the one-time setup below is done.
-
Link Blacksmith (if not already done for another repo's CI): sign in at app.blacksmith.sh with a
CurateLabsorg member, install the Blacksmith GitHub App on theCurateLabsorg (or just this repo), and grant it access tographforge-vscode. -
Create the Pulumi ESC environment (requires the Pulumi CLI and a
curatelabsPulumi Cloud org login):esc env init curatelabs/graphforge-vscode/production esc env edit curatelabs/graphforge-vscode/production
Paste (adjust org name if
curatelabsdiffers from what's already used for other CurateLabs Pulumi environments):values: ovsx: pat: fn::secret: "<paste the Open VSX access token>" environmentVariables: OVSX_PAT: ${ovsx.pat}
Verify the projection locally before trusting CI with it:
pulumi env run curatelabs/graphforge-vscode/production -- bash -c 'test -n "$OVSX_PAT" && echo ok' -
Register GitHub Actions as an OIDC issuer in Pulumi Cloud (one-time, replaces minting a long-lived
PULUMI_ACCESS_TOKEN). In Pulumi Cloud, as acuratelabsorg admin:- Go to Organization settings → OIDC issuers → Register issuer (docs: Configuring OpenID Connect for GitHub).
- Name it (e.g.
github-actions) and set the issuer URL tohttps://token.actions.githubusercontent.com. - Add an authorization policy: Decision
Allow, Token typeOrganization, Audurn:pulumi:org:curatelabs, Subrepo:CurateLabs/graphforge-vscode:*. TheSubclaim scopes the trust to this repo only; tighten it further torepo:CurateLabs/graphforge-vscode:environment:productionif you want tokens issued only to jobs running in theproductionGitHub Environment (the publish job qualifies).
No
gh secret setis needed — the workflow'spulumi/auth-actionsstep exchanges the job's OIDC token for a short-lived Pulumi token at run time. If aPULUMI_ACCESS_TOKENrepo secret was previously added, delete it once the OIDC path is verified:gh secret delete PULUMI_ACCESS_TOKEN --repo CurateLabs/graphforge-vscode
-
Create the
productionGitHub Environment (Settings → Environments → New environment, name itproduction) so thepublishjob'senvironment: productionreference resolves. Optionally add required reviewers or restrict deployment to themainbranch/v*tags for an extra approval gate before a real Open VSX publish. -
Never print
OVSX_PAT(or any Pulumi token) values in logs, PRs, or issues — only secret names and the ESC environment path should ever appear in this repo.
- Open VSX automatic: push a
vX.Y.Ztag matching thepackage.jsonversion. Thebuildjob packages, andpublishruns immediately after (gated only by theproductionenvironment's protection rules, if any). - Open VSX manual dry run: run the
Publish Open VSXworkflow viaworkflow_dispatchwithdry_run: true(the default) to exercisecheck/compile/test:unit/vsce packageand download the.vsixartifact without touching Open VSX. - Marketplace manual publish: merge the version bump, then run the Azure DevOps pipeline from
approved
main. The first default run prints the managed identity resource ID for the one-time Publisher Contributor assignment. For a release, selectpublish: true; the service connection branch control must pass beforevsce publish --azure-credentialcan run.