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
2 changes: 1 addition & 1 deletion .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"name": "trellis",
"source": "./trellis",
"description": "Install and maintain rogueoak's shared AI-agent rules and templates in your repo.",
"version": "0.3.0",
"version": "0.4.0",
"keywords": ["conventions", "rules", "agents", "templates", "rogueoak"]
}
]
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"name": "trellis",
"source": "./trellis",
"description": "Install and maintain rogueoak's shared AI-agent rules and templates in your repo.",
"version": "0.3.0",
"version": "0.4.0",
"keywords": ["conventions", "rules", "agents", "templates", "rogueoak"]
}
]
Expand Down
2 changes: 1 addition & 1 deletion .cursor-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"name": "trellis",
"source": "./trellis",
"description": "Install and maintain rogueoak's shared AI-agent rules and templates in your repo.",
"version": "0.3.0",
"version": "0.4.0",
"keywords": ["conventions", "rules", "agents", "templates", "rogueoak"]
}
]
Expand Down
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: CI

# Runs the dependency-free test suites and the version-sync + compliance guards on every push to
# main and every PR. The Release workflow gates on this workflow succeeding on main (workflow_run),
# so its name must stay "CI".
on:
push:
branches: [main]
pull_request:

# Cancel superseded runs on a PR branch, but never cancel a push run on main - the Release
# workflow waits on it, and a cancelled main run would drop that release.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

permissions:
contents: read

jobs:
test:
name: tests + guards
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
- name: Compliance checker tests
run: sh trellis/scripts/check-compliance.test.sh
- name: Version bump tests
run: sh trellis/scripts/bump-version.test.sh
- name: Version in sync across manifests
run: sh scripts/bump-version.sh --check
- name: Dogfood - installed template matches its source
run: |
base=trellis/templates/plugin-release/owned
for f in $(cd "$base" && find . -type f | sed 's#^\./##'); do
diff "$base/$f" "$f" || { echo "drift: installed $f differs from $base/$f"; exit 1; }
done
- name: Compliance pass (report)
run: sh trellis/scripts/check-compliance.sh
62 changes: 62 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Release

# Shipped by Trellis's "plugin-release" template and OWNED by Trellis - re-run trellis-update
# to refresh it. Do not hand-edit.
#
# Tags a bare-semver release and publishes a GitHub Release whenever the root VERSION moves.
# It runs AFTER your CI workflow succeeds on a push to main (workflow_run), so it never publishes
# unvalidated code and needs no knowledge of your CI's job names.
#
# This file is OWNED by the plugin-release template and refreshed by trellis-update - do NOT edit
# it. It waits on a CI workflow named "CI": to adopt the template, name your CI workflow `CI`
# rather than changing the line below (any edit here is overwritten on the next update).
on:
workflow_run:
workflows: ["CI"]
types: [completed]

# Default to read-only; the release job elevates itself for the one write it needs.
permissions:
contents: read

