Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .github/workflows/node-simple-pnpm.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,16 @@ on:
required: false
default: true

verify-docker-images:
description: |
Before publishing, check that every docker image referenced by the
block's entrypoint descriptors is present in the registry.
Guards against publishing a block whose images were built but never
pushed, which only surfaces as a runtime 404.
type: boolean
required: false
default: true

notify-slack:
description: |
Enable Slack notifications
Expand Down Expand Up @@ -1074,6 +1084,15 @@ jobs:
test-coverage-reports: ${{ inputs.test-coverage-reports }}
test-results-reports: ${{ inputs.test-results-reports }}

# Gate publication on the images actually being in the registry. Runs on the
# publish path only: on PRs the descriptors are not shipped anywhere, and a
# branch build may legitimately not push.
- name: Verify referenced docker images exist
if: github.ref_name == 'main'
&& steps.check-changes.outputs.has-changes == '0'
&& inputs.verify-docker-images
uses: milaboratory/github-ci/blocks/monorepo/verify-docker-images@v4

- name: Perform security scan checks before publication
uses: milaboratory/github-ci/actions/docker/scan-pnpm-repo@v4

Expand Down
101 changes: 101 additions & 0 deletions blocks/monorepo/verify-docker-images/action.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
name: Verify referenced docker images exist
author: 'MiLaboratories'
description: |
Check that every docker image referenced by a block's built entrypoint
descriptors is actually present in the registry.

A block's `.sw.json` descriptors carry the image tag the backend will pull at
runtime. The build writes those descriptors whether or not the push happened,
so a misconfigured package (historically a stray `"private": true`, which
gates pl-pkg auto-push) yields a green build and a block that 404s on first
run. This step closes that gap before publication.

The tag in the descriptor is the PULL address, which may differ from the push
alias (PL_DOCKER_REGISTRY_PUSH_TO). Verifying the pull address is deliberate:
it is what the backend resolves, so it covers the push and any CDN mapping in
front of the registry.

inputs:
fail-on-missing:
description: |
Fail the step when a referenced image is missing.
Set to 'false' to report without blocking.
required: false
default: 'true'

runs:
using: "composite"

steps:
- name: Verify referenced docker images exist
env:
FAIL_ON_MISSING: ${{ inputs.fail-on-missing }}
shell: bash
run: |
set -euo pipefail

# Repo-owned descriptors only. node_modules holds descriptors belonging to
# published dependencies (SDK runenvs); their images are not this block's
# to guarantee, and failing on them would block a release on upstream state.
mapfile -t descriptors < <(find . -name node_modules -prune -o -name '*.sw.json' -print | sort)

