diff --git a/.github/workflows/node-simple-pnpm.yaml b/.github/workflows/node-simple-pnpm.yaml index 08f8f5a5..e266959b 100644 --- a/.github/workflows/node-simple-pnpm.yaml +++ b/.github/workflows/node-simple-pnpm.yaml @@ -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 @@ -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 diff --git a/blocks/monorepo/verify-docker-images/action.yaml b/blocks/monorepo/verify-docker-images/action.yaml new file mode 100644 index 00000000..05ff2f1f --- /dev/null +++ b/blocks/monorepo/verify-docker-images/action.yaml @@ -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) + + 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 diff --git a/merge-beta.sh b/merge-beta.sh index 8906807a..36a03696 100755 --- a/merge-beta.sh +++ b/merge-beta.sh @@ -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.