boatyard 0.6.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. boatyard-0.6.0/.gitattributes +1 -0
  2. boatyard-0.6.0/.github/workflows/cleanup-pr-releases.yml +15 -0
  3. boatyard-0.6.0/.github/workflows/integrate.yml +24 -0
  4. boatyard-0.6.0/.github/workflows/merge-gate.yml +19 -0
  5. boatyard-0.6.0/.github/workflows/release.yml +34 -0
  6. boatyard-0.6.0/.gitignore +12 -0
  7. boatyard-0.6.0/.python-version +1 -0
  8. boatyard-0.6.0/AGENTS.md +195 -0
  9. boatyard-0.6.0/CLAUDE.md +1 -0
  10. boatyard-0.6.0/LICENSE +201 -0
  11. boatyard-0.6.0/NOTICE +1 -0
  12. boatyard-0.6.0/PKG-INFO +12 -0
  13. boatyard-0.6.0/README.md +3 -0
  14. boatyard-0.6.0/closeout.md +518 -0
  15. boatyard-0.6.0/design/AGENTS.md +22 -0
  16. boatyard-0.6.0/design/CLAUDE.md +1 -0
  17. boatyard-0.6.0/design/adoption-semver-wave.md +31 -0
  18. boatyard-0.6.0/design/apt-hermeticity.md +109 -0
  19. boatyard-0.6.0/design/dev-overlay.md +17 -0
  20. boatyard-0.6.0/design/gpu-template-collapse.md +21 -0
  21. boatyard-0.6.0/design/mirror-digest-retention.md +85 -0
  22. boatyard-0.6.0/design/precision-attribution.md +90 -0
  23. boatyard-0.6.0/design/semver-image-bundle-lines.md +110 -0
  24. boatyard-0.6.0/publish.yml +6 -0
  25. boatyard-0.6.0/pyproject.toml +43 -0
  26. boatyard-0.6.0/ruff.base.toml +204 -0
  27. boatyard-0.6.0/ruff.toml +1 -0
  28. boatyard-0.6.0/scripts/preflight.py +3 -0
  29. boatyard-0.6.0/src/boatyard/__init__.py +0 -0
  30. boatyard-0.6.0/src/boatyard/build.py +267 -0
  31. boatyard-0.6.0/src/boatyard/cli.py +23 -0
  32. boatyard-0.6.0/src/boatyard/compose.py +24 -0
  33. boatyard-0.6.0/src/boatyard/config/__init__.py +0 -0
  34. boatyard-0.6.0/src/boatyard/config/config.py +787 -0
  35. boatyard-0.6.0/src/boatyard/config/lock.py +152 -0
  36. boatyard-0.6.0/src/boatyard/config/values.py +188 -0
  37. boatyard-0.6.0/src/boatyard/crane.py +23 -0
  38. boatyard-0.6.0/src/boatyard/dockerfiles.py +201 -0
  39. boatyard-0.6.0/src/boatyard/lock.py +233 -0
  40. boatyard-0.6.0/src/boatyard/markers.py +201 -0
  41. boatyard-0.6.0/src/boatyard/paths.py +5 -0
  42. boatyard-0.6.0/src/boatyard/publish.py +72 -0
  43. boatyard-0.6.0/src/boatyard/py.typed +0 -0
  44. boatyard-0.6.0/src/boatyard/score.py +267 -0
  45. boatyard-0.6.0/src/boatyard/score_compose.py +274 -0
  46. boatyard-0.6.0/src/boatyard/score_k8s.py +152 -0
  47. boatyard-0.6.0/src/boatyard/score_patch_templates/gpu-cuda.tpl +24 -0
  48. boatyard-0.6.0/src/boatyard/score_patch_templates/gpu-pod.tpl +43 -0
  49. boatyard-0.6.0/src/boatyard/score_patch_templates/gpu-rocm.tpl +30 -0
  50. boatyard-0.6.0/src/boatyard/score_patch_templates/probe-adapter-healthchecks.tpl +55 -0
  51. boatyard-0.6.0/src/boatyard/score_patch_templates/restart.tpl +21 -0
  52. boatyard-0.6.0/src/boatyard/score_patch_templates/scale-to-zero.tpl +30 -0
  53. boatyard-0.6.0/src/boatyard/temp.py +5 -0
  54. boatyard-0.6.0/src/boatyard/verbs/__init__.py +0 -0
  55. boatyard-0.6.0/src/boatyard/verbs/build.py +54 -0
  56. boatyard-0.6.0/src/boatyard/verbs/down.py +39 -0
  57. boatyard-0.6.0/src/boatyard/verbs/lock.py +34 -0
  58. boatyard-0.6.0/src/boatyard/verbs/matrix.py +57 -0
  59. boatyard-0.6.0/src/boatyard/verbs/mirror.py +50 -0
  60. boatyard-0.6.0/src/boatyard/verbs/publish.py +138 -0
  61. boatyard-0.6.0/src/boatyard/verbs/score.py +161 -0
  62. boatyard-0.6.0/src/boatyard/verbs/tree_tags.py +35 -0
  63. boatyard-0.6.0/src/boatyard/verbs/up.py +265 -0
  64. boatyard-0.6.0/src/boatyard/yaml.py +34 -0
  65. boatyard-0.6.0/tests/conftest.py +3 -0
  66. boatyard-0.6.0/tests/test_assembly.py +283 -0
  67. boatyard-0.6.0/tests/test_build_docker.py +847 -0
  68. boatyard-0.6.0/tests/test_compose.py +1606 -0
  69. boatyard-0.6.0/tests/test_config.py +1536 -0
  70. boatyard-0.6.0/tests/test_daemon_gc.py +83 -0
  71. boatyard-0.6.0/tests/test_lock.py +1139 -0
  72. boatyard-0.6.0/tests/test_markers.py +297 -0
  73. boatyard-0.6.0/tests/test_matrix.py +143 -0
  74. boatyard-0.6.0/tests/test_mirror.py +357 -0
  75. boatyard-0.6.0/tests/test_provisioners.py +48 -0
  76. boatyard-0.6.0/tests/test_publish_compose.py +431 -0
  77. boatyard-0.6.0/tests/test_score_generate.py +1380 -0
  78. boatyard-0.6.0/tests/test_secrets.py +45 -0
  79. boatyard-0.6.0/tests/test_service_builds.py +412 -0
  80. boatyard-0.6.0/tests/test_tree_tags.py +107 -0
  81. boatyard-0.6.0/uv.lock +423 -0
