Repository navigation
Add frontmatter policy for local-only Docker image validation #66284
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
- Loading branch information
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,79 @@ set +o histexpand | |
|
|
||
| set -euo pipefail | ||
|
|
||
| case "${GH_AW_DOCKER_IMAGE_PULL_POLICY-always}" in | ||
| always) ;; | ||
| never) | ||
| if (( $# == 0 )); then | ||
| echo "Local-only image validation requires at least one image" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| declare -A alias_digests=() | ||
| declare -a aliases=() | ||
| declare -a expected_digests=() | ||
| for image in "$@"; do | ||
| if [[ ! "$image" =~ ^([a-z0-9][a-z0-9.-]*(:[0-9]+)?/([a-z0-9]+([._-][a-z0-9]+)*/)*[a-z0-9]+([._-][a-z0-9]+)*)(:([A-Za-z0-9_][A-Za-z0-9_.-]*))?@sha256:([a-f0-9]{64})$ ]]; then | ||
| echo "Local-only mode requires a fully qualified repository[:tag]@sha256:<64 lowercase hex> reference: $image" >&2 | ||
| exit 1 | ||
| fi | ||
| repository="${BASH_REMATCH[1]}" | ||
| tag="${BASH_REMATCH[7]:-latest}" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Untagged digest reference silently gets a When a This directly contradicts the new docs added in this PR ( The new test suite ( Suggested fix: either reject untagged digest references in @copilot please address this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Local-only mode now rejects digest references without an explicit tag, preventing a |
||
| digest="${BASH_REMATCH[8]}" | ||
| registry="${repository%%/*}" | ||
| if [[ "$registry" != *.* && "$registry" != *:* && "$registry" != localhost ]]; then | ||
| echo "Local-only mode requires a registry-qualified image: $image" >&2 | ||
| exit 1 | ||
| fi | ||
| alias="${repository}:${tag}" | ||
| if [[ -v alias_digests["$alias"] && "${alias_digests[$alias]}" != "$digest" ]]; then | ||
| echo "Conflicting digests requested for alias $alias" >&2 | ||
| exit 1 | ||
| fi | ||
| alias_digests["$alias"]="$digest" | ||
| aliases+=("$alias") | ||
| expected_digests+=("${repository}@sha256:${digest}") | ||
| done | ||
|
|
||
| # Validate every exact repository digest before modifying any local tag. | ||
| index=0 | ||
| for image in "$@"; do | ||
| expected="${expected_digests[$index]}" | ||
| if ! digests=$(timeout 30s docker image inspect --format '{{range .RepoDigests}}{{println .}}{{end}}' "$image"); then | ||
| echo "Local-only image inspection failed: $image" >&2 | ||
| exit 1 | ||
| fi | ||
| found=false | ||
| while IFS= read -r entry; do | ||
| if [[ "$entry" == "$expected" ]]; then | ||
| found=true | ||
| break | ||
| fi | ||
| done <<< "$digests" | ||
| if [[ "$found" != true ]]; then | ||
| echo "Local-only image is missing exact RepoDigest $expected" >&2 | ||
| exit 1 | ||
| fi | ||
| index=$((index + 1)) | ||
| done | ||
|
|
||
| index=0 | ||
| for image in "$@"; do | ||
| if ! timeout 30s docker tag "$image" "${aliases[$index]}"; then | ||
| echo "Local-only image tagging failed: $image" >&2 | ||
| exit 1 | ||
| fi | ||
| index=$((index + 1)) | ||
| done | ||
| echo "All local images validated and tagged successfully" | ||
| exit 0 | ||
| ;; | ||
| *) | ||
| echo "Invalid GH_AW_DOCKER_IMAGE_PULL_POLICY (expected always or never)" >&2 | ||
| exit 1 | ||
| ;; | ||
| esac | ||
|
|
||
| # Helper function to pull Docker images with retry logic | ||
| docker_pull_with_retry() { | ||
| local image="$1" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| #!/usr/bin/env bash | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/tdd] This mock-Docker regression suite isn't invoked anywhere — 💡 Suggested fixAdd a line to the bash actions/setup/sh/download_docker_images_local_test.shWithout this, the comprehensive edge-case coverage (conflicting digests, inspect failures, tag failures, uppercase digest rejection, etc.) written in this PR provides no actual safety net. @copilot please address this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Registered the mock-Docker suite in
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New mock-Docker regression suite is never run in CI
As a result, CI can pass without ever exercising the new fail-closed local-only validation logic — the comprehensive edge-case coverage in this file currently provides no safety net. Suggested fix: add this script to the bash actions/setup/sh/download_docker_images_local_test.sh@copilot please address this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Registered the mock-Docker suite in |
||
| set -euo pipefail | ||
|
|
||
| SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| WORKDIR="$(mktemp -d)" | ||
| trap 'rm -rf "$WORKDIR"' EXIT | ||
| export DOCKER_LOG="$WORKDIR/docker.log" | ||
| export MOCK_DIGESTS="$WORKDIR/digests" | ||
|
|
||
| cat > "$WORKDIR/docker" <<'MOCK' | ||
| #!/usr/bin/env bash | ||
| set -euo pipefail | ||
| printf '%s\n' "$*" >> "$DOCKER_LOG" | ||
| case "$1" in | ||
| image) | ||
| [[ "$2" == inspect ]] || exit 1 | ||
| [[ "${MOCK_INSPECT_FAIL:-}" != 124 ]] || exit 124 | ||
| [[ "${MOCK_INSPECT_FAIL:-}" != 1 ]] || exit 1 | ||
| cat "$MOCK_DIGESTS" | ||
| ;; | ||
| tag) | ||
| [[ "${MOCK_TAG_FAIL:-}" != 1 ]] | ||
| ;; | ||
| pull) | ||
| [[ "${MOCK_PULL_FAIL:-}" != 1 ]] | ||
| ;; | ||
| *) exit 1 ;; | ||
| esac | ||
| MOCK | ||
| chmod +x "$WORKDIR/docker" | ||
| export PATH="$WORKDIR:$PATH" | ||
|
|
||
| a="$(printf 'a%.0s' {1..64})" | ||
| b="$(printf 'b%.0s' {1..64})" | ||
| first="registry.example.com:5000/team/app:v1@sha256:$a" | ||
| second="registry.example.com:5000/team/other:v2@sha256:$b" | ||
| printf 'registry.example.com:5000/team/app@sha256:%s\nregistry.example.com:5000/team/other@sha256:%s\n' "$a" "$b" > "$MOCK_DIGESTS" | ||
|
|
||
| run_ok() { | ||
| : > "$DOCKER_LOG" | ||
| GH_AW_DOCKER_IMAGE_PULL_POLICY=never bash "$SCRIPT_DIR/download_docker_images.sh" "$@" > "$WORKDIR/output" 2>&1 || { | ||
| cat "$WORKDIR/output" | ||
| exit 1 | ||
| } | ||
| if grep -q '^pull ' "$DOCKER_LOG"; then | ||
| echo "Unexpected pull in local-only mode" >&2 | ||
| exit 1 | ||
| fi | ||
| } | ||
|
|
||
| run_fail() { | ||
| : > "$DOCKER_LOG" | ||
| if GH_AW_DOCKER_IMAGE_PULL_POLICY=never bash "$SCRIPT_DIR/download_docker_images.sh" "$@" > "$WORKDIR/output" 2>&1; then | ||
| echo "Expected local-only validation failure: $*" >&2 | ||
| exit 1 | ||
| fi | ||
| ! grep -Eq '^(pull|tag) ' "$DOCKER_LOG" | ||
| } | ||
|
|
||
| run_ok "$first" "$second" | ||
| grep -Fxq "image inspect --format {{range .RepoDigests}}{{println .}}{{end}} $first" "$DOCKER_LOG" | ||
| grep -Fxq "tag $first registry.example.com:5000/team/app:v1" "$DOCKER_LOG" | ||
| grep -Fxq "tag $second registry.example.com:5000/team/other:v2" "$DOCKER_LOG" | ||
| [[ "$(grep -c '^image ' "$DOCKER_LOG")" == 2 ]] | ||
| [[ "$(grep -c '^tag ' "$DOCKER_LOG")" == 2 ]] | ||
| [[ "$(sed -n '2p' "$DOCKER_LOG")" == image\ inspect* ]] | ||
| [[ "$(tail -1 "$DOCKER_LOG")" == "tag $second registry.example.com:5000/team/other:v2" ]] | ||
| ! grep -q 'app:latest' "$DOCKER_LOG" || exit 1 | ||
|
|
||
| run_fail | ||
| run_fail alpine:latest | ||
| run_fail "registry.example.com/team/app:v1" | ||
| run_fail "registry.example.com/team/app:v1@sha256:${a^^}" | ||
| run_fail $'registry.example.com/team/app:v1\nbad@sha256:'"$a" | ||
| run_fail "$first" "registry.example.com:5000/team/app:v1@sha256:$b" | ||
| printf 'registry.example.com:5000/team/app@sha256:%s\n' "$b" > "$MOCK_DIGESTS" | ||
| run_fail "$first" | ||
| printf '%s\n' "registry.example.com:5000/team/app:v1@sha256:$a" > "$MOCK_DIGESTS" | ||
| run_fail "$first" | ||
| printf 'registry.example.com:5000/team/app@sha256:%s\n' "$a" > "$MOCK_DIGESTS" | ||
| MOCK_INSPECT_FAIL=1 run_fail "$first" | ||
| MOCK_INSPECT_FAIL=124 run_fail "$first" | ||
| printf 'registry.example.com:5000/team/app@sha256:%s\n' "$a" > "$MOCK_DIGESTS" | ||
| run_fail "$first" "$second" | ||
| MOCK_TAG_FAIL=1 GH_AW_DOCKER_IMAGE_PULL_POLICY=never bash "$SCRIPT_DIR/download_docker_images.sh" "$first" > "$WORKDIR/output" 2>&1 && exit 1 | ||
| if grep -q '^pull ' "$DOCKER_LOG"; then | ||
| echo "Unexpected pull after local-only tagging failure" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| : > "$DOCKER_LOG" | ||
| GH_AW_DOCKER_IMAGE_PULL_POLICY=always bash "$SCRIPT_DIR/download_docker_images.sh" "$first" > "$WORKDIR/output" 2>&1 | ||
| grep -q '^pull --quiet ' "$DOCKER_LOG" | ||
| : > "$DOCKER_LOG" | ||
| env -u GH_AW_DOCKER_IMAGE_PULL_POLICY bash "$SCRIPT_DIR/download_docker_images.sh" "$first" > "$WORKDIR/output" 2>&1 | ||
| grep -q '^pull --quiet ' "$DOCKER_LOG" | ||
| for policy in '' sometimes; do | ||
| : > "$DOCKER_LOG" | ||
| if GH_AW_DOCKER_IMAGE_PULL_POLICY="$policy" bash "$SCRIPT_DIR/download_docker_images.sh" "$first" > "$WORKDIR/output" 2>&1; then | ||
| echo "Expected invalid policy failure" >&2 | ||
| exit 1 | ||
| fi | ||
| [[ ! -s "$DOCKER_LOG" ]] | ||
| done | ||
| echo "Local-only Docker image tests passed" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,6 @@ | ||
| package workflow | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "sort" | ||
| "strings" | ||
|
|
||
|
|
@@ -288,15 +287,19 @@ func mergeDockerImagePins(existing, newPins []GHAWManifestContainer) []GHAWManif | |
| } | ||
|
|
||
| // generateDownloadDockerImagesStep generates the step to download Docker images | ||
| func generateDownloadDockerImagesStep(yaml *strings.Builder, dockerImages []string) { | ||
| func generateDownloadDockerImagesStep(yaml *strings.Builder, dockerImages []string, localOnly bool) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The generator now preserves unset,
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [/codebase-design] 💡 Suggested improvementConsider threading a tri-state policy ( @copilot please address this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The generator now preserves all three policy states and explicitly emits
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Since the shell script reads Suggested fix: thread a tri-state policy ( @copilot please address this.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The generator now preserves all three policy states and explicitly emits |
||
| if len(dockerImages) == 0 { | ||
| return | ||
| } | ||
|
|
||
| yaml.WriteString(" - name: Download container images\n") | ||
| if localOnly { | ||
| yaml.WriteString(" env:\n GH_AW_DOCKER_IMAGE_PULL_POLICY: never\n") | ||
| } | ||
| yaml.WriteString(" run: bash \"${RUNNER_TEMP}/gh-aw/actions/download_docker_images.sh\"") | ||
| for _, image := range dockerImages { | ||
| fmt.Fprintf(yaml, " %s", image) | ||
| yaml.WriteString(" ") | ||
| yaml.WriteString(image) | ||
| } | ||
| yaml.WriteString("\n") | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[/tdd] When a digest reference omits a tag (e.g.
repo@sha256:...),tagdefaults tolatest, so the script tags it asrepository:latest. This contradicts the new docs' claim that "a versioned reference creates only its versioned alias, not:latest" and silently produces a floating-tag alias in a mode whose entire purpose is pinned, non-floating references.💡 Suggested fix
Either reject untagged digest references in
nevermode (require an explicit tag) or document/test the:latestfallback explicitly so behavior matches the stated guarantee. The current test suite (download_docker_images_local_test.sh) only exercises explicitly-tagged references, so this gap isn't caught.@copilot please address this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Local-only mode now rejects digest references without an explicit tag, preventing a
:latestalias; added a regression case. Fixed in 61a19e3.