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 < .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); diff --git a/tinyland.repo.json b/tinyland.repo.json new file mode 100644 index 0000000..9b7ab24 --- /dev/null +++ b/tinyland.repo.json @@ -0,0 +1,48 @@ +{ + "$schema": "https://raw.githubusercontent.com/tinyland-inc/site.scaffold/main/docs/schemas/tinyland-repo-manifest.schema.json", + "schema_version": 1, + "repo": { + "name": "scheduling-kit", + "github": "Jesssullivan/scheduling-kit", + "description": "Backend-agnostic, headless scheduling library with pluggable adapters, payment processors, and Svelte UI; published from the Bazel //:pkg artifact.", + "linear": { + "initiative": "TIN-89", + "issue": "TIN-2110" + } + }, + "taxonomy": { + "primary_role": "package-producer", + "layers": ["org-wide-repo-contract", "bazel-package-cache-rbe"] + }, + "contracts": { + "agent_contract": "AGENTS.md", + "just": "justfile", + "nix": "flake.nix", + "github_actions": ".github/workflows", + "secrets_scan": "none", + "conformance": "nix develop --command just cache-contract-strict" + }, + "boundaries": { + "owns_runtime_backend": false, + "owns_auth": false, + "owns_payments": false, + "owns_activitypub_delivery": false, + "owns_live_broker_fetch": false, + "owns_static_projection_ingest": false, + "owns_gitops_apply": false, + "owns_cloudflare_mutation": false, + "owns_bazel_module_authority": true + }, + "authorities": { + "ci_templates": "tinyland-inc/ci-templates", + "cache_rbe_authority": "tinyland-inc/GloriousFlywheel", + "package_registry": "tinyland-inc/bazel-registry" + }, + "supply_chain": { + "sbom": { + "status": "not-required", + "formats": ["CycloneDX JSON"], + "notes": "scheduling-kit enrollment dimensions (TIN-2110 pilot): forge scope = Jesssullivan personal account; operator overlay = jesssullivan-infra; execution pool = tinyland-nix (in-cluster ARC); substrate mode = shared-cache-backed (cache-first, TIN-1997 Option D). The Bazel cache endpoint is injected at runtime by the in-cluster nix-setup (never baked); enrollment is self-verified via scripts/cache-attachment-contract.sh --strict. Cache-first only: no remote executor / REAPI lane is wired." + } + } +}