jobs:
release:
name: Tag + GitHub Release on version bump
runs-on: ubuntu-latest
# Only after a push to main's CI concluded successfully. Requiring event == 'push' is the
# fork guard: a fork PR's CI runs as 'pull_request', so it can never reach this write token -
# even a fork whose branch is named 'main' (which would otherwise satisfy head_branch). The
# head_branch check is kept as defense in depth.
if: >-
github.event.workflow_run.event == 'push' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.head_branch == 'main'
permissions:
contents: write
env:
GH_TOKEN: ${{ github.token }}
steps:
# Pinned to a full SHA (v4.2.2) because this job carries a write token.
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
ref: ${{ github.event.workflow_run.head_sha }}
- name: Create the release when VERSION is new
# Idempotent: a run whose VERSION already has a release does nothing, so this only
# publishes on the commit that bumps VERSION. Prefer a hand-written notes file (its
# first non-heading line becomes the README headline via whats-new.yml); fall back to
# auto-generated notes from the conventional-commit titles since the previous tag.
run: |
v="$(cat VERSION)"
if gh release view "$v" >/dev/null 2>&1; then
echo "Release $v already exists; nothing to do."
exit 0
fi
sha="$(git rev-parse HEAD)"
notes="docs/releases/$v.md"
if [ -f "$notes" ]; then
echo "Publishing release $v from $notes"
gh release create "$v" --target "$sha" --notes-file "$notes"
else
echo "Publishing release $v with auto-generated notes (no $notes)"
gh release create "$v" --target "$sha" --generate-notes
fi
8 changes: 8 additions & 0 deletions .version-manifests
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Manifests that must match the root VERSION. One path per line; '#' and blanks ignored.
.claude-plugin/marketplace.json
.cursor-plugin/marketplace.json
.agents/plugins/marketplace.json
trellis/.claude-plugin/plugin.json
trellis/.codex-plugin/plugin.json
trellis/.cursor-plugin/plugin.json
trellis/gemini-extension.json
1 change: 1 addition & 0 deletions VERSION
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.4.0
17 changes: 17 additions & 0 deletions docs/overview/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,22 @@
another tool's vendored docs. Reuse by a consumer's own pre-commit/CI is deferred and will need a
copy-into-repo step like `install-hooks.sh` has - today the script only ships in the plugin, so
only this repo (where `trellis/` is committed) can invoke it directly.
- **Optional templates.** `trellis/templates/<name>/` bundles are opt-in, unlike `rules/`. Each
splits into `owned/` (the mechanism Trellis maintains) and `seed/` (the consumer's inputs),
both mirroring their target paths. Install (`--template <name>`) merges `owned/` (clobber) and
`seed/` (`cp -Rn`) into the repo, records the name in `docs/rules/.trellis-templates`, and lists
the owned files in `docs/rules/.trellis-owned-<name>`. Update needs no flag: it walks that
registry and re-syncs each template's owned files exactly as it re-syncs rules (refresh + prune),
never touching `seed/` targets. The boundary is one-way - no consumer-editable content lives in
an owned file - so a clobbering refresh is always safe. Reuses the `.trellis-owned`
ownership-manifest idea rather than inventing a parallel one.
- **plugin-release pipeline.** The first template. Repo-specific variation (the manifest list)
lives in a consumer-owned `.version-manifests`, so the owned `bump-version.sh` ships identical
everywhere and updates centrally. `release.yml` is standalone and triggered by `workflow_run` on
the consumer's `CI` workflow (not `needs:` on job names), so it is portable; it is the only job
with `contents: write` (job-scoped), gated to CI-success on `main`, SHA-pinned checkout,
idempotent via `gh release view`. Trellis self-adopts: a `CI` workflow runs the test suites +
`bump-version.sh --check` + a dogfood diff (installed copies must equal the template source), and
`release.yml` gates on it.
- **Built under Spectra.** `docs/{specs,plans,feedback,overview}` track this repo's own
development; the two systems compose - Spectra is the process, Trellis is the conventions.
13 changes: 12 additions & 1 deletion docs/overview/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,15 @@
`/trellis-update` run it non-blocking, passing `--fix` through when invoked as
`/trellis-install --fix`. A developer-owned `docs/rules/.compliance-ignore` (gitignore-lite)
skips content another tool vendors, e.g. `docs/spectra/`.
- **Templates.** `trellis/templates/` is reserved for shared templates; empty for now.
- **Optional templates.** Beyond the universal rules, Trellis ships **opt-in** templates under
`trellis/templates/<name>/` that only some repos want. A repo adds one with
`/trellis-install --template <name>`; plain `/trellis-update` then keeps it current (registry at
`docs/rules/.trellis-templates`), no flag needed. Each template splits into `owned/` (Trellis
refreshes these on update) and `seed/` (copied once, then the consumer's), so an update never
clobbers consumer content. See `trellis/templates/README.md`.
- **plugin-release template.** For repos that are themselves published marketplace plugins: a root
`VERSION` as single source of truth, `scripts/bump-version.sh` to rewrite it and every manifest
in `.version-manifests` in lockstep (`--check` fails CI on drift, semver-only, format-preserving),
a standalone `.github/workflows/release.yml` that tags + publishes a GitHub Release once CI
succeeds on `main` (via `workflow_run`, composing with "What's new"), and the
`docs/releases/<x.y.z>.md` notes convention. Trellis dogfoods it for its own seven manifests.
12 changes: 12 additions & 0 deletions docs/overview/learnings.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,15 @@
in-place fix into arbitrary file overwrite. Skip symlinks (`[ -L ]`), create the temp with
`mktemp`, and guard hostile filenames with `--`/`./` so a name like `-rf` is not read as an
option. (spec 0004)
- To share tooling that has per-repo variation, ship the mechanism verbatim and push the variation
into a consumer-owned config file (plugin-release's `bump-version.sh` is identical everywhere;
each repo's manifest list lives in its own `.version-manifests`). The mechanism then updates
centrally and never needs hand-editing per repo - the same reason rules are copied, not forked.
(spec 0005)
- An optional, updatable component needs a registry of what is installed plus a strict owned-vs-seed
ownership split: update reads the registry and refreshes only owned files (so a clobber is always
safe), while consumer inputs live in seed files it never touches. Without the registry, update
cannot maintain a component the user opted into without being told again. (spec 0005)
- A portable CI-triggered release must not depend on another workflow's job names: gate it with
`workflow_run` on the upstream workflow's *name* (success + `main`), not `needs: [job]`, so the
same file drops into any repo. (spec 0005)
81 changes: 81 additions & 0 deletions docs/plans/0005-plugin-release-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# 0005 - Optional templates, and a plugin-release template (plan)

Source: `docs/specs/0005-plugin-release-template.md`.

## Layout to create

```
trellis/templates/README.md # the optional-template convention
trellis/templates/plugin-release/
README.md # adoption guide + the one CI-name knob
owned/ # refreshed on update (Trellis-owned)
scripts/bump-version.sh
.github/workflows/release.yml
docs/releases/README.md
seed/ # copied once, never clobbered (consumer-owned)
VERSION
.version-manifests
```

Consumer install target (functional paths, mirrors the subtrees):
`scripts/bump-version.sh`, `.github/workflows/release.yml`, `docs/releases/README.md` (owned);
`VERSION`, `.version-manifests` (seed). Tracking: `docs/rules/.trellis-templates` (names) +
`docs/rules/.trellis-owned-<name>` (owned files).

## Steps
1. **Plan** (this) + reflect later.
2. **`bump-version.sh`** (owned, POSIX sh + python3): identical to Spectra #28's hardened script
except the manifest list comes from `.version-manifests` (one path per line, `#` comments and
blanks skipped) instead of a hardcoded list, and the root override env is neutral
(`BUMP_VERSION_ROOT`). Modes: no-arg prints VERSION; `--check`; `X.Y.Z`. Carry over: surgical
one-token substitution, validate-in-memory-then-write-VERSION-last, exactly-one-token guard,
`if check; then` (set -e safe). Harden the semver gate to reject a multiline arg (a newline in
the arg fails the test). Missing `.version-manifests` is a clear error.
3. **`release.yml`** (owned): `on: workflow_run: { workflows: ["CI"], types: [completed] }`.
Job `release`: `if` success + `head_branch == 'main'`; `runs-on: ubuntu-latest`;
`permissions: { contents: write }`; checkout pinned to the same SHA whats-new.yml uses, with
`ref: ${{ github.event.workflow_run.head_sha }}`; step reads `v=$(cat VERSION)`, idempotent
`gh release view` guard, `gh release create "$v" --target "$(git rev-parse HEAD)"` with
`--notes-file docs/releases/$v.md` when present else `--generate-notes`; `GH_TOKEN` ambient.
4. **`docs/releases/README.md`** (owned): per-version notes convention (first non-heading line is
the headline `whats-new.yml` extracts); note the generate-notes fallback caveat.
5. **`seed/VERSION`** = `0.0.0` sample; **`seed/.version-manifests`** = a commented sample listing
the common manifest paths, for the consumer to edit.
6. **`templates/README.md`** + **`plugin-release/README.md`**: the convention (owned vs seed,
`--template` add, auto-refresh on update) and adoption steps (set VERSION, fill
`.version-manifests`, ensure a CI workflow named to match `release.yml`'s `workflows:` list).
7. **trellis-install SKILL.md**: a new step - if invoked with `--template <name>`, verify
`$SRC/templates/<name>` exists, copy `owned/.` and `seed/.` to repo root (owned clobbers; seed
via `cp -Rn`), append `<name>` to `docs/rules/.trellis-templates` (dedup), and write
`docs/rules/.trellis-owned-<name>` (the owned file list, derived from `owned/`). Confirm.
8. **trellis-update SKILL.md**: a new step - for each name in `docs/rules/.trellis-templates`,
re-copy `$SRC/templates/<name>/owned/.` (clobber), rewrite `.trellis-owned-<name>`, prune any
listed owned file no longer shipped; never touch `seed/` targets. No `--template` needed.
9. **Trellis self-adoption**: root `VERSION` = `0.4.0`; `.version-manifests` listing the 4
manifests; copy the owned script/workflow/convention to functional paths; add minimal
`.github/workflows/ci.yml` (push + PR: run `trellis/scripts/check-compliance.test.sh`,
`trellis/scripts/bump-version.test.sh`, and `check-compliance.sh`); run
`scripts/bump-version.sh 0.4.0` to set all 4 manifests; confirm `--check` is clean.
10. **`trellis/scripts/bump-version.test.sh`** in the repo idiom (sandbox repos, `check`/`checkeq`):
`--check` on a synced sandbox, semver accept/reject (incl. multiline + empty), sandbox write
converges, **negative drift** (edit one manifest -> `--check` non-zero), **two-token guard**
(planted second token -> non-zero), missing-manifest error. Plus a dogfood-integrity check
that the repo's `scripts/bump-version.sh` matches the template's owned copy.
11. **Reflect**: `docs/overview/features.md` (optional templates + plugin-release), `architecture.md`
(the owned/seed model, registry, release pipeline), `learnings.md` if a lesson arises.

## Verify
- `sh trellis/scripts/bump-version.test.sh` passes; `sh trellis/scripts/check-compliance.test.sh`
still passes.
- `scripts/bump-version.sh --check` exits 0 on the repo; all 4 manifests + VERSION read `0.4.0`.
- Every new shell/yaml file is ASCII-only (guidelines.md); `python3 -m json.tool` parses manifests.
- `release.yml` and `ci.yml` are valid YAML; `release.yml` gates on the `ci.yml` workflow name.
- Dry-run the install/update template logic in a scratch dir to confirm copy + registry + prune.

## Review
Personas (engineer/tester/architect/security):
- **engineer** - the config-driven script, the skill copy/prune logic, release.yml shell.
- **tester** - bump-version.test.sh coverage + the install/update dry-run.
- **architect** - the optional-template model, owned/seed boundary, registry, release pipeline.
- **security** - release.yml privilege (job-scoped write, success+main gate, SHA-pinned checkout,
ambient token, no untrusted interpolation); the script's arg/env handling.
22 changes: 22 additions & 0 deletions docs/releases/0.4.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Optional templates, and a plugin-release template for single-source versioning and automated releases.

## What changed
Trellis can now ship **optional templates** - opt-in bundles a repo installs with
`/trellis-install --template <name>`, kept current thereafter by plain `/trellis-update`. Unlike
rules (which every Trellis repo follows), a template applies only to repos that want it.

The first template is **plugin-release**, for repos that are themselves published marketplace
plugins:

- A root `VERSION` file is the single source of truth; `scripts/bump-version.sh` rewrites it and
every manifest listed in `.version-manifests` together, and `--check` fails CI on any drift.
- A standalone `.github/workflows/release.yml` tags the version and publishes a GitHub Release
once your CI workflow succeeds on `main` - composing with the existing "What's new" automation.

Each template splits into `owned/` (Trellis refreshes these on update) and `seed/` (copied once,
then yours), so an update can never clobber your content. See `trellis/templates/README.md`.

## Dogfood
Trellis adopts plugin-release for its own seven manifests (previously hand-synced at `0.3.0`),
adds a `CI` workflow that runs its test suites and the version-sync guard, and cuts this `0.4.0`
release through the new pattern.
25 changes: 25 additions & 0 deletions docs/releases/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Release notes

One file per published version: `docs/releases/<x.y.z>.md` (bare semver, no `v`). The
`Release` workflow uses it as the GitHub Release body when you bump `VERSION` to that number; if
the file is absent it falls back to auto-generated notes from your Conventional Commit titles.

This file is shipped and owned by Trellis's plugin-release template (refreshed by
`trellis-update`). The per-version `<x.y.z>.md` notes are yours.

## Convention

- **The first non-heading line is the headline.** The "What's new" automation extracts it
verbatim into the README, so make it a single plain sentence - no leading `#`, no list marker.
Everything after it is the Release body.
- Keep it ASCII and in the repo's voice.
- The auto-generated fallback (no `<x.y.z>.md`) yields a raw commit bullet as the headline, so
write the file for any release whose headline matters.

## Releasing

1. `scripts/bump-version.sh X.Y.Z` - rewrites `VERSION` + every manifest in `.version-manifests`.
2. Write `docs/releases/X.Y.Z.md` (headline first, then details).
3. Open a PR and squash-merge to `main`.
4. Your CI workflow runs; on success the `Release` workflow tags `X.Y.Z` and publishes the
Release from your notes. The "What's new" block then refreshes from the headline.
3 changes: 3 additions & 0 deletions docs/rules/.trellis-owned-plugin-release
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
docs/releases/README.md
scripts/bump-version.sh
.github/workflows/release.yml
1 change: 1 addition & 0 deletions docs/rules/.trellis-templates
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
plugin-release
Loading
Loading