Scaffold for an Azure DevOps extension that lets you share a single ggshield API key across every pipeline in an organization — without editing each YAML or routing through variable groups / Key Vault. It ships:
- A custom task (
ggshield@0) that reads credentials from a typed Generic service connection (connectedService:Generic) — the only ADO context where endpoint fields actually resolve in YAML pipelines. - A pipeline decorator that auto-injects that task right after the implicit
checkoutstep of every agent job.
- Node.js 18+ and npm
tfx-cli—npm install -g tfx-cli- A publisher account on the Visual Studio Marketplace
In vss-extension.json:
- Replace
REPLACE-WITH-YOUR-PUBLISHER-IDwith your Marketplace publisher ID. Required —tfxwill refuse to package otherwise, and the.vsixfilename encodes this value. - Bump the top-level
versionfor each release. The build script intentionally does not auto-bump.
ADO tracks two versions for this kind of extension: vss-extension.json → version (drives Marketplace upgrade detection) and ggshield-scan-task/task.json → version: { Major, Minor, Patch } (drives whether agents pull new task bits). They must agree, or you get a stale task on agents or a rejected upload.
Edit only vss-extension.json's version. build.sh parses it and rewrites the Major/Minor/Patch block in task.json before packaging — commit both files together.
./build.shOutput: a .vsix in the project root, named <publisher>.ggshield-ado-private-extension-<version>.vsix.
-
Go to https://marketplace.visualstudio.com/manage and upload the
.vsix. The extension shows up under your publisher with Availability: Private (shared with…): -
Click
...→ Share/Unshare and add your ADO organization (e.g.https://dev.azure.com/myorg). -
In your ADO org: Organization Settings → Extensions → Shared → click the extension → Install:
Private extensions are not reviewed by Microsoft and become available to the target org immediately after sharing.
In the target ADO project: Project Settings → Service connections → New service connection → Generic, then:
- Server URL: your GitGuardian dashboard URL —
https://dashboard.gitguardian.com(SaaS US),https://dashboard.eu1.gitguardian.com(SaaS EU), or your self-hosted dashboard URL. ggshield derives the API URL from this; passing the API URL directly will fail to authenticate. Leave blank for the SaaS US default. - Username: leave blank.
- Password/Token Key: your ggshield API key.
- Service connection name:
gitguardian-api(must match exactly — the decorator YAML references this name). - Tick Grant access permission to all pipelines.
-
In a throwaway repo, create a minimal
azure-pipelines.yml:trigger: [main] pool: vmImage: ubuntu-latest steps: - script: echo "my real build steps go here"
-
Run the pipeline — a
ggshield - secret scanstep should appear right afterCheckout. -
Add a hardcoded test secret to verify the env-var plumbing; the scan should fail the build:
variables:
skipGGShield: trueUseful for the pipeline that builds this extension itself (otherwise you'll get infinite recursion of self-scans).
Need a flag the task doesn't expose directly (--exit-zero, --exclude, --banlist-detector, etc.)? Skip the auto-injected decorator and call ggshield@0 explicitly with additionalArguments:
variables:
skipGGShield: true
steps:
- task: ggshield@0
inputs:
gitguardianConnection: 'gitguardian-api'
scanMode: 'path'
scanTarget: '.'
additionalArguments: '--exit-zero --exclude "tests/**"'additionalArguments is split with POSIX-shell-style quoting and forwarded to ggshield (with the one exception noted below), so anything from the upstream CLI reference works.
--show-secretsis ignored. The task strips it before invokingggshield, so detected secrets are never printed in plaintext to the pipeline logs (which are visible to anyone with access to the run, and this task runs in every pipeline via the decorator).ggshieldmasks secret values in its output by default.
The decorator fires on every agent job in every pipeline, so a broad rollout meaningfully increases ggshield API traffic. Those calls are subject to API rate limits shared across your workspace — review your quotas and headroom first: usage, quotas, and rate limiting.
Built-in safety net. The task's scanTimeoutSeconds input (default 80) terminates ggshield and completes the step as SucceededWithIssues when the scan runs long. This contains rate-limit incidents: pygitguardian retries 429s indefinitely, so without the cap a transient event would turn into an org-wide pipeline outage. Tune it down (e.g. 30) on fast pipelines, or up for large monorepos.
- ggshield is auto-installed on demand if missing. The task tries
pipx, thenpython3 -m pip,python -m pip,pip3,pip, in that order. Bake ggshield into self-hosted agent images to remove ~5s of cold-start overhead per job. - Only
secret scan ci/path/dockermodes are exposed. - Windows-hosted agents need Python 3.8+ on
PATHfor the auto-install fallback. - The decorator fires on every agent job; gate by branch or path with
${{ if ... }}indecorator/ggshield-decorator.ymlif needed. - The task strips
--show-secretsfromadditionalArguments, but a repo can still unmask its own secrets by committing a.gitguardian.yamlwithsecret.show_secrets: true(a repo-localggshieldconfig the task intentionally honors for legitimate settings likepaths-ignoreand detector banlists). If you need to prevent that org-wide, have agents run with a pinned config viaggshield --config-path— at the cost of ignoring all repo-local.gitguardian.yamlfiles.
- Publish scan results as an artifact / custom tab in the run summary.
- JSON → SARIF conversion for the ADO security UI.



