From 028ec89ed6a45b3b4c21bb07f075d32d22873189 Mon Sep 17 00:00:00 2001 From: Jess Sullivan Date: Sun, 14 Jun 2026 13:02:58 -0400 Subject: [PATCH 1/2] ci(bazel): enroll scheduling-kit in the shared cache-backed lane (TIN-2110) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cache-first (TIN-1997 Option D) enrollment pilot — the reusable template for the spoke fan-out. Opt-in, default-off; no remote executor / REAPI is wired. - Repin both workflows from a bare ci-templates SHA to the immutable tag @v2.3.0 (which ships the opt-in cache_backed path) and set cache_backed: true. - .bazelrc: add the endpoint-free ci-cached config block (--config=ci + --remote_upload_local_results=false + --remote_download_minimal + --remote_timeout=60) plus cache-readonly / no-remote-cache, and empty the disk cache under :ci so a hit proves the REMOTE shared cache. Endpoints are injected at runtime, never baked. - scripts/cache-attachment-contract.sh: vendor the GF#889-proven fail-closed classifier (TIN-2108 naming: GF_BAZEL_SUBSTRATE_MODE; shared-cache-backed / compatibility-local-only / executor-backed). Executor is classified but never selected by this lane. - tinyland.repo.json: declare kit's enrollment dimensions (forge scope = Jesssullivan personal; operator overlay = jesssullivan-infra; execution pool = tinyland-nix; substrate mode = shared-cache-backed) per the ci-templates manifest schema. - AGENTS.md: add the GloriousFlywheel cache-enrollment stanza near the top. - justfile + flake just: org house-style recipes (info / cache-contract-strict / flywheel-build / flywheel-test) behind a FLYWHEEL flag. No executor recipe. When cache_backed is unset/false the existing bazelisk validation path is byte-identical (zero behavior change for non-opted ci-templates consumers). --- .bazelrc | 19 +++ .github/workflows/ci.yml | 8 +- .github/workflows/publish.yml | 5 +- AGENTS.md | 27 ++++ flake.nix | 1 + justfile | 70 +++++++++ scripts/cache-attachment-contract.sh | 221 +++++++++++++++++++++++++++ tinyland.repo.json | 48 ++++++ 8 files changed, 397 insertions(+), 2 deletions(-) create mode 100644 justfile create mode 100755 scripts/cache-attachment-contract.sh create mode 100644 tinyland.repo.json diff --git a/.bazelrc b/.bazelrc index 7e9ab53..f1c0ae4 100644 --- a/.bazelrc +++ b/.bazelrc @@ -29,9 +29,28 @@ build:ci --color=no build:ci --curses=no build:ci --show_progress_rate_limit=30 build:ci --noshow_loading_progress +# Empty the disk cache in CI so a remote cache hit proves the SHARED remote +# cache, not an incidental local disk hit. The cache-backed lane layers +# ci-cached on top of this base (cache-first, TIN-1997 Option D / TIN-2110). +build:ci --disk_cache= test:ci --test_output=errors test:ci --test_summary=detailed +# Shared-cache-backed lane (cache-first, TIN-1997 Option D / TIN-2110). +# Endpoint-free by contract: the ci-templates cache-backed path validates +# BAZEL_REMOTE_CACHE via scripts/cache-attachment-contract.sh and then supplies +# --remote_cache=$BAZEL_REMOTE_CACHE at invocation. NEVER bake an endpoint here. +# Mirrors the proven MassageIthaca / ci-templates bazelrc/ci-cached.bazelrc shape; +# executor lines are intentionally omitted (cache-first only, no REAPI/executor). +build:ci-cached --config=ci +build:ci-cached --remote_upload_local_results=false +build:ci-cached --remote_download_minimal +build:ci-cached --remote_timeout=60 + +# Explicit read-only and disable knobs for proof/debug lanes. +build:cache-readonly --remote_upload_local_results=false +build:no-remote-cache --remote_cache= + # Local development (bazel build --config=local //...) build:local --disk_cache=~/.cache/bazel-scheduling-kit diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08ec02a..c87f5dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ concurrency: jobs: package: - uses: tinyland-inc/ci-templates/.github/workflows/js-bazel-package.yml@61cd1338ca9dae8a25985c0a36ff7beb111449be + uses: tinyland-inc/ci-templates/.github/workflows/js-bazel-package.yml@v2.3.0 with: runner_mode: repo_owned runner_labels_json: ${{ vars.PRIMARY_LINUX_RUNNER_LABELS_JSON }} @@ -37,6 +37,12 @@ jobs: build_command: pnpm build package_check_command: pnpm check:package bazel_targets: "//:typecheck //:pkg //:test" + # Cache-first shared-cache-backed Bazel validation (TIN-2110 pilot). + # Routes the Bazel validation through the fail-closed cache-attachment + # contract + --config=ci-cached --remote_cache=$BAZEL_REMOTE_CACHE + # (read-only, no executor). Endpoint is injected at runtime by the + # in-cluster nix-setup; nothing is baked. Cache-first only. + cache_backed: true package_dir: ./bazel-bin/pkg npm_access: public github_package_name: "@jesssullivan/scheduling-kit" diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0c3dbd4..403bef1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,7 +23,7 @@ concurrency: jobs: package: - uses: tinyland-inc/ci-templates/.github/workflows/js-bazel-package.yml@61cd1338ca9dae8a25985c0a36ff7beb111449be + uses: tinyland-inc/ci-templates/.github/workflows/js-bazel-package.yml@v2.3.0 with: runner_mode: repo_owned runner_labels_json: ${{ vars.PRIMARY_LINUX_RUNNER_LABELS_JSON }} @@ -42,6 +42,9 @@ jobs: build_command: pnpm build package_check_command: pnpm check:package bazel_targets: "//:typecheck //:pkg //:test" + # Cache-first shared-cache-backed Bazel validation (TIN-2110 pilot). + # Same opt-in lane as CI; endpoint injected at runtime, no executor. + cache_backed: true package_dir: ./bazel-bin/pkg npm_access: public github_package_name: "@jesssullivan/scheduling-kit" diff --git a/AGENTS.md b/AGENTS.md index 69bbb9a..c65c951 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,6 +2,33 @@ This file is the operating brief for AI agents and LLMs working in `@tummycrypt/scheduling-kit`. +## GloriousFlywheel Cache Enrollment (cache-first) + +`scheduling-kit` enrolls in the GloriousFlywheel shared Bazel cache (cache-first, +TIN-1997 Option D; pilot tracked as TIN-2110). + +- **Do NOT** create runners or a bespoke cache instance. Route everything through + the shared `tinyland-inc/ci-templates` surface and the existing GloriousFlywheel + substrate. +- **Do NOT** run raw `bazel build` as validation enrollment. A green build on the + `tinyland-nix` runner with only `--disk_cache` is **NOT** cache-backed and is + not enrollment. +- Attach to the shared substrate via the cache-backed lane: the ci-templates + `js-bazel-package.yml` `cache_backed: true` input, which runs the fail-closed + contract checker and then `--config=ci-cached --remote_cache=$BAZEL_REMOTE_CACHE + --remote_upload_local_results=false`. The cache endpoint is injected at runtime + by the in-cluster `nix-setup` (resolved from cluster DNS); it is never baked + into `.bazelrc`. +- Self-verify with `scripts/cache-attachment-contract.sh --strict` (or + `nix develop --command just cache-contract-strict`). The checker fails closed + on unset/placeholder/non-grpc endpoints, so a misconfigured lane surfaces the + BLOCKED state instead of silently building local-only. +- REAPI / remote executor is **out of scope** (cache-first only). Never wire + `--remote_executor` or `--config=executor-backed`. +- Cache attach is **not** org-migration closure. A green cache-backed build does + not close GF#412 / TIN-1516; org-migration vs widened-ARC-scope remains a + separate operator decision. + ## Repo Role `scheduling-kit` is the reusable, headless scheduling library. diff --git a/flake.nix b/flake.nix index 9e1aaf1..7bf84f2 100644 --- a/flake.nix +++ b/flake.nix @@ -82,6 +82,7 @@ bazelWrapper docsPython jdk21_headless + just nodejs_22 pnpm typescript diff --git a/justfile b/justfile new file mode 100644 index 0000000..102578c --- /dev/null +++ b/justfile @@ -0,0 +1,70 @@ +# scheduling-kit justfile +# +# Org house-style: invoke recipes through the repo flake devShell, e.g. +# nix develop --command just info +# nix develop --command just cache-contract-strict +# +# Cache-first only (TIN-1997 Option D / TIN-2110). There is intentionally NO +# executor recipe: REAPI / remote execution is out of scope. The shared Bazel +# cache endpoint is supplied at runtime via BAZEL_REMOTE_CACHE (injected in CI by +# the in-cluster nix-setup); it is never baked into .bazelrc. + +# FLYWHEEL gates the cache-backed lane. When FLYWHEEL=1 the build/test recipes +# attach to the shared cache (--config=ci-cached --remote_cache=$BAZEL_REMOTE_CACHE, +# read-only). When unset/0 they run the plain local Bazel path (byte-identical to +# the non-opted default), so contributors without cluster cache reachability are +# never blocked. +FLYWHEEL := env_var_or_default("FLYWHEEL", "0") +BAZEL_REMOTE_CACHE := env_var_or_default("BAZEL_REMOTE_CACHE", "") +BAZEL_TARGETS := "//:typecheck //:pkg //:test" + +# List available recipes. +default: + @just --list + +# Print enrollment + cache posture for this checkout. +info: + @echo "scheduling-kit — GloriousFlywheel cache enrollment (cache-first, TIN-2110)" + @echo "FLYWHEEL: {{FLYWHEEL}}" + @echo "BAZEL_REMOTE_CACHE: {{ if BAZEL_REMOTE_CACHE == '' { 'unset (compatibility-local-only)' } else { BAZEL_REMOTE_CACHE } }}" + @echo "bazel targets: {{BAZEL_TARGETS}}" + @echo "executor: out of scope (cache-first only; no REAPI)" + @echo "endpoint policy: injected at runtime by nix-setup; never baked into .bazelrc" + +# Fail-closed cache-attachment contract checker (the enrollment self-verify). +# Asserts a real shared-cache endpoint before any cache-backed Bazel work. +cache-contract-strict: + GF_BAZEL_SUBSTRATE_MODE="${GF_BAZEL_SUBSTRATE_MODE:-shared-cache-backed}" \ + bash scripts/cache-attachment-contract.sh --strict + +# Bazel build; FLYWHEEL=1 attaches to the shared cache (read-only), else local. +flywheel-build: + #!/usr/bin/env bash + set -euo pipefail + if [ "{{FLYWHEEL}}" = "1" ]; then + GF_BAZEL_SUBSTRATE_MODE="${GF_BAZEL_SUBSTRATE_MODE:-shared-cache-backed}" \ + bash scripts/cache-attachment-contract.sh --strict + bazel build {{BAZEL_TARGETS}} \ + --config=ci-cached \ + --remote_cache="${BAZEL_REMOTE_CACHE}" \ + --remote_upload_local_results=false \ + --verbose_failures + else + bazel build {{BAZEL_TARGETS}} --verbose_failures + fi + +# Bazel test; FLYWHEEL=1 attaches to the shared cache (read-only), else local. +flywheel-test: + #!/usr/bin/env bash + set -euo pipefail + if [ "{{FLYWHEEL}}" = "1" ]; then + GF_BAZEL_SUBSTRATE_MODE="${GF_BAZEL_SUBSTRATE_MODE:-shared-cache-backed}" \ + bash scripts/cache-attachment-contract.sh --strict + bazel test //:test \ + --config=ci-cached \ + --remote_cache="${BAZEL_REMOTE_CACHE}" \ + --remote_upload_local_results=false \ + --verbose_failures + else + bazel test //:test --verbose_failures + fi diff --git a/scripts/cache-attachment-contract.sh b/scripts/cache-attachment-contract.sh new file mode 100755 index 0000000..af97dec --- /dev/null +++ b/scripts/cache-attachment-contract.sh @@ -0,0 +1,221 @@ +#!/usr/bin/env bash +# Classify the current Bazel cache/executor attachment without running Bazel. +# +# Generalized from MassageIthaca/scripts/cache-attachment-contract.sh (the merged, +# GF#889-proven shape) into a shared ci-templates entrypoint so any spoke can +# fail closed before a cache-backed Bazel invocation. +# +# Naming aligns with the TIN-2108 in-flight scripts (GF_BAZEL_SUBSTRATE_MODE; +# modes compatibility-local-only / shared-cache-backed / executor-backed) for +# easy convergence, while the fail-closed endpoint validation mirrors the proven +# MI logic verbatim. +# +# Classification: +# BAZEL_REMOTE_EXECUTOR set => executor-backed (out of scope this lane; classified, never selected) +# else BAZEL_REMOTE_CACHE set => shared-cache-backed +# else => compatibility-local-only +# +# Fail-closed (exit 1) when: +# - either endpoint contains a literal ${...} placeholder (unexpanded secret/var) +# - either endpoint does not start with grpc://, grpcs://, http://, or https:// +# - localhost/127.0.0.1/::1 endpoint without GF_BAZEL_ALLOW_LOCALHOST_PROOF=true +# - executor set without a cache endpoint +# - executor != cache unless GF_BAZEL_ALLOW_SEPARATE_EXECUTOR_CACHE=true +# - declared GF_BAZEL_SUBSTRATE_MODE disagreeing with endpoint presence +# - --strict with an empty BAZEL_REMOTE_CACHE + +set -euo pipefail + +STRICT=false + +usage() { + cat >&2 <<'EOF' +Usage: scripts/cache-attachment-contract.sh [--strict] + +Without --strict this reports whether the current shell is +compatibility-local-only, shared-cache-backed, or executor-backed. With --strict +it requires a real BAZEL_REMOTE_CACHE endpoint before cache-backed Bazel work +may run (the fail-closed gate for the cache-backed lane). + +Environment: + BAZEL_REMOTE_CACHE Shared Bazel remote cache endpoint (grpc/grpcs/http/https). + BAZEL_REMOTE_EXECUTOR Optional remote executor endpoint. Classified as + executor-backed but NOT selected by the cache-first lane. + GF_BAZEL_SUBSTRATE_MODE Optional declared mode; must agree with endpoint presence. + GF_BAZEL_ALLOW_LOCALHOST_PROOF + Set true to permit a localhost endpoint (explicit proof only). + GF_BAZEL_ALLOW_SEPARATE_EXECUTOR_CACHE + Set true to permit executor != cache (default: GF REAPI cell + uses one endpoint for both). +EOF +} + +for arg in "$@"; do + case "${arg}" in + --strict) + STRICT=true + ;; + -h | --help) + usage + exit 0 + ;; + *) + usage + exit 2 + ;; + esac +done + +remote_cache="${BAZEL_REMOTE_CACHE:-}" +remote_executor="${BAZEL_REMOTE_EXECUTOR:-}" +mode="${GF_BAZEL_SUBSTRATE_MODE:-}" + +if [[ -n ${remote_executor} ]]; then + expected_mode="executor-backed" +elif [[ -n ${remote_cache} ]]; then + expected_mode="shared-cache-backed" +else + expected_mode="compatibility-local-only" +fi + +if [[ -z ${mode} ]]; then + effective_mode="${expected_mode}" +else + effective_mode="${mode}" +fi + +context="developer-machine" +if [[ ${GITHUB_ACTIONS:-} == "true" ]]; then + context="github-actions" +elif [[ -n ${CI:-} ]]; then + context="ci" +fi + +literal_cache=false +if [[ ${remote_cache} == *'${'* ]] || [[ ${remote_cache} == *'}'* ]]; then + literal_cache=true +fi + +literal_executor=false +if [[ ${remote_executor} == *'${'* ]] || [[ ${remote_executor} == *'}'* ]]; then + literal_executor=true +fi + +unsupported_cache=false +if [[ -n ${remote_cache} ]] && [[ ! ${remote_cache} =~ ^(grpc|grpcs|http|https):// ]]; then + unsupported_cache=true +fi + +unsupported_executor=false +if [[ -n ${remote_executor} ]] && [[ ! ${remote_executor} =~ ^(grpc|grpcs|http|https):// ]]; then + unsupported_executor=true +fi + +endpoint_is_localhost() { + local endpoint="$1" + local host + host="${endpoint#*://}" + host="${host%%/*}" + host="${host%%:*}" + host="${host#[}" + host="${host%]}" + case "${host}" in + localhost | 127.0.0.1 | ::1 | 0.0.0.0) return 0 ;; + *) return 1 ;; + esac +} + +allow_localhost="${GF_BAZEL_ALLOW_LOCALHOST_PROOF:-false}" +localhost_cache=false +if [[ -n ${remote_cache} ]] && endpoint_is_localhost "${remote_cache}"; then + localhost_cache=true +fi +localhost_executor=false +if [[ -n ${remote_executor} ]] && endpoint_is_localhost "${remote_executor}"; then + localhost_executor=true +fi + +cat < Date: Sun, 14 Jun 2026 13:07:11 -0400 Subject: [PATCH 2/2] ci(metadata): accept immutable ci-templates v-tag pin (TIN-2110) The release-metadata guard previously required the js-bazel-package.yml uses: ref to be a 40-char commit SHA, which is exactly the bare-commit pin debt the enrollment converges off. Accept an immutable semver release tag (vMAJOR.MINOR.PATCH, e.g. @v2.3.0) as a valid pin too, while still rejecting floating refs (@main, @v2). This unblocks the @v2.3.0 repin without re-introducing pin debt. --- scripts/check-release-metadata.mjs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/check-release-metadata.mjs b/scripts/check-release-metadata.mjs index 28fa2a1..b2a9a2c 100644 --- a/scripts/check-release-metadata.mjs +++ b/scripts/check-release-metadata.mjs @@ -35,8 +35,13 @@ const scalar = (value) => .replace(/^(['"])(.*)\1\s*(?:#.*)?$/, '$2') .replace(/\s+#.*$/, '') .trim(); +// An acceptable pin is either a 40-char commit SHA or an immutable semver +// release tag (e.g. @v2.3.0). The ci-templates README rule is to pin to an +// immutable release tag; a floating branch ref such as @main is not a valid +// pin. Both forms are accepted so kit can converge off the bare-commit pin +// onto the immutable v-tag (TIN-2110) without re-introducing pin debt. const usesPinnedPackageWorkflow = (workflow) => - /uses:\s*tinyland-inc\/ci-templates\/\.github\/workflows\/js-bazel-package\.yml@[0-9a-fA-F]{40}/.test( + /uses:\s*tinyland-inc\/ci-templates\/\.github\/workflows\/js-bazel-package\.yml@(?:[0-9a-fA-F]{40}|v[0-9]+\.[0-9]+\.[0-9]+)\b/.test( workflow, ); const hasWorkflowConcurrency = (workflow) => /\nconcurrency:\n/.test(workflow);