if [ ${#descriptors[@]} -eq 0 ]; then
echo "No .sw.json descriptors found. Nothing to verify."
exit 0
fi
echo "Scanning ${#descriptors[@]} entrypoint descriptor(s)."

mapfile -t tags < <(jq -r 'select(.docker != null and .docker.tag != null) | .docker.tag' "${descriptors[@]}" | sort -u)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Descriptor parse failures pass verification

When a descriptor is malformed or unreadable, or jq is unavailable, the process substitution can fail without failing mapfile, leaving tags empty and causing the publication gate to exit successfully without checking any images.

Suggested change
mapfile -t tags < <(jq -r 'select(.docker != null and .docker.tag != null) | .docker.tag' "${descriptors[@]}" | sort -u)
tags_file="$(mktemp)"
trap 'rm -f "${tags_file}"' EXIT
if ! jq -r 'select(.docker != null and .docker.tag != null) | .docker.tag' "${descriptors[@]}" | sort -u > "${tags_file}"; then
echo "::error::failed to parse entrypoint descriptors"
exit 1
fi
mapfile -t tags < "${tags_file}"
Prompt To Fix With AI
This is a comment left during a code review.
Path: blocks/monorepo/verify-docker-images/action.yaml
Line: 48

Comment:
**Descriptor parse failures pass verification**

When a descriptor is malformed or unreadable, or `jq` is unavailable, the process substitution can fail without failing `mapfile`, leaving `tags` empty and causing the publication gate to exit successfully without checking any images.

```suggestion
        tags_file="$(mktemp)"
        trap 'rm -f "${tags_file}"' EXIT
        if ! jq -r 'select(.docker != null and .docker.tag != null) | .docker.tag' "${descriptors[@]}" | sort -u > "${tags_file}"; then
          echo "::error::failed to parse entrypoint descriptors"
          exit 1
        fi
        mapfile -t tags < "${tags_file}"
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code


if [ ${#tags[@]} -eq 0 ]; then
echo "No docker-backed entrypoints. Nothing to verify."
exit 0
fi

# Resolve the inspect command once. Both talk to the registry directly and
# reuse the docker logins this workflow already performed; which one exists
# depends on the runner's docker CLI.
if docker manifest inspect --help >/dev/null 2>&1; then
image_exists() { docker manifest inspect "${1}" >/dev/null 2>&1; }
elif docker buildx imagetools inspect --help >/dev/null 2>&1; then
image_exists() { docker buildx imagetools inspect "${1}" >/dev/null 2>&1; }
else
echo "::error::no registry inspect command available (tried 'docker manifest inspect' and 'docker buildx imagetools inspect')"
exit 1
fi

missing=()
for tag in "${tags[@]}"; do
if image_exists "${tag}"; then
echo " ok ${tag}"
else
echo " MISSING ${tag}"
missing+=( "${tag}" )
fi
done

echo "Checked ${#tags[@]} image(s), ${#missing[@]} missing."
if [ ${#missing[@]} -eq 0 ]; then
exit 0
fi

{
echo "### Referenced docker images missing from the registry"
echo
echo "The build wrote entrypoint descriptors pointing at images that were never pushed."
echo "A block published in this state fails at runtime when the backend pulls them."
echo
for tag in "${missing[@]}"; do echo "- \`${tag}\`"; done
echo
echo "Most common cause: \`\"private\": true\` in the software package.json."
echo "pl-pkg gates docker auto-push on \`!isPrivate\`, so the image is built and"
echo "referenced but never uploaded. Software packages must not be private."
} >> "${GITHUB_STEP_SUMMARY}"

if [ "${FAIL_ON_MISSING}" != "true" ]; then
echo "::warning::${#missing[@]} referenced docker image(s) missing from the registry"
exit 0
fi

echo "::error::${#missing[@]} referenced docker image(s) missing from the registry"
exit 1
15 changes: 12 additions & 3 deletions merge-beta.sh
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,26 @@ else
git checkout -b "${MERGE_BRANCH}"
fi

# Keep two distinct values:
# SOURCE_BRANCH — the bare branch NAME (e.g. "v4-beta"), used verbatim in the
# `@${SOURCE_BRANCH}` self-ref rewrite below. It must never be
# prefixed with "origin/", or the sed searches for the
# non-existent tag "@origin/v4-beta" and silently rewrites
# nothing (leaving @v4-beta self-refs on the target branch).
# SOURCE_REF — the REF to merge from. When no local branch exists we merge
# the remote-tracking ref "origin/${SOURCE_BRANCH}".
if git branch | grep -qE " ${SOURCE_BRANCH}( |$)"; then
echo "Found source branch in local repository, syncing it with remote..."
git fetch origin "${SOURCE_BRANCH}:${SOURCE_BRANCH}" || true
SOURCE_REF="${SOURCE_BRANCH}"
else
echo "No source branch found in local repository, using remote..."
SOURCE_BRANCH="origin/${SOURCE_BRANCH}"
SOURCE_REF="origin/${SOURCE_BRANCH}"
fi

git merge \
--message "Merge ${SOURCE_BRANCH} into ${TARGET_BRANCH}" \
"${SOURCE_BRANCH}" \
--message "Merge ${SOURCE_REF} into ${TARGET_BRANCH}" \
"${SOURCE_REF}" \
--strategy-option theirs

# Replace @v4-beta -> @v4 in milaboratory/github-ci self-refs only.
Expand Down