A static artifact is a directory, not one executable. Recording only the source commit or homepage checksum leaves gaps: a missing image, stale alias, or changed download can survive unnoticed. Enumerate every regular file in a frozen artifact before transport.

A frozen regular-file source is transported to a destination. Each produces an external sorted manifest; the destination list includes an illustrative extra old.js record. Both lists feed a full-record diff. A separate branch hashes the source manifest into a compact release identifier. Listed-file checking cannot detect extras, and matching bytes do not establish authorship.
Freeze writers, reject symlinks and special files, and keep both manifests outside the artifact trees. A listed-file check detects missing or changed bytes; recomputing and comparing the complete lists also detects extras. Hashing the trusted source manifest supplies a compact identifier, with separate controls for authenticity and metadata.

Generate a stable list

Run this in Bash with GNU findutils and coreutils, from the directory containing public/. Keep the manifest outside the measured tree. The release must contain only directories and regular files: inspect and reject symlinks or special files first, since they are outside this manifest’s contract.

set -euo pipefail
manifest="$PWD/release.sha256"
(
  cd public
  nonregular=$(find . ! -type d ! -type f -print -quit)
  test -z "$nonregular"
  find . -type f -print0 | LC_ALL=C sort -z | xargs -0 -r sha256sum --
) > "$manifest"
test -s "$manifest"

Sort filenames before hashing. NUL-delimited paths preserve spaces, tabs, and embedded newlines between tools; GNU sha256sum escapes special names in its ordinary checkable output. Do not parse that output with awk '{print $2}': whitespace is legal in filenames. The separate assignment preserves a failed find exit status; testing only its output could mistake a failed scan for an empty result. Treat any failed scan or pipeline as an incomplete manifest and do not publish it. Stop writers while measuring; these commands do not create a filesystem snapshot.

Verify after transport

In one disposable Bash session on the destination, run the remaining fragments in order so the paths and set options stay in scope. Set the manifest and artifact paths to their actual locations:

set -euo pipefail
manifest=/path/to/release.sha256
artifact=/srv/www/site.new
(
  cd "$artifact"
  sha256sum -c "$manifest"
)

Checking listed files catches missing or changed bytes, but not extra files. Recompute the destination manifest with the same procedure and compare the complete records:

observed=$(mktemp "${manifest}.observed.XXXXXX")
trap 'rm -f "$observed"' EXIT
(
  cd "$artifact"
  nonregular=$(find . ! -type d ! -type f -print -quit)
  test -z "$nonregular"
  find . -type f -print0 | LC_ALL=C sort -z | xargs -0 -r sha256sum --
) > "$observed"
diff -u "$manifest" "$observed"

No diff and exit status zero means the regular-file paths and hashes agree. An extra stale file now fails too. The temporary output remains outside the artifact so the check never includes itself. Empty directories, permissions, ownership, and timestamps are not measured; validate those separately if deployment depends on them.

Add an aggregate digest for release records

Hash the manifest itself for a compact release identifier:

sha256sum "$manifest"

Its meaning depends on fixed rules: relative paths, sorting locale, algorithm, tool format, and included metadata. Keep the trusted manifest with the source commit and builder version.

Do not confuse integrity with authenticity

Matching SHA-256 digests are strong practical evidence of matching bytes, not proof of authorship. An attacker able to replace both artifact and manifest can make the check pass. Signing and trusted distribution are separate controls; verify only a manifest obtained through the intended trusted channel.

Sources