blob: 8842de35a8e9b13b90d8b5f9efb8888cf7a1d295 [file]
#!/bin/sh
# What no-image-push.sh refuses, and what it must go on allowing. A wrong pass lets an
# image out, so the refusals are the cases that matter; the allowed ones keep it from
# being switched off for refusing the repository's own builds. The whole-repository run is
# tested on a repository made here, with a push in a workflow and in a file whose name git
# would quote, and then run over this repository as it stands, which must pass.
#
# Run by `make check`.
set -eu
# A pre-commit hook in a linked worktree runs with GIT_DIR and GIT_INDEX_FILE set, and
# `git -C <dir>` does not override them: the scratch repositories below would be made in,
# and staged into, the real one. None of these may reach the cases.
unset GIT_DIR GIT_INDEX_FILE GIT_WORK_TREE GIT_PREFIX GIT_COMMON_DIR GIT_OBJECT_DIRECTORY \
GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_NAMESPACE GIT_CEILING_DIRECTORIES
here=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
script="$here/no-image-push.sh"
dir=$(mktemp -d)
trap 'rm -rf "$dir"' EXIT
failures=0
n=0
# check refuse|pass NAME TEXT: a file NAME holding TEXT, and whether the script lets it
# through. TEXT may hold several lines.
check() {
want=$1 name=$2 text=$3
n=$((n + 1))
mkdir -p "$dir/$n"
file="$dir/$n/$name"
printf '%s\n' "$text" > "$file"
status=0
sh "$script" "$file" 2>/dev/null || status=$?
if [ "$want" = pass ] && [ "$status" -ne 0 ]; then
printf 'refused, should pass: %s: %s\n' "$name" "$text" >&2
failures=$((failures + 1))
elif [ "$want" = refuse ] && [ "$status" -ne 1 ]; then
printf 'not refused (exit %s), should be: %s: %s\n' "$status" "$name" "$text" >&2
failures=$((failures + 1))
fi
}
# a push, by each tool that can make one
check refuse run.sh 'podman push durable-memory/harness:dev ghcr.io/x/harness:dev'
check refuse run.sh 'docker push ghcr.io/x/harness'
check refuse Makefile ' wslc.exe push $(IMAGE)'
check refuse Makefile ' wslc push $(IMAGE)'
check refuse run.sh 'buildah push harness docker://ghcr.io/x/harness'
check refuse run.sh 'nerdctl push x/harness'
check refuse run.sh 'podman-remote push x'
check refuse run.sh '/usr/bin/podman push x'
check refuse run.sh 'docker image push ghcr.io/x/harness'
check refuse run.sh 'podman manifest push harness docker://ghcr.io/x/harness'
check refuse run.sh 'podman artifact push x'
check refuse run.sh 'docker compose push'
check refuse run.sh 'docker-compose push'
check refuse run.sh 'podman-compose push'
check refuse run.sh 'ctr images push ghcr.io/x/h'
check refuse run.sh 'helm push chart.tgz oci://ghcr.io/x'
check refuse run.sh 'helm registry login ghcr.io'
check refuse run.sh 'podman farm build -t ghcr.io/x/h .'
# past the options, through the words in front, and across a continued line
check refuse run.sh 'docker -H tcp://builder:2375 push ghcr.io/x/h'
check refuse run.sh 'podman --remote push x'
check refuse run.sh 'cd images && podman push x'
check refuse run.sh 'sudo -E podman push x'
check refuse run.sh 'env -i PATH=/usr/bin podman push x'
check refuse run.sh 'echo x | xargs podman push'
check refuse run.sh 'if true; then podman push x; fi'
check refuse run.sh 'for t in a b; do podman push "$t"; done'
check refuse run.sh '! podman push x'
check refuse run.sh "sh -c 'podman push x'"
check refuse run.sh "bash -lc 'podman push x'"
check refuse run.sh 'podman \
push x'
check refuse run.sh 'docker buildx build \
--push -t ghcr.io/x/h .'
check refuse run.sh 'PODMAN PUSH x'
# quotes and variables
check refuse run.sh '"podman" push x'
check refuse run.sh "podman 'push' x"
check refuse run.sh '"$runtime" push "$image" ghcr.io/x/h'
check refuse run.sh '$ENGINE push x'
check refuse run.sh '${DOCKER:-docker} push x'
check refuse Makefile ' $(ENGINE) push $(IMAGE)'
check refuse Makefile ' @podman push $(IMAGE)'
# the other ways an image goes out
check refuse run.sh 'docker build --push -t ghcr.io/x/h .'
check refuse run.sh 'docker buildx build --cache-to type=registry,ref=ghcr.io/x/c .'
check refuse run.sh 'skopeo sync --src dir x --dest docker ghcr.io/x'
check refuse ci.yml ' run: docker buildx build --push -t ghcr.io/x/h images/harness'
check refuse ci.yml ' run: docker buildx build --output type=registry .'
check refuse ci.yml ' run: docker buildx build --output type=image,push=true .'
check refuse run.sh 'docker buildx imagetools create -t ghcr.io/x/h a b'
check refuse run.sh 'skopeo copy containers-storage:x docker://ghcr.io/x/h'
check refuse run.sh 'skopeo sync --src dir x --dest docker ghcr.io/x'
check refuse run.sh 'crane push image.tar ghcr.io/x/h'
check refuse run.sh 'crane cp a ghcr.io/x/h'
check refuse run.sh 'crane append -b a -f l.tar -t ghcr.io/x/h'
check refuse run.sh 'gcrane push x'
check refuse run.sh 'oras push ghcr.io/x/h:1 file'
check refuse run.sh 'oras cp a ghcr.io/x/h'
check refuse run.sh 'regctl image copy a ghcr.io/x/h'
check refuse run.sh 'ko publish ./cmd/x'
check refuse run.sh 'gh release upload v1 harness.tar'
check refuse run.sh 'gh release create v1 harness.tar'
check refuse run.sh 'docker compose -f local/compose.yml push'
check refuse run.sh 'podman compose -f a.yml -f b.yml push'
check refuse run.sh 'docker-compose -f a.yml -f b.yml push'
check refuse run.sh 'ctr -n k8s.io images push x'
check refuse run.sh 'docker --context remote image push x'
check refuse run.sh 'sudo -u root podman push x'
check refuse run.sh 'nice -n 5 podman push x'
check refuse run.sh 'xargs -n 1 podman push < tags'
check refuse run.sh 'timeout 600 docker push x'
check refuse run.sh 'timeout -s KILL 600 docker push x'
check refuse run.sh 'doas podman push x'
check refuse run.sh 'time podman push x'
check refuse run.sh 'nohup podman push x'
check refuse run.sh 'exec podman push x'
check refuse run.sh 'command podman push x'
check refuse run.sh 'ionice -c 3 podman push x'
check refuse run.sh 'eval podman push x'
check refuse run.sh 'if a; then b; else podman push x; fi'
check refuse run.sh 'while true; do podman push x; done'
check refuse run.sh 'until false; do podman push x; done'
check refuse run.sh 'if podman push x; then :; fi'
check refuse run.sh 'while podman push x; do :; done'
check refuse run.sh 'until podman push x; do :; done'
check refuse run.sh "ksh -c 'podman push x'"
check refuse run.sh "zsh -c 'podman push x'"
check refuse run.sh "dash -c 'podman push x'"
check refuse run.sh "bash -o pipefail -c 'podman push x'"
check refuse run.sh 'buildah commit ctr docker://ghcr.io/x/h'
check refuse run.sh 'gh release edit v1 --draft=false'
check refuse run.sh 'gh api repos/apache/x/releases -f tag_name=v1'
check refuse run.sh 'podman push x
'
# a login is the step before a push, and nothing here needs one
check refuse run.sh 'echo "$T" | podman login ghcr.io -u x --password-stdin'
check refuse ci.yml ' run: docker login -u x -p "$T"'
check refuse run.sh 'skopeo login ghcr.io'
check refuse run.sh 'oras login ghcr.io'
check refuse run.sh 'crane auth login ghcr.io'
check refuse ci.yml ' - run: podman push x'
check refuse ci.yml ' deploy: podman push x'
# workflows
check refuse ci.yml ' - uses: ncipollo/release-action@v1'
check refuse ci.yml ' - uses: actions/create-release@v1'
check refuse ci.yml ' - uses: goreleaser/goreleaser-action@v6'
check refuse ci.yml ' - uses: googleapis/release-please-action@v4'
check refuse ci.yml ' - uses: aws-actions/amazon-ecr-login@v2'
check refuse ci.yml ' - uses: azure/docker-login@v2'
check refuse ci.yml ' - uses: mr-smithers-excellent/docker-build-push@v6'
check refuse ci.yml ' - uses: macbre/push-to-ghcr@v13'
check refuse ci.yml ' - uses: actions/deploy-pages@v5'
check refuse ci.yml ' pages: write'
check refuse ci.yml ' push: true'
check refuse docker-bake.hcl ' output = ["type=registry"]'
check refuse docker-bake.json '{"target": {"h": {"push": true}}}'
check refuse Dockerfile 'CMD ["podman", "push", "x"]'
check refuse compose.yml ' command: ["docker","login","ghcr.io"]'
check refuse ci.yml ' - uses: docker/build-push-action@v6'
check refuse ci.yml ' - uses: redhat-actions/push-to-registry@v2'
check refuse ci.yml ' - uses: docker/login-action@v3'
check refuse ci.yml ' - uses: redhat-actions/podman-login@v1'
check refuse ci.yml ' - uses: docker/bake-action@v5'
check refuse ci.yml ' - uses: softprops/action-gh-release@v2'
check refuse ci.yml ' packages: write'
check refuse ci.yml ' packages: "write"'
check refuse ci.yml ' packages : write'
check refuse ci.yml 'permissions: write-all'
check refuse ci.yml ' - run: |
docker build -t x .
docker push x'
check refuse Dockerfile 'RUN podman push x'
# code that runs a push, however the arguments are passed
check refuse main.rs 'Command::new("podman").arg("push").arg(&tag)'
check refuse main.rs 'Command::new(wslc).args(["push", &tag])'
check refuse main.rs 'run(rt, &["push", image])'
check refuse main.rs 'run_ok(&s(&["push", tag]))'
check refuse main.rs 'let args = vec![
"push",
tag,
];'
check refuse main.rs 'const VERB: &str = "push";'
check refuse main.rs 'cmd.arg("podman push x")'
check refuse main.rs 'Command::new("podman").args(["login", "ghcr.io"])'
check refuse x.py 'subprocess.run(["podman", "push", tag])'
check refuse main.rs 'let rt = "podman";
let cmd = format!("{rt} push {tag}");'
check refuse x.py "os.system('docker push x')"
check refuse x.mjs 'spawn("podman", ["push", t])'
check refuse x.ts "execFile('docker', ['login'])"
# what the repository does and must go on doing
check pass Makefile ' else podman build -t $(IMAGE) images/harness; fi'
check pass Makefile ' wslc.exe build -t $(IMAGE) "$$(wslpath -w images/harness)"; \
elif command -v wslc >/dev/null 2>&1; then wslc build -t $(IMAGE) images/harness; \
else podman build -t $(IMAGE) images/harness; fi'
check pass ci.yml ' run: docker build --pull -t harness:${{ github.sha }} images/harness'
check pass ci.yml ' git push -f origin bot/harness-pins'
check pass ci.yml ' gh pr create --fill --base main --head bot/harness-pins'
check pass ci.yml ' contents: write'
check pass ci.yml ' pull-requests: write'
check pass ci.yml ' packages: none'
check pass ci.yml ' packages: read'
check pass ci.yml ' - uses: actions/upload-artifact@v7'
check pass run.sh 'podman pull docker.io/library/node:24'
check pass run.sh 'podman run --rm -v "$PWD":/r:Z -w /r docker.io/library/node:24 sh x.sh'
check pass run.sh 'podman run --rm --name login alpine true'
check pass run.sh 'podman inspect login'
check pass run.sh 'podman logs push'
check pass run.sh 'podman save harness | tar -x'
check pass run.sh 'podman load -i harness.tar'
check pass run.sh 'podman export "$(podman create x)" | tar -x -C rootfs'
check pass run.sh 'podman compose up -d'
check pass run.sh 'exec "$runtime" run --rm -v "$root":/r x'
check pass run.sh '"$ENGINE" save x -o x.tar'
check pass run.sh 'skopeo inspect docker://docker.io/library/node:24'
check pass run.sh 'gh release download release-1 -R google/gvisor -p x.tar.bz2'
check pass run.sh 'git push origin HEAD'
check pass run.sh 'echo "$x" push'
check pass run.sh 'pushd /tmp && popd'
check pass run.sh 'docker-push-helper --help'
check pass Dockerfile 'RUN npm ci --no-fund --no-audit'
check pass main.rs 'v.push(item);'
check pass main.rs 'Command::new("git").arg("pushd")'
check pass main.rs '(_, AuthMode::Subscription) => (!f.logged_in.contains(&h)).then(|| "login".to_string()),'
check pass x.ts 'items.push(x); const pushed = 1;'
check pass run.sh 'docker buildx imagetools inspect node:24'
check pass run.sh 'docker --debug logs push'
check pass run.sh 'podman --remote run --rm --name login alpine true'
check pass run.sh 'skopeo copy docker://docker.io/library/node:24 oci:node'
check pass run.sh 'ko build --local ./cmd/x'
check pass run.sh '$GIT push origin main'
check pass run.sh '"$git" push origin HEAD'
check pass run.sh 'bash -o pipefail -c "cargo test 2>&1 | tee t.log"'
check pass run.sh 'bash --login -c "make check"'
check pass run.sh 'sudo apt-get install -y podman'
check pass run.sh 'env FOO=1 cargo test'
check pass run.sh 'nice -n 5 make check'
check pass run.sh 'ls | xargs -0 grep -l x'
# an escaped backslash ends the line, so the push on the next one runs
check refuse run.sh 'echo a \\
podman push x'
check pass main.rs 'return Err("a login is the harness own".into());'
check pass ci.yml ' - uses: actions/checkout@v7'
check pass ci.yml ' - uses: 1password/load-secrets-action@v2'
check pass ci.yml ' - uses: aws-actions/aws-secretsmanager-get-secrets@v2'
check pass run.sh 'GIT_PUSH=true make release-notes'
check pass run.sh 'echo "push: true" > settings.yml'
check pass run.sh '"$SHELL" -c "git push origin main"'
check pass run.sh '$CARGO login'
check pass run.sh '"$PY" scripts/conformance.py --push'
check pass run.sh 'sh "$script" push'
check pass run.sh 'bash "$f" login'
check pass Makefile ' $(MAKE) push'
check pass Makefile ' $(MAKE) -C docs login'
check pass run.sh 'docker buildx build --cache-from type=registry,ref=ghcr.io/x/c --load .'
check pass run.sh 'docker buildx build --cache-from=type=registry,ref=ghcr.io/x/c --load .'
check pass run.sh 'docker run -e auto_push=true img'
check pass run.sh 'podman run --rm img mytool --push'
check pass run.sh 'skopeo sync --src docker --dest dir docker.io/library/alpine /tmp/mirror'
check pass run.sh 'skopeo copy docker://node:24 dir:/tmp/node --retry-times 3'
check pass run.sh 'ko build -L ./cmd/app'
check pass run.sh 'crane auth get index.docker.io'
check pass run.sh 'oras copy x --to-oci-layout ./dir'
check pass ci.yml ' - uses: Swatinem/rust-cache@v2'
# a file it cannot read, and a directory, fail; they do not pass
n=$((n + 1))
status=0
sh "$script" "$dir/missing" 2>/dev/null || status=$?
if [ "$status" -eq 0 ]; then
echo 'a missing file passed, should fail' >&2
failures=$((failures + 1))
fi
n=$((n + 1))
status=0
sh "$script" "$dir" 2>/dev/null || status=$?
if [ "$status" -eq 0 ]; then
echo 'a directory passed, should fail' >&2
failures=$((failures + 1))
fi
# an awk that fails is a failure, not a pass
n=$((n + 1))
mkdir -p "$dir/badbin"
printf '#!/bin/sh\nexit 2\n' > "$dir/badbin/awk"
chmod +x "$dir/badbin/awk"
printf 'podman build -t x .\n' > "$dir/ok.sh"
status=0
PATH="$dir/badbin:$PATH" sh "$script" "$dir/ok.sh" 2>/dev/null || status=$?
if [ "$status" -eq 0 ]; then
echo 'a run where awk failed passed, should fail' >&2
failures=$((failures + 1))
fi
# NUL bytes, and UTF-16 text, do not hide a push
n=$((n + 1))
printf 'podman push x\n\000\n' > "$dir/nul.sh"
printf 'p\000o\000d\000m\000a\000n\000 \000p\000u\000s\000h\000 \000x\000\n\000' > "$dir/utf16.ps1"
for f in "$dir/nul.sh" "$dir/utf16.ps1"; do
status=0
sh "$script" "$f" 2>/dev/null || status=$?
if [ "$status" -ne 1 ]; then
echo "a push in $f passed, should be refused" >&2
failures=$((failures + 1))
fi
done
# the whole-repository run: a repository with a push in a workflow and in a file whose
# name git quotes, and Markdown that mentions one, must fail and name both files
n=$((n + 1))
repo="$dir/repo"
mkdir -p "$repo/scripts" "$repo/.github/workflows"
cp "$script" "$repo/scripts/no-image-push.sh"
printf ' - run: docker push x\n' > "$repo/.github/workflows/x.yml"
printf 'podman push x\n' > "$repo/scripts/d\303\251ploy.sh"
printf 'podman push x\n' > "$repo/scripts/tab x.sh"
printf 'Never run `podman push`.\n' > "$repo/README.md"
# untracked files are read too, so nothing is staged
git -C "$repo" init -q
status=0
err=$(sh "$repo/scripts/no-image-push.sh" 2>&1) || status=$?
case $status:$err in
1:*x.yml*déploy.sh*tab*x.sh* | 1:*x.yml*tab*x.sh*déploy.sh* | 1:*déploy.sh*x.yml*) : ;;
*)
printf 'the whole-repository run missed a push (exit %s): %s\n' "$status" "$err" >&2
failures=$((failures + 1))
;;
esac
case $err in
*déploy.sh*) : ;;
*) echo 'the whole-repository run skipped a file whose name git quotes' >&2; failures=$((failures + 1)) ;;
esac
case $err in
*tab*) : ;;
*) echo 'the whole-repository run skipped a file with a tab in its name' >&2; failures=$((failures + 1)) ;;
esac
# CR line endings do not break a continued line
n=$((n + 1))
printf 'podman \\\r\npush x\r\n' > "$dir/crlf.sh"
status=0
sh "$script" "$dir/crlf.sh" 2>/dev/null || status=$?
if [ "$status" -ne 1 ]; then
echo 'a push across a CRLF continuation passed, should be refused' >&2
failures=$((failures + 1))
fi
# a tracked file deleted from the working tree is skipped; a name git lists that is not a
# file, a link to a directory here, fails the run
n=$((n + 1))
repo2="$dir/repo2"
mkdir -p "$repo2/scripts"
cp "$script" "$repo2/scripts/no-image-push.sh"
printf 'podman build -t x .\n' > "$repo2/gone.sh"
git -C "$repo2" init -q
git -C "$repo2" add gone.sh
rm "$repo2/gone.sh"
status=0
sh "$repo2/scripts/no-image-push.sh" 2>/dev/null || status=$?
if [ "$status" -ne 0 ]; then
echo 'a tracked file deleted from the working tree failed the run, should be skipped' >&2
failures=$((failures + 1))
fi
n=$((n + 1))
ln -s "$dir" "$repo2/elsewhere"
status=0
sh "$repo2/scripts/no-image-push.sh" 2>/dev/null || status=$?
if [ "$status" -eq 0 ]; then
echo 'a listed name that is not a file passed, should fail' >&2
failures=$((failures + 1))
fi
# an untracked binary file, an image tarball say, is not read; once added, it is
n=$((n + 1))
repo4="$dir/repo4"
mkdir -p "$repo4/scripts"
cp "$script" "$repo4/scripts/no-image-push.sh"
printf 'layer\000\npodman push x\n' > "$repo4/harness.tar"
git -C "$repo4" init -q
status=0
sh "$repo4/scripts/no-image-push.sh" 2>/dev/null || status=$?
if [ "$status" -ne 0 ]; then
echo 'an untracked binary file was read, should be skipped' >&2
failures=$((failures + 1))
fi
n=$((n + 1))
git -C "$repo4" add harness.tar
status=0
sh "$repo4/scripts/no-image-push.sh" 2>/dev/null || status=$?
if [ "$status" -ne 1 ]; then
echo 'a binary file added to the index was not read, should be' >&2
failures=$((failures + 1))
fi
# a repository where git lists nothing: nothing was checked, so it fails
n=$((n + 1))
repo3="$dir/repo3"
mkdir -p "$repo3/scripts"
cp "$script" "$repo3/scripts/no-image-push.sh"
printf '*\n' > "$repo3/.gitignore"
git -C "$repo3" init -q
status=0
sh "$repo3/scripts/no-image-push.sh" 2>/dev/null || status=$?
if [ "$status" -eq 0 ]; then
echo 'a run where git listed no files passed, should fail' >&2
failures=$((failures + 1))
fi
# outside a repository, git cannot list: that fails, it does not pass
n=$((n + 1))
mkdir -p "$dir/norepo/scripts"
cp "$script" "$dir/norepo/scripts/no-image-push.sh"
status=0
GIT_CEILING_DIRECTORIES="$dir" sh "$dir/norepo/scripts/no-image-push.sh" 2>/dev/null || status=$?
if [ "$status" -eq 0 ]; then
echo 'a run where git could not list the files passed, should fail' >&2
failures=$((failures + 1))
fi
# this repository as it stands
n=$((n + 1))
if ! sh "$script"; then
echo 'the repository itself does not pass' >&2
failures=$((failures + 1))
fi
if [ "$failures" -ne 0 ]; then
echo "no-image-push.test.sh: $failures failures in $n cases" >&2
exit 1
fi
echo "no-image-push.sh: every case holds"