- diff view gains a Packages tab with added/removed/changed/unchanged package tables and purl-derived type/license/upstream links - captain records verified against the DID's atcr_hold service before caching (processor + batch backfill), preventing forged holds - fix empty-handle updates clobbering cached handles and colliding on the UNIQUE constraint - move fillPrevCIDs into repo.go; DirectRepoOperator is now canonical, repomgr kept as a test oracle - surface read-only crew status in hold selector - reconcile docs
16 KiB
Distribution Strategy for ATCR Credential Helper
Overview
The ATCR credential helper is distributed as pre-built binaries for Linux,
macOS, and Windows. Builds are produced with GoReleaser and the resulting
artifacts are published to the project's Tangled repository as ATProto records
(sh.tangled.repo.artifact) on the repo owner's PDS. There is no GitHub
release pipeline. This document describes the actual distribution flow.
Why Go is Ideal for Credential Helpers
Go is a natural fit for Docker credential helpers:
- Cross-compilation - Single command builds for all platforms
- Static binaries - No runtime dependencies (unlike Node.js, Python, Ruby)
- Industry standard - Most Docker credential helpers are written in Go:
- docker-credential-helpers (official)
- docker-credential-gcr (Google)
- docker-credential-ecr-login (AWS)
- docker-credential-pass (community)
Multi-Brand Structure
The credential helper is built as multiple brand-specific binaries from a single shared implementation:
-
pkg/credhelper/holds the entire implementation (Docker protocol commands, device-flow auth, config storage, self-update). It exposescredhelper.Run(credhelper.Config{...}). -
cmd/credential-helper/atcr/andcmd/credential-helper/seamark/are thinmainpackages (each with its owngo.mod) that supply a per-brandConfigand callRun. They differ only in brand identity:Field atcr binary seamark binary Binary name docker-credential-atcrdocker-credential-seamarkDefault registry atcr.ioseamark.crConfig dir (under $HOME)~/.atcr~/.seamarkSecret prefix atcr_device_seamark_device_Both brands point
ReleasesBaseURLat the same Tangled repo (https://tangled.org/did:plc:e3kzdezk5gsirzh7eoqplc64) for self-update and download.
Each brand module is independently installable via go install (see
From Source). The atcr module path is
atcr.io/cmd/credential-helper/atcr; the seamark module path is
seamark.dev/cmd/credential-helper/seamark.
Supported Platforms
| Platform | Arch | Format | Status |
|---|---|---|---|
| Linux | amd64 | tar.gz | Available |
| Linux | arm64 | tar.gz | Available |
| macOS | amd64 (Intel) | tar.gz | Available |
| macOS | arm64 (Apple Silicon) | tar.gz | Available |
| Windows | amd64 | tar.gz | Available |
| Windows | arm64 | tar.gz | Available |
Distribution Methods
1. Tangled Releases (Automated)
Trigger: Push a version tag (e.g. v1.0.0).
git tag v1.0.0
git push origin v1.0.0
What happens:
- The Tangled CI workflow
.tangled/workflows/release-credential-helper.ymlruns on tags matchingv*. - It installs
goat(ATProto CLI) andgoreleaser, then logs into the repo owner's PDS once withgoat account login. goreleaser release --cleanbuilds binaries for all platforms.- GoReleaser's
releaseblock is disabled (release.disable: true), so no GitHub/forge release is created. Instead, a custompublishersblock runs./scripts/publish-artifact.shfor each built archive and the checksums file. publish-artifact.shuploads each artifact as a PDS blob (goat blob upload) and creates ansh.tangled.repo.artifactrecord referencing it. The record uses a deterministic rkey derived from(tag, artifact name)so retries are idempotent (Tangled's PDS returns HTTP 500, not 409, on duplicate rkey).
Workflow file: .tangled/workflows/release-credential-helper.yml
Config file: .goreleaser.yaml
Publisher script: scripts/publish-artifact.sh
The artifacts become downloadable from the Tangled repo's tag download path (see Manual Download).
2. Install Scripts
Both scripts are served by the AppView from its static directory
(pkg/appview/public/static/) at /static/install.sh and
/static/install.ps1. They resolve the latest version by following the
{repo}/tags/latest redirect chain on Tangled, then download the matching
archive from the tag download path.
Linux/macOS: install.sh
- Detects OS and architecture
- Resolves the latest tag from Tangled and downloads the archive
- Installs to
/usr/local/bin(override withINSTALL_DIR) - Makes executable and verifies installation
Windows: install.ps1
- Detects architecture
- Resolves the latest tag from Tangled and downloads the archive
- Installs to
%ProgramFiles%\ATCR(override withATCR_INSTALL_DIR) - Adds to system PATH (requires Administrator to modify the machine PATH)
- Uses the bundled
tar.exeto extract the.tar.gz
Usage:
# Linux/macOS
curl -fsSL https://atcr.io/static/install.sh | bash
# Windows (PowerShell)
iwr -useb https://atcr.io/static/install.ps1 | iex
Pin a specific version by setting ATCR_VERSION (e.g. ATCR_VERSION=v1.0.0)
before running either script.
3. Homebrew (macOS) — Not Currently Available
Homebrew distribution is not available yet. A brews: block exists in
.goreleaser.yaml but is commented out. If/when it is enabled, the formula
would live in the project's Tangled repo under Formula/ and pull artifacts
from the Tangled tag download path. There is no published tap to brew tap
today; use one of the other methods.
4. Manual Download
Download the archive directly from the Tangled repo's tag download path. The
artifacts are published as sh.tangled.repo.artifact records on the repo
owner's PDS and served by Tangled at:
https://tangled.org/<did-or-handle>/<repo>/tags/<version>/download/<artifact>
# Example: Linux amd64
REPO=https://tangled.org/did:plc:e3kzdezk5gsirzh7eoqplc64
VERSION=v1.0.0
curl -LO "${REPO}/tags/${VERSION}/download/docker-credential-atcr_${VERSION#v}_Linux_x86_64.tar.gz"
tar -xzf docker-credential-atcr_${VERSION#v}_Linux_x86_64.tar.gz
sudo install -m 755 docker-credential-atcr /usr/local/bin/
Tangled redirects DID to handle for the repo URL, so curl -L is needed to
follow the redirect.
5. From Source
For users with Go installed. Each brand is a separate installable module:
# atcr.io brand
go install atcr.io/cmd/credential-helper/atcr@latest
sudo mv "$(go env GOPATH)/bin/atcr" /usr/local/bin/docker-credential-atcr
# seamark.dev brand
go install seamark.dev/cmd/credential-helper/seamark@latest
sudo mv "$(go env GOPATH)/bin/seamark" /usr/local/bin/docker-credential-seamark
The installed binary takes the name of the leaf package directory (atcr /
seamark); rename it to docker-credential-<brand> so Docker can discover it.
Note: This requires Go 1.26+ and compiles locally. Locally (inside the repo
workspace) go.work resolves the atcr.io dependency; standalone installs
resolve the require atcr.io vX.Y.Z line pinned in each brand's go.mod.
Release Process
Creating a New Release
-
Bump the pinned
atcr.ioversion in each brand'sgo.mod(cmd/credential-helper/atcr/go.mod,cmd/credential-helper/seamark/go.mod) ifgo installconsumers need the new code. -
Commit and tag:
git add . git commit -m "Release v1.0.0" git tag -a v1.0.0 -m "Release v1.0.0" git push origin main git push origin v1.0.0 -
Wait for CI:
- The Tangled workflow builds and publishes artifacts automatically.
-
Verify release:
- Confirm the
tags/latestredirect resolves to the new tag and the archive downloads:ATCR_VERSION=v1.0.0 curl -fsSL https://atcr.io/static/install.sh | bash docker-credential-atcr version
- Confirm the
Version Information
GoReleaser injects version info at build time via ldflags into the brand
main packages:
var (
version = "dev" // Set to tag (e.g., "v1.0.0")
commit = "none" // Set to git commit hash
date = "unknown" // Set to build timestamp
)
Usage:
$ docker-credential-atcr version
docker-credential-atcr v1.0.0 (commit: abc123, built: 2025-01-15T10:30:00Z)
Distribution Configuration
GoReleaser Config (.goreleaser.yaml)
Key sections:
Builds:
- One
credential-helperbuild withdir: ./cmd/credential-helper/atcr, binary namedocker-credential-atcr(Windows:.exeauto-added) - Targets: Linux, macOS, Windows (amd64, arm64)
- CGO disabled for static binaries
- Ldflags inject version/commit/date
Archives:
- Format: tar.gz for all platforms
- Naming:
docker-credential-atcr_VERSION_OS_ARCH.tar.gz - Includes: LICENSE, README, INSTALLATION
Release:
release.disable: true— no GitHub/forge release is created.
Publishers:
- A custom
atproto-pdspublisher runs./scripts/publish-artifact.shfor each artifact, forwarding the Tangled-providedTANGLED_REF_NAME,TANGLED_REPO_DID, andREPO_URLenv vars (GoReleaser publishers run in a sanitized sub-shell, so these must be forwarded explicitly).
Brews:
- Present but commented out (Homebrew not currently enabled).
Changelog:
- Auto-generated from commits
- Filters out docs/test/chore commits
Docker and Docker Desktop
How Docker Finds Credential Helpers
Docker looks for binaries named docker-credential-* in PATH:
- User types:
docker push atcr.io/alice/app:latest - Docker reads
~/.docker/config.json:{ "credHelpers": { "atcr.io": "atcr" } } - Docker looks for
docker-credential-atcrin PATH - Calls
docker-credential-atcr getwithatcr.ioon stdin (a plain string, not JSON) - Helper returns credentials as JSON on stdout
The credHelpers map value is the binary-name suffix after
docker-credential- (so atcr for docker-credential-atcr). The
configure-docker command (and the prompt at the end of login) writes this
entry automatically.
PATH Requirements
Linux/macOS:
- Common locations:
/usr/local/bin,/usr/bin,$HOME/.local/bin - Check with:
which docker-credential-atcr
Windows:
- Common locations:
C:\Windows\System32,%ProgramFiles%\ATCR - Check with:
where docker-credential-atcr
CI/CD Secrets
Required Tangled Secret
PUBLISH_APP_PASSWORD— an ATProto app password for the account that owns the repo's artifact records. The workflow runsgoat account login -u "$TANGLED_REPO_DID" -p "$PUBLISH_APP_PASSWORD"once before GoReleaser, and everypublish-artifact.shinvocation reuses that session.
There is no GITHUB_TOKEN or HOMEBREW_TAP_TOKEN — the project does not
release through GitHub.
Helper Behavior
The helper implements the standard Docker credential helper protocol plus a
few user-facing commands. Implementation lives in pkg/credhelper/.
Docker Protocol Commands (hidden)
Called by Docker, not users (pkg/credhelper/protocol.go):
get— reads the server URL from stdin, resolves the stored account, validates the device secret against the AppView, and returns{ServerURL, Username, Secret}JSON. If the OAuth session has expired it prints the login URL and fails so Docker re-prompts; on a generic invalid result it removes the bad account.store— reads{ServerURL, Username, Secret}from stdin. Only stores the credential ifSecretcarries the brand's secret prefix (e.g.atcr_device_); other secrets (e.g. an app password fromdocker login) are ignored.erase— removes the active (or sole) account for the server URL.list— returns{ "host": "username", ... }for all stored registries.
Device-Flow Authentication
login (pkg/credhelper/cmd_login.go, pkg/credhelper/device_auth.go) runs
an OAuth-style device authorization flow against the AppView:
POST {appview}/auth/device/codewith{"device_name": "<hostname>"}. Response:device_code,user_code,verification_uri,expires_in,interval.- The helper shows the
user_code, then opens (or prints){verification_uri}?user_code=<code>for the user to approve in a browser. - The helper polls
POST {appview}/auth/device/tokenwith{"device_code": ...}everyintervalseconds untilexpires_in.authorization_pendingmeans keep polling; any other error aborts. On success the response carriesdevice_secret,handle, anddid. - The account (handle, did, device secret) is saved to the brand config dir, and the user is offered automatic Docker configuration.
Credential Validation
get validates a stored device secret by calling GET {appview}/auth/token?service={appview} with HTTP Basic auth
(handle:device_secret) and a 5s timeout (validateCredentials in
device_auth.go):
200→ valid.401with body{"error":"oauth_session_expired", "login_url": ...}→ expired; the user is told to re-login.401otherwise → invalid; the account is removed.- Network errors or other status codes → treated as valid (don't re-auth on transient server issues).
Other User Commands
login [registry]— device-flow auth (default registry from brand Config).logout [registry]— remove a stored account.status— show configured registries and accounts.switch— change the active account for a registry.configure-docker— writecredHelpersentries to~/.docker/config.jsonfor all configured registries.update [--check]— self-update by resolving{ReleasesBaseURL}/tags/latestand downloading the matching archive.getalso performs a cached (24h) background update check and prints a notice if a newer version exists.
Testing the Distribution
Before Tagging a Release
-
Test GoReleaser locally:
goreleaser build --snapshot --clean ls dist/ -
Test specific platform:
goreleaser build --snapshot --clean --single-target ./dist/credential-helper_*/docker-credential-atcr version -
Test full release without publishing:
goreleaser release --snapshot --clean --skip=publish
After Release
-
Test install script:
# Clean install in fresh environment docker run --rm -it ubuntu:latest bash apt update && apt install -y curl curl -fsSL https://atcr.io/static/install.sh | bash -
Test Docker integration:
echo '{"credHelpers":{"atcr.io":"atcr"}}' > ~/.docker/config.json docker push atcr.io/test/image:latest
Future Enhancements
Package Managers
- Homebrew (enable the commented
brews:block in.goreleaser.yaml) .deb/.rpmpackages (via GoReleaser nfpm)- Arch AUR, Chocolatey, Scoop, Winget
Docker Distribution
The credential helper could also ship as a container image for CI/CD use, but native binaries remain the primary distribution method.
Troubleshooting
"Binary not in PATH"
Symptom: docker push fails with "credential helper not found"
Solution:
# Check if installed
which docker-credential-atcr
# or on Windows:
where docker-credential-atcr
# If not in PATH, add install dir to PATH
export PATH="/usr/local/bin:$PATH" # Linux/macOS
# or add to ~/.bashrc, ~/.zshrc
"Permission denied"
Symptom: Can't execute binary
Solution:
chmod +x /usr/local/bin/docker-credential-atcr
"Wrong architecture"
Symptom: exec format error or bad CPU type
Solution: Download correct architecture:
- x86_64/amd64 for Intel/AMD
- arm64/aarch64 for Apple Silicon, ARM servers
Check with:
uname -m
Documentation
- User installation: INSTALLATION.md
- Project docs: CLAUDE.md
- README: README.md