The code your team reviewed
Tests, configuration, and source files before packaging. Useful context, but not necessarily the bytes a registry will serve.
Learn Drydock
Drydock is a release checkpoint for package maintainers. It opens the package that is about to be published, compares it with the last release, points to risky changes, and gives a human the final decision.
About 5 minutes · No security background required · npm, PyPI, VS Code, and atpm
Start here
Pull requests show source changes. Registries deliver built archives. Drydock sits between those two moments and reviews the exact release candidate before it becomes public.
The code your team reviewed
Tests, configuration, and source files before packaging. Useful context, but not necessarily the bytes a registry will serve.
The package your users receive
Generated files, bundled dependencies, lifecycle scripts, binaries, and package metadata after the build has finished.
Does this release candidate contain anything the maintainer should understand before it ships?
npm, atpm, or GitHub pauses a built release candidate.
Drydock compares the artifact, explains findings, and records provenance.
A maintainer approves or rejects. Drydock never publishes the package.
The release adds an install-time script and a new network-capable file.
Findings are pinned to the changed files and lines that introduced them.
Pre-existing package risk stays visible without drowning out this release.
Names, versions, baselines, and SHA-256 digests show exactly what was reviewed.
A finding is evidence, not a verdict. Open the file, read the highlighted diff, check whether the behavior is intended, and record the reason for your decision. Clean reports still wait for a human when the release path is gated.
Drydock reads bounded file listings and text samples. It never installs, imports, builds, executes, or renders package-provided active content.
npm tokens are isolated from the sandbox. Workflow releases keep their publish credential in GitHub Actions. Drydock never needs it.
The AI reviewer is advisory and on by default behind a per-organization killswitch. Its findings can raise concern but never downgrade a deterministic finding.
Missing, malformed, ambiguous, or unverifiable evidence keeps a gated release blocked instead of silently passing it.
Choose a release path
Every path creates the same kind of Drydock report. The difference is who holds the candidate while you review it and where the final decision happens.
npm stage publishnpm stage publish --provenanceenvironment: productionUse npm staging for npm's private candidate store. For atpm, open the credential-free staged review from the link on your staged dashboard. For PyPI, the VS Code Marketplace, or other CI-first releases, use a GitHub workflow gate.
Path 1 · npm stage publish
This is the shortest route for npm. Stage the package, let Drydock review the private tarball, then return to npm to publish or discard it.
Packages and scopes: Read-only on the packages you stage and Organizations: No access. A scoped package like @nanostores/i18n is covered by selecting the @nanostores scope there; the Organizations permission is for member and settings management, which Drydock never reads.Organization settings → npm access and paste the token.Check npm on the dashboard when you want an immediate refresh.npm stage publish. npm uploads the candidate but does not make it public.Check npm.npm stage approve / npm stage reject command Drydock shows you after saving.The npm token is encrypted at rest and only attached by the registry gateway for allowed npm endpoints. The archive sandbox never sees it, and Drydock never receives the credential that completes the publish.
Path 2 · atpm trusted publishing
atpm uses GitHub OIDC for trusted publishing, so CI needs no long-lived token. A staged release is a public record in the publisher's AT Protocol repository, and Drydock verifies its provenance and reviews its content-addressed blob directly.
dev.atpm.alpha.trustPublisher record with the GitHub owner, repository, and workflow allowed to stage it.allowStage: true and allowPublish: false. The workflow may upload a candidate, but it cannot make that candidate public — so the release is already paused before anyone reviews it.--provenance. The Sigstore attestation is what lets a review say where the release was built, and whether that matches the record above.npm stage publish --provenance. atpm stores the candidate without publishing it.npm stage approve <id> or npm stage reject <id>. Drydock takes no part in that decision.Drydock reads public AT Protocol records and content-addressed blobs. It receives no atpm credential and cannot stage, approve, reject, or publish a package.
Path 3 · GitHub workflow gates Preview
CI uploads built files and reaches a protected publish job. GitHub asks Drydock for a decision before the held job continues.
Organization settings → GitHub App and install the Drydock GitHub App on the account that hosts your repository.production and enable Drydock as a custom deployment protection rule.For npm, PyPI, and VS Code, there is no Drydock manifest to maintain. Upload built .whl, .tar.gz, .tgz, or .vsix files before the protected job starts. Drydock derives the ecosystem, package name, and version from metadata inside each archive.
Generate SHA256SUMS beside the artifacts during the build, upload it with them, and verify it in the publish job. The digests in the Drydock report should match. Most importantly: download and publish the uploaded files—never rebuild after approval.
Large compiled PyPI releases can upload one bounded artifact per wheel or sdist. Name the shards pypi-release-candidate-* and set the release target's ecosystem to PyPI; Drydock then processes them one at a time while keeping every distribution in the review and provenance. A target left on auto-detect has no name to match, so it keeps the smaller single-upload limits.
Drydock groups uploaded files by package and opens a separate report for each one. The held job continues only after every package is approved; rejecting one blocks the release set.
Each workflow has the same contract: build once, record checksums, upload, pause at the environment, verify the download, and publish without rebuilding.
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.x"
- run: python -m pip install build
- run: python -m build
- run: cd dist && sha256sum *.whl *.tar.gz > SHA256SUMS
- uses: actions/upload-artifact@v4
with:
name: pypi-release-candidate
path: dist/
publish:
needs: build
environment: production
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: pypi-release-candidate
path: dist
- run: cd dist && sha256sum --check --strict SHA256SUMS
- run: rm dist/SHA256SUMS
- uses: pypa/gh-action-pypi-publish@release/v1jobs:
pack:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run pack:all # write dist/*.tgz
- run: cd dist && sha256sum *.tgz > SHA256SUMS
- uses: actions/upload-artifact@v4
with:
name: npm-release-candidates
path: dist/
publish:
needs: pack
environment: production
permissions:
id-token: write
contents: read
steps:
- uses: actions/download-artifact@v4
with:
name: npm-release-candidates
path: dist
- run: cd dist && sha256sum --check --strict SHA256SUMS
- run: |
for tgz in dist/*.tgz; do
npm publish "$tgz" --access public --provenance
donejobs:
package:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npx @vscode/vsce package --out dist/extension.vsix
- run: cd dist && sha256sum *.vsix > SHA256SUMS
- uses: actions/upload-artifact@v4
with:
name: vscode-release-candidate
path: dist/
publish:
needs: package
environment: production
steps:
- uses: actions/download-artifact@v4
with:
name: vscode-release-candidate
path: dist
- run: cd dist && sha256sum --check --strict SHA256SUMS
- run: npx @vscode/vsce publish --packagePath dist/extension.vsixAdd the registry authentication your release already uses—for example PyPI Trusted Publishing, npm trusted publishing or a scoped token, or a VS Code Marketplace token—to the protected publish job. Drydock does not receive those credentials.
environment: production. GitHub pauses it and sends Drydock a signed protection-rule request.SHA256SUMS and publishes the downloaded files. Any rejection stops the whole release.If Drydock cannot verify the webhook, resolve the artifact set, parse a package, finish a scan, or return a safe decision, the workflow does not get a silent pass.
Diffs in dependency PRs
The same public diff pages work from the consumer side. A Renovate or Dependabot PR names an exact version pair, so the published-package diff has a predictable URL — no account, token, or API call. Set it up once and every dependency bump links its own diff.
Extend the shared preset and each npm or PyPI update row gains a Drydock column linking the exact published pair being merged:
{
"extends": [
"config:recommended",
"github>JoviDeCroock/drydock//renovate/diff-links"
]
}List the preset after your base presets so its column layout wins. Updates without two distinct published versions of one package — pins, digests, replacements, and some lockfile-only changes — omit the link, and columns that end up empty are dropped from the table.
Dependabot cannot template PR bodies, so a small workflow comments the links instead. It reads the bumps from the PR metadata and never checks out or executes the updated code. Grouped update PRs get one link per dependency in a single comment, rewritten in place each time Dependabot revises the group.
name: drydock-diff-link
on:
pull_request:
types: [opened, synchronize]
# Dependabot-triggered runs honor this permissions key; the read-only default
# token does not apply when permissions are set explicitly.
permissions:
pull-requests: write
# Dependabot rewrites a grouped PR in place as the group's contents change, so
# the comment is rebuilt on every push. Serialize per PR so two pushes cannot
# race the read-then-write below into two comments.
concurrency:
group: drydock-diff-link-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
diff-link:
if: github.event.pull_request.user.login == 'dependabot[bot]'
runs-on: ubuntu-latest
steps:
# Fails the job unless the PR's first commit is an authentic, signed
# Dependabot commit, which is what makes it safe for the next step to
# parse that commit message. Also derives the ecosystem from the branch.
- id: meta
uses: dependabot/fetch-metadata@v3
- env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR: ${{ github.event.pull_request.number }}
ECOSYSTEM: ${{ steps.meta.outputs.package-ecosystem }}
NAMES: ${{ steps.meta.outputs.dependency-names }}
FROM: ${{ steps.meta.outputs.previous-version }}
TO: ${{ steps.meta.outputs.new-version }}
run: |
set -euo pipefail
case "$ECOSYSTEM" in
npm_and_yarn) prefix="" ;;
pip) prefix="pypi/" ;;
*) echo "No /diff pages for $ECOSYSTEM."; exit 0 ;;
esac
api="repos/$REPO"
# A single-dependency PR carries its one pair in the action's outputs.
# A grouped PR carries one `Updates ...` line per dependency in the
# commit message, and is the only kind that does. Those lines are read
# directly rather than through the action, which collapses a group to
# one pair and fills the new version from the commit's
# `dependency-version` metadata -- a value that can lag the version the
# PR actually merges once Dependabot revises the group.
case "$NAMES" in
*,*)
msg=$(gh api "$api/pulls/$PR/commits" --jq '.[0].commit.message')
pairs=$(printf '%s\n' "$msg" | sed -n \
's/^Updates `\([^`]*\)` from \([^ ]*\) to \([^ ]*\)$/\1 \2 \3/p')
;;
*)
pairs="$NAMES $FROM $TO"
;;
esac
# Anything that is not two distinct published versions of one package
# gets no link: no link beats a confidently wrong one.
links=""
count=0
while read -r name from to; do
[ -n "$name" ] && [ -n "$from" ] && [ -n "$to" ] || continue
[ "$from" != "$to" ] || continue
url="https://drydock.org/diff/$prefix$name/$from/$to"
links="$links- [$name $from → $to]($url)
"
count=$((count + 1))
done < <(printf '%s\n' "$pairs")
if [ "$count" -eq 0 ]; then
echo "No linkable version pair in this PR."
exit 0
fi
lead="Read the diff this PR merges:"
if [ "$count" -gt 1 ]; then
lead="Read the diff of each update in this group:"
fi
MARKER="<!-- drydock:diff-link -->"
body="$MARKER
$lead
$links"
# Upsert, so a revised group updates its comment instead of
# stacking a second one underneath the now-stale first.
comments="$api/issues/$PR/comments"
existing=$(MARKER="$MARKER" gh api "$comments" --paginate \
--jq '[.[] | select(.body | contains(env.MARKER))][0].id // empty')
# --paginate applies the filter per page and concatenates, so keep
# the first line only. Trimmed in bash rather than through `head`,
# which under `pipefail` can fail the job on EPIPE.
existing=${existing%%$'\n'*}
if [ -n "$existing" ]; then
method=PATCH
target="$api/issues/comments/$existing"
else
method=POST
target="$comments"
fi
gh api -X "$method" "$target" -f body="$body" --silentBoth integrations add plain markdown links. Nothing contacts Drydock when a PR renders — only when a reviewer clicks — and the linked pages serve public-registry data anonymously.
Ready when your release is
Connect npm for the shortest path, or install the GitHub App to protect a CI release. Your first report will make the model concrete.