Skip to content

How the three repositories release

This page is for contributors and maintainers of knowledge-trust-ladder, KTL Registrar and KTL Curator, which release the same way. Each repository’s semantic-release.yml runs the same pinned semantic-release tool. Each workflow’s own header comment carries that repository’s detail and the reasons behind it.

The version is computed; you write the commit message

Section titled “The version is computed; you write the commit message”

Nobody picks a version number. Commits typed with Conventional Commits decide it:

Commit type Release
feat: minor
fix:, security: patch
BREAKING CHANGE: in a footer, or ! after the type major
docs:, chore:, refactor:, style:, test:, ci: none: the change merges, releases nothing, and its changelog entries wait for the next release that does

Type the commit for what the change is. A behaviour change is a feat: even when most of the diff is prose. If a pull request should release and none of its commits has a type that releases, squash-merge it and give the squash commit the right type.

The reverse trap is a squash merge that takes its subject from the pull request title, which GitHub defaults to the branch name in words, with no type. So a typed fix: commit reaches main as “Fix the thing (#46)” and releases nothing. The plan job refuses a pull request whose commits would release but whose title would not. Type the title like a commit. The workflow runs on edited pull requests as well as on pushes, so retitling re-runs the check on its own.

The release note is CHANGELOG.md’s ## [Unreleased] section, written as you go: a line or two per change, with the detail left to the code’s comments. The pipeline releases only what has already been written up.

changelog-release.mjs check refuses an empty section. The plan job runs it on each pull request, so a forgotten entry fails the pull request rather than the release. The check runs only when the pull request’s own commits would release, so a chore:, docs: or ci: pull request, or a Dependabot bump, is not asked for notes.

  1. semantic-release computes the next version from the commits since the last release, and stops if none warrant one.
  2. changelog-release.mjs check refuses to proceed if ## [Unreleased] is empty.
  3. That section is retitled ## [X.Y.Z] - YYYY-MM-DD, a fresh empty ## [Unreleased] is inserted above it, and the result is committed to main. In knowledge-trust-ladder, tagging is a separate later step. A second qualifying merge before publish.yml runs then computes the same version again, and its entries merge into the still-untagged section, which takes the new version and date.
  4. From this step on, the repositories differ:
Repository Tag Then
knowledge-trust-ladder None from this workflow. gh skill publish is the one tag creator, so a maintainer runs publish.yml by hand with the version to release, typed with its v (v0.16.0); the workflow cross-checks it against the promoted changelog first. Tags carry the v, changelog headings never do. The six skills publish to the registry under that one tag. publish.yml then dispatches knowledge-release.yaml, which attaches the bundle to the release as knowledge-vX.Y.Z-knowledge-trust-ladder.zip when it changed since the last release that carries one.
KTL Registrar, KTL Curator Bare X.Y.Z, Obsidian’s convention, after package.json, manifest.json and versions.json are bumped as npm version would. release.yml builds, attests provenance and opens a draft release carrying main.js, manifest.json and styles.css; a maintainer reviews and publishes it. A hand-pushed tag takes the same path. Publishing the draft is a person’s event, not the token’s, so it starts knowledge-release.yaml once KNOWLEDGE_RELEASE_ENABLED is true there; no dispatch is needed.

Everything from step 3 on runs behind the release GitHub Environment. Configure required reviewers on it once, in each repository’s Settings → Environments, or a qualifying merge is released unattended. Creating the environment does not do that for you.

After a releasing merge to knowledge-trust-ladder, what follows

Section titled “After a releasing merge to knowledge-trust-ladder, what follows”

The scaffolding template’s TRUST_LADDER_SKILLS_REF is one release behind. When semantic-release.yml promotes the changelog, the new heading exists but its tag does not yet, so the release commit moves the pin to the newest tag a host can clone. It moves nothing under .github/workflows/, which GITHUB_TOKEN may not push.

The pin is two values, TRUST_LADDER_SKILLS_REF and TRUST_LADDER_SKILLS_SHA, the commit the tag names, and the install step refuses a tag that names any other commit. Check 15 of validate-repository.sh requires the pin to name one of the two newest releases, a tag that carries skills/ktl-librarian, and the commit that tag names. Never bump the pin by hand: that commit triggers a needless release, and the next promotion moves the pin anyway.

The sibling repositories carry byte-identical copies of the templates and their own pin. Only the first line of .lokf/justfile, which names the host, and its schema := line are a sibling’s own. The copies and the pin move together from one tag, because the skill a tag installs is written against the wrapper, gate and preflight that tag includes. Once a sibling turns on its scheduled librarian, the pin decides which agent instructions run unattended.

Each sibling keeps itself current. Its knowledge-sync.yaml reads the pin and this repository’s newest release each week. When the pin is behind, it runs the sibling’s own .lokf/scripts/knowledge-sync.sh, which reads what to copy from the release’s templates/knowledge-sync.manifest, and opens one pull request with the diff: the copies, the pin, and the changelog entry under ## [Unreleased] and ### Security. The sync keeps one pull request open: a later week’s run refreshes it to the newest release until it is merged.

publish.yml asks the siblings named in the KNOWLEDGE_SYNC_HOSTS repository variable to sync as soon as the tag exists, with the KNOWLEDGE_SYNC_DISPATCH_TOKEN secret. The variable lists owner/repo names, and false asks nobody. The sidecar’s automation.md says what each sibling sets up.

A new template goes in the manifest and in ktl-sidecar’s Step 5 table, and nowhere else: the preflight and contract check 11 read the manifest.

To open the pull requests yourself on the day of a release, run scripts/sync-sidecar.sh --pr <tag> <sibling>... from this repository once the tag is on origin. It runs each sibling’s own copy of knowledge-sync.sh, or the tag’s in a sibling that has none, on a branch of its own, sync-<tag>, from origin’s default branch. A sibling with uncommitted changes is skipped. Without --pr, --push or --commit it leaves the sync uncommitted on the branch, with the commit command printed, and the diff is the review. Once the pull requests are merged, scripts/sync-sidecar.sh --finish <tag> <sibling>... puts each sibling back on its default branch, brings it up to date and removes the branch. A release that touched neither the templates nor the librarian skill needs no sync, and the workflow and the script both say so.

Three settings explain what a pull request waits on, or refuses, in every one of these repositories:

  • Changes reach main by pull request, but no ruleset enforces it. A ruleset that requires pull requests, or passing status checks, would also reject the release job’s own push of the promoted changelog, and github-actions[bot] cannot be put on a ruleset’s bypass list. So open a pull request anyway, and treat a red check as yours to fix: CI runs on every pull request and is read before merge, but it is not what blocks one.
  • “Require signed commits” as a branch rule is off, and must stay off. A commit made inside a runner is unsigned: GitHub only auto-signs commits made through the web UI or API. So the rule would reject the release job’s promotion commit and break every release.
  • Sign your own commits anyway. It is required only for a pull request that records a human: confirmation in a knowledge bundle, and worth doing on all of them: Signing your commits.