diff --git a/.github/workflows/framework-build-base.yml b/.github/workflows/framework-build-base.yml new file mode 100644 index 00000000000..5049d030901 --- /dev/null +++ b/.github/workflows/framework-build-base.yml @@ -0,0 +1,313 @@ +name: Framework Build Base Image + +# Builds and pushes ghcr.io/objectstack-ai/framework-build-base: from +# docker/framework-build-base/Dockerfile: ONE framework commit on `main`, its +# workspace dependencies installed, every package built, and the Console built +# into packages/console/dist. It is a builder stage for downstream images that +# link the framework by path and pin it by commit, so they can start FROM it +# instead of installing and building the whole framework on every cold cache. +# It is NOT the runtime image; that one is docker-publish.yml. +# +# On demand only, never per commit: +# - workflow_dispatch, from main, with `sha` = a full commit on main; +# - repository_dispatch, event type `framework-build-base`, with +# client_payload {"sha": "<40 hex>"}: the hook a downstream pin move can call; +# - pull_request touching this file or docker/framework-build-base/: the dry +# run of a change to the image definition. It builds the PR's base commit +# with the PR's Dockerfile and pushes nothing; its job has no packages grant. +# +# A tag is written once. A request for a commit whose tag already exists reports +# the existing digest and builds nothing (dispatch with `rebuild: true` to +# replace it), so a downstream can ask on every pin move at no cost. +# +# The image definition always comes from THIS run's checkout of main; only the +# build context is the requested commit. So a commit that predates this file can +# be packaged, and a commit cannot bring its own Dockerfile into a published image. +# +# Visibility: GitHub creates a new container package private on its first push, +# and no API changes that. The maintainer sets it Public once, in the package's +# settings (Danger Zone, Change visibility; it cannot be made private again). +# Until then the anonymous-pull step below is red and names that fix. + +# dispatch-gates: no-check-families -- the steps are an image build, a registry read and an in-image check script (docker/framework-build-base/check-image.sh, which needs the built image), not named local checks + +on: + workflow_dispatch: + inputs: + sha: + description: 'Full 40-hex framework commit on main to package (e.g. a downstream .objectstack-sha)' + required: true + type: string + rebuild: + description: 'Rebuild and re-push even when the tag already exists (the digest changes)' + required: false + type: boolean + default: false + repository_dispatch: + types: [framework-build-base] + pull_request: + paths: + - '.github/workflows/framework-build-base.yml' + - 'docker/framework-build-base/**' + +permissions: + contents: read + +# One build per commit at a time; a second request for the same commit waits, +# then finds the tag and builds nothing. A PR's dry run is superseded by its +# next push. +concurrency: + group: framework-build-base-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || github.event.client_payload.sha || inputs.sha }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + IMAGE: ghcr.io/objectstack-ai/framework-build-base + +jobs: + verify: + name: Build framework build base (dry run, no push) + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + # The same build as `publish` below; see its budget note. + timeout-minutes: 60 + permissions: + contents: read + steps: + - name: Free runner disk + run: | + set -euxo pipefail + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL + sudo docker image prune -af || true + df -h / + + - name: Check out the image definition (this pull request) + uses: actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.head.sha }} + path: builder + persist-credentials: false + sparse-checkout: docker/framework-build-base + + - name: Check out the framework commit (the pull request's base) + uses: actions/checkout@v7 + with: + ref: ${{ github.event.pull_request.base.sha }} + path: objectstack + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + + - name: Note the build start + id: clock + run: echo "start=$(date +%s)" >> "$GITHUB_OUTPUT" + + - name: Build (load into the runner, never push) + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 + with: + context: objectstack + file: builder/docker/framework-build-base/Dockerfile + platforms: linux/amd64 + push: false + load: true + provenance: false + sbom: false + build-args: | + FRAMEWORK_SHA=${{ github.event.pull_request.base.sha }} + tags: framework-build-base:dry-run + + - name: Look inside the image + env: + SHA: ${{ github.event.pull_request.base.sha }} + START: ${{ steps.clock.outputs.start }} + run: | + set -euo pipefail + BUILD_S=$(( $(date +%s) - START )) + READINGS="$(bash builder/docker/framework-build-base/check-image.sh framework-build-base:dry-run "$SHA")" + echo "$READINGS" + { + echo "### Framework build base, dry run (nothing pushed)" + echo + echo "- framework commit (PR base): \`$SHA\`" + echo "- build wall: ${BUILD_S} s" + echo "$READINGS" | sed 's/^/- /' + } >> "$GITHUB_STEP_SUMMARY" + + publish: + name: Build & push ghcr.io/objectstack-ai/framework-build-base + # Trusted runs only: a dispatch from main, or a repository_dispatch (which + # always runs main's copy of this file). This `if` is the policy, not a + # security boundary: anyone who can push a branch can also edit it there. + if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + # One cold reading on record, the `verify` job of run 42464408854: job wall + # 18m45s, of which the build 15m25s (install 33 s, 82 framework tasks + # 1m58s, 74 Console tasks 10m47s, load into the runner 94 s). This job + # skips the load and adds a push and an anonymous pull of about 0.8 GB. + # 60 minutes is about 3x, room for a slow npm or a slow registry. + timeout-minutes: 60 + permissions: + contents: read + packages: write + steps: + - name: Resolve the framework commit + id: sha + env: + REQUESTED: ${{ github.event_name == 'repository_dispatch' && github.event.client_payload.sha || inputs.sha }} + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + SHA="$(printf '%s' "$REQUESTED" | tr -d '[:space:]')" + if ! printf '%s' "$SHA" | grep -Eq '^[0-9a-f]{40}$'; then + echo "::error::the requested commit is not a full 40-hex lowercase sha" + exit 1 + fi + # Only commits on main are packaged: main must be ahead of the commit, + # or be it. Anything else (a branch head, a fork's pull ref) is refused. + if ! STATUS="$(gh api "repos/$REPO/compare/$SHA...main" --jq .status)"; then + echo "::error::cannot compare $SHA with main (is it a commit of $REPO?)" + exit 1 + fi + case "$STATUS" in + ahead|identical) ;; + *) echo "::error::$SHA is not on main (compare status: $STATUS); only commits on main are packaged"; exit 1 ;; + esac + echo "sha=$SHA" >> "$GITHUB_OUTPUT" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + + - name: Log in to ghcr.io + uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Is this commit already packaged + id: existing + env: + REF: ${{ env.IMAGE }}:${{ steps.sha.outputs.sha }} + REBUILD: ${{ inputs.rebuild && 'true' || 'false' }} + run: | + set -euo pipefail + DIGEST="$(docker buildx imagetools inspect "$REF" --format '{{json .Manifest}}' 2>/dev/null | jq -r '.digest // empty' || true)" + if [ -n "$DIGEST" ] && [ "$REBUILD" != true ]; then + echo "$REF already exists at $DIGEST; nothing to build (dispatch with rebuild: true to replace it)." + echo "skip=true" >> "$GITHUB_OUTPUT" + echo "digest=$DIGEST" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Free runner disk + if: steps.existing.outputs.skip != 'true' + run: | + set -euxo pipefail + sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL + sudo docker image prune -af || true + df -h / + + - name: Check out the image definition (this run's main) + if: steps.existing.outputs.skip != 'true' + uses: actions/checkout@v7 + with: + path: builder + persist-credentials: false + sparse-checkout: docker/framework-build-base + + - name: Check out the framework commit (the build context) + if: steps.existing.outputs.skip != 'true' + uses: actions/checkout@v7 + with: + ref: ${{ steps.sha.outputs.sha }} + path: objectstack + persist-credentials: false + + - name: Note the build start + if: steps.existing.outputs.skip != 'true' + id: clock + env: + SHA: ${{ steps.sha.outputs.sha }} + run: | + set -euo pipefail + [ "$(git -C objectstack rev-parse HEAD)" = "$SHA" ] || { echo "::error::the context checkout is not $SHA"; exit 1; } + echo "start=$(date +%s)" >> "$GITHUB_OUTPUT" + + - name: Build and push + if: steps.existing.outputs.skip != 'true' + id: build + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 + with: + context: objectstack + file: builder/docker/framework-build-base/Dockerfile + platforms: linux/amd64 + push: true + provenance: false + sbom: false + build-args: | + FRAMEWORK_SHA=${{ steps.sha.outputs.sha }} + tags: ${{ env.IMAGE }}:${{ steps.sha.outputs.sha }} + + - name: Note the build wall + if: steps.existing.outputs.skip != 'true' + id: wall + env: + START: ${{ steps.clock.outputs.start }} + run: echo "seconds=$(( $(date +%s) - START ))" >> "$GITHUB_OUTPUT" + + # The image is for anyone to pull, so it is read back the way a stranger + # would: logged out. A fresh build is pulled whole and looked inside; an + # existing tag only has its manifest read, anonymously. + - name: Pull the image anonymously + id: pull + env: + SHA: ${{ steps.sha.outputs.sha }} + DIGEST: ${{ steps.build.outputs.digest || steps.existing.outputs.digest }} + FRESH: ${{ steps.existing.outputs.skip != 'true' }} + run: | + set -euo pipefail + docker logout ghcr.io + REF="$IMAGE@$DIGEST" + PRIVATE="::error::$IMAGE cannot be read anonymously. A new container package is private: the maintainer sets it Public once, in the package's settings (Danger Zone, Change visibility)." + if [ "$FRESH" = true ]; then + docker pull "$REF" || { echo "$PRIVATE"; exit 1; } + bash builder/docker/framework-build-base/check-image.sh "$REF" "$SHA" | tee "$RUNNER_TEMP/readings.txt" + else + TOKEN="$(curl -fsS "https://ghcr.io/token?scope=repository:objectstack-ai/framework-build-base:pull&service=ghcr.io" | jq -r .token)" \ + && curl -fsS -o /dev/null -H "Authorization: Bearer $TOKEN" \ + -H 'Accept: application/vnd.oci.image.manifest.v1+json' \ + -H 'Accept: application/vnd.docker.distribution.manifest.v2+json' \ + "https://ghcr.io/v2/objectstack-ai/framework-build-base/manifests/$DIGEST" \ + || { echo "$PRIVATE"; exit 1; } + echo "manifest readable anonymously" > "$RUNNER_TEMP/readings.txt" + fi + + - name: Summary + if: always() && steps.sha.outputs.sha != '' + env: + SHA: ${{ steps.sha.outputs.sha }} + DIGEST: ${{ steps.build.outputs.digest || steps.existing.outputs.digest }} + SKIPPED: ${{ steps.existing.outputs.skip }} + WALL: ${{ steps.wall.outputs.seconds }} + PULL: ${{ steps.pull.outcome }} + run: | + { + echo "### $IMAGE" + echo + echo "- framework commit: \`$SHA\`" + echo "- tag: \`$IMAGE:$SHA\`" + echo "- digest: \`${DIGEST:-none}\`" + if [ "$SKIPPED" = true ]; then + echo "- build: skipped, the tag already existed" + else + echo "- build wall: ${WALL:-not reached} s" + fi + echo "- anonymous read: ${PULL:-not reached}" + if [ -f "$RUNNER_TEMP/readings.txt" ]; then sed 's/^/- /' "$RUNNER_TEMP/readings.txt"; fi + if [ -n "$DIGEST" ]; then + echo + echo "Pin it by digest: \`FROM $IMAGE:$SHA@$DIGEST\`" + fi + } >> "$GITHUB_STEP_SUMMARY" diff --git a/docker/framework-build-base/Dockerfile b/docker/framework-build-base/Dockerfile new file mode 100644 index 00000000000..e2f9aabe686 --- /dev/null +++ b/docker/framework-build-base/Dockerfile @@ -0,0 +1,147 @@ +# syntax=docker/dockerfile:1 +# +# ObjectStack framework BUILD BASE image +# ghcr.io/objectstack-ai/framework-build-base: +# +# Built and pushed by .github/workflows/framework-build-base.yml, on demand, +# for one framework commit on `main`. NOT the runtime image: that one is +# ghcr.io/objectstack-ai/objectstack (docker/Dockerfile), a global npm install +# of a RELEASED @objectstack/cli. This one is a builder stage frozen at a +# commit: the framework source tree at /repo/objectstack with its workspace +# dependencies installed (dev dependencies included), every package built, and +# the Console built into packages/console/dist. +# +# Who it is for: a downstream image that consumes the framework as a pnpm +# workspace by `link:` path (`link:../../../objectstack/packages/*`) and pins it +# by commit rather than by release. It starts `FROM` this image and adds its own +# tree beside /repo/objectstack, instead of installing and building the whole +# framework (and the Console) itself on every cold cache or pin move. +# +# Build invocation (what the workflow runs; the context is a checkout of the +# commit being packaged, this file can come from another checkout): +# docker build -f /docker/framework-build-base/Dockerfile \ +# --build-arg FRAMEWORK_SHA=<40-hex sha of the checkout> +# +# Every build step below is the framework stage downstream images run today, +# with the same arguments: `pnpm install --frozen-lockfile --prod=false`, the +# typeless build of every package except docs and `ui/`, then the typed Console +# build. Keep them in step: an image built here with different arguments is not +# a drop-in for that stage. +# +# Only this repository's own tree goes in (Apache-2.0 outside `ui/`, MIT inside +# it, LICENSING.md). The image is public, so: no `.git` (it carries the +# checkout's credentials header; Dockerfile.dockerignore excludes it and the +# workflow checks out with persist-credentials: false), no build secret, no +# token in any layer. + +FROM node:22-slim + +# python3/make/g++ for native modules (the install compiles some from source), +# git for the build scripts that shell out to it. +RUN apt-get update \ + && apt-get install -y --no-install-recommends python3 make g++ ca-certificates git \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /repo + +ENV CI=true + +# Skip .d.ts generation, as downstream image builds do: the runtime never loads +# a declaration, and the dts pass is the largest single build cost. It stays SET +# in the finished image on purpose, because the downstream stage that starts +# FROM here builds its own packages with it too. The one exception is the +# Console's build closure, which is rebuilt typed below (the Console compiles +# against those packages' dist/*.d.ts). turbo hashes OS_SKIP_DTS (turbo.json +# globalEnv), so the two passes never replay each other's outputs. +ENV OS_SKIP_DTS=1 + +# The version `packageManager` in package.json names; corepack refuses any other. +ARG PNPM_VERSION=10.31.0 +RUN corepack enable && corepack prepare pnpm@${PNPM_VERSION} --activate + +ENV PNPM_HOME=/pnpm +ENV PATH=$PNPM_HOME:$PATH + +# The store lives in a cache mount during the build, so it never lands in a +# layer. A downstream install that mounts the same path finds the setting. +RUN pnpm config set store-dir /pnpm/store + +COPY . /repo/objectstack/ + +# The authorable-surface deletion check in @objectstack/spec's build fails +# closed without a baseline. With no `.git` in this stage it anchors on the +# committed packages/spec/authorable-surface.base.json; assert the file arrived, +# so a context that dropped it fails here, in one second, with the reason. +RUN set -eu; \ + base=/repo/objectstack/packages/spec/authorable-surface.base.json; \ + if [ ! -f "$base" ]; then \ + echo "✗ $base is missing: the authorable-surface deletion check has no baseline" >&2; \ + echo " to anchor on in a build stage with no .git, and fails closed. Either this" >&2; \ + echo " commit predates the committed baseline, or the context excluded packages/spec." >&2; \ + exit 1; \ + fi; \ + echo "→ authorable-surface deletion-check anchor: in-tree baseline $(node -p "require('$base').baseRev")" + +WORKDIR /repo/objectstack + +# Install with the framework's own lockfile, then build every package except +# the docs site and the `ui/` workspace (the next RUN builds the part of `ui/` +# the Console needs, typed). The 12 GB heap: @objectstack/spec's gen:schema +# loads the whole schema set in one tsx worker thread, which outgrew 8 GB +# (ERR_WORKER_OUT_OF_MEMORY is a per-worker V8 limit, not an OS kill), and +# --concurrency=1 keeps that one heavy worker alone on a 16 GB runner. +RUN --mount=type=cache,id=pnpm-store,target=/pnpm/store \ + --mount=type=cache,id=fw-turbo,target=/repo/objectstack/.turbo,sharing=locked \ + --mount=type=cache,id=fw-node-cache,target=/repo/objectstack/node_modules/.cache,sharing=locked \ + npm_config_registry=https://registry.npmjs.org \ + NODE_OPTIONS=--max-old-space-size=12288 \ + pnpm install --frozen-lockfile --prefer-offline --prod=false \ + && NODE_OPTIONS=--max-old-space-size=12288 pnpm exec turbo run build --filter='!@objectstack/docs' --filter='!./ui/**' --concurrency=1 + +# The Console, built from this tree's ui/apps/console into +# packages/console/dist (the directory the CLI serves) by the root +# `console:build` pipeline, typed (`env -u OS_SKIP_DTS`). ORDER IS +# LOAD-BEARING: it runs after the typeless build above, so the typed rebuild of +# the Console's closure is the one left on disk. +# +# Its build identity: the help menu names the commit the Console was built +# from, through VITE_APP_VERSION (the first 8 hex of FRAMEWORK_SHA). No `.git` +# reaches this stage, so Vite's git fallback has nothing to read; the value must +# be passed. turbo infers VITE_* for a Vite package, so it reaches Vite and +# moves the Console task's hash. +# +# The three VITE_SENTRY_* assignments are the levers the self-managed image +# built with, so one Console here serves every downstream image: an empty DSN, +# the kill switch off, the PII default off. On this tree's Console they are no +# longer read (client error reporting is configured at runtime), so they cost +# nothing; they stay so that a build-time DSN that ever comes back cannot ship +# silently in a customer artefact. They sit on the turbo command through `env`, +# never on a command chained after it. +# +# ui/ is removed at the end: the runtime serves packages/console/dist, and no +# package outside ui/ depends on an @object-ui/* one. +ARG FRAMEWORK_SHA +RUN --mount=type=cache,id=fw-turbo,target=/repo/objectstack/.turbo,sharing=locked \ + --mount=type=cache,id=fw-node-cache,target=/repo/objectstack/node_modules/.cache,sharing=locked \ + set -eu; \ + if ! printf '%s' "${FRAMEWORK_SHA:-}" | grep -Eq '^[0-9a-f]{40}$'; then \ + echo "✗ FRAMEWORK_SHA must be the full 40-hex commit of the build context, got '${FRAMEWORK_SHA:-}'" >&2; \ + exit 1; \ + fi; \ + env -u OS_SKIP_DTS \ + VITE_SENTRY_DSN= \ + VITE_SENTRY_ENABLED=false \ + VITE_SENTRY_SEND_DEFAULT_PII=false \ + VITE_APP_VERSION="$(printf '%.8s' "$FRAMEWORK_SHA")" \ + NODE_OPTIONS=--max-old-space-size=12288 \ + pnpm exec turbo run build:dist --filter=@objectstack/console --concurrency=1 \ + && test -f /repo/objectstack/packages/console/dist/index.html \ + && rm -rf /repo/objectstack/ui + +WORKDIR /repo + +LABEL org.opencontainers.image.source="https://github.com/objectstack-ai/objectstack" \ + org.opencontainers.image.title="ObjectStack framework build base" \ + org.opencontainers.image.description="Framework source at one commit, dependencies installed, every package and the Console built: a builder stage for images that link the framework by path" \ + org.opencontainers.image.licenses="Apache-2.0 AND MIT" \ + org.opencontainers.image.revision="${FRAMEWORK_SHA}" diff --git a/docker/framework-build-base/Dockerfile.dockerignore b/docker/framework-build-base/Dockerfile.dockerignore new file mode 100644 index 00000000000..7bb354e57b6 --- /dev/null +++ b/docker/framework-build-base/Dockerfile.dockerignore @@ -0,0 +1,19 @@ +# BuildKit ignore file for docker/framework-build-base/Dockerfile. +# +# BuildKit reads `.dockerignore` next to the Dockerfile, so +# this file applies whatever the context is. Patterns are relative to the +# CONTEXT root, which is a checkout of the commit being packaged. +# +# Keep it artifact-only: patterns match every path in the context, and a broad +# name (`**/data`, `**/build`) silently deletes source directories such as +# packages/spec/src/data. + +# The image is public, and a checkout's .git/config can carry the credentials +# header actions/checkout writes. `.git` also differs on every checkout, so +# admitting it would rehash the COPY layer on every build. +**/.git + +# Regenerated inside the image; never source. +**/node_modules +**/.pnpm-store +**/.turbo diff --git a/docker/framework-build-base/check-image.sh b/docker/framework-build-base/check-image.sh new file mode 100755 index 00000000000..0ec8ad9da3d --- /dev/null +++ b/docker/framework-build-base/check-image.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash +# Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +# +# Look inside a built framework build base image and fail when it is not the +# image docker/framework-build-base/Dockerfile promises: +# +# bash docker/framework-build-base/check-image.sh <40-hex framework sha> +# +# Run by .github/workflows/framework-build-base.yml against the image it just +# built (the pull-request dry run) or just pulled anonymously (a publish). The +# image must already be in the local Docker daemon. Prints one `key=value` line +# per reading, for the job summary. +set -euo pipefail + +REF="${1:?usage: check-image.sh <40-hex framework sha>}" +SHA="${2:?usage: check-image.sh <40-hex framework sha>}" +case "$SHA" in + *[!0-9a-f]*|'') echo "✗ framework sha must be 40 lowercase hex, got '$SHA'" >&2; exit 2 ;; +esac +[ "${#SHA}" -eq 40 ] || { echo "✗ framework sha must be 40 lowercase hex, got '$SHA'" >&2; exit 2; } + +# Everything below runs inside the image, as the image's default user, with no +# network: what is asserted is what a downstream `FROM` gets. +docker run --rm --network none -e SHA8="${SHA:0:8}" "$REF" sh -eu -c ' + fw=/repo/objectstack + fail() { echo "✗ $*" >&2; exit 1; } + + # Public image: no checkout metadata (it can carry a credentials header). + [ ! -e "$fw/.git" ] || fail "$fw/.git is in the image" + [ -z "$(find /repo -name .git -print -quit)" ] || fail "a .git directory is in the image" + + # Installed and built. + [ -d "$fw/node_modules/.pnpm" ] || fail "$fw/node_modules was not installed by pnpm" + [ -f "$fw/packages/spec/dist/index.js" ] || fail "@objectstack/spec was not built" + [ -f "$fw/packages/core/dist/index.js" ] || fail "@objectstack/core was not built" + + # The Console: built into the directory the CLI serves, ui/ removed after. + dist="$fw/packages/console/dist" + [ -f "$dist/index.html" ] || fail "$dist/index.html is missing" + [ ! -e "$fw/ui" ] || fail "$fw/ui is still in the image" + # Its build identity is the first 8 hex of the framework commit. + grep -rqF "$SHA8" "$dist" || fail "no Console asset names the build identity $SHA8" + + # The posture the downstream stage relies on. + [ "${OS_SKIP_DTS:-}" = 1 ] || fail "OS_SKIP_DTS is not 1 in the image environment" + + echo "node=$(node --version)" + echo "pnpm=$(pnpm --version)" + echo "dist_dirs_under_packages=$(find "$fw/packages" -mindepth 2 -maxdepth 4 -type d -name dist -not -path "*/node_modules/*" | wc -l)" + echo "console_assets=$(find "$dist" -type f | wc -l)" +' + +echo "image_bytes=$(docker image inspect --format '{{.Size}}' "$REF")" diff --git a/scripts/pm/dispatch-gates.self-test.mjs b/scripts/pm/dispatch-gates.self-test.mjs index e7bfed34aed..3a1384c9440 100644 --- a/scripts/pm/dispatch-gates.self-test.mjs +++ b/scripts/pm/dispatch-gates.self-test.mjs @@ -8053,7 +8053,7 @@ export function selfTest({ tier = 'full' } = {}) { } const censusRows = liveMarkerCensus.map((r) => `${r.file}:${r.line} ${r.key}`).sort(); t( - `the live tree carries the ten declarations measured for this census, and no others (${censusRows.join(' · ') || 'none'})`, + `the live tree carries the eleven declarations measured for this census, and no others (${censusRows.join(' · ') || 'none'})`, censusRows.join(' · ') === [ // Seventh row, added with the declaration it names: `checklist-status.yml` // is paths-filtered (its `pull_request:` trigger is filtered to itself) and @@ -8063,6 +8063,11 @@ export function selfTest({ tier = 'full' } = {}) { // precisely so a seventh declaration reddens WITH the six already read // printed beside it, and the author adds their row instead of a number. '.github/workflows/checklist-status.yml:57 no-check-families', + // Eleventh row, added with the declaration it names: `framework-build-base.yml` + // is paths-filtered (its own file and docker/framework-build-base/) and builds + // an image, then checks it with a script that needs the built image, so it + // discovers no `check:` family. + '.github/workflows/framework-build-base.yml:32 no-check-families', '.github/workflows/merged-branch-reaper.yml:212 no-check-families', '.github/workflows/os-create-smoke.yml:48 no-check-families', '.github/workflows/scaffold-e2e.yml:23 no-check-families', @@ -8093,7 +8098,7 @@ export function selfTest({ tier = 'full' } = {}) { const censusCut = liveMarkerCensus.filter((r) => !r.whole).map((r) => `${r.file}:${r.line} ${r.key}`); t( `every live reason on those markers ENDS on its own marker line (cut: ${censusCut.join(', ') || 'none'})`, - censusCut.length === 0 && liveMarkerCensus.length === 10, + censusCut.length === 0 && liveMarkerCensus.length === 11, ); t( 'and every one of them carries a non-empty reason — whole is not the same claim as present, and both are owed',