diff --git a/.github/workflows/action-version.yml b/.github/workflows/action-version.yml new file mode 100644 index 0000000..2a954eb --- /dev/null +++ b/.github/workflows/action-version.yml @@ -0,0 +1,46 @@ +name: Action version pin + +# action.yml pins a specific modelith release as its `version` input default. +# That bump is a manual step after every release (see CLAUDE.md's release +# checklist) and nothing else catches a forgotten one — a stale default just +# keeps installing an old release with no error. This guard compares the pinned +# default against the latest published release. + +on: + push: + branches: [main] + paths: [action.yml] + pull_request: + paths: [action.yml] + # Catch a forgotten post-release bump even when no PR touches action.yml. + schedule: + - cron: "17 8 * * 1" + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + - name: action.yml version default matches the latest release + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + pinned=$(yq '.inputs.version.default' action.yml) + # `gh release list` returns an empty array (exit 0) when there are no + # releases, but a non-zero exit on a real API error — so `set -e` + # surfaces a genuine failure instead of it masquerading as "no release + # yet" and silently skipping the guard. + latest=$(gh release list --json tagName,isLatest --jq 'map(select(.isLatest)) | .[0].tagName // ""') + if [ -z "$latest" ]; then + echo "No published release yet; nothing to compare against." + exit 0 + fi + echo "action.yml pins: $pinned ; latest release: $latest" + if [ "$pinned" != "$latest" ]; then + echo "::error file=action.yml::action.yml version default ($pinned) does not match the latest release ($latest). Bump the 'version' input default — see CLAUDE.md's release checklist." + exit 1 + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5886095..3e2d5f3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -15,6 +15,17 @@ jobs: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: fetch-depth: 0 + # A v* tag cut from any branch would otherwise fire a public release. + # GitHub can't branch-filter a tag trigger, so guard at runtime: the + # tagged commit must be contained in main's history. + - name: Ensure the tag is on main + run: | + set -euo pipefail + git fetch --no-tags origin main + if ! git merge-base --is-ancestor "$GITHUB_SHA" FETCH_HEAD; then + echo "::error::release tags must be cut from a commit on main; $GITHUB_SHA is not on main" + exit 1 + fi - uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 with: go-version: "1.26.x" diff --git a/docs/06-schema-reference.md b/docs/06-schema-reference.md index 6beb922..38c15b8 100644 --- a/docs/06-schema-reference.md +++ b/docs/06-schema-reference.md @@ -202,6 +202,34 @@ A scenario is a diagnostic, not a backlog item: it tests whether the entities and actions actually hang together. If writing one reveals an invariant that can't be satisfied — or that doesn't exist yet — fix the model, not the scenario. +## What this format deliberately leaves out + +modelith is a light, agent-authored subset of domain-driven design, not a full +DDD notation. Several classic DDD concepts are left out on purpose. Knowing what +is *not* here is as useful as knowing what is. + +- **Aggregates and aggregate roots.** There is no first-class aggregate + boundary. A consistency boundary is expressed by the invariants that must hold + and the entity that owns them, not by a declared aggregate. Deliberate: the + boundary lives in the rules, which the format already captures. +- **Value objects.** There is no value-object type. Model a value-shaped concept + as an owned entity or as attributes on its owner. The parking-garage example + models `Ticket` this way and names the tension. First-class structured value + types are being explored + ([issue #11](https://github.com/stacklok/modelith/issues/11)). +- **Domain events.** There is no event construct. A state change is expressed as + an `action` plus the invariants it `preserves`, and enums **name** states + while invariants govern the legal transitions between them. Deliberate, and + consistent with why enums carry no transition edges. +- **Bounded contexts and context maps.** One model is one context. There is no + construct for relating multiple contexts or mapping shared concepts across + them. Compose several `*.modelith.yaml` models at the repository level instead + of expressing context boundaries inside one file. + +These omissions keep the format small enough for an agent to author reliably and +for a human to read in one sitting. Any of them can become a roadmap item if a +real model needs it; none is here yet beyond what is linked above. + ## What the linter adds on top of the schema The JSON Schema covers structure. [`modelith lint`](./07-cli.md) adds: @@ -226,3 +254,8 @@ The JSON Schema covers structure. [`modelith lint`](./07-cli.md) adds: - **Completeness** checks (advisory warnings): entities with no invariants; entities no scenario exercises; a glossary term nothing references; an enum no attribute uses. + + These are advisory on purpose. An entity that genuinely has no rule to state + is fine — leave its invariants empty rather than inventing a filler rule that + only restates its cardinality or its type. The warning is a prompt to check, + not a demand to fill. diff --git a/internal/lint/lint.go b/internal/lint/lint.go index ce49f83..260ff6b 100644 --- a/internal/lint/lint.go +++ b/internal/lint/lint.go @@ -498,7 +498,7 @@ func runCompleteness(m *model.Model, res *Result) { Severity: SeverityWarning, Category: CategoryCompleteness, Path: fmt.Sprintf("/entities/%s", name), - Message: fmt.Sprintf("entity %q has no invariants — the rules that govern it are where the real behavior lives", name), + Message: fmt.Sprintf("entity %q has no invariants — fine if no rule must always hold for it, otherwise the rules that govern it are worth capturing", name), }) } }