Skip to content

GitHub Action

Pack in CI with no shell glue. The composite action downloads the signed release binary for the runner, verifies it against the release checksums, and runs pack:

- uses: schubydoo/scratchsmith@v<ver>   # pin to a release tag
  with:
    binary: ./dist/app         # your prebuilt dynamic glibc binary
    sbom: true                 # needs syft on the runner
    strip: true
    smoke: true                # fail the job if the packed image can't start

To publish the built image, log in first and set push:

- uses: docker/login-action@v3
  with: { registry: ghcr.io, username: ${{ github.actor }}, password: ${{ secrets.GITHUB_TOKEN }} }
- uses: schubydoo/scratchsmith@v<ver>   # pin to a release tag
  with:
    binary: ./dist/app
    push: ghcr.io/${{ github.repository }}:latest

Nine inputs take one value per line: cmd, env, label, healthcheck, add-file, locale, include, deny, and require. Set image metadata, gate on vulnerabilities, or cap the size:

- uses: schubydoo/scratchsmith@v<ver>
  with:
    binary: ./dist/app
    entrypoint: /app
    env: |
      LANG=C.UTF-8
    workdir: /data
    label: |
      org.opencontainers.image.source=https://github.com/you/app
      role=api
    healthcheck: |
      /app
      --health
    scan: true
    scan-fail-on: high        # needs grype on the runner
    max-size: 25MB

Pin @v<ver> to a specific release tag (or a commit SHA). That is the same supply-chain hygiene the tool itself practices. version: overrides which scratchsmith release the action runs (defaults to the pinned tag, else latest).

Inputs

Most inputs map to the pack flag of the same name. See Configuration for what each does, and Usage for the equivalent command-line recipes. A few inputs are action-specific. version picks which release to download. output maps to --no-build --output. args is a verbatim escape hatch for any flag without a dedicated input, for example --sign or --oci-archive. Note that push publishes with docker tag/docker push, not pack's daemonless, cosign-signable --push.

Input Default Description
binary (required) Path to the dynamically linked glibc ELF binary to pack.
version (the pinned tag, else latest) Which scratchsmith release to download: a tag like v1.0.0, or latest.
output Stage the rootfs into this directory instead of building an image (--no-build).
entrypoint (the binary's path) Image ENTRYPOINT.
cmd Default arguments appended to the entrypoint, one per line.
env Image environment entries, one KEY=VALUE per line.
workdir Image WORKDIR.
user 65532 Image user UID[:GID]. 0 warns.
label OCI image labels, one KEY=VALUE per line.
healthcheck HEALTHCHECK in exec form, one token per line. It must name an executable in the image.
strip false Strip symbols from the binary and libraries.
upx false Compress the binary with UPX (needs upx on the runner).
smoke false After the build, run the image once. If the binary cannot start, the job fails.
sbom false Generate an SBOM of the packed rootfs (needs syft on the runner).
sbom-file sbom.json SBOM output path. Requires sbom, and is ignored on its own.
sbom-format cyclonedx-json SBOM format: cyclonedx-json or spdx-json.
scan false Vulnerability-scan the rootfs with grype (needs grype on the runner). When sbom is set, it reuses that SBOM.
scan-fail-on Fail on a grype finding at or above this severity (negligible…critical). Implies scan.
ca-certs false Add the TLS CA bundle to the image.
tz false Add the resolved local timezone to the image.
init false Add a minimal init (tini) as pid 1 wrapping the entrypoint.
add-file Copy host files into the image, one SRC:DST per line. A bare absolute SRC keeps its own path. Regular files only, and a missing source, a directory, or a DST already in the image fails the pack. A symlinks mode that stages a link changes two of those rules for a symlink source. A preserved link is staged as a link, so a link to a directory no longer fails. Under skip-unsafe, an entry the pack cannot honor stages nothing and warns. A taken DST still fails under every mode. The file lands owned by uid 0 at mode 0644, or 0755 for an executable source, so do not add a secret this way.
locale Compiled glibc locales to stage at /usr/lib/locale, one name per line, for example en_US.UTF-8. A name, never a path. Needs a prebuilt directory on the runner, or localedef plus the glibc sources at /usr/share/i18n. Staging a locale does not select it, so also set LANG through env. The host locale-archive is never copied.
symlinks copy-all What a symlink you name becomes in the image: copy-all, preserve, copy-unsafe, or skip-unsafe. Covers the packed binary's own path and each add-file source, never the resolved libraries.
include Extra libraries to force-stage, for example dlopen'd plugins, one soname/path per line.
nss files,dns NSS modules to stage for glibc lookups, comma-separated (files,dns, or none). Fewer modules trim CVE surface.
max-size If the fully-staged image exceeds this size, the job fails. Write the size as 25MB, 512KiB, or a byte count.
deny If a listed library ships, fail the pack (soname or staged file name), one per line.
require If a listed library does not ship, fail the pack (same scope as deny), one per line.
push Tag the built image as this registry ref and docker push it. Needs a prior registry login. Not compatible with output.
args Extra raw pack flags appended verbatim: the escape hatch for anything above.

Outputs

Output Description
image The loaded image tag. When output staged a rootfs instead, this is empty.
rootfs The staged rootfs directory. When an image was built instead, this is empty.
report The full pack report as JSON.