ci-tools
Internal CI tooling for this repository. Built with Typer and Pydantic.
Requirements
- Python 3.12+
- uv
Usage
Run from the repository root via uv run:
uv run --project scripts/ci-tools ci-tools [OPTIONS] COMMAND [ARGS]...
Or install into a local venv and invoke directly:
cd scripts/ci-tools
uv sync
uv run ci-tools --help
Pass --verbose / -v to enable debug logging to stderr.
Commands
github setup-labels
Creates or updates the two autorelease labels required by the release workflow. This is a one-time setup operation run from a local machine.
Requires the GitHub CLI (gh) to be installed and authenticated.
# auto-detect repo from the current directory
uv run --project scripts/ci-tools ci-tools github setup-labels
# explicit repo
uv run --project scripts/ci-tools ci-tools github setup-labels --repo elastic/agent-skills
Labels managed:
| Name | Colour | Description |
|---|---|---|
autorelease: pending |
#FBCA04 |
Release PR awaiting merge |
autorelease: tagged |
#0E8A16 |
Release PR has been tagged and published |
| Option | Default | Description |
|---|---|---|
--repo / -r |
auto-detected via gh repo view |
Target repo as OWNER/NAME |
release extract
Reads .release-manifest.json and CHANGELOG.md and emits a JSON object to stdout with the version, git tag, and changelog entry for that version. Used by the release workflow to populate $GITHUB_OUTPUT and the release notes file.
uv run --project scripts/ci-tools ci-tools release extract \
--manifest .github/.release-manifest.json \
--changelog CHANGELOG.md
Output:
{
"version": "1.2.3",
"tag": "v1.2.3",
"release_notes": "..."
}
| Option | Default | Description |
|---|---|---|
--manifest |
.github/.release-manifest.json |
Path to the release manifest JSON |
--changelog |
CHANGELOG.md |
Path to the changelog |
--tag-prefix |
v |
Prefix prepended to the version to form the git tag (e.g. v, release/, ``) |
release validate
Validates all release artifacts for consistency and emits a Markdown summary to stdout. Exits 1 if any check fails. The output is suitable for piping directly into $GITHUB_STEP_SUMMARY.
Checks performed:
- Manifest version is valid semver
- CHANGELOG.md contains a non-empty entry for the version
- The target git tag does not already exist (when
--tags-fileis provided) plugin.jsonandmarketplace.jsonversions match the manifest (when provided)- First release tag matches
--initial-version(when tags file is empty)
git tag --list 'v*' > /tmp/tags.txt
uv run --project scripts/ci-tools ci-tools release validate \
--manifest .github/.release-manifest.json \
--changelog CHANGELOG.md \
--plugin-json .claude-plugin/plugin.json \
--marketplace-json .claude-plugin/marketplace.json \
--tags-file /tmp/tags.txt \
--initial-version 0.1.0
| Option | Default | Description |
|---|---|---|
--manifest |
.github/.release-manifest.json |
Path to the release manifest JSON |
--changelog |
CHANGELOG.md |
Path to the changelog |
--plugin-json |
— | Path to plugin.json (version consistency check) |
--marketplace-json |
— | Path to marketplace.json (version consistency check) |
--tags-file |
— | File containing one existing git tag per line |
--initial-version |
— | Expected version for the very first release (e.g. 1.0.0) |
--tag-prefix |
v |
Prefix prepended to the version to form the git tag |
release sync
Reads the version from .release-manifest.json and writes it into plugin.json and/or marketplace.json. Emits a JSON summary of which files were updated. Exits 0 even if nothing changed.
uv run --project scripts/ci-tools ci-tools release sync \
--manifest .github/.release-manifest.json \
--plugin-json .claude-plugin/plugin.json \
--marketplace-json .claude-plugin/marketplace.json
Output:
{
"version": "1.2.3",
"updated": [".claude-plugin/plugin.json"]
}
| Option | Default | Description |
|---|---|---|
--manifest |
.github/.release-manifest.json |
Path to the release manifest JSON |
--plugin-json |
— | Path to plugin.json to update |
--marketplace-json |
— | Path to marketplace.json to update |
Development
cd scripts/ci-tools
uv sync # install deps + dev deps
uv run pytest # run the test suite
Project layout
src/ci_tools/
├── cli.py # top-level Typer app; register new command groups here
├── settings.py # CISettings(BaseSettings) — env var config
└── commands/
├── github/
│ └── cli.py # Typer sub-app (setup-labels)
└── release/
├── models.py # Pydantic domain models
├── core.py # pure logic — no Typer imports, raises ValueError
└── cli.py # Typer sub-app (extract / validate / sync)
tests/
├── test_cli_top.py # top-level app tests
├── github/
│ └── test_cli.py
└── release/
├── test_models.py
├── test_core.py
└── test_cli.py
Adding a new command group
- Create
src/ci_tools/commands/<name>/withmodels.py,core.py, andcli.py - Define
<name>_app = typer.Typer(name="<name>", ...)incli.py - In
src/ci_tools/cli.py, add one line:app.add_typer(<name>_app, name="<name>") - Add tests under
tests/<name>/
Architecture
- Core is pure —
core.pyhas no Typer or Click imports and is fully unit-testable without CLI machinery. It raisesValueErroron bad input; the CLI layer catches these and callstyper.Exit(1). - JSON to stdout, logs to stderr — all commands write machine-readable output to stdout and diagnostic logs to stderr via loguru, keeping the two streams cleanly separated for CI pipelines.
TagsProviderprotocol — the DI seam for tag-checking logic.FileTagsProvideris the production implementation; a futureGitHubTagsProvider(or any test stub) can be substituted with no changes to core.CISettings—pydantic-settingsmodel that auto-reads standard CI environment variables (GITHUB_TOKEN,GITHUB_REPOSITORY, etc.) from the environment or a local.envfile.