Assemble AWS CodeArtifact packages from remote sources. No local artifacts required.
cob reads a YAML manifest that declares what goes into a package by pointing at S3 objects, other CodeArtifact packages, or local files. Assets stream from source to CodeArtifact in constant memory -- no size limit, and the full asset is never held in RAM (it spills through a transient temp file; see Known limitations).
Requirements: Go 1.25 or newer (chosen so a build picks up the latest standard-library security patches, which govulncheck enforces in CI). On an older toolchain, go install fails with a "module requires Go 1.25" error — install a newer Go via go.dev/dl or the Go toolchain auto-download before retrying.
go install github.com/jmurray2011/cob/cmd/cob@latestOr build from source:
go build -o cob ./cmd/cobPrebuilt binaries for Linux / macOS / Windows are attached to each GitHub release (no Go toolchain required); see Verifying release binaries below for the cosign verification step.
Release archives ship a checksums.txt signed with cosign keyless signing (no key to trust -- the signature is tied to the GitHub Actions release workflow's identity and logged in the public Rekor transparency log). To verify a download:
cosign verify-blob checksums.txt \
--signature checksums.txt.sig \
--certificate checksums.txt.pem \
--certificate-identity-regexp '^https://github.com/jmurray2011/cob/\.github/workflows/release\.yml@refs/tags/v' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com
sha256sum --check checksums.txt # then check the binary against the verified listWrite a manifest that describes your package:
# my-package.yaml
domain: my-domain
repository: dev
namespace: my-namespace
package: my-package
sources:
app: s3://my-bucket/builds/app-${VERSION}.tar.gz
config: s3://my-bucket/configs/app-config.yaml
baseline: ca://acme/shared/common-configs@1.0.0/defaults.yaml
local: ./local-overrides.yaml
promote:
stages: [dev, staging, prod]Publish it:
cob publish my-package.yaml --version 2.1.0Reads a manifest, resolves variables, pulls from each source, publishes to CodeArtifact.
cob publish my-package.yaml --version 2.1.0
cob publish my-package.yaml --version 2.1.0 --dry-run # verify sources, don't publish
cob publish my-package.yaml --version 2.1.0 --force # overwrite existing version
cob publish my-package.yaml --version 2.1.0 --resume # finish an interrupted publish
cob publish my-package.yaml --version 2.1.0 --yes # skip confirmation prompt
COB_VERSION=2.1.0 cob publish my-package.yaml # version from envFlags: --version, --dry-run, --force, --resume, --yes, --concurrency
If a publish is interrupted partway, the version is left Unfinished in CodeArtifact. --resume continues it from the same manifest: it uploads only the assets not already present and then writes the finalizer -- no re-uploading what already landed. A present asset is taken as complete (CodeArtifact validated its SHA-256 on the original upload); if a source changed since the interrupted run, use --force instead. --resume and --force are mutually exclusive.
publish and promote prompt before mutating. With no TTY (CI, pipes) they
do not silently proceed -- they refuse unless --yes is given, so a
pipeline can't delete or overwrite by accident. Pass --yes in CI.
--dry-run never prompts.
Every publish also writes a cob-provenance.json asset (the finalizer) --
see Provenance.
cob pull <SOURCE> [DESTINATION] — same shape as cp/scp/rsync/
aws s3 cp. SOURCE is a manifest or compact coordinates; an inline
filter :asset[,asset...] after coordinates downloads only the named
files. DESTINATION defaults to the current directory.
# Whole package into a directory
cob pull my-domain/dev/my-namespace/my-package@2.1.0 ./assets/
# Latest version into the cwd (no DESTINATION → '.')
cob pull my-domain/dev/my-namespace/my-package@latest
# One asset to a specific file path
cob pull my-domain/dev/my-namespace/my-package@latest:app.tar.gz ./app.tar.gz
# Two named assets into a directory
cob pull my-domain/dev/my-namespace/my-package@2.1.0:app.tar.gz,sha256.txt ~/downloads/
# With a manifest -- pull all assets the manifest declares
cob pull my-package.yaml --version 2.1.0 ./assets/
# Combined with `cob use` — when a current package is set:
cob pull @latest:app.tar.gz ./d/ # @version override + filter + dest
cob pull vtdocs/vtdocs-installer@6.1.3 ~/d/ # ns/pkg shorthand + dest
cob pull ~/d/ # just the dest — uses current pkg + versionSkips files that already exist with a matching SHA-256.
A whole-package pull into a directory (no inline filter) also
writes a cob-manifest.yaml next to the assets -- the same manifest
cob manifest would produce (reconstructed from
provenance, or inferred). So cob pull <coords> ./d/ gives you the
assets, their cob-provenance.json, and a manifest to re-publish or
inspect from. Filtered pulls don't (the manifest would misrepresent
the package); a manifest hiccup only warns -- the assets are already
down.
Inline filter is rejected in manifest mode (the manifest is the source
of truth for which assets exist). One caveat: : and , are
technically legal in CodeArtifact generic asset names; the inline
filter syntax can't express filters for asset names that contain
either. You can always pull the whole version and pick the file out
of the destination.
Flags: --version (manifest mode only), --concurrency
Copies a package version between repositories. No local disk involved.
# Compact coordinates
cob promote my-domain/dev/my-namespace/my-package@2.1.0 --to staging
# With manifest -- source repo inferred from promote.stages
cob promote my-package.yaml --version 2.1.0 --to staging # dev -> staging
cob promote my-package.yaml --version 2.1.0 --to prod # staging -> prod
# Promote whatever is latest in the source repo
cob promote my-domain/dev/my-namespace/my-package@latest --to staging
# Preview the move without copying anything
cob promote my-domain/dev/my-namespace/my-package@2.1.0 --to staging --dry-run
# Finish an interrupted promote without re-copying what already landed
cob promote my-domain/dev/my-namespace/my-package@2.1.0 --to staging --resumeFlags: --to (required), --version, --force, --resume, --yes, --dry-run, --concurrency
--resume works the same way as on publish: it requires the destination version to be Unfinished (left behind by a partial promote), copies only assets not already present, then writes the finalizer. Mutually exclusive with --force.
Deletes a package version. cob treats published versions as immutable, so the command is gated in three tiers:
# Tier 1 (default): clean up a failed/abandoned publish
cob rm my-domain/dev/my-namespace/my-package@2.1.0-rc1
# Tier 2 (--force): delete a real Published version with no downstream copies
cob rm my-domain/dev/my-namespace/my-package@2.1.0 --force
# Tier 3 (--force --everywhere): delete even when other repos have promoted from this version
cob rm my-domain/dev/my-namespace/my-package@2.1.0 --force --everywhererm refuses @latest as a typo-shield — the only destructive verb makes
you name the bytes explicitly. With --force on a Published version, cob
probes every other repo in the same domain; if any holds the same version
(meaning a promote once recorded from: <this-repo>), the deletion
refuses and lists those repos. If a probe can't be completed (a transient
error), cob treats that repo as unverified rather than empty and refuses
the same way — a failed check is not evidence of "no downstream copy".
--force --everywhere overrides either case; the confirm prompt then names
the chains that will dangle.
Deletion destroys the cob-provenance.json along with the assets;
there is no soft-delete or archive.
Flags: --force, --everywhere, --yes
Drill into CodeArtifact at any level:
cob ls my-domain/dev # list packages
cob ls my-domain/dev/my-namespace/my-package # list versions
cob ls my-domain/dev/my-namespace/my-package@2.1.0 # list assets
cob ls 'my-domain/*/my-namespace/my-package@2.1.0' # promotion status across repos
cob ls my-domain/dev/my-namespace/my-package@latest # assets in latest version(Quote * for zsh; bash leaves an unmatched literal * alone.)
For multi-level discovery use -R (flat, fully-qualified) or tree
(indented tree):
cob ls -R # every package, one per line
cob ls -R my-domain/dev --depth versions # every version under a repo
cob ls -R my-domain/dev/my-namespace/my-package --depth assets # every asset under a package--depth accepts domains|repos|packages|versions|assets; the default is
packages, with one extra level when you target a specific node (so
cob ls -R my-domain/dev lists packages without spelling out --depth packages,
and targeting a single package descends one level into versions).
Flags: -R/--recursive, --depth
Tree-shaped view of the same walk ls -R produces — useful for "what do
we have?" exploration. Branches that can't be listed (denied repo, throttled
call) are marked inline with ! and don't abort the rest of the walk.
cob tree # everything down to packages
cob tree my-domain # one domain
cob tree my-domain/dev --depth versions # one repo down to versions
cob tree my-domain/dev/my-namespace/my-package # versions of a package (default-bumped)
cob tree my-domain/dev/my-namespace/my-package --depth assetsDefault depth is packages. Targeting a node and omitting --depth
descends one level into it. JSON mode emits a nested tree
({name, kind, path, meta, children, error}) rather than the flat array
ls -R --json produces — pick whichever shape your consumer wants.
Flags: --depth
Resolves the latest published version and prints the version string. Designed for scripting.
# Print just the version string
cob resolve my-domain/dev/my-namespace/my-package
# -> 2.1.0
# Use in scripts
VERSION=$(cob resolve my-domain/dev/my-namespace/my-package)
cob pull my-domain/dev/my-namespace/my-package@$VERSION ./assets/
# JSON output
cob resolve my-domain/dev/my-namespace/my-package --json
# -> {"package": "my-namespace/my-package", "repository": "my-domain/dev", "version": "2.1.0"}Resolution is by publication timestamp, not semver.
Generates a manifest from a directory of files. Scans <dir> (default
.) for top-level regular files and writes a manifest with each file
as a local ./<filename> source. Coordinates are placeholders unless
you pass --for. By default the manifest is written to
<dir>/cob-manifest.yaml so the ./<filename> source paths resolve
without editing.
# Scaffold from the current directory's files
cob init
# Scaffold from a specific directory
cob init ~/build-output
# Pre-fill coordinates so you don't have to edit them after
cob init ~/build-output --for acme/dev/tools/my-app
# Print to stdout instead of writing a file
cob init . -o -
# Bare schema (no comments, no promote stages)
cob init . --minimalSkips hidden files, sub-directories, and the cob-generated cob-manifest.yaml
and cob-provenance.json, so re-running on a pulled directory doesn't
recurse on its own metadata. Refuses to overwrite an existing
cob-manifest.yaml unless you pass --force (common case: you pulled
something into . and then absent-mindedly ran cob init — you
probably want to look at the file you'd clobber first).
The generated manifest is guaranteed to pass cob diff <manifest> lint
(schema + URI syntax + local-file existence) — same pre-flight that
publish/promote/pull run implicitly.
Flags: --for <coords>, --force, -o <path> (- for stdout), --minimal
Read-only print of a version's chain of evidence — who published it, who
promoted it, when, and where — plus per-asset origins (where each file
physically came from, recursing through ca:// upstreams). No integrity
check; that's what diff <coords> is for. Useful when you want
the history without paying for the asset listing and hash comparison.
cob log my-domain/dev/my-namespace/my-package@2.1.0
cob log my-domain/dev/my-namespace/my-package@latest
# Machine-readable: emits the full Provenance struct as JSON
cob log my-domain/dev/my-namespace/my-package@2.1.0 --json
# Surface chain references that point at deleted versions
cob log my-domain/staging/my-namespace/my-package@2.1.0 --check-references--check-references probes each chain event's referenced repository
(publish.repository, promote.from/promote.to) once and annotates
inline:
(deleted)— the referenced version no longer exists in that repo (e.g. it was removed bycob rm --force --everywhere)(?)— the probe itself failed (auth, throttle, network); distinct from(deleted)so you don't mistake "couldn't check" for "definitely gone"
Adds one VersionStatus call per unique referenced repository (the
probe set dedupes), so a long chain that bounces between two repos still
costs only two probes. Text-mode only — a JSON consumer can run their
own loop over prov.chain[].
Exits with an error if the version has no cob-provenance.json (a non-cob
publisher or pre-provenance version). For those, cob diff <coords>
still won't help (no provenance to compare against), but CodeArtifact's
stored asset hashes can be inspected via cob ls <coords>@<v>.
A note on the dangling-chain situation --check-references surfaces: the
destination's provenance is self-contained (promote carries the
source's chain forward and appends its own event, and reconcilePromotedAssets
preserves each asset's original Source URI and S3 Origin record). So a
deleted upstream affects only the ability to follow a from: link by
hand — it does not affect cob diff <coords> self-integrity, manifest-
vs-published diff, or --deep rehash, all of which work entirely from
the destination's own records.
diff is cob's only comparison verb. Five modes, picked from the
positional shape; none mutate; exit code is 0 if identical, non-zero on
any drift.
Match rows are terse by default: glyph, name, size, method (one line).
Mismatch rows expand to the full audit form (URI, size, full SHA-256 for
both sides) — that's where the bytes matter. Pass -v / --verbose to
put the URI and full hash on every row, not just mismatches.
| Mode | Invocation | What it answers |
|---|---|---|
| Manifest lint (offline) | cob diff <m.yaml> |
Is this manifest well-formed? |
| Manifest vs published | cob diff <m.yaml> --version X |
Does this manifest still produce @X? |
| Self-integrity | cob diff <coords> |
Is this published version intact? |
| Local dir vs published | cob diff <dir> <coords> |
Do my local files match what was published? |
| Version vs version | cob diff <coords-A> <coords-B> |
What changed between these two releases? |
Manifest lint -- schema + URI syntax + local-file existence. No AWS
calls. The same checks run implicitly at the top of every other
manifest-based command (publish, promote, pull, diff --version),
so a manifest that publishes cleanly will always lint cleanly first. This
mode is the user-facing report of that pre-flight.
cob diff my-package.yamlOutput labels each row: (42.0 MB) for verified-local,
(remote, syntax only) for unverified-remote. Safe in pre-commit / CI
lint stages without credentials.
Manifest vs published -- hashes each source and compares to the
published asset of the same name. Run before publish --force to see
exactly what would change. Source hash precedence, cheapest first:
- a known checksum -- S3 object with
--checksum-algorithm SHA256, aca://source, or a local file (no download); - for an unchecksummed S3 source, the recorded origin (see Provenance):
a
HeadObjectnow, comparingversion_id/etagto what was recorded at publish -- a no-download drift signal. Unchanged → recorded SHA is trusted (match(origin)); changed/unreadable → reported as drift; - otherwise the recorded
cob-provenance.jsonSHA (match(provenance)); - with
--deep, cob downloads and hashes the source (no S3 writes).
cob diff my-package.yaml --version 2.1.0
cob diff my-package.yaml --version 2.1.0 --deep # hash unchecksummed sourcesSelf-integrity -- fetches the recorded cob-provenance.json for
<coords> and compares each entry's SHA-256 to what CodeArtifact
currently stores. The verdict (match / altered / missing) prints first
so it's the first thing seen; the chain of evidence (who published/
promoted it, where each file came from, recursing through ca://)
follows as supporting context. Use cob log <coords> instead when
you only want the chain and don't need the byte check. Audit a version
you didn't build, with nothing but its coordinates:
cob diff my-domain/dev/my-namespace/my-package@2.1.0
cob diff my-domain/dev/my-namespace/my-package@latestLocal dir vs published -- for each published asset of <coords>,
looks for a local file of the same name in <dir>, hashes it, and
compares to the published SHA-256. No manifest is consulted, no remote
URIs dereferenced. The straight answer to "do these local files match
what was published?"
# After `cob pull` (which writes ca:// pinned sources for re-publish
# lineage), the most direct check that your local copy is intact:
cob diff ~/pulled-dir my-domain/dev/my-namespace/my-package@2.1.0A typical mismatch row:
✗ readability.jar mismatch
local 195.5 MB /home/me/pulled-dir/readability.jar a3f2b8c9d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1
published 195.5 MB my-domain/dev/my-namespace/my-package@2.1.3 25c4517cdef0123456789abcdef0123456789abcdef0123456789abcdef01234
sha256sum the local file and you can string-compare the full hash --
no truncation guesswork.
Version vs version -- compares two published versions of the same
package by the SHA-256 each side recorded in CodeArtifact. No downloads.
cob-provenance.json is excluded from both sides (its bytes trivially
differ on every publish/promote -- chain timestamps, IDs -- but that
isn't a package change). The two coordinates must reference the same
package; cross-repo same-package is allowed.
# What changed in this release?
cob diff acme/dev/tools/my-app@2.0.0 acme/dev/tools/my-app@2.1.0
# Did the promote preserve the bytes?
cob diff acme/dev/tools/my-app@2.1.0 acme/prod/tools/my-app@2.1.0Flags: --version (manifest mode: COB_VERSION fallback), --deep
(manifest vs published only), -v/--verbose, --check-references
(manifest + self-check modes: probe each chain reference's repository
and warn if any are deleted — mirrors cob log --check-references).
Prints a manifest (YAML, to stdout) for an existing version -- recover the manifest for a package you have only coordinates for.
If the version has a cob-provenance.json, it is reconstructed
faithfully: original sources: keys, with each URI as it resolved at
publish. This is a pinned snapshot -- ${VERSION}/${env.*} are already
expanded, and promote.stages is not recoverable (provenance records actual
promotes, not the declared list).
Otherwise it is inferred: the version has no provenance (non-cob or
pre-provenance), so real origins are unknown -- each asset is sourced from
the package itself via ca://. Re-publishing the inferred manifest
reproduces the same bytes. cob-provenance.json is never emitted as a
source.
cob manifest my-domain/dev/my-namespace/my-package@2.1.0 > my-package.yaml
cob manifest my-domain/dev/my-namespace/my-package@latestFlags: --version (or use @version / COB_VERSION)
When you're debugging or auditing, you tend to run several read-only
commands against the same coordinates back-to-back. Instead of retyping
acme/prod/tools/my-app@2.1.0 three times:
cob use acme/prod/tools/my-app@2.1.0
cob log # picks it up
cob diff # picks it up
cob pull ./assets/ # picks it up — destination as second positionalStorage is project-local by default: ./.cob/current. A .gitignore is
dropped in alongside on creation so a local current package can't leak
into git. Add --global to write ~/.config/cob/current instead.
Resolution precedence when a read-only command sees no positional arg:
--packageflag on the root commandCOB_PACKAGE_COORDSenv (distinct fromCOB_PACKAGE, which overrides thepackage:field in a manifest).cob/currentin the cwd, walking up parent directories~/.config/cob/current(global)
First match wins. When fallback fires, an Using <coords> (from <source>)
header is printed to stderr (not stdout) so a captured cob resolve
still emits just the version string.
Commands that inherit: log, diff (self-check, dir, version-vs-version),
pull, manifest, resolve. Commands that do not: rm, publish,
promote. Destructive commands deliberately require explicit coordinates
so an operator who cd'd into the wrong project can't have cob rm
silently target a stale cob use setting.
cob use --show # print current coords and where they came from
cob use --clear # remove the pointer (idempotent)
cob use --global X # write to ~/.config/cob/currentPrints the build version. With --json (the global flag), emits a structured document for CI pin checks:
cob version # → v0.0.2 (abc123, go1.25.3, 2026-05-23T16:21:44Z)
cob version --json # → {"version":"v0.0.2","commit":"abc123","time":"...","go":"go1.25.3","os":"linux","arch":"amd64"}The same version string is reachable via cob --version for parity with other cobra-based tools; both paths render through one BuildInfo.HumanString, so they can't drift.
Shell completion is auto-generated by cobra:
cob completion bash > /etc/bash_completion.d/cob # bash
cob completion zsh > "${fpath[1]}/_cob" # zsh
cob completion fish > ~/.config/fish/completions/cob.fish # fish
cob completion powershell | Out-String | Invoke-Expression # PowerShellRun cob completion <shell> --help for shell-specific install instructions.
Every cob publish writes one extra asset, cob-provenance.json -- a
chain-of-evidence document, published last (it finalizes the version). It
records, with no S3 writes:
chain-- an append-only event log.publishrecords the origin repo/version, time, region, the manifest's SHA-256, and the AWS principal (account/arn/user_id, from STS).promotedoes not copy the file verbatim -- it appends apromotelink (from/to, time, actor), so the chain shows exactly who moved the version where.assets[].origin-- where each file physically came from at packaging time:- s3:
bucket,key,version_id,etag,last_modified,region(the no-download drift signaldiffuses formatch(origin)); - ca: the upstream coordinates, plus the upstream's own
cob-provenance.jsonembedded recursively -- the full transitive history travels inside the package. An upstream not published by cob is recorded honestly asupstream_status: no-cob-provenancerather than failing; a re-check that finds it deleted reportsmissing(non-fatal -- the embedded evidence still stands). - file:
pathandmtime.
- s3:
Because each upstream's provenance is already complete when cob reads it,
ca:// embedding is one fetch per direct source and terminates naturally
(publish order is acyclic). The file grows with the dependency closure;
that's the intended trade-off for a self-contained evidence trail.
promote carries assets forward unchanged (same bytes) and only appends
to chain. The file appears in cob ls <pkg>@ver and is fetched by
cob pull; it is excluded from diff's "not in manifest" reporting.
Read it with cob diff <coordinates> (the self-integrity mode): cob fetches
this document, compares each recorded SHA-256 to what CodeArtifact stores,
and prints the chain plus the recursive origin tree -- audit any cob-
published version with nothing but its coordinates. For chain-only viewing
without the byte comparison, use cob log <coordinates>.
publish, pull, and promote transfer assets in parallel, bounded by
--concurrency (default 4; 1 = sequential, the old behaviour). For
publish/promote the cob-provenance.json write is the finalizer and is
always sequenced last, after every other asset has uploaded, so a failure
leaves the version Unfinished exactly as before.
| Prefix | Description | Example |
|---|---|---|
s3:// |
S3 object | s3://bucket/path/to/file.tar.gz |
ca:// |
CodeArtifact asset | ca://domain/repo/ns/pkg@version/asset |
./ or path |
Local file | ./local-config.yaml |
Relative paths resolve from the manifest file's directory, not the working directory. Bare filenames (without ./ prefix) are also treated as relative paths.
A manifest is executable configuration -- like a Makefile. publish and validate read whatever local files it names, so don't run a manifest you don't trust.
@latest is not supported in ca:// source URIs. Use ${VERSION} instead.
The YAML key in sources: is only a label -- for the manifest and logs. The
asset name stored in CodeArtifact is the source's basename:
| Source | Stored asset name |
|---|---|
s3://bucket/builds/app-2.1.0.tar.gz |
app-2.1.0.tar.gz |
ca://acme/dev/tools/shared-lib@2.0.0/shared-lib-2.0.deb |
shared-lib-2.0.deb |
./local-overrides.yaml |
local-overrides.yaml |
cob pull <coords>:<asset> selects by this stored name, not by the
manifest key. Put ${VERSION} in the source URI so the version travels
with the filename (app-${VERSION}.tar.gz).
Because the basename is the identity, two sources that resolve to the same
basename are rejected -- one would silently overwrite the other in
CodeArtifact. validate reports this offline.
S3 objects uploaded with --checksum-algorithm SHA256 store the hash in metadata; cob cross-checks it against the SHA-256 it computes while streaming and rejects the asset if they disagree. There is no size limit -- assets are streamed through a temp file, not held in memory (see Known limitations).
aws s3 cp file.tar.gz s3://bucket/key --checksum-algorithm SHA256cob resolves an S3 bucket's region automatically, so --region need not
match the bucket's region for s3:// sources.
Source URIs support two variable namespaces:
${VERSION}-- from--versionflag orCOB_VERSIONenv var${env.NAME}-- reads the environment variableCOB_VAR_NAME
${env.NAME} deliberately does not read an arbitrary variable named
NAME. It reads COB_VAR_ + NAME, so ${env.GIT_SHA} resolves from
COB_VAR_GIT_SHA. This namespacing keeps a manifest from pulling a secret
like AWS_SECRET_ACCESS_KEY into a source URI -- a resolved URI is recorded
in published provenance and sent to the URI's host.
Unresolved variables are a hard error.
sources:
release: s3://my-bucket/builds/app-${VERSION}.tar.gz
config: s3://my-bucket/builds/${env.GIT_SHA}/config.yamlCOB_VAR_GIT_SHA=abc123 cob publish my-package.yaml --version 2.1.0--profile AWS profile
--region AWS region
--json Machine-readable JSON output
--quiet, -q Suppress headers, summaries, and progress (errors still print)
--verbose Log cob's own steps -- coordinate resolution, source list, skip reasons, per-asset timing, and one line per AWS call -- to stderr (also: COB_VERBOSE)
--debug Log AWS API responses/retries to stderr
--tmpdir Directory for streaming spill files (default: $TMPDIR)
--no-tui Force line-stream output even on a TTY (also: COB_TUI=0)
--timeout Deadline for long-running ops (pull/publish/promote/diff); e.g. 30m. 0 = no deadline (also: COB_TIMEOUT)
--package Coordinates the read-only commands (log/diff/pull/manifest/resolve) fall back to when no positional given (also: COB_PACKAGE_COORDS, .cob/current; see `cob use`). Destructive commands (rm/publish/promote) ignore this.
On an interactive terminal, publish / pull / promote render a live
multi-row progress view -- one row per asset, in-place updates, progress
bar / rate / ETA per row, totals at the bottom. The view scrolls into
shell history when the command exits (no alt-screen takeover). Output
piped to a file, JSON mode, and --quiet automatically fall back to the
stream renderer (one OK <name> line per completed asset, no in-place
updates). --no-tui (or COB_TUI=0) forces stream output even on a
TTY -- useful for screen recording, exotic terminal emulators, or
copy-paste-friendly logs.
--verbose and --debug are different lenses. --verbose narrates what
cob is doing -- which coordinates a target resolved to, which source URIs
it's publishing, why an asset was skipped, how long each transfer took, and
one aws <Operation> <coords> line per CodeArtifact/S3 call -- as
verbose:-prefixed lines on stderr (never stdout, so --json stays clean;
emitted even under --quiet, since you asked for it). It has no -v short:
diff keeps -v for its per-row detail.
--debug is the lower-level lens for when an AWS call fails for a
non-obvious reason (region, credentials, throttling) -- it logs every AWS
response status line and retry attempt to stderr without touching stdout.
Request logging is deliberately omitted: a signed AWS request header carries
a live session credential (X-Amz-Security-Token), and --debug output
often ends up in CI logs.
Tries in order:
--profileflag orCOB_PROFILEAWS_ACCESS_KEY_ID+AWS_SECRET_ACCESS_KEY- Default credential chain (instance roles, ECS task roles)
No automatic SSO login. If an SSO token is expired, cob tells you to run aws sso login.
All COB_* variables sit in the middle of the precedence chain: CLI flags > env vars > manifest file.
COB_VERSION Package version (--version fallback, ${VERSION} in source URIs)
COB_DOMAIN Override manifest domain
COB_REPOSITORY Override manifest repository
COB_NAMESPACE Override manifest namespace
COB_PACKAGE Override manifest package (a manifest field; NOT the same as COB_PACKAGE_COORDS)
COB_PACKAGE_COORDS Current-package coordinates for read-only commands (see `cob use`)
COB_PROFILE AWS profile (--profile fallback)
COB_REGION AWS region (--region fallback)
COB_TMPDIR Spill directory (--tmpdir fallback)
COB_TIMEOUT Deadline for long-running ops (--timeout fallback, e.g. 30m)
COB_JSON Set 1/true to default to --json output
COB_QUIET Set 1/true to default to --quiet output
COB_VERBOSE Set 1/true to default to --verbose step/AWS-call tracing
COB_DEBUG Set 1/true to default to --debug logging
COB_TUI Set 0 to disable the live progress view (same as --no-tui)
ACCESSIBLE Set 1 to force the line-stream renderer (screen-reader friendly)
COB_VAR_* Values for ${env.*} in source URIs (see Variable substitution)
ACCESSIBLE is a cross-tool convention (Charm libs, gh, others) for
"I'm using a screen reader; please degrade to plain text." cob honors it
identically to --no-tui / COB_TUI=0 -- the live TUI's box-drawing and
cursor-positioning escapes are unusable in that mode.
Standard AWS environment variables (AWS_REGION, AWS_PROFILE, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) are also respected through the default credential chain.
This means a single manifest can target different domains/repos in CI without editing the file:
COB_DOMAIN=acme-prod COB_REPOSITORY=prod cob publish my-package.yaml --version 1.0.0When an env var overrides a manifest field, cob prints a warning so you know it's active:
Warning: using COB_DOMAIN=acme-prod (overrides manifest domain)
A few things to know about scope:
- Manifest overrides only apply to manifest-based operations. Compact coordinates (
cob pull mydom/repo/ns/pkg@1.0.0) are explicit and ignoreCOB_DOMAINand friends entirely. COB_REPOSITORYdoes not affect promote's source inference. Promote walks thepromote.stageslist to determine the source repo. SettingCOB_REPOSITORYchanges where publish targets, but promote still moves between defined stages. This is intentional -- promote's purpose is moving between stages, not targeting an arbitrary repo.
Most commands support --json for machine-readable output. JSON goes to stdout, errors always go to stderr. A JSON object is emitted even on early failures (auth, config) so CI pipelines can reliably parse the output.
publish, pull, promote, diff, rm emit a CommandResult object with command, package, repository, region, actor, assets, status, etc. region and actor (STS account/ARN/user_id) identify which account/role and region executed the operation -- audit pipelines no longer need to scan per-asset Origin records or the provenance chain to attribute a run. Any warnings raised during the command are collected into its warnings array -- warnings also print to stderr, but --json consumers should read this field.
Each entry in assets[] carries a kind discriminator alongside method, so a consumer can select(.kind == "transfer") or select(.kind == "compare") without grepping method string values:
kind |
Produced by | method values |
|---|---|---|
transfer |
publish, pull, promote |
spilled, skipped |
compare |
diff (manifest / self-check / dir / versions) |
match, match(provenance), match(source), match(deep), match(origin), mismatch, added, removed, changed, same, missing, missing-local, altered, unknown |
lint |
diff <manifest> (offline lint) |
exists, syntax(s3), syntax(ca) |
dry-run |
publish --dry-run, promote --dry-run |
dry-run |
ls emits an array of the relevant type: packages, versions, assets, or promotion statuses.
ls -R emits a flat array of strings (one fully-qualified coordinate per leaf), for shell pipelines.
tree emits a flat array of typed records -- one per walked node, in walk order, no nesting. Each record carries path, kind (domain|repo|package|version|asset), the matching coordinate components, and a typed payload (repo_count, package_count, version_count/latest_version, asset_count/published, size/sha256) -- jq filters on select(.kind == "package" and .version_count > 5) work directly, without recursion or parsing stringy metadata.
log emits the full Provenance struct: schema, package, chain[] of events (publish/promote), assets[] with per-asset SHA + origin (recursing into ca:// upstreams).
resolve emits its own minimal schema designed for scripting:
{"package": "ns/pkg", "repository": "domain/repo", "version": "2.1.0"}One exception: manifest always prints a YAML manifest (that is its output -- --json does not apply). Every other command honours --json.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Error -- the command could not be completed (auth, config, network) |
| 2 | Not found (package/version/asset doesn't exist) |
| 3 | Conflict (version exists, use --force) |
| 4 | Mismatch -- the check ran and found a difference (diff SHA mismatch or drift) |
| 130 | Interrupted -- user hit Ctrl-C; in-flight transfers were aborted |
Code 4 is the one that matters for diff as a CI gate: it means the
published bytes genuinely differ from what was expected, as opposed to code 1
which means the check itself could not run. Both are non-zero, so either still
blocks a pipeline.
Code 130 follows the POSIX shell convention (128 + SIGINT). Distinguishes a
deliberate cancellation from a transient failure, so CI retry policies can
choose whether to honor it. publish and promote leave the version
Unfinished after a 130 -- re-run with --resume to continue from where
the cancel landed.
If the same artifact appears in multiple packages, extract it into its own package and reference it with ca:// URIs.
Instead of four manifests each pulling the same file from S3:
# bad -- same S3 source repeated in 4 manifests, 4 copies in CodeArtifact
sources:
shared-lib: s3://artifacts/shared-lib-2.0.debCreate a dedicated package:
# tools/shared-lib@2.0.0 has one asset: shared-lib-2.0.deb
cob publish shared-lib.yaml --version 2.0.0Then reference it from consuming packages:
# my-app.yaml
sources:
shared-lib: ca://acme/dev/tools/shared-lib@2.0.0/shared-lib-2.0.deb
app: s3://artifacts/my-app-${VERSION}.tar.gzOne source of truth, one place to update when the version changes.
Always pin to a specific version in ca:// source URIs. @latest is not supported (and is explicitly rejected), and ${VERSION} expands to the version of the package you're publishing, not the version of the dependency you're pulling from.
If you need the dependency version to vary per environment, use an env var:
sources:
shared-lib: ca://acme/dev/tools/shared-lib@${env.LIB_VERSION}/shared-lib.debBut a pinned version is usually better -- it makes builds reproducible. Same manifest, same output, every time.
When a consuming package references a ca:// source in a specific repository, that source must already exist there. If my-app references ca://acme/staging/tools/shared-lib@2.0.0/..., then tools/shared-lib@2.0.0 must be promoted to staging before my-app can be published or promoted to staging.
In practice this means your CI pipeline should promote dependencies before dependents:
# Promote the shared package first
cob promote acme/dev/tools/shared-lib@2.0.0 --to staging
# Then promote the consuming package
cob promote acme/dev/apps/my-app@1.5.0 --to stagingIf your ca:// URIs use a variable for the repository (ca://acme/${env.TARGET_REPO}/...), set it at publish time so each stage's package points to its own repo.
Name packages for what they are, not for the fact that they're shared. tools/shared-lib is clear. common/misc-stuff becomes a junk drawer. If assets aren't related to each other, they belong in separate packages even if multiple consumers reference them.
--forceis not atomic. Deletes the existing version then re-publishes. Brief window where the version doesn't exist.- Transfers spill to a temp file, not memory. CodeArtifact's API requires an
io.ReadSeeker(Content-Length + retries), so true end-to-end streaming isn't possible; cob streams each asset through a temp file in$TMPDIRinstead of buffering in RAM. Memory stays bounded and there is no asset size limit, but a publish/promote needs free temp disk for the largest single asset. Note that$TMPDIRistmpfs(RAM-backed) on many Linux systems -- for large assets, point--tmpdir/COB_TMPDIRat real disk with room forconcurrency× the largest asset. @latestresolves by timestamp, not semver. The most recently published version wins, regardless of version string ordering.
MIT -- see LICENSE.