Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/action-version.yml
Original file line number Diff line number Diff line change
@@ -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
11 changes: 11 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
33 changes: 33 additions & 0 deletions docs/06-schema-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.
2 changes: 1 addition & 1 deletion internal/lint/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
})
}
}
Expand Down
Loading