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
57 changes: 57 additions & 0 deletions .cz.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Commitizen config for the commit check (cz check) and the CI bump (cz bump).
# Pinned in CI and in every local check: uvx --from commitizen==4.19.2 cz ...
# The vocabulary is __reports__/release_lifecycle/commit_vocabulary_v0.md (D1-D7); how to write a message is in CONTRIBUTING.md.
# Nobody runs `cz bump` by hand: the `release` job of .github/workflows/guard-binaries.yml does, after a merge to main.
[tool.commitizen]
name = "cz_customize"

# the version is the latest `v*` tag; the only file the bump rewrites is the plugin manifest below
version_provider = "scm"
version_scheme = "semver"
tag_format = "v$version"
annotated_tag = true
major_version_zero = true
update_changelog_on_bump = false
version_files = ['plugins/murmuration/.claude-plugin/plugin.json:"version"']
bump_message = "chore(release): murmuration $new_version [skip ci]"

# the schema_pattern below is the only authority: no `Merge`, `Revert`, `fixup!` or `squash!` shortcut (git's own `Merge ...` subjects are in the pattern)
allowed_prefixes = []

[tool.commitizen.customize]
example = "fix(hooks): stop a verdict hook from reading a closed pipe"
schema = "<type>(<scope>): <subject>, or <trace type>(<id>): <subject>; see CONTRIBUTING.md"
# feat fix perf docs test build ci chore refactor revert: scope required (D6), `!` allowed
# claim verdict: (wNN-cN) wave: (wNN) synthesis user_words: optional (wNN) reports rdm merge: scope required git's own `Merge ...`
# no length cap (D7)
schema_pattern = '(?s)(?:(?:feat|fix|perf|docs|test|build|ci|chore|refactor|revert)\([a-z0-9][a-z0-9_-]*\)!?|(?:claim|verdict)\(w\d{2}-c\d+\)|wave\(w\d{2}\)|(?:synthesis|user_words)(?:\(w\d{2}\))?|(?:reports|rdm|merge)\([a-z0-9][a-z0-9_-]*\)):( [^\n\r]+)((\n\n.*)|(\s*))?$|Merge .*'

# only these release: feat (minor), fix and perf (patch), a breaking `!` on those, and a BREAKING CHANGE footer (minor while major_version_zero).
# Every other type fails the pattern, so refactor, docs, test, build, ci, chore, revert, merge, rdm, reports and the research types never bump.
# cz searches each line of a message, so a footer line `BREAKING CHANGE: ...` counts.
bump_pattern = '^((BREAKING[\-\ ]CHANGE|feat|fix|perf)(\(.+\))?!?):'

# release notes (cz changelog --dry-run): the three release types and breaking changes only
changelog_pattern = '^((BREAKING[\-\ ]CHANGE|feat|fix|perf)(\(.+\))?!?):'
commit_parser = '^((?P<change_type>feat|fix|perf|BREAKING CHANGE)(?:\((?P<scope>[^()\r\n]*)\)|\()?(?P<breaking>!)?):\s(?P<message>.*)?'
change_type_order = ["BREAKING CHANGE", "Feat", "Fix", "Perf"]

[tool.commitizen.customize.change_type_map]
feat = "Feat"
fix = "Fix"
perf = "Perf"

[tool.commitizen.customize.bump_map]
'^BREAKING[\-\ ]CHANGE' = "MAJOR"
'^(feat|fix|perf)(\(.+\))?!$' = "MAJOR"
'^feat' = "MINOR"
'^fix' = "PATCH"
'^perf' = "PATCH"

# major_version_zero: a MAJOR is cut as a MINOR
[tool.commitizen.customize.bump_map_major_version_zero]
'^BREAKING[\-\ ]CHANGE' = "MINOR"
'^(feat|fix|perf)(\(.+\))?!$' = "MINOR"
'^feat' = "MINOR"
'^fix' = "PATCH"
'^perf' = "PATCH"
43 changes: 43 additions & 0 deletions .github/scripts/check_trace_paths.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Fail when a commit typed as research trace touches the shipped plugin (D4 of __reports__/release_lifecycle/commit_vocabulary_v0.md).

