Test the same bytes you deploy.
SameByte is a local, open-source CLI that traces OCI artifact identity through GitHub Actions workflows. A green workflow does not necessarily test the image it deploys:
npm build → npm test
↓
docker build → deploy
$ samebyte tests/fixtures/rebuild.yml
SB001 HIGH [mismatch]
Production artifact was never tested. Source tests ran, but no recognized OCI test consumes the deployed digest.Build the image once, publish its digest, and pass that digest to every consumer:
build → OCI digest
├── docker run (tests)
├── Trivy
├── attestation
└── deployment
$ samebyte tests/fixtures/correct.yml
Deployment: ghcr.io/acme/api@<symbolic build digest> [proven]
test: proven
scan: proven
attest: proven
Artifact lineage verified.The output above abbreviates the symbolic digest. SameByte does not run a build or fetch its real digest. It proves that supported consumers reference the same output. Complete examples are in tests/fixtures.
SameByte requires Node.js 22 or later. Each stable vX.Y.Z tag creates a GitHub Release with a versioned npm package tarball and SHA-256 checksums. The package tarball installs through npm without a registry account:
npm install --global https://github.com/0then0/samebyte/releases/download/v0.1.0/samebyte-0.1.0.tgz
samebyte .Replace 0.1.0 in both places with the release version you want. To verify a downloaded asset before installing it:
curl -fLO https://github.com/0then0/samebyte/releases/download/v0.1.0/samebyte-0.1.0.tgz
curl -fLO https://github.com/0then0/samebyte/releases/download/v0.1.0/SHA256SUMS
sha256sum --check SHA256SUMS
npm install --global ./samebyte-0.1.0.tgzOn macOS, use shasum -a 256 --check SHA256SUMS instead of sha256sum --check.
samebyte .
samebyte .github/workflows
samebyte .github/workflows/release.yml
samebyte check . --format text
samebyte explain .
samebyte graph .
samebyte . --format json
samebyte . --format sarif > samebyte.sarif
samebyte . --config samebyte.config.jsonA repository path scans .github/workflows/*.yml and *.yaml. A directory path scans its immediate YAML files. Files are analyzed independently: identity is not joined across workflow runs.
Text findings include source locations and evidence paths. explain also prints known reasons that an identity remains unknown, including untracked OCI platform selection. graph shows artifacts, producers, and consumers; --format json exposes the structured graph, operations, findings, diagnostics, and deployment checks. --format sarif emits SARIF 2.1.0 results with workflow line locations and evidence properties.
Exit codes:
0: analysis completed without high-confidence findings. This can include unknown lineage or no supported deployments.1: at least one high-confidence finding, including a mutable deployment reference.2: an input, configuration, YAML, dependency-graph, or analysis error. Errors take precedence over findings.
Artifact lineage verified requires all detected deployments to have proven test, scan, and attestation identity, with no findings or errors. Missing operations remain unknown; their absence alone does not cause a violation.
The analysis separates workflow parsing, symbolic values, operation adapters, an artifact graph, and rules. Adapters recognize producers and consumers; the rules compare their resolved identities and execution order.
docker/build-push-actionproduces a symbolic OCI digest throughsteps.<id>.outputs.digest.- Step outputs propagate through job
outputs, directneeds.<job>.outputs, and workflow/job/stepenv. - GitHub expressions are parsed using GitHub's @actions/expressions AST. Direct property access, bracket notation, string literals, and
github.shaare supported. Functions, dynamic indexing, and compound expressions remain unknown. - Simple
echo "name=value" >> "$GITHUB_OUTPUT"forwards a value, including resolved expressions and environment variables. - Full 64-hex
sha256digests provide immutable identity. Tags, including${{ github.sha }}, are mutable references. A source revision is not an OCI digest. - Job dependencies, step order, and completion order matter. A later test, a parallel sibling job, status checks that can bypass success, matrix jobs, and ignored failures do not establish unconditional verification. A plain
if: success()condition preserves the success gate. background: truesteps only establish a check afterwaitorwait-all. Their outputs remain unknown until then. If waiting ignores failure withcontinue-on-error, the result stays unknown. A dependent job starts after background steps in its prerequisite job finish.parallelsyntax is accepted, but its nested operations are not analyzed yet. SameByte reports an analysis diagnostic and exits with code2instead of claiming lineage is verified.- A multi-platform build and
docker run --platform ...do not prove a specific runtime manifest. SameByte keeps these identities unknown because the selected OCI child manifest can depend on the target platform. - A concrete digest used by
docker runwithout a recognized single-platform producer also remains unknown for testing. It may identify an OCI index, and a test runner can select a different child manifest from the production platform. Identity mismatches between distinct concrete digests are still reported.
The common supported pipeline is:
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v7
# Configure registry authentication for your own workflow.
- id: build
uses: docker/build-push-action@v7
with:
push: true
tags: ghcr.io/acme/api:release
verify:
needs: build
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
attestations: write
artifact-metadata: write
env:
IMAGE: ghcr.io/acme/api@${{ needs.build.outputs.digest }}
steps:
- run: docker run --rm "$IMAGE" npm test
- uses: aquasecurity/trivy-action@0.36.0
with:
image-ref: ${{ env.IMAGE }}
- uses: actions/attest@v4
with:
subject-name: ghcr.io/acme/api
subject-digest: ${{ needs.build.outputs.digest }}
deploy:
needs: [build, verify]
runs-on: ubuntu-latest
steps:
- run: kubectl set image deployment/api api=ghcr.io/acme/api@${{ needs.build.outputs.digest }}This is a lineage example, not a complete registry/cluster setup. Configure the necessary credentials, permissions, and tooling for actual execution.
- Build:
docker/build-push-action(v6 and v7),docker build,docker buildx build. Shell builds record a producer and supported-t/--tagreference, but do not invent an externally accessible digest output. - Test: foreground
docker runwith common flags. This means that the image is exercised; SameByte does not assess its test command or coverage.npm test,pnpm test, andyarn testare source tests, not OCI tests.docker compose runis recognized with unknown identity because Compose file interpretation is outside this MVP. - Scan:
aquasecurity/trivy-action(image-ref),docker/scout-action(imagewithcommand: cves,quickview, orcompare),anchore/scan-action(image); simpletrivy image,grype, anddocker scout cves/quickviewcommands. Trivy'sinputarchive option takes precedence over the image reference and therefore remains unknown. Scout commands such asenvironmentandattestation-addare not treated as scans. - Attestation:
actions/attestandactions/attest-build-provenance(subject-name,subject-digest), plusgh attestation verify oci://.... This tracks the subject identity; it does not validate signatures or policy itself. - Deploy: simple
kubectl set imagecontainer assignments andhelm upgrade/helm install. Helm chart values alone do not prove what a chart renders, so Helm deployments have unknown identity unless an explicit annotation describes the image. Custom deployment actions require annotations.
Adapters recognize the action repository independently of the pinned ref. This assumes that the referenced action implements its documented interface; SameByte does not audit the action's code. Unsupported flags and inputs may reduce coverage to unknown.
- SB001: a production image with a proven digest link was built after source tests, and no recognized OCI test consumes that digest. A matching mutable tag only yields a medium confidence candidate because it cannot establish which bytes the registry served. Unrelated image tests never suppress this finding.
- SB002: preceding tests use a different concrete digest from deployment for the same repository.
- SB003: preceding scans use a different concrete digest from deployment for the same repository.
- SB004: preceding attestation operations use a different concrete digest from deployment for the same repository.
- SB005: deployment uses a mutable image reference. The mutable-reference finding is high confidence; the artifact relationship is still
unknown. - SB006: a recognized deployment has unresolved identity or crosses an unsupported shell boundary. This is medium confidence and does not alone fail the CLI.
Two independent build outputs may contain the same bytes. SameByte reports that relationship as unknown, not as a proven mismatch. It also avoids comparing unrelated image repositories as mismatches.
SameByte does not execute or read arbitrary scripts. ./deploy.sh by itself is not enough to infer deployment. Complex shell control flow, substitutions, sourced files, heredocs, custom shells, and dynamic transforms cannot prove identity. Shell environment mutation invalidates the affected analysis context; $GITHUB_ENV is not interpreted as a supported value transfer.
For a custom step, explicitly declare its behavior in a JSON file:
{
"annotations": [
{
"workflow": "release.yml",
"job": "deploy",
"step": "ship",
"operation": "deploy",
"image": "ghcr.io/acme/api@${{ needs.build.outputs.digest }}"
}
]
}workflow matches the filename, job matches its job ID, and step matches an explicit step id. Operations are build, test, scan, attest, or deploy. A build annotation may specify output (default digest) to declare an OCI digest step output, and image to record its reference. Consumer annotations require image.
Annotations replace automatic interpretation of that step. They are trusted user assertions, appear in evidence, and are not independently verified. Use distinct workflow filenames when applying one config across several files.
The repository includes a composite action.yml. Use a release tag to select a version:
- uses: actions/checkout@v7
- uses: 0then0/samebyte@v0.1.0
with:
path: .github/workflows
format: textFor stronger supply-chain protection, replace v0.1.0 with the full reviewed commit SHA shown on that release.
The action uses Node 22 and builds the CLI from its npm lockfile, including development dependencies needed by esbuild even when the calling workflow sets NODE_ENV=production. It accepts optional config and preserves the CLI exit status. SARIF output is printed to stdout; uploading it to GitHub Code Scanning is a separate workflow step.
Push a stable SemVer tag matching package.json to run the release workflow. It reruns typecheck, lint, and tests, creates the npm package tarball, writes a SHA-256 checksum file, and publishes both as assets on a GitHub Release with generated release notes.
npm version patch
git push origin main --follow-tagsnpm version updates package.json and package-lock.json, creates the matching vX.Y.Z tag, and commits the version change. Review that commit and tag before pushing. The release workflow rejects tags that do not match the package version. Release assets can be installed directly with npm as shown above.
npm ci --ignore-scripts
npm run typecheck
npm run lint
npm run format
npm test
npm run build
npm pack --dry-runTo install the current working tree locally during development, run npm install --global . after building.
Biome handles formatting and linting. Tests use node:test, including CLI subprocess checks. Fixtures cover a correct pipeline, rebuild after source tests, and an unknown shell boundary; regression tests cover mismatches, mutable tags, expressions, dependency order, annotations, output formats, and conservative shell handling.
Source layout:
src/parser.ts: discovery, YAML locations, structural checks, dependency ordering.src/expressions.ts: GitHub expression AST, symbolic values, env propagation, OCI identities.src/adapters.tsandsrc/shell.ts: action interfaces and a restricted shell tokenizer.src/analyzer.tsandsrc/model.ts: transfer of values and artifact/operation graph.src/rules.ts: identity relationships, check ordering, and findings.src/output.tsandsrc/cli.ts: text, JSON, SARIF, graph, and process exit codes.
SameByte checks lineage and identity only. It does not replace SLSA, in-toto, or Sigstore; scan vulnerabilities; sign artifacts; assess Dockerfiles or test quality; run CI; or guarantee supply-chain security. Reusable workflows, arbitrary binaries, non-GitHub CI, and generic build-system analysis are outside this version.