@@ -0,0 +1 @@
1
+ ruff.base.toml -text
@@ -0,0 +1,15 @@
1
+ name: Cleanup PR releases
2
+
3
+ on:
4
+ pull_request:
5
+ types: [closed]
6
+
7
+ permissions:
8
+ contents: write
9
+
10
+ jobs:
11
+ cleanup-pr-releases:
12
+ if: github.event.pull_request.merged == false
13
+ uses: outernet-foundation/github-actions-toolkit/.github/workflows/cleanup-pr-releases.yml@c6221cf68594e2d6bf4445da02beea0b8ee58440
14
+ with:
15
+ pr-number: ${{ github.event.pull_request.number }}
@@ -0,0 +1,24 @@
1
+ name: Integrate
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ pull_request:
6
+ branches: [dev]
7
+
8
+ concurrency:
9
+ group: integrate-${{ github.ref }}
10
+ cancel-in-progress: true
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ jobs:
16
+ preflight:
17
+ uses: outernet-foundation/github-actions-toolkit/.github/workflows/preflight.yml@c6221cf68594e2d6bf4445da02beea0b8ee58440
18
+
19
+ release-pr-builds:
20
+ needs: [preflight]
21
+ permissions:
22
+ contents: write
23
+ packages: read
24
+ uses: outernet-foundation/github-actions-toolkit/.github/workflows/release-pr-builds.yml@c6221cf68594e2d6bf4445da02beea0b8ee58440
@@ -0,0 +1,19 @@
1
+ name: Merge gate
2
+
3
+ on:
4
+ pull_request:
5
+ branches: [dev]
6
+ types: [labeled]
7
+
8
+ concurrency:
9
+ group: merge-gate-${{ github.event.pull_request.number }}
10
+
11
+ permissions:
12
+ contents: read
13
+ packages: write
14
+
15
+ jobs:
16
+ merge-gate:
17
+ uses: outernet-foundation/github-actions-toolkit/.github/workflows/merge-gate.yml@c6221cf68594e2d6bf4445da02beea0b8ee58440
18
+ secrets:
19
+ merge-bot-private-key: ${{ secrets.MERGE_BOT_APP_PRIVATE_KEY }}
@@ -0,0 +1,34 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ branches: [main, dev]
6
+
7
+ concurrency:
8
+ group: release-${{ github.ref }}
9
+
10
+ permissions:
11
+ contents: read
12
+
13
+ jobs:
14
+ prerelease:
15
+ if: github.ref == 'refs/heads/dev'
16
+ runs-on: ubuntu-latest
17
+ permissions:
18
+ contents: write
19
+ id-token: write
20
+ steps:
21
+ - uses: outernet-foundation/github-actions-toolkit/.github/actions/release@c6221cf68594e2d6bf4445da02beea0b8ee58440
22
+ with:
23
+ channel: dev
24
+
25
+ release:
26
+ if: github.ref == 'refs/heads/main'
27
+ runs-on: ubuntu-latest
28
+ permissions:
29
+ contents: write
30
+ id-token: write
31
+ steps:
32
+ - uses: outernet-foundation/github-actions-toolkit/.github/actions/release@c6221cf68594e2d6bf4445da02beea0b8ee58440
33
+ with:
34
+ channel: stable
@@ -0,0 +1,12 @@
1
+ **/__pycache__
2
+ **/*.pyc
3
+ **/*.pyo
4
+ **/*.pyd
5
+ **/.venv/
6
+ **/*.egg-info
7
+ **/build/
8
+ dist/
9
+ .pytest_cache
10
+ .ruff_cache
11
+ .env
12
+ bookmark.md
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,195 @@
1
+ # boatyard
2
+
3
+ Docker-stack lifecycle verbs as a generic package for any repo shipping a compose graph; `up` derives the whole assembly from the root `boatyard.yml`. No Python
4
+ API — the contracts are the verb CLI (`boatyard <verb>`), the declaration schema (`boatyard.yml` + `boatyard.lock.yml`), and committed artifact formats. Three-class principle: authored facts in the config, registry-resolved in the lock, tree-derived
5
+ (`tree-<hash>`) computed on demand, never committed. PyPI identity starts at `0.1.0`; deps resolve from PyPI, git-source pins only in scratch branches. github-actions-toolkit owns the consumer workflow contract and is never a project dependency (resolver cycle); from 1.0 on breaking changes ship with a manually bumped `major_minor`. Fresh-repo assumption: no code handles an obsolete format
6
+ — a past artifact gets the plain parse refusal, never a migration path.
7
+
8
+ ## Commands
9
+
10
+ Quick loop: `uv run pytest`, `uv run ruff check`; full gate: `uv run --locked scripts/preflight.py`.
11
+
12
+ ## Verbs
13
+
14
+ One module per command under `verbs/`, each with its local bare `app = typer.Typer()` + `@app.command()` on the function; `cli.py` composes them with nameless `app.add_typer(...)` — flat top-level verbs (a named `add_typer` nests a group); one `boatyard`
15
+ console script. Exception law: nothing is ever caught only to be re-raised as a different exception with added information — hand-raised guards and third-party exceptions surface raw, uncaught, with full traceback; `typer.BadParameter`
16
+ owns the CLI-flag layer; `raise SystemExit` only in `verbs/publish.py` (registry credentials), `verbs/score.py` (stale artifacts), and `lock.py` (stale lock). Flags live in the typer signatures in `verbs/*.py`; the flag laws: `build`'s
17
+ `--push`/`--push-to` refuse together; `generate-score --target both` refuses (BadParameter) a one-substrate repo naming the matching narrower `--target` (the rendering pipelines carry no substrate-presence checks); `docker-build-matrix` answers a bare JSON array.
18
+ `--lock-only`/`--check`/`--upgrade` belong to `lock`, never `build`; no `--gpu`, no `--compose-file` (the verbs own the compose invocation); `tree-tags` answers a bare string — no JSON, no `{SERVICE}_SHA` names.
19
+
20
+ ## Modules
21
+
22
+ `config/config.py` schema + load; `ConfigFile.resolve_services(choices)` folds a selection into leaf services; `dockerfiles.py` Dockerfile declarations + sha machinery (`ServiceBuild`, `derive_services`); `config/lock.py` lock schema + paired `LockFile.load(path, config)` + the projection members; `yaml.py` `load_yaml_model` + its duplicate-key walk (the YAML→pydantic parse boundary); `config/values.py` the typed values file; `paths.py` the consumer-repo document paths (`DEFAULT_CONFIG_PATH`/`VALUES_FILE`/`LOCK_PATH`); `temp.py` the shared digest/platform regex spellings
23
+ (`ValuesFile.load`, `PublicUrl`); `lock.py` relock engine + crane resolution; `compose.py` the compose invocation spelling `compose_command` (both verbs); `build.py` build pipeline; `markers.py` marker resolution; `score.py` score shared base; `score_compose.py`/`score_k8s.py` rendering pipelines; `verbs/score.py` the score verb + `ensure_tools`;
24
+ `publish.py` publish preparation; `crane.py` pinned crane installer; `verbs/up.py` selection + rendering inlined in `up` + the up-only `render_env_entries`/`lock_env_unwind`; `verbs/*` one module per command. Model modules run root → leaves: root record first
25
+ (`RootModel` subscripts quote forward refs; the one inversion is a parent whose field default constructs the child), functions callers-before-callees; imports absolute; `dockerfiles` ← `config/config` ← `config/lock` ← `lock` one-way. Also in `config/config.py` (the shared leaf): `Yaml`,
26
+ `is_mapping`/`is_list`, `double_dollars` (the one `$`-doubling home), `PLACEHOLDER`/`COMPOSE_SLOT`/`ENV_NAME_PATTERN`, `EDGE_SERVICE_NAME`, `lock_key_prefix`, `chart_lock_key`, `bundle_lock_key`, `env_lock_slot`/`LOCK_SLOT_MARKER`,
27
+ `compose_bundle_output_path` (the one compose-output path join), `qualified_option_name` (the one qualified-name join), `required_compose_slots` (the odd-parity slot scan over compose texts), `scan_bundle_texts`
28
+ (the refuse-then-requires wrapper answering `BundleScan`), `refuse_unresolved_references`
29
+ (the shared route/port refusal over compose texts; answers the services keys; `prefix` composes the `{entry}-` namespace).
30
+
31
+ ## ConfigFile schema
32
+
33
+ - `extra="forbid"` everywhere; per-field shape laws in `field_validator`s, cross-field laws in one `model_validator(mode="after")` per model; both run in declaration order.
34
+ - `requires` self-checks the installed version at load (`0.0.0.dev0` dev sentinel bypasses the check, not the field); duplicate keys, unknown top-level keys, dead sections (`generate-score`, `publish-compose`, `lifecycle`, `bundles`,
35
+ `lifecycle_prefix`) refuse.
36
+ - `base_images`: ALL_CAPS keys become build args; upstream-native `path:tag`/`path@digest` values; `{uv}`/`{python}` templates over toolchain pins (`{uv}` from `tool.uv.required-version`, exact `==` pin only; `{python}` from
37
+ `.python-version`); a literal value in place of a template = the deliberate-skew escape hatch.
38
+ - `variants`: ALL_CAPS axis → bare lowercase values, first = the flagless build's default; declared, never probed. `platforms` required exactly when `base_images` is; charts arch-neutral, exempt.
39
+ - `project_name` required on every config; `gateway` is optional — routes require it, and declaring it requires `CADDY_IMAGE` and renders the edge, routes or not (caddy == gateway).
40
+ - `services`: groups (`options:` mutually exclusive) + bare riding entries; every entry/option is `ref:` (tag-pinned OCI include — digest/`latest`/range spellings refuse at load; any published channel spelling pins) or `workloads:`/`vars:`; workload references are flat `*.yaml` names (both sections; `metadata.name` pins on stems); an option's name outside its group is its
41
+ qualified `group-dash-option` name; the vocabulary (qualified + bare spellings) refuses duplicates at load; `auth` reserved — options declare exactly one route; routes `/`-rooted, edge-wide unique, `strip_prefix` requires a wildcard; ports use the mapping form (service, published, target).
42
+ - `k8s`: `workloads`, `vars`, `charts` name → `oci://…:tag|@digest`; the map key IS the `{NAME}_CHART` lock key; a section declaring neither workloads nor charts refuses.
43
+ - Load refusals (`ConfigFile.resolve`): Dockerfile services or local entries without `repository`; a declared chart without `stack/score/k8s/values/{chart}.yaml`; routes without `gateway`; a declared `gateway` without `CADDY_IMAGE` in `base_images` (the lock half of that law rides the paired read's coverage). Config load never reads the lock — the lock verb bootstraps a first lock from nothing; lock consumers refuse wholesale on drift at the paired read, a missing entry refuses per `require`, and both name the remedy.
44
+ - Every `load()` takes a full path and rides `load_yaml_model` (`yaml.py`: read → duplicate-key walk → `yaml.safe_load` → None→`{}` → `model_validate` with context); a file parsed straight into a pydantic model rides it too (secrets, tool state, compose output); a missing file surfaces as raw `FileNotFoundError` at the call site that failed to check — `LockFile.load` is the one exception (its missing-as-empty policy is stated under Lock). Missing-file policy lives at callers: `lock`/`matrix`/`score`/`publish` self-skip or answer empty via their own `is_file` gate; relock's committed read is schema-only inline (`load_yaml_model` in `lock.py` — drift and old formats are its input, never refused); `score_compose` reads `platform` as `None` when the values file is absent; `up` refuses without it at load; `down` never reads it.
45
+ - `mirrored_ref(reference, prefix)` = the only prefix join — injects the prefix, rejects an already-prefixed reference.
46
+ - Derivation shape law: `ConfigFile` carries authored data and validation; a member may hold real derivation logic (`local_service_entries`, `declared_charts`, `resolve_services`), but a member that only delegates to another module's function is a
47
+ pass-through — call the function at the consumer, `compute_service_shas` module-qualified (test seam); `root` defaults to the working directory, `resolve` injects the load root, excluded, never-authored.
48
+ - `boatyard.values.yaml` (consumer root, gitignored) is the only machine-values source — the ambient shell fills nothing; `up` refuses without it, score generation and `down` run without it (`score_compose` reads `platform` as `None` on fresh checkouts). `ValuesFile.load(path, config)` hands the raw document to `model_validate` — the selection fold into `choices`, the selection refusals, and the path-override vocabulary/ref-entry refusals ride pydantic validators, with `ConfigFile` arriving via validation context. Typed fields (`bundles` list, `path_overrides` map keyed by qualified name, `public_url`, `platform`) carve the consumed spellings; every other key is a slot value on the open `variables` map (ALL_CAPS enforced — `ENV_NAME_PATTERN`; keys carrying `LOCK_SLOT_MARKER` (`__LOCK__`) refuse — the lock fills those slots, never a hand). Path-override refusals (empty, URL-scheme, non-`stack/generated/compose/` layout) and the scheme-less `public_url` refusal are pydantic `ValidationError`s at parse; paths resolve in `up` against the repo root.
49
+
50
+ ## Service derivation
51
+
52
+ - The Dockerfile-derived build set computes in `dockerfiles.py`'s `derive_services`, never authored: Dockerfile dirs one level below a top-level dir = services (spanning several top-level dirs refuses); context edges from bare-identifier `FROM`s naming a
53
+ sibling (stage names skipped); a literal `FROM` or slash-bearing `COPY --from=` refuses (base images ride as build args); `ARG <axis>` fans over the axis's declared values, dependents inherit the base's variants exactly (independent axes
54
+ over fanned bases, disagreeing parents, cycles refuse); `ARG {SERVICE}_SHA` declares the stamp receiver.
55
+ - `.dockerignore` = the allowlist the sha machinery hashes over a disk walk, staged per service into a temp git index (`git -c core.autocrlf=false update-index --add --stdin`, `git write-tree` minting the `tree-` OID); an empty service set returns `{}` without walking.
56
+
57
+ ## Lock
58
+
59
+ - Format: top-level `platforms:` records the platform set the lock resolved for; `images:` name → `{index, platforms: {os/arch: reference}}` with every entry's platform keys equal to the top-level set; `charts:` name → flat digest-pinned reference; `bundles:` name → `{reference, requires}` — digest-pinned reference plus the
60
+ bundle's odd-parity `${NAME}` slots minus `:-`-defaulted ones, recorded at fresh resolution and recomputed from the pinned
61
+ artifact while the digest freezes (identical by content-addressing); `bundles:` keys are `{QUALIFIED_NAME}_BUNDLE` (qualified selection name upper-cased + `_BUNDLE`; a bare entry is its own name), arch-neutral; written only by the `lock` verb and
62
+ `build`'s incremental freeze (`relock` inline), over declared names only — built-image digests are unrepresentable, orphans prune by construction; never hand-authored.
63
+ - `LockFile.load(path, config)` = the only parser consumers use — config rides validation context (the `ValuesFile.load` precedent) and a missing lock reads as the empty lock paired against the config (`score` pre-gates with its own lock-required refusal); schema-only refusals fire on every parse (duplicate keys refuse naming the line; non-digest-pinned entries, cross-section name collisions reject; malformed or non-mapping documents surface raw pydantic/pyyaml errors; empty lock legal); the context-gated pairing validators refuse wholesale staleness naming the lock verb: recorded platforms ≠ `config.platforms`, an image entry's platform keys ≠ the top-level set, base_images/charts keys the lock lacks (coverage — bundles exempt, path overrides skip their entry), lock names the config no longer declares (orphans).
64
+ - `relock` (module-level in `lock.py`; reads the committed lock schema-only inline — drift and old formats are its input, never refused) computes per `base_images` entry the mirror-prefixed reference resolved into the index digest + one platform-manifest digest per declared platform (`crane manifest` + `crane digest`, crane always installed at relock entry; `crane copy` byte-identity holds both digests); `@digest`
65
+ declarations pass through prefixed-unresolved; declaration-incremental freeze: unchanged declarations copy forward, tag bump or platform-set change re-resolves, `--upgrade` re-resolves everything; charts freeze by
66
+ the same prefix match; bundle refs resolve direct — first-party, never mirror-prefixed — via `crane digest`, frozen by the same tag match.
67
+ - Upstream manifest selection (inline in `relock`) = a pure function of the declarations — the writing machine's arch never participates; exact `os`/`architecture` match; `unknown/unknown` + attestation entries skipped; a platform matching anything but exactly one manifest refuses.
68
+ - Projection selection law: `lock.index_references()` answers the lock's image index references (build edge); `lock.manifest_references(platform)` answers the concrete per-platform manifests (compose edge) and owns platform selection over the lock's recorded set — a recorded singleton self-selects, a multi-platform lock names `platform:` through `ValuesFile.platform` (missing/not recorded refuses listing the recorded platforms), never host introspection; the consumer's machine platform governs producer locks too; charts ride in both (single depth).
69
+ - The `lock` verb self-skips where no config or no declarations exist (print, exit 0 — CI's freshness step runs it unconditionally); `--check` asserts instead of writes (in-memory relock, exit 1, nothing touches the
70
+ tree).
71
+
72
+ ## Mirror namespace
73
+
74
+ - Third-party images and charts are consumed through the org mirror namespace: upstream manifests get GC'd; CI = the single writer — engineers never push, and error text never teaches them to.
75
+ - Declarations are upstream-native; the prefixed form is derived; `mirrored_ref` = the single join; the `mirror` verb never enumerates bundle declarations.
76
+ - `up` never probes the registry — compose's include fetch and `--pull missing` surface missing digests, unclassified, at bring-up; a partial stack under `-d` is the accepted cost of no pre-flight probe.
77
+ - The `mirror` verb probes presence per reference over every recorded digest (`crane digest`; partially mirrored still triggers the copy; probe failure falls through to `crane copy`, the loud gate — probe errors never classified); one
78
+ `crane copy {ref} {prefix}/{ref}` per lacking reference copies the whole index; every declared `base_images`/charts reference is a target — nothing filtered; zero declared targets = a loud error; the
79
+ footer splits copied/present; crane's installer always runs so the pin, not a preexisting binary, decides the version.
80
+ - `build --push` leaves bake's `metadata.json` in the working directory — the handoff the toolkit's builds_ref step reads; boatyard reads no `REGISTRY` facts.
81
+
82
+ ## Compose assembly and selection
83
+
84
+ - Selection law: `bundles:` names options only, by qualified `group-dash-option` spelling (a bare entry's name refuses as unselectable); ≤ one option per group; zero options leave the riders/routes carrying the assembly (omitting the auth pick = the anonymous edge) — an options-only
85
+ repo with an empty selection hands compose a name-only root, edge-only when a gateway is declared; unknown names refuse listing the qualified vocabulary; every selection refusal (unknown names, one-per-group) and the fold into `choices` (group → option) ride
86
+ `ValuesFile.load(root, config)` (the model's validators) on every values load (`up` always; `generate-score` when the file exists; `down` never). Membership law: selection consumption,
87
+ rendering, and staged inputs live inline in `verbs/up.py`; `compose.py` carries only the invocation spelling `compose_command` (both verbs); the up-only `render_env_entries`/`lock_env_unwind` live in `verbs/up.py`; nothing else lands in either.
88
+ - Leaf-service law: `ConfigFile.resolve_services(choices)` answers leaf services — the option's routes/ports ride before its group's (the auth option's route is `routes[0]`), its resolved service carries `auth: True`, an unselected group rides nothing; `up` consumes only these.
89
+ - Bundle overrides ride the values file's `path_overrides` map per machine (qualified name → a `stack/generated/compose/`-layout path, relative to the repo root); unknown names refuse listing
90
+ the vocabulary, unselected names are inert, and a name must resolve to a `ref:` entry — a local-entry name refuses listing the ref entries; an overridden ref skips its lock-entry requirement; the override carries the local-output teeth (exists, parses, no `edge` service, names and
91
+ requirements validated against the file) and contributes
92
+ the producer repo's config + paired lock — the nearest `boatyard.yml` above the override, then `ConfigFile.load` + the paired `LockFile.load`, projected via `manifest_references` under the consumer's platform — to the per-run env file under the entry prefix; no repo root above the override, or a drifted producer lock, refuses naming the remedy.
93
+ - The materialized root carries the project `name:`, the `include:` list (remote entries as `oci://` + the lock's `{NAME}_BUNDLE` digest ref — the yaml tag never rides an include; a selected ref lacking its entry refuses naming
94
+ `boatyard lock`; local outputs by absolute path; `services:` declaration order), the emitted edge (iff a gateway is declared — routes or not — attached to every selected bundle's `{entry}-net` network), parent-file port stanzas; compose
95
+ merges parent ports into included services and honors `!reset` — the merge needs compose ≥ 2.40.
96
+ - Namespacing law: score renders every bundle resource under `{entry}-` (services, configs keys) on a per-bundle network carrying the bare name as a service alias — include collisions are structurally impossible,
97
+ rendered hostnames still resolve, and accidental cross-bundle DNS fails instead of silently resolving: a bundle references only its own services; cross-bundle coupling rides the consumer's declared surface
98
+ (env slots, route upstreams). `up` composes `{entry}-` onto authored route upstreams and port owners; `boatyard.yml` stays bare. Local-output lock-key slots (the lock's key set draws the line)
99
+ rewrite under `env_lock_slot(entry, key)` — the `{ENTRY}__LOCK__KEY` spelling, the `LOCK_SLOT_MARKER` infix reserving lock-filled slots against the values file (whose parse refuses the marker); consumer-surface slots (edge image is baked, gateway/ports, secrets, consumer-interpolated files) stay bare; publish fans
100
+ its bake substitution keys with the same spelling mechanically.
101
+ - Validation teeth: relock reads every declared bundle's content inline (the compose files ride as inline base64 layer data in `crane manifest`, `crane blob` the
102
+ fallback) and refuses any namespaced `{entry}-` route upstream or port owner that resolves to no service in the bundle; `up` applies the same refusal to local outputs and path overrides by scanning the local
103
+ file; `up` refuses a selected bundle whose recorded `requires` no environment source fills (boatyard.values.yaml or the
104
+ lock env file; lock-key slots are satisfied by construction; a shell-set unfilled name gains a "set in the shell and ignored" clause); a selected bundle output that does not exist refuses naming generate-score; `up` never invokes `docker compose config`.
105
+ - Edge stanza: `image:` carries the lock's platform manifest for `CADDY_IMAGE` baked at assembly (the config-declared key is coverage-guaranteed in the lock), the Caddyfile configs entry, `gateway:` as published port, `FORWARDED_PROTO`/`FORWARDED_PORT` derived from `public_url` under a selected auth option (refusing when absent); the
106
+ edge does no token validation — re-protection is a bundle-shipped oauth2-proxy sidecar treaty; the service name `edge` is reserved.
107
+ - Caddyfile: one site on internal `:8443`; `handle` blocks mutually exclusive (caddy sorts longest-path-first, declaration order cannot shadow); `handle_path` for `strip_prefix`; `header_up X-Forwarded-*` only on the auth route; `raw:`
108
+ content rides verbatim (escape hatch, review flag); the whole file rides inline as `configs.content` with every `$` doubled.
109
+ - Per-run files: lifetimes ride the caller-owned `ExitStack`, never an exception handler; the handoff execs — a successful run never unwinds, the files ride out in the OS temp dir; on failure the compose files unlink, the env file
110
+ keeps with a path-line-only echo (`lock_env_unwind` — its contents may carry secrets); `up` writes ONE per-run dotenv inline (`render_env_entries` from `verbs/up.py`): values then lock entries under `#` source headers — the lock section carries only `{ENTRY}__LOCK__KEY` fans (no bare consumer entries; the edge bakes its image), one per selected local entry from the consumer lock and one per path override from its producer lock (producer
111
+ pins win; a remote-only selection attaches none); the values parse's marker refusal is the collision law — no merge check exists;
112
+ `compose_command` = the one docker compose invocation spelling, prefixed `env -u K1 -u K2 …` (sorted) when `slot_names` is non-empty — the scrub defeating compose's shell-beats-env-file precedence; `up` passes values ∪ lock entries ∪ requirement names; `down` reads no values file, no lock, no selection — a name-only root + `--remove-orphans` removes
113
+ every container the project ever created; a config-less repo refuses at load and a `services:`-less repo refuses on `up` only.
114
+
115
+ ## Build
116
+
117
+ - `run_build` renders the deterministic per-run bake file (authored key order, byte-identical for identical configs; bake validates its own input — no document model); it takes the already-loaded `config` — the build verb owns its flag refusals; a failed bake re-raises with the rendered content attached.
118
+ - Push axis: `--push` (bool) pushes for the config's `repository`; `--push-to <addr>` re-roots tags + cache refs under that registry; the flags together refuse; an address with a scheme, trailing slash, or host
119
+ carrying neither `.` nor `:` refuses; `--push` without a declared `repository` refuses.
120
+ - `host_platform()` = the one host fact left: the flagless default set is every service's first-declared variant restricted to this machine's arch, and an empty default set refuses naming the arch + `--targets` (bake with no target arguments
121
+ builds every service, silently defeating the selection); `--targets` spelling is shape-dictated — bare `name` for an axis-less service, `name/variant` for an axis service (a bare name on an axis service refuses naming its variants; the dashed
122
+ target name is bake-internal and refuses; repeated addresses ride through — bake owns set semantics); unknown names/variants refuse listing the vocabulary.
123
+ - Per target: dockerfile by convention; platforms collapse to the host arch in load mode (a single declared platform rides through); args — every `base_images` key as `${KEY:-}` on every service, the axis
124
+ ARG at its variant value, `{SERVICE}_SHA` as `${…:?err}`; tags `{namespace}/{target}:latest` + `:${SERVICE_SHA}`; `target:`-prefixed `additional_contexts`; registry cache refs under `{namespace}/build-cache:{target}` in push mode.
125
+ - The incremental lock write rides every building run (the `uv sync` shape — anyone who builds also locks), landing only after target selection + its refusals; bake reads the fresh entries as environment (index form); a building run with
126
+ zero derived services refuses naming the lock verb.
127
+ - `metadata.json`: cwd-stable, unlinked before the run, coverage-checked against the requested targets after; local default-target builds run the daemon
128
+ GC-limit check (`MIN_KEEP_STORAGE_GB` = 60; absent/null `defaultKeepStorage` refuses with a sample snippet, an explicit zero is a set value refusing below the floor; no readable daemon.json warns and continues).
129
+
130
+ ## Score generation
131
+
132
+ - Deterministic generation from `stack/score/` into `stack/generated/`; one compose rendering per local option + bare entry (output `stack/generated/compose/compose.{name}.yaml`; the compose state dir is re-created per entry); k8s = one
133
+ monolithic rendering over `k8s.workloads` + `k8s.vars`, untouched by `bundles:`, with instance names deterministic by construction — state.yaml pre-seeds `InstanceSuffix` = `-` + first 10 hex of sha256(metadata.name) per workload between init
134
+ and generate (state-format coupling bounded by the pinned tool version); `--local` writes the gitignored local output with the local storage class; charts render through `helm template` against the
135
+ lock's `{NAME}_CHART` entry (a chart without its lock entry refuses naming the lock verb; helm is a renderer, never a deployer); `stack/score/` stays read-only; output reproduces from a fresh clone.
136
+ - Substrate law: compose renderings consume the lock's platform manifests (the artifact records the exact bytes one machine runs); k8s renderings consume the index form (kubelet resolves per node).
137
+ - `ensure_tools` installs the three pinned tools into `/usr/local/bin`: version-probe each, fetch on absence or pin mismatch.
138
+ - `--check` = the CI staleness gate: self-skips (exit 0) where nothing generates; otherwise regenerates in-process; any `git status` change over the outputs exits 1 printing the diff; `--check` + `--local` are mutually
139
+ exclusive.
140
+ - Post-passes on compose output are typed transformations on `ComposeDocument`, dumped exactly once — pydantic's declared-fields-first order is the artifacts' deterministic key order: a workload-major pass collapses `{workload}-{container}` services to the container name and inlines every `type: bind` volume as a top-level
141
+ `configs.content` entry — zero host-filesystem references in committed artifacts; the `$`-doubling exception rides the owning workload's consumer-interpolated declaration; workload-less services ride through under
142
+ their rendered names; the barrier pins the `ALPINE_IMAGE` lock slot, namespacing like every service (`{entry}-wait-for-resources`, `depends_on` rewritten per rendered dependency) and every lock-key slot under the
143
+ entry prefix; each collapsed service name is
144
+ claimed once (collision loud); the `{entry}-` namespace pass covers services, configs keys, and the per-bundle network with bare aliases.
145
+ - The image lock is required and non-empty; the compose target requires the `ALPINE_IMAGE` entry.
146
+ - `$`-doubling law, stated once: a literal `$` doubles whenever file text rides inside a compose document (edge Caddyfile, publish's file-sourced configs, score's bind inlining); text outside a compose document (k8s-side inclusion, k8s manifests) never doubles.
147
+ - `docker-build-matrix` leg schema (`targets`, `variant`) = a named contract with the binding — renaming a key breaks its fanout YAML; one partition per closure × variant; `targets` carries `--targets` addresses (bare `name` / `name/variant`), never dashed target names.
148
+
149
+ ## Provisioners and markers
150
+
151
+ - Authored provisioner files carry `state`/`outputs`/`volumes`/`services` as real YAML mappings + `manifests` as a real list — score's string form is generated, never authored; a field already in string form = a loud validation error; `extra="forbid"`.
152
+ - Compilation resolves include markers, then lock markers, both before validation; the compiled form lives in a per-run tempdir under `score_dir`, never the committed tree; `--provisioners` paths stay POSIX-relative;
153
+ authored key order controls dump order at every level; comments are dropped; registration derives by resource-type closure in sorted-filename order;
154
+ orphan provisioner files silently no-ship.
155
+ - Marker grammar: a single-key mapping (authored sources stay plain-YAML-parseable); `include:` inserts bytes verbatim, `include-yaml:` inserts parsed structure; neither transforms content (`${VAR}` slots pass through; `$`-handling is
156
+ per-consumer); paths anchor at the repo root — absolute paths, `..` escapes, missing files, cycles = loud errors; text content is never scanned for markers; `lock: NAME`/`lock-image: NAME` substitute the full reference /
157
+ registry/repository/tag/digest (grafana's `digest_field: sha`), scoped to provisioner compiles and chart values (k8s provisioners have no env file); workloads use `${NAME}` slots; a literal `image:` (no `${`/`{{`
158
+ slot) refuses at every marker resolution — provisioner compiles and every chart values file, markers or not — and at the workload lens.
159
+
160
+ ## Secrets
161
+
162
+ - `stack/secrets.yaml` is required (missing refuses at generate-score; an empty file = the empty declaration) — `KEY: description` or `KEY: {description, mint: true}` (string specs coerce; keys ALL_CAPS); each rendering's consumed keys validate as a subset (pair-variance legal);
163
+ the cluster Secret derives `{project_name}-secrets`; k8s consumption = `secretKeyRef` naming it (`envFrom`/`extraEnvFrom` `secretRef`s impose no per-key constraint); compose consumption = `${ALL_CAPS}` brace slots with odd `$`-parity (`$$`-escaped
164
+ `configs.content` doesn't count; lock-key slots don't count — the per-run env file fills them); the mint list artifact (`stack/generated/secrets.txt`) generates from the declaration alone.
165
+
166
+ ## Publish
167
+
168
+ - Publishing happens iff local entries are declared — no flag, no section; artifact names derive from `repository` (`{repository}/{qualified-or-entry-name}`); `--plan` prints the entries as a bare JSON array (`[]` when none), no
169
+ login, no publish — the CI getter's derivation source; bake substitutes `${ALL_CAPS}` slots against `service_shas()` tree-tags + the lock's index projection, the lock keys fanned under the bundle prefix for the
170
+ artifact's namespaced spellings; unresolved vars stay slots in the artifact for the consumer's values file
171
+ (non-empty stubs exist only so publish's interpolation validation passes); authored `configs.content` rides verbatim (single-`$` slots = the include consumer's interpolation surface — `$`-doubling is score's concern, not publish's); image
172
+ refs under `{repository}/` pin to `@sha256` via memoized `crane digest`; each entry publishes once under the `<sha>` tag, aliasing `<branch>` via `crane tag` (compose stamps wall-clock `created` — only the alias makes both tags name one digest); registry login reads `REGISTRY_USERNAME`/`REGISTRY_TOKEN`.
173
+
174
+ ## Deployment-surface annotations
175
+
176
+ - Everything the substrate needs that score's container model doesn't carry lives in the workload file as `outernet.internal/*` annotations — `boatyard.yml` never carries a per-workload runtime fact; the shipped patch templates
177
+ (package data in `src/boatyard/score_patch_templates/`) are the annotation reader — one annotation, one template, one behavior; consumer repos never author one; refusal laws live in boatyard code (templates are data and cannot
178
+ refuse); the lens is `WorkloadDocument`/`ComposeWorkloadDocument` (`metadata.name` pinned to the workload file stem; the compose lens adds httpGet-requires-probe-adapter), one spelling `WorkloadDocument.parse(raw, file_name)`;
179
+ score parses workloads once and propagates `metadata.annotations` into both renderings.
180
+
181
+ | Annotation | Behavior |
182
+ |---|---|
183
+ | `outernet.internal/gpu` | cuda/rocm compose pair registers per `GPU_VARIANT`; `gpu-pod.tpl` on every k8s init; nodeSelector `placeframe.io/gpu`; runtime vocabulary, distinct from build-time variant axes |
184
+ | `outernet.internal/restart` | compose enum (`no`/`always`/`on-failure`/`unless-stopped`); outside refuses |
185
+ | `outernet.internal/probe-adapter` | `python-urllib` or `bash-http`; an httpGet probe without the annotation refuses loudly — score-compose silently drops httpGet |
186
+ | `outernet.internal/consumer-interpolated` | declared `files:` names ride un-doubled; a name matching no declared file refuses |
187
+
188
+ ## This file's rules
189
+
190
+ The only reader is a coding agent: the code is the source of truth for mechanism; this file carries only what an agent would otherwise get wrong.
191
+
192
+ - Budget: 200 lines and 26KB, both hard — an addition crossing either cap cuts something else here; admit a line only if removing it would cause a mistake.
193
+ - One law, one home: search before adding; an existing law updates in place, never a second copy. The module map carries one line per module; laws live in sections named for the concern.
194
+ - Identifiers, flags, file paths, and refusal phrasings stay exact — they are what agents grep; no line exceeds 300 characters.
195
+ - A code change that invalidates a line here is a doc change in the same commit; a fact verifiable only against another repo or CI is a pointer to that owner, never restated here.
@@ -0,0 +1 @@
1
+ @AGENTS.md
boatyard-0.6.0/LICENSE ADDED
@@ -0,0 +1,201 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work
38
+ (an example is provided in the Appendix below).
39
+
40
+ "Derivative Works" shall mean any work, whether in Source or Object
41
+ form, that is based on (or derived from) the Work and for which the
42
+ editorial revisions, annotations, elaborations, or other modifications
43
+ represent, as a whole, an original work of authorship. For the purposes
44
+ of this License, Derivative Works shall not include works that remain
45
+ separable from, or merely link (or bind by name) to the interfaces of,
46
+ the Work and Derivative Works thereof.
47
+
48
+ "Contribution" shall mean any work of authorship, including
49
+ the original version of the Work and any modifications or additions
50
+ to that Work or Derivative Works thereof, that is intentionally
51
+ submitted to Licensor for inclusion in the Work by the copyright owner
52
+ or by an individual or Legal Entity authorized to submit on behalf of
53
+ the copyright owner. For the purposes of this definition, "submitted"
54
+ means any form of electronic, verbal, or written communication sent
55
+ to the Licensor or its representatives, including but not limited to
56
+ communication on electronic mailing lists, source code control systems,
57
+ and issue tracking systems that are managed by, or on behalf of, the
58
+ Licensor for the purpose of discussing and improving the Work, but
59
+ excluding communication that is conspicuously marked or otherwise
60
+ designated in writing by the copyright owner as "Not a Contribution."
61
+
62
+ "Contributor" shall mean Licensor and any individual or Legal Entity
63
+ on behalf of whom a Contribution has been received by Licensor and
64
+ subsequently incorporated within the Work.
65
+
66
+ 2. Grant of Copyright License. Subject to the terms and conditions of
67
+ this License, each Contributor hereby grants to You a perpetual,
68
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69
+ copyright license to reproduce, prepare Derivative Works of,
70
+ publicly display, publicly perform, sublicense, and distribute the
71
+ Work and such Derivative Works in Source or Object form.
72
+
73
+ 3. Grant of Patent License. Subject to the terms and conditions of
74
+ this License, each Contributor hereby grants to You a perpetual,
75
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76
+ (except as stated in this section) patent license to make, have made,
77
+ use, offer to sell, sell, import, and otherwise transfer the Work,
78
+ where such license applies only to those patent claims licensable
79
+ by such Contributor that are necessarily infringed by their
80
+ Contribution(s) alone or by combination of their Contribution(s)
81
+ with the Work to which such Contribution(s) was submitted. If You
82
+ institute patent litigation against any entity (including a
83
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
84
+ or a Contribution incorporated within the Work constitutes direct
85
+ or contributory patent infringement, then any patent licenses
86
+ granted to You under this License for that Work shall terminate
87
+ as of the date such litigation is filed.
88
+
89
+ 4. Redistribution. You may reproduce and distribute copies of the
90
+ Work or Derivative Works thereof in any medium, with or without
91
+ modifications, and in Source or Object form, provided that You
92
+ meet the following conditions:
93
+
94
+ (a) You must give any other recipients of the Work or
95
+ Derivative Works a copy of this License; and
96
+
97
+ (b) You must cause any modified files to carry prominent notices
98
+ stating that You changed the files; and
99
+
100
+ (c) You must retain, in the Source form of any Derivative Works
101
+ that You distribute, all copyright, patent, trademark, and
102
+ attribution notices from the Source form of the Work,
103
+ excluding those notices that do not pertain to any part of
104
+ the Derivative Works; and
105
+
106
+ (d) If the Work includes a "NOTICE" text file as part of its
107
+ distribution, then any Derivative Works that You distribute must
108
+ include a readable copy of the attribution notices contained
109
+ within such NOTICE file, excluding those notices that do not
110
+ pertain to any part of the Derivative Works, in at least one
111
+ of the following places: within a NOTICE text file distributed
112
+ as part of the Derivative Works; within the Source form or
113
+ documentation, if provided along with the Derivative Works; or,
114
+ within a display generated by the Derivative Works, if and
115
+ wherever such third-party notices normally appear. The contents
116
+ of the NOTICE file are for informational purposes only and
117
+ do not modify the License. You may add Your own attribution
118
+ notices within Derivative Works that You distribute, alongside
119
+ or as an addendum to the NOTICE text from the Work, provided
120
+ that such additional attribution notices cannot be construed
121
+ as modifying the License.
122
+
123
+ You may add Your own copyright statement to Your modifications and
124
+ may provide additional or different license terms and conditions
125
+ for use, reproduction, or distribution of Your modifications, or
126
+ for any such Derivative Works as a whole, provided Your use,
127
+ reproduction, and distribution of the Work otherwise complies with
128
+ the conditions stated in this License.
129
+
130
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
131
+ any Contribution intentionally submitted for inclusion in the Work
132
+ by You to the Licensor shall be under the terms and conditions of
133
+ this License, without any additional terms or conditions.
134
+ Notwithstanding the above, nothing herein shall supersede or modify
135
+ the terms of any separate license agreement you may have executed
136
+ with Licensor regarding such Contributions.
137
+
138
+ 6. Trademarks. This License does not grant permission to use the trade
139
+ names, trademarks, service marks, or product names of the Licensor,
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
+
143
+ 7. Disclaimer of Warranty. Unless required by applicable law or
144
+ agreed to in writing, Licensor provides the Work (and each
145
+ Contributor provides its Contributions) on an "AS IS" BASIS,
146
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147
+ implied, including, without limitation, any warranties or conditions
148
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149
+ PARTICULAR PURPOSE. You are solely responsible for determining the
150
+ appropriateness of using or redistributing the Work and assume any
151
+ risks associated with Your exercise of permissions under this License.
152
+
153
+ 8. Limitation of Liability. In no event and under no legal theory,
154
+ whether in tort (including negligence), contract, or otherwise,
155
+ unless required by applicable law (such as deliberate and grossly
156
+ negligent acts) or agreed to in writing, shall any Contributor be
157
+ liable to You for damages, including any direct, indirect, special,
158
+ incidental, or consequential damages of any character arising as a
159
+ result of this License or out of the use or inability to use the
160
+ Work (including but not limited to damages for loss of goodwill,
161
+ work stoppage, computer failure or malfunction, or any and all
162
+ other commercial damages or losses), even if such Contributor
163
+ has been advised of the possibility of such damages.
164
+
165
+ 9. Accepting Warranty or Additional Liability. While redistributing
166
+ the Work or Derivative Works thereof, You may choose to offer,
167
+ and charge a fee for, acceptance of support, warranty, indemnity,
168
+ or other liability obligations and/or rights consistent with this
169
+ License. However, in accepting such obligations, You may act only
170
+ on Your own behalf and on Your sole responsibility, not on behalf
171
+ of any other Contributor, and only if You agree to indemnify,
172
+ defend, and hold each Contributor harmless for any liability
173
+ incurred by, or claims asserted against, such Contributor by reason
174
+ of your accepting any such warranty or additional liability.
175
+
176
+ END OF TERMS AND CONDITIONS
177
+
178
+ APPENDIX: How to apply the Apache License to your work.
179
+
180
+ To apply the Apache License to your work, attach the following
181
+ boilerplate notice, with the fields enclosed by brackets "[]"
182
+ replaced with your own identifying information. (Don't include
183
+ the brackets!) The text should be enclosed in the appropriate
184
+ comment syntax for the file format. We also recommend that a
185
+ file or class name and description of purpose be included on the
186
+ same "printed page" as the copyright notice for easier
187
+ identification within third-party archives.
188
+
189
+ Copyright [yyyy] [name of copyright owner]
190
+
191
+ Licensed under the Apache License, Version 2.0 (the "License");
192
+ you may not use this file except in compliance with the License.
193
+ You may obtain a copy of the License at
194
+
195
+ http://www.apache.org/licenses/LICENSE-2.0
196
+
197
+ Unless required by applicable law or agreed to in writing, software
198
+ distributed under the License is distributed on an "AS IS" BASIS,
199
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200
+ See the License for the specific language governing permissions and
201
+ limitations under the License.
boatyard-0.6.0/NOTICE ADDED
@@ -0,0 +1 @@
1
+ Copyright 2026 Tyler Hatch
@@ -0,0 +1,12 @@
1
+ Metadata-Version: 2.5
2
+ Name: boatyard
3
+ Version: 0.6.0
4
+ Summary: Deploy-files definition language and compiler, plus a runtime for the compose target
5
+ License-File: LICENSE
6
+ License-File: NOTICE
7
+ Requires-Python: >=3.13
8
+ Requires-Dist: bashrun>=0.1.0
9
+ Requires-Dist: packaging>=24.1
10
+ Requires-Dist: pydantic>=2.11.7
11
+ Requires-Dist: pyyaml>=6.0
12
+ Requires-Dist: typer>=0.17.4
@@ -0,0 +1,3 @@
1
+ # boatyard
2
+
3
+ A deploy-files definition language and compiler, plus a runtime for the compose target — lifecycle verbs (`boatyard up`, `down`, `build`, `lock`, `mirror`, `publish-compose`, `generate-score`) driven by a per-repo `boatyard.yml`. Agent-facing documentation lives in [`AGENTS.md`](./AGENTS.md).