python3 -I .github/scripts/check_trace_paths.py <rev-range>

`cz check` reads messages and sees no paths, so this is the other half of the vocabulary: a commit whose type is one of TRACE and that changes a file under plugins/murmuration/
would ship code under a type that never releases. Merge commits are skipped (their files belong to the commits they merge). Standard library only; lives outside plugins/murmuration so that it is not shipped.
"""
import re
import subprocess
import sys

TRACE = {"claim", "verdict", "wave", "synthesis", "user_words", "reports", "rdm", "merge"}
SHIPPED = "plugins/murmuration/"


def git(*args):
return subprocess.run(["git", *args], check=True, capture_output=True, text=True, encoding="utf-8").stdout


def main(argv):
if len(argv) != 2:
print(__doc__)
return 2
bad = 0
for sha in git("rev-list", "--no-merges", "--reverse", argv[1]).split():
subject = git("log", "-1", "--format=%s", sha).rstrip("\n")
m = re.match(r"([a-z_]+)(?:\(|:|!)", subject)
if not m or m.group(1) not in TRACE:
continue
files = [f for f in git("diff-tree", "--no-commit-id", "--name-only", "-r", "--root", "-z", sha).split("\0") if f]
hits = [f for f in files if f.startswith(SHIPPED)]
if hits:
bad += 1
more = " (and %d more)" % (len(hits) - 5) if len(hits) > 5 else ""
print("::error title=trace type on the shipped plugin::%s %s: type `%s` releases nothing, yet it changes %s%s. Use feat, fix, perf, refactor, test, build, ci, chore or docs for plugin changes, or split the commit." % (sha[:7], subject, m.group(1), ", ".join(hits[:5]), more))
if bad:
return 1
print("check_trace_paths: no trace-typed commit in %s touches %s" % (argv[1], SHIPPED))
return 0


if __name__ == "__main__":
sys.exit(main(sys.argv))
43 changes: 43 additions & 0 deletions .github/scripts/manifest_matches.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
"""Exit 0 when plugins/murmuration/bin/manifest.json was written from the sources in this checkout, else 1.

python3 -I .github/scripts/manifest_matches.py

Compares scripts_sha256 and rust_sources_sha256, the two fields install_binary.py trusts, against what build_manifest.py computes from the tree now, and requires a binary for every target in ALL_TARGETS.
Used by the `changes` job (a stale manifest makes a push gated) and by the `release` job (never release a tree whose binaries are not its own).
Standard library only; lives outside plugins/murmuration so that it is not shipped.
"""
import json
import os
import sys

ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", ".."))
PLUGIN = os.path.join(ROOT, "plugins", "murmuration")
KEYS = ("scripts_sha256", "rust_sources_sha256")


def main():
sys.dont_write_bytecode = True # no __pycache__ in the plugin tree: the release job bumps a clean checkout
sys.path.insert(0, os.path.join(PLUGIN, "scripts"))
import build_manifest

try:
with open(os.path.join(PLUGIN, "bin", "manifest.json"), encoding="utf-8") as fh:
have = json.load(fh)
except (OSError, ValueError) as err:
print("manifest_matches: cannot read bin/manifest.json: %s" % err)
return 1
now = build_manifest.build(PLUGIN)
differ = [k for k in KEYS if have.get(k) != now[k]]
missing = [t for t in build_manifest.ALL_TARGETS if t not in (have.get("binaries") or {})]
if missing:
print("manifest_matches: bin/manifest.json has no binary for %s" % ", ".join(missing))
return 1
if differ:
print("manifest_matches: bin/manifest.json differs from the sources in %s: %s" % (", ".join(differ), PLUGIN))
return 1
print("manifest_matches: bin/manifest.json matches the sources (rust %s)" % now["rust_sources_sha256"][:12])
return 0


if __name__ == "__main__":
sys.exit(main())
Loading
Loading