Documentation
How Drydock guards a publish.
Drydock reviews what a release ships before it goes public, and never holds your publish credential. How it hooks in depends on the registry: npm hands Drydock a staged tarball to inspect; registries without a staged artifact (PyPI today) use a workflow gate, where a GitHub Actions deployment-protection rule holds the publish until the build is reviewed.
Staged publishing — npm
The registry holds the candidate; the maintainer holds the keys.
npm exposes a staged-publish primitive: a maintainer runs npm publish --stage and the registry parks the candidate tarball behind a stageId until the same maintainer confirms the publish with 2FA. Drydock reviews what is inside that staged tarball before that confirmation runs.
Set it up
- 01Sign in and switch the org picker to the organization that publishes the package.
- 02Open
Organization settings → npm accessand paste an automation or granular npm token that can read the org's packages and list staged publishes. - 03Save — Drydock encrypts the token, hashes a fingerprint, and runs the registry auth check. Add a real
stageIdif you want to prove staged-tarball access on the same screen. - 04From here, hit
Check npmon the dashboard to discover open staged publishes on demand, or let the 15-minute auto-discovery cron pick them up automatically.
Review lifecycle
- 01A new staged publish is discovered (see Auto-discovery below). The worker queues a scan for any
stageIdit hasn't seen before, and the UI starts polling that scan's status. - 02The worker resolves the active organization's npm connection. The token is stored encrypted with AES-256-GCM in D1 and is decrypted only at the moment the npm adapter needs to authenticate a registry request.
- 03A short-lived sandbox Worker downloads the staged tarball from
/-/stage/<stageId>/tarballthrough a credentialed outbound gateway — the sandbox itself never receives the token. - 04The sandbox unpacks the archive into bounded file metadata and text samples and hands them back to the parent worker. Package contents are evidence, never instructions.
- 05The parent worker resolves a tag-aware baseline version, computes the package-to-package diff, runs deterministic findings against the changed files, and persists a redacted report.
- 06The maintainer reads the report at
/dashboard/scans/:id. Approval happens in npm with normal 2FA — Drydock never publishes on their behalf.
Auto-discovery
A */15 * * * * cron sweeps every validated npm connection, lists the organization's open staged publishes via GET /-/stage, and queues a scan for any new stageId. Stages that already completed in another organization are skipped, so the same tarball is never fetched twice. When an auto-discovered scan finishes, the connection's creator receives an email linking to the report. Tokens marked invalid are skipped without contacting the registry.
Workflow gating — GitHub Actions Preview
When the registry can't hold the candidate, the workflow does.
Some registries don't expose a staged candidate. For those, the publish job itself becomes the boundary: CI builds the release artifacts, uploads them as a release candidate, and a GitHub Environment with a Drydock-owned deployment protection rule holds the publish job. Drydock reviews the candidate and records a recommendation, but never approves on its own — a maintainer approves or rejects from the review workbench, and only then is the held job released or blocked. The publish runs on the workflow's own credential (e.g. Trusted Publishing via OIDC); Drydock never holds it.
PyPI is the first supported ecosystem. The GitHub plumbing below — install, gate, fetch, review, decide — is shared, so future ecosystems plug in behind the same gate; this walkthrough uses PyPI as the example.
Set it up
- 01Sign in and switch the org picker to the organization that owns the PyPI project.
- 02Open
Organization settings → GitHub Appand install the Drydock GitHub App on the GitHub account that hosts your repository. You'll be redirected to GitHub to pick the account and grant access to the repo, then back to Drydock. - 03In the repository, create a GitHub Environment (e.g.
pypi) and configure it as a PyPI Trusted Publisher. Drydock attaches its deployment-protection rule to that same environment. - 04Map the repository + environment + PyPI package on the same settings page so the webhook can resolve a delivery to your organization. The mapping is unique per
(organization, repository, environment). - 05Add the build and publish workflow below. The build job uploads the wheels and sdists as a GitHub Actions artifact; the publish job runs in
environment: pypi, downloads the same artifact, and stays blocked until a maintainer approves the review in Drydock.
Release-candidate bundle
There is no manifest to write. The boundary between your workflow and Drydock is the workflow run's uploaded artifacts: CI builds and uploads dist/*, and Drydock treats every .whl, .tar.gz, and .tgz it finds as the release set. The settings form can narrow discovery to one artifact name, but leaving it blank lets Drydock inspect every non-expired upload from the held run.
The release identity is derived from the artifacts themselves — package name and version from each wheel's METADATA and each sdist's PKG-INFO, with every sha256 recomputed server-side from the uploaded bytes. The reviewed wheel or sdist must be the exact file published: the publish job only downloads the reviewed bundle and never rebuilds, so the reviewed bytes are the published bytes.
Monorepo releases
One workflow run can publish several distinct packages — a monorepo cutting many wheels and sdists at once. Drydock groups the uploaded artifacts by package identity and fans the gate out into one review per package, each diffed against its own previously-published baseline. A release target left without a pinned ecosystem auto-detects each package's ecosystem from its artifacts, so a single gate can cover every package the environment publishes.
The held deployment is released only once every discovered package is individually approved; rejecting any single package blocks the whole release. The review workbench lists the package roster, tracks how many are approved, and links each package to its own diff-first review.
Workflow shape
jobs:
build-release-artifacts:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: python -m build
- uses: actions/upload-artifact@v4
with:
name: pypi-release-candidate
path: dist/*
publish:
needs: build-release-artifacts
environment: pypi
permissions:
id-token: write
contents: read
steps:
- uses: actions/download-artifact@v4
with:
name: pypi-release-candidate
# publish the downloaded dist/* to PyPI via Trusted Publishing (OIDC)No manifest or checksum step is required — CI just builds and uploads dist/*. The environment: pypi line is the gate: configure the same environment in PyPI Trusted Publishers and attach Drydock as a custom deployment protection rule on it. The publish job stays blocked until a maintainer approves the review in Drydock, then publishes the downloaded bundle with whatever tool you prefer.
Decision flow
- 01GitHub fires a signed
deployment_protection_rulewebhook toPOST /webhooks/github. The handler HMAC-verifies the signature against the raw body in constant time — unsigned or malformed requests are rejected. - 02The handler resolves the
(installationId, repositoryId, environment)triple against the organization's release-target table and persists apendinggithub_workflow_gatesrow keyed on the GitHub delivery ID, so retries are idempotent. Deliveries that don't match a mapped target are acknowledged but ignored. - 03Drydock derives the release set from the uploaded bundle, recomputes each artifact's sha256, and runs the scan pipeline — one review per discovered package, each against its own baseline — recording an advisory recommendation. The review never posts to GitHub — it leaves the gate
pendingand hands off to a human. - 04A maintainer opens the review in the diff-first workbench at
/dashboard/scans/:idand approves or rejects each package. The held publish job is released only once every package is approved, and blocked the moment any one is rejected — only that final decision is posted back to GitHub. A bundle whose artifacts can't be verified is auto-rejected fail-closed — no human is needed to block something Drydock can't identify. - 05The decision callback hits
POST /repos/<owner>/<repo>/actions/runs/<run_id>/deployment_protection_rulewith a fresh installation access token. The callback URL is pinned toapi.github.comand the deployment-protection path — a spoofed URL in the webhook payload is rejected even when the signature is valid. - 06The transition out of
pendingis a single compare-and-set, so a double-submit or a race between a human decision and the fail-closed reject calls GitHub exactly once.
Set up an organization