ai-guidelines 1.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ai_guidelines-1.0.1/.github/guidelines/dependency-management.guidelines.md +14 -0
- ai_guidelines-1.0.1/.github/guidelines/documentation.guidelines.md +13 -0
- ai_guidelines-1.0.1/.github/guidelines/git-commit-message.guidelines.md +60 -0
- ai_guidelines-1.0.1/.github/guidelines/git-commit.guidelines.md +13 -0
- ai_guidelines-1.0.1/.github/guidelines/python-development.guidelines.md +17 -0
- ai_guidelines-1.0.1/.github/guidelines/python-unittest.guidelines.md +61 -0
- ai_guidelines-1.0.1/.github/skills/gh-release-notes/SKILL.md +74 -0
- ai_guidelines-1.0.1/.github/skills/gh-release-notes/scripts/generate_release_notes.py +373 -0
- ai_guidelines-1.0.1/.github/workflows/ci.yml +92 -0
- ai_guidelines-1.0.1/.github/workflows/publish.yml +86 -0
- ai_guidelines-1.0.1/.github/workflows/release-notes.yml +130 -0
- ai_guidelines-1.0.1/.github/workflows/release.yml +151 -0
- ai_guidelines-1.0.1/.github/workflows/test-pypi.yml +80 -0
- ai_guidelines-1.0.1/.gitignore +223 -0
- ai_guidelines-1.0.1/.readthedocs.yaml +28 -0
- ai_guidelines-1.0.1/AGENTS.md +37 -0
- ai_guidelines-1.0.1/CHANGELOG.md +12 -0
- ai_guidelines-1.0.1/CONSTITUTION.md +92 -0
- ai_guidelines-1.0.1/CONTRIBUTING.md +111 -0
- ai_guidelines-1.0.1/LICENSE +21 -0
- ai_guidelines-1.0.1/PKG-INFO +122 -0
- ai_guidelines-1.0.1/README.md +81 -0
- ai_guidelines-1.0.1/SECURITY.md +24 -0
- ai_guidelines-1.0.1/docs/source/_static/.gitkeep +0 -0
- ai_guidelines-1.0.1/docs/source/changelog.md +6 -0
- ai_guidelines-1.0.1/docs/source/conf.py +57 -0
- ai_guidelines-1.0.1/docs/source/explanation/apm-comparison.md +35 -0
- ai_guidelines-1.0.1/docs/source/explanation/caching.md +118 -0
- ai_guidelines-1.0.1/docs/source/explanation/index.md +11 -0
- ai_guidelines-1.0.1/docs/source/explanation/why-guidelines.md +70 -0
- ai_guidelines-1.0.1/docs/source/how-to/index.md +13 -0
- ai_guidelines-1.0.1/docs/source/how-to/manage-the-cache.md +64 -0
- ai_guidelines-1.0.1/docs/source/how-to/migrate-an-existing-project.md +90 -0
- ai_guidelines-1.0.1/docs/source/how-to/pin-a-version.md +97 -0
- ai_guidelines-1.0.1/docs/source/how-to/update-guidelines.md +71 -0
- ai_guidelines-1.0.1/docs/source/how-to/use-a-local-or-private-source.md +96 -0
- ai_guidelines-1.0.1/docs/source/index.md +52 -0
- ai_guidelines-1.0.1/docs/source/installation.md +25 -0
- ai_guidelines-1.0.1/docs/source/reference/api.md +46 -0
- ai_guidelines-1.0.1/docs/source/reference/cli.md +10 -0
- ai_guidelines-1.0.1/docs/source/reference/index.md +13 -0
- ai_guidelines-1.0.1/docs/source/reference/lockfile.md +96 -0
- ai_guidelines-1.0.1/docs/source/reference/manifest.md +96 -0
- ai_guidelines-1.0.1/docs/source/reference/source-grammar.md +168 -0
- ai_guidelines-1.0.1/docs/source/tutorials/getting-started.md +78 -0
- ai_guidelines-1.0.1/docs/source/tutorials/index.md +9 -0
- ai_guidelines-1.0.1/examples/commit-message.guideline.md +11 -0
- ai_guidelines-1.0.1/examples/project-loading-instruction.md +10 -0
- ai_guidelines-1.0.1/examples/project-specific.guideline.md +11 -0
- ai_guidelines-1.0.1/examples/python-testing.guideline.md +11 -0
- ai_guidelines-1.0.1/examples/ui-design-system.guideline.md +11 -0
- ai_guidelines-1.0.1/guidelines.lock.json +33 -0
- ai_guidelines-1.0.1/guidelines.yml +6 -0
- ai_guidelines-1.0.1/justfile +149 -0
- ai_guidelines-1.0.1/pyproject.toml +100 -0
- ai_guidelines-1.0.1/src/ai_guidelines/__init__.py +82 -0
- ai_guidelines-1.0.1/src/ai_guidelines/_validation.py +205 -0
- ai_guidelines-1.0.1/src/ai_guidelines/api.py +75 -0
- ai_guidelines-1.0.1/src/ai_guidelines/atomic.py +102 -0
- ai_guidelines-1.0.1/src/ai_guidelines/cache.py +831 -0
- ai_guidelines-1.0.1/src/ai_guidelines/cli.py +459 -0
- ai_guidelines-1.0.1/src/ai_guidelines/discovery.py +259 -0
- ai_guidelines-1.0.1/src/ai_guidelines/fetch.py +618 -0
- ai_guidelines-1.0.1/src/ai_guidelines/frontmatter.py +27 -0
- ai_guidelines-1.0.1/src/ai_guidelines/locations.py +574 -0
- ai_guidelines-1.0.1/src/ai_guidelines/lockfile.py +90 -0
- ai_guidelines-1.0.1/src/ai_guidelines/manifest.py +80 -0
- ai_guidelines-1.0.1/src/ai_guidelines/models.py +812 -0
- ai_guidelines-1.0.1/src/ai_guidelines/paths.py +220 -0
- ai_guidelines-1.0.1/src/ai_guidelines/py.typed +0 -0
- ai_guidelines-1.0.1/src/ai_guidelines/reconcile.py +655 -0
- ai_guidelines-1.0.1/src/ai_guidelines/reporter.py +78 -0
- ai_guidelines-1.0.1/src/ai_guidelines/sparse.py +71 -0
- ai_guidelines-1.0.1/src/ai_guidelines/sync.py +866 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/cli/__init__.py +0 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/cli/test_add_remove.py +235 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/cli/test_cache.py +66 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/cli/test_help.py +77 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/cli/test_search.py +103 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/cli/test_sync_list.py +275 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/cli/test_update.py +225 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/conftest.py +42 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/contract/__init__.py +0 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/contract/test_cli_workflow.py +114 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/contract/test_distribution.py +115 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/contract/test_documentation.py +144 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/contract/test_repository_hygiene.py +89 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/contract/test_workflow.py +122 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/test_api.py +70 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/test_atomic.py +50 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/test_cache.py +424 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/test_discovery.py +172 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/test_fetch.py +445 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/test_frontmatter.py +45 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/test_generate_release_notes.py +117 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/test_locations.py +156 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/test_lockfile.py +100 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/test_manifest.py +84 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/test_models.py +390 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/test_paths.py +86 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/test_reconcile.py +279 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/test_release_notes.py +65 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/test_sparse.py +30 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/test_sync.py +403 -0
- ai_guidelines-1.0.1/src/ai_guidelines/tests/test_update.py +293 -0
- ai_guidelines-1.0.1/src/ai_guidelines/update.py +432 -0
- ai_guidelines-1.0.1/src/ai_guidelines/update_acquisition.py +135 -0
- ai_guidelines-1.0.1/uv.lock +2463 -0
- ai_guidelines-1.0.1/uv.toml +7 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Dependency Management Guidelines
|
|
3
|
+
description: Dependency and reproducibility rules for ai-guidelines
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Dependency Management Guidelines
|
|
7
|
+
|
|
8
|
+
Use `uv` with the repository's `uv.toml` and commit intentional `uv.lock`
|
|
9
|
+
changes. Keep runtime dependencies minimal, public, and compatible with Python
|
|
10
|
+
3.10+.
|
|
11
|
+
|
|
12
|
+
Use `just update` for dependency upgrades. Do not rely on inherited user-level
|
|
13
|
+
package indexes or hidden environment configuration. Do not add a dependency
|
|
14
|
+
when a small standard-library implementation is sufficient.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Documentation Guidelines
|
|
3
|
+
description: Documentation maintenance rules for ai-guidelines
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Documentation Guidelines
|
|
7
|
+
|
|
8
|
+
Update user-facing documentation when commands, options, file formats, or API
|
|
9
|
+
behavior changes. Keep examples aligned with the implementation and avoid
|
|
10
|
+
claiming guarantees that the code does not provide.
|
|
11
|
+
|
|
12
|
+
Use clear Markdown headings and concise prose. Keep public Python docstrings
|
|
13
|
+
useful and consistent with the documented contract.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Git Commit Message Guideline 3.0
|
|
3
|
+
description: Guidelines for writing conventional commits that communicate intent and user impact
|
|
4
|
+
metadata:
|
|
5
|
+
owner: Gaetan Semet <gaetan.semet@ampere.cars>
|
|
6
|
+
guideline-id: 566d8c89-0f00-4390-a64e-caa3dc425267
|
|
7
|
+
keywords: [git, commits, conventional-commits, version-control, developer-workflow]
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Git Commit Message Guideline
|
|
11
|
+
|
|
12
|
+
Write a user-impact-focused Conventional Commit. State what users gain or
|
|
13
|
+
which limitation is resolved, not implementation details.
|
|
14
|
+
|
|
15
|
+
## Rules
|
|
16
|
+
|
|
17
|
+
1. Format the subject as `type(scope): description`; `scope` is optional. Use
|
|
18
|
+
only `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `ci`,
|
|
19
|
+
`chore`, `build`, or `revert`. Keep it at most 71 characters.
|
|
20
|
+
2. Apply limits to the final physical Git lines, not visual wrapping. Body
|
|
21
|
+
lines must be at most 79 characters; 80 is invalid. Keep every trailer on
|
|
22
|
+
one unwrapped, unindented line. Blank lines are exempt. Unbreakable URLs,
|
|
23
|
+
hashes, and similar strings may exceed 79 characters only on their own
|
|
24
|
+
line. A workflow may be stricter, never looser or conflicting.
|
|
25
|
+
3. Prefer `git commit -F <message-file>`. With `-m`, the first value is the
|
|
26
|
+
subject and each later value is a paragraph separated by a blank line; use
|
|
27
|
+
one value per paragraph, never per wrapped line.
|
|
28
|
+
4. Body text must describe user benefits, required user knowledge, or the
|
|
29
|
+
resolved limitation. Do not describe refactoring, internal functions,
|
|
30
|
+
changed files, methods, tests, or the number of checks run. Rewrite any
|
|
31
|
+
diff summary so it says what users can now do or what limitation was fixed.
|
|
32
|
+
5. For breaking changes, put `!` in the subject and include a
|
|
33
|
+
`BREAKING CHANGE:` section with explicit migration steps.
|
|
34
|
+
6. Do not add `Signed-off-by` unless a human requests it. Do not use
|
|
35
|
+
`git commit -s` or add it implicitly. If requested, add and verify it
|
|
36
|
+
deliberately. A human may use it to indicate ownership of AI-generated
|
|
37
|
+
content.
|
|
38
|
+
7. If AI generated most of the message, append this body trailer:
|
|
39
|
+
`Assisted-by: MODEL_PROVIDER:MODEL_NAME FRAMEWORK`. Use the underlying
|
|
40
|
+
model family/vendor, never the interface or IDE; use the specific model
|
|
41
|
+
version; and include the optional SDD framework only when it drove the
|
|
42
|
+
implementation. Determine provider and model from the current harness or
|
|
43
|
+
session metadata. Never copy attribution or guess. If it cannot be
|
|
44
|
+
verified, stop and obtain it.
|
|
45
|
+
8. If your workflow tooling owns a set of generated metadata trailers, do not
|
|
46
|
+
hand-author them. Let the tool write them, and treat its trailer vocabulary
|
|
47
|
+
as authoritative rather than reproducing older names as aliases.
|
|
48
|
+
|
|
49
|
+
Valid attribution uses forms such as `Assisted-by: Claude:Sonnet-4.6`,
|
|
50
|
+
`Assisted-by: Claude:Sonnet-4.6 SpecKit`, and `Assisted-by: GPT:4o`.
|
|
51
|
+
`GitHub Copilot`, `Cursor`, and `AI` are invalid attribution values, because
|
|
52
|
+
they name an interface rather than a model.
|
|
53
|
+
|
|
54
|
+
## Positive example
|
|
55
|
+
|
|
56
|
+
```text
|
|
57
|
+
fix(auth): show which credential field is invalid
|
|
58
|
+
|
|
59
|
+
Users can identify the invalid field without retrying valid credentials.
|
|
60
|
+
```
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Git Commit Guidelines
|
|
3
|
+
description: Commit message and change hygiene rules for ai-guidelines
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Git Commit Guidelines
|
|
7
|
+
|
|
8
|
+
Use Conventional Commits with a concise, user-impact-oriented subject. Keep a
|
|
9
|
+
commit focused on one coherent change and do not include generated caches,
|
|
10
|
+
virtual environments, coverage output, or unrelated worktree changes.
|
|
11
|
+
|
|
12
|
+
Review the final diff for secrets, private infrastructure references, and
|
|
13
|
+
accidental public API changes before committing.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Python Development Guidelines
|
|
3
|
+
description: Python implementation and API rules for ai-guidelines
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Python Development Guidelines
|
|
7
|
+
|
|
8
|
+
Use Python 3.10+ syntax and type every maintained function signature. Keep
|
|
9
|
+
modules focused and use absolute `ai_guidelines.*` imports; relative imports are
|
|
10
|
+
not permitted.
|
|
11
|
+
|
|
12
|
+
Use Pydantic v2 for persisted domain models and validate external input before
|
|
13
|
+
filesystem or Git side effects. Keep public exports deliberate and preserve
|
|
14
|
+
sanitized errors at public boundaries.
|
|
15
|
+
|
|
16
|
+
Add useful Google-style docstrings to public APIs. Include `Args`, `Returns`,
|
|
17
|
+
and `Raises` sections when they clarify non-obvious behavior.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: Python Unit Test Guideline 2.0
|
|
3
|
+
description: Guidelines for Python unit testing
|
|
4
|
+
metadata:
|
|
5
|
+
owner: Gaetan Semet <gaetan.semet@ampere.cars>
|
|
6
|
+
keywords: [python, testing, unit-test, pytest, fixtures]
|
|
7
|
+
guideline-id: 0b2bac4f-c5f2-4c24-a3cb-aecf5df02dbf
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Python Unit Test Guidelines
|
|
11
|
+
|
|
12
|
+
Use pytest, function-based tests, and pytest fixtures.
|
|
13
|
+
|
|
14
|
+
## Rules
|
|
15
|
+
|
|
16
|
+
1. Write standalone `def test_*():` functions, not test classes.
|
|
17
|
+
2. For multiple inputs with the same logic, use `@Parametrization` from the
|
|
18
|
+
`pytest-parametrization` dev dependency, not `pytest.mark.parametrize`.
|
|
19
|
+
3. Use pytest fixtures instead of manual setup/teardown, including
|
|
20
|
+
`tmp_path`, `monkeypatch`, `caplog`, and `mocker`.
|
|
21
|
+
4. Keep test code at most 120 characters wide. Preserve indentation in inline
|
|
22
|
+
code and strings, using `textwrap.dedent()` when needed.
|
|
23
|
+
5. Put `test_<modulename>.py` in the `tests/` subdirectory parallel to its
|
|
24
|
+
source module; for example, tests for `src/craftman/models.py` go in
|
|
25
|
+
`src/craftman/tests/test_models.py`.
|
|
26
|
+
|
|
27
|
+
## Tools
|
|
28
|
+
|
|
29
|
+
- Framework: pytest.
|
|
30
|
+
- Parametrization: `pytest-parametrization`, not `pytest.mark.parametrize`.
|
|
31
|
+
- Fixtures: `tmp_path`, `monkeypatch`, `mocker`, and `caplog`.
|
|
32
|
+
- Set appropriate function, module, or session fixture scopes.
|
|
33
|
+
|
|
34
|
+
## Validation
|
|
35
|
+
|
|
36
|
+
Find the project's test gate, such as `just tests-coverage`, and run the full
|
|
37
|
+
test suite with coverage reporting.
|
|
38
|
+
|
|
39
|
+
## Positive example
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
def test_file_operations(tmp_path):
|
|
43
|
+
config_file = tmp_path / "config.yml"
|
|
44
|
+
config_file.write_text("key: value")
|
|
45
|
+
|
|
46
|
+
assert config_file.exists()
|
|
47
|
+
assert "key:" in config_file.read_text()
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Using pytest-parametrization:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from parametrization import Parametrization
|
|
54
|
+
|
|
55
|
+
@Parametrization.autodetect_parameters()
|
|
56
|
+
@Parametrization.case(name="valid_input", input_val=1, expected=2)
|
|
57
|
+
@Parametrization.case("zero_input", input_val=0, expected=1)
|
|
58
|
+
@Parametrization.case("negative_input", input_val=-1, expected=0)
|
|
59
|
+
def test_increment_function(input_val, expected):
|
|
60
|
+
assert increment(input_val) == expected
|
|
61
|
+
```
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: gh-release-notes
|
|
3
|
+
description: Generate end-user-friendly GitHub release notes from the actual diff between releases, including user impact, examples, breaking changes, and public documentation links.
|
|
4
|
+
argument-hint: "from_ref=... to_ref=... repo_path=..."
|
|
5
|
+
user-invocable: true
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Release Notes Generator
|
|
9
|
+
|
|
10
|
+
Generate clean, end-user-friendly release notes from the actual Git diff between
|
|
11
|
+
an exclusive starting ref and an inclusive ending ref. The output is written
|
|
12
|
+
for use as a GitHub Release body and must contain release-note sections only.
|
|
13
|
+
GitHub supplies the release title, so never add a version heading, title,
|
|
14
|
+
preamble, file summary, commit summary, or closing explanation.
|
|
15
|
+
|
|
16
|
+
## Output contract
|
|
17
|
+
|
|
18
|
+
- Start with one of: `## New Features`, `## Enhancements`, `## Bug Fixes`,
|
|
19
|
+
`## Breaking Changes`, `## Examples`, `## Documentation`, or `## Maintenance`.
|
|
20
|
+
- Include only changes that ordinary users of the published `ai-guidelines`
|
|
21
|
+
package can do, observe, configure, rely on, or learn differently.
|
|
22
|
+
- Exclude CI, release automation, tests, Git evidence, contributor guidance,
|
|
23
|
+
agent instructions, governance, internal process, and repository housekeeping.
|
|
24
|
+
- Translate implementation details into user outcomes. Never list files,
|
|
25
|
+
functions, commit hashes, authors, or internal workflow mechanics.
|
|
26
|
+
- Use one concise line per bullet and omit empty sections.
|
|
27
|
+
- Include `## Maintenance` only when no user-facing change qualifies. It must be
|
|
28
|
+
the only section and say that the release contains maintenance and internal
|
|
29
|
+
improvements without inventing a benefit.
|
|
30
|
+
- Never combine `## Maintenance` with a user-facing section.
|
|
31
|
+
- Include `## Breaking Changes` only when the diff proves a public API, format,
|
|
32
|
+
configuration, default, or supported-workflow break.
|
|
33
|
+
- Add a concrete `## Examples` section for changed commands, APIs, configuration,
|
|
34
|
+
or before/after workflows when public usage evidence exists.
|
|
35
|
+
- Link relevant public documentation with descriptive inline Markdown, for example
|
|
36
|
+
`See the [installation guide](https://ai-guidelines.readthedocs.io/en/stable/).`
|
|
37
|
+
Never emit a bare URL or a repository-relative documentation path.
|
|
38
|
+
- Do not use code fences, a `#` title heading, or headings outside the permitted
|
|
39
|
+
seven section names.
|
|
40
|
+
|
|
41
|
+
## Analysis workflow
|
|
42
|
+
|
|
43
|
+
1. Read the exact `from_ref..to_ref` commit log and diff.
|
|
44
|
+
2. Inspect public README, documentation, CLI help, and API references for evidence
|
|
45
|
+
of user-visible behavior and the closest trustworthy documentation links.
|
|
46
|
+
3. Apply the normal-user audience test to every candidate change.
|
|
47
|
+
4. Detect breaking changes from commit markers and actual public contract changes.
|
|
48
|
+
5. Consolidate multiple commits that describe one final user outcome.
|
|
49
|
+
6. Categorize the remaining outcomes and write concise Markdown.
|
|
50
|
+
7. Review every bullet for user impact, documentation-link correctness, and the
|
|
51
|
+
output contract before writing the requested file.
|
|
52
|
+
|
|
53
|
+
## Automated invocation
|
|
54
|
+
|
|
55
|
+
When the generator is invoked by a CI job with an explicit output path:
|
|
56
|
+
|
|
57
|
+
- Treat the requested file as mandatory and write the final Markdown directly to it.
|
|
58
|
+
- Do not use the Copilot response stream as the output artifact.
|
|
59
|
+
- Do not modify any other repository files.
|
|
60
|
+
- The first line must be a permitted `##` heading.
|
|
61
|
+
- The file must contain only release-note Markdown, without a title, preamble,
|
|
62
|
+
code fence, or explanatory text.
|
|
63
|
+
|
|
64
|
+
For an empty or intentionally maintenance-only range, write exactly:
|
|
65
|
+
|
|
66
|
+
```markdown
|
|
67
|
+
## Maintenance
|
|
68
|
+
|
|
69
|
+
This release contains maintenance and internal improvements. No user-facing behavior changed.
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The bundled generator precomputes Git evidence, invokes this skill through the
|
|
73
|
+
Copilot CLI, and validates the resulting file before the workflow creates the
|
|
74
|
+
GitHub Release.
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Generate and validate release notes through the GitHub Copilot CLI."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
SKILL_NAME = "gh-release-notes"
|
|
14
|
+
DEFAULT_OUTPUT = Path("release-notes.md")
|
|
15
|
+
MAX_GIT_CONTEXT_LENGTH = 60_000
|
|
16
|
+
TOKEN_ENV_VARS = ("COPILOT_GITHUB_TOKEN", "GH_TOKEN", "GITHUB_TOKEN")
|
|
17
|
+
PERMITTED_HEADINGS = frozenset(
|
|
18
|
+
{
|
|
19
|
+
"## New Features",
|
|
20
|
+
"## Enhancements",
|
|
21
|
+
"## Bug Fixes",
|
|
22
|
+
"## Breaking Changes",
|
|
23
|
+
"## Examples",
|
|
24
|
+
"## Documentation",
|
|
25
|
+
"## Maintenance",
|
|
26
|
+
}
|
|
27
|
+
)
|
|
28
|
+
TRACE_MARKERS = (
|
|
29
|
+
"<function_call",
|
|
30
|
+
"<thinking>",
|
|
31
|
+
"<system_notification>",
|
|
32
|
+
"assistant.reasoning",
|
|
33
|
+
"function_calls",
|
|
34
|
+
"to=bash.exec",
|
|
35
|
+
"to=functions.exec",
|
|
36
|
+
)
|
|
37
|
+
MAINTENANCE_NOTES = (
|
|
38
|
+
"## Maintenance\n\n"
|
|
39
|
+
"This release contains maintenance and internal improvements. "
|
|
40
|
+
"No user-facing behavior changed.\n"
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
45
|
+
"""Build the command-line argument parser."""
|
|
46
|
+
parser = argparse.ArgumentParser(
|
|
47
|
+
description="Generate or validate release notes for the gh-release-notes skill."
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"--from-ref",
|
|
51
|
+
help="Starting Git ref. It is excluded: the range is from_ref..to_ref.",
|
|
52
|
+
)
|
|
53
|
+
parser.add_argument(
|
|
54
|
+
"--to-ref",
|
|
55
|
+
help="Ending Git ref. It is included: the range is from_ref..to_ref.",
|
|
56
|
+
)
|
|
57
|
+
parser.add_argument(
|
|
58
|
+
"--repo",
|
|
59
|
+
type=Path,
|
|
60
|
+
default=Path.cwd(),
|
|
61
|
+
help="Repository to inspect (default: current directory).",
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--output",
|
|
65
|
+
type=Path,
|
|
66
|
+
default=DEFAULT_OUTPUT,
|
|
67
|
+
help="Release-note output path (default: release-notes.md).",
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--maintenance-only",
|
|
71
|
+
action="store_true",
|
|
72
|
+
help="Write deterministic maintenance notes without invoking Copilot.",
|
|
73
|
+
)
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--model",
|
|
76
|
+
default=None,
|
|
77
|
+
help="Copilot CLI model identifier; defaults to COPILOT_MODEL or the CLI default.",
|
|
78
|
+
)
|
|
79
|
+
parser.add_argument(
|
|
80
|
+
"--validate",
|
|
81
|
+
type=Path,
|
|
82
|
+
metavar="FILE",
|
|
83
|
+
help="Verify that an existing release-note file follows the output contract.",
|
|
84
|
+
)
|
|
85
|
+
return parser
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def build_git_context(repo: Path, from_ref: str, to_ref: str) -> str:
|
|
89
|
+
"""Collect generic Git evidence for Copilot environments without Git access."""
|
|
90
|
+
log = git_output(
|
|
91
|
+
repo, "commit log", "log", "--format=%h %s", "--no-merges", f"{from_ref}..{to_ref}"
|
|
92
|
+
)
|
|
93
|
+
diff_stat = git_output(
|
|
94
|
+
repo, "diff summary", "diff", "--stat", "--no-ext-diff", f"{from_ref}..{to_ref}"
|
|
95
|
+
)
|
|
96
|
+
diff = git_output(repo, "diff", "diff", "--no-ext-diff", "--unified=3", f"{from_ref}..{to_ref}")
|
|
97
|
+
context = (
|
|
98
|
+
f"Precomputed Git evidence for {from_ref}..{to_ref}:\n\n"
|
|
99
|
+
f"Commit log:\n{log or '(no commits)'}\n\n"
|
|
100
|
+
f"Diff summary:\n{diff_stat or '(empty)'}\n\n"
|
|
101
|
+
f"Diff:\n{diff or '(empty)'}"
|
|
102
|
+
)
|
|
103
|
+
if len(context) <= MAX_GIT_CONTEXT_LENGTH:
|
|
104
|
+
return context
|
|
105
|
+
|
|
106
|
+
truncated = context[:MAX_GIT_CONTEXT_LENGTH]
|
|
107
|
+
return (
|
|
108
|
+
f"{truncated}\n\n[Git evidence truncated at {MAX_GIT_CONTEXT_LENGTH} characters; "
|
|
109
|
+
"use the included summary and inspect the checked-out files when needed.]"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def build_prompt(
|
|
114
|
+
from_ref: str,
|
|
115
|
+
to_ref: str,
|
|
116
|
+
repo: Path,
|
|
117
|
+
output: Path,
|
|
118
|
+
git_context: str | None = None,
|
|
119
|
+
) -> str:
|
|
120
|
+
"""Build the small orchestration prompt; the skill owns release-note policy."""
|
|
121
|
+
try:
|
|
122
|
+
output_reference = output.relative_to(repo).as_posix()
|
|
123
|
+
except ValueError:
|
|
124
|
+
output_reference = str(output)
|
|
125
|
+
|
|
126
|
+
prompt = (
|
|
127
|
+
f"Use the /{SKILL_NAME} skill. Generate release notes for the exact Git range "
|
|
128
|
+
f"{from_ref}..{to_ref} in {repo}. The skill is authoritative for analysis, "
|
|
129
|
+
f"classification, wording, documentation, and Markdown format. Write the final "
|
|
130
|
+
f"release-note Markdown directly to the shared repository file "
|
|
131
|
+
f"{output_reference} using the file-writing tools. Its resolved path is {output}. "
|
|
132
|
+
"The release workflow reads this exact file as its notes input; the Copilot "
|
|
133
|
+
"response stream is discarded. Do not put release notes or a summary in your "
|
|
134
|
+
"response, and do not modify any other files. Before writing, enforce the "
|
|
135
|
+
"skill's final output contract: render every documentation URL as concise inline "
|
|
136
|
+
"Markdown, never as a bare URL; exclude all internal CI, release automation, "
|
|
137
|
+
"governance, contributor, agent, generator, Git-evidence, and maintainer content; "
|
|
138
|
+
"and omit Maintenance whenever any user-facing section remains."
|
|
139
|
+
)
|
|
140
|
+
if git_context:
|
|
141
|
+
prompt += (
|
|
142
|
+
" Treat the following locally collected Git evidence as authoritative input; "
|
|
143
|
+
"the starting ref is excluded and the ending ref is included.\n\n"
|
|
144
|
+
f"<git-evidence>\n{git_context}\n</git-evidence>"
|
|
145
|
+
)
|
|
146
|
+
return prompt
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def build_copilot_command(prompt: str, model: str | None) -> list[str]:
|
|
150
|
+
"""Build the non-interactive Copilot CLI command."""
|
|
151
|
+
command = [
|
|
152
|
+
"gh",
|
|
153
|
+
"copilot",
|
|
154
|
+
"--",
|
|
155
|
+
"--prompt",
|
|
156
|
+
prompt,
|
|
157
|
+
"--silent",
|
|
158
|
+
"--no-ask-user",
|
|
159
|
+
"--no-auto-update",
|
|
160
|
+
"--no-color",
|
|
161
|
+
"--output-format",
|
|
162
|
+
"text",
|
|
163
|
+
"--disable-builtin-mcps",
|
|
164
|
+
"--allow-all-tools",
|
|
165
|
+
"--available-tools=read,create,edit,bash",
|
|
166
|
+
"--allow-url=https://github.com",
|
|
167
|
+
"--allow-url=https://ai-guidelines.readthedocs.io",
|
|
168
|
+
]
|
|
169
|
+
if model:
|
|
170
|
+
command.extend(["--model", model])
|
|
171
|
+
return command
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def resolve_path(path: Path, repo: Path) -> Path:
|
|
175
|
+
"""Resolve a path relative to the repository when it is not absolute."""
|
|
176
|
+
return path if path.is_absolute() else repo / path
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def run_git(repo: Path, *arguments: str) -> subprocess.CompletedProcess[str]:
|
|
180
|
+
"""Run a Git command in the target repository."""
|
|
181
|
+
return subprocess.run(
|
|
182
|
+
["git", *arguments],
|
|
183
|
+
cwd=repo,
|
|
184
|
+
check=False,
|
|
185
|
+
capture_output=True,
|
|
186
|
+
text=True,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def git_output(repo: Path, description: str, *arguments: str) -> str:
|
|
191
|
+
"""Run Git and return its output, raising one consistent error on failure."""
|
|
192
|
+
result = run_git(repo, *arguments)
|
|
193
|
+
if result.returncode != 0:
|
|
194
|
+
detail = result.stderr.strip() or "unknown Git error"
|
|
195
|
+
raise ValueError(f"Unable to collect the release {description}: {detail}")
|
|
196
|
+
return result.stdout.strip()
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def validate_range(repo: Path, from_ref: str, to_ref: str) -> None:
|
|
200
|
+
"""Verify that the requested refs exist and form an ancestor range."""
|
|
201
|
+
if from_ref == to_ref:
|
|
202
|
+
raise ValueError("--from-ref and --to-ref must be different refs.")
|
|
203
|
+
|
|
204
|
+
for ref in (from_ref, to_ref):
|
|
205
|
+
result = run_git(repo, "rev-parse", "--verify", f"{ref}^{{commit}}")
|
|
206
|
+
if result.returncode != 0:
|
|
207
|
+
detail = result.stderr.strip() or "unknown Git error"
|
|
208
|
+
raise ValueError(f"Git ref {ref!r} is not available: {detail}")
|
|
209
|
+
|
|
210
|
+
result = run_git(repo, "merge-base", "--is-ancestor", from_ref, to_ref)
|
|
211
|
+
if result.returncode != 0:
|
|
212
|
+
raise ValueError(f"Git ref {from_ref!r} is not an ancestor of {to_ref!r}.")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def range_has_commits(repo: Path, from_ref: str, to_ref: str) -> bool:
|
|
216
|
+
"""Return whether the requested Git range contains at least one commit."""
|
|
217
|
+
count = git_output(repo, "commit count", "rev-list", "--count", f"{from_ref}..{to_ref}")
|
|
218
|
+
return count != "0"
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def write_maintenance_notes(output: Path) -> None:
|
|
222
|
+
"""Write the deterministic notes required for a maintenance-only release."""
|
|
223
|
+
output.write_text(MAINTENANCE_NOTES, encoding="utf-8")
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def run_skill_check(repo: Path) -> None:
|
|
227
|
+
"""Ensure the release-note skill is installed in the Copilot CLI."""
|
|
228
|
+
result = subprocess.run(
|
|
229
|
+
["gh", "copilot", "--", "skill", "list"],
|
|
230
|
+
cwd=repo,
|
|
231
|
+
check=False,
|
|
232
|
+
capture_output=True,
|
|
233
|
+
text=True,
|
|
234
|
+
)
|
|
235
|
+
output = f"{result.stdout}\n{result.stderr}"
|
|
236
|
+
if result.returncode != 0:
|
|
237
|
+
raise RuntimeError(f"Unable to list Copilot skills:\n{output.strip()}")
|
|
238
|
+
if SKILL_NAME not in output:
|
|
239
|
+
raise RuntimeError(f"Copilot skill {SKILL_NAME!r} is not installed.")
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def require_copilot_token() -> None:
|
|
243
|
+
"""Fail early when no supported GitHub token environment variable exists."""
|
|
244
|
+
if not any(os.environ.get(name) for name in TOKEN_ENV_VARS):
|
|
245
|
+
names = ", ".join(TOKEN_ENV_VARS[:-1]) + f", or {TOKEN_ENV_VARS[-1]}"
|
|
246
|
+
raise RuntimeError(f"Set one of {names} before running Copilot CLI.")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def run_copilot(repo: Path, prompt: str, model: str | None) -> None:
|
|
250
|
+
"""Run Copilot CLI and leave the generated Markdown in the shared output file."""
|
|
251
|
+
result = subprocess.run(
|
|
252
|
+
build_copilot_command(prompt, model),
|
|
253
|
+
cwd=repo,
|
|
254
|
+
check=False,
|
|
255
|
+
capture_output=True,
|
|
256
|
+
text=True,
|
|
257
|
+
)
|
|
258
|
+
if result.returncode != 0:
|
|
259
|
+
output = f"{result.stdout}\n{result.stderr}".strip()
|
|
260
|
+
raise RuntimeError(f"Copilot CLI failed with exit code {result.returncode}:\n{output}")
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def validate_output(output: Path) -> None:
|
|
264
|
+
"""Verify that generated release notes follow the output contract."""
|
|
265
|
+
try:
|
|
266
|
+
content = output.read_text(encoding="utf-8")
|
|
267
|
+
except OSError as error:
|
|
268
|
+
raise RuntimeError(f"Unable to read release-note output {output}: {error}") from error
|
|
269
|
+
if not content.strip():
|
|
270
|
+
raise RuntimeError(f"Copilot created an empty release-note file: {output}")
|
|
271
|
+
|
|
272
|
+
lines = content.splitlines()
|
|
273
|
+
first_line = lines[0] if lines else ""
|
|
274
|
+
if first_line not in PERMITTED_HEADINGS:
|
|
275
|
+
raise RuntimeError(
|
|
276
|
+
f"Release-note output must start with one of the permitted section headings: {output}"
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
lowered = content.lower()
|
|
280
|
+
leaked_markers = [marker for marker in TRACE_MARKERS if marker.lower() in lowered]
|
|
281
|
+
if leaked_markers:
|
|
282
|
+
markers = ", ".join(leaked_markers)
|
|
283
|
+
raise RuntimeError(
|
|
284
|
+
f"Release-note output contains Copilot trace markers ({markers}): {output}"
|
|
285
|
+
)
|
|
286
|
+
if "```" in content:
|
|
287
|
+
raise RuntimeError(f"Release-note output must not contain a code fence: {output}")
|
|
288
|
+
if any(line.startswith("# ") for line in lines):
|
|
289
|
+
raise RuntimeError(f"Release-note output must not contain a title heading: {output}")
|
|
290
|
+
headings = [line for line in lines if line.startswith("## ")]
|
|
291
|
+
invalid_headings = [heading for heading in headings if heading not in PERMITTED_HEADINGS]
|
|
292
|
+
if invalid_headings:
|
|
293
|
+
raise RuntimeError(
|
|
294
|
+
f"Release-note output contains an invalid section heading "
|
|
295
|
+
f"{invalid_headings[0]!r}: {output}"
|
|
296
|
+
)
|
|
297
|
+
print("Release-note output file verified.")
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def generate_release_notes(
|
|
301
|
+
repo: Path,
|
|
302
|
+
from_ref: str,
|
|
303
|
+
to_ref: str,
|
|
304
|
+
output: Path,
|
|
305
|
+
model: str | None,
|
|
306
|
+
maintenance_only: bool = False,
|
|
307
|
+
) -> None:
|
|
308
|
+
"""Generate release notes, then validate the resulting Markdown file."""
|
|
309
|
+
repo = repo.resolve()
|
|
310
|
+
output = resolve_path(output, repo).resolve()
|
|
311
|
+
validate_range(repo, from_ref, to_ref)
|
|
312
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
313
|
+
output.write_text("", encoding="utf-8")
|
|
314
|
+
|
|
315
|
+
print(f"Generating release notes from {from_ref} (exclusive) to {to_ref} (inclusive).")
|
|
316
|
+
print(f"Copilot model: {model or 'CLI default'}")
|
|
317
|
+
if maintenance_only or not range_has_commits(repo, from_ref, to_ref):
|
|
318
|
+
write_maintenance_notes(output)
|
|
319
|
+
validate_output(output)
|
|
320
|
+
print(f"Release notes written to {output}")
|
|
321
|
+
return
|
|
322
|
+
|
|
323
|
+
require_copilot_token()
|
|
324
|
+
run_skill_check(repo)
|
|
325
|
+
git_context = build_git_context(repo, from_ref, to_ref)
|
|
326
|
+
run_copilot(
|
|
327
|
+
repo,
|
|
328
|
+
build_prompt(from_ref, to_ref, repo, output, git_context),
|
|
329
|
+
model,
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
validate_output(output)
|
|
333
|
+
print(f"Release notes written to {output}")
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
337
|
+
"""Parse arguments, generate notes, and return an exit code."""
|
|
338
|
+
parser = build_parser()
|
|
339
|
+
args = parser.parse_args(argv)
|
|
340
|
+
|
|
341
|
+
if args.validate is not None:
|
|
342
|
+
if (args.from_ref is None) != (args.to_ref is None):
|
|
343
|
+
parser.error("--validate requires both --from-ref and --to-ref for range validation")
|
|
344
|
+
try:
|
|
345
|
+
repo = args.repo.resolve()
|
|
346
|
+
if args.from_ref is not None and args.to_ref is not None:
|
|
347
|
+
validate_range(repo, args.from_ref, args.to_ref)
|
|
348
|
+
validate_output(resolve_path(args.validate, repo))
|
|
349
|
+
except (OSError, RuntimeError, ValueError) as error:
|
|
350
|
+
print(f"error: {error}", file=sys.stderr)
|
|
351
|
+
return 1
|
|
352
|
+
return 0
|
|
353
|
+
|
|
354
|
+
if args.from_ref is None or args.to_ref is None:
|
|
355
|
+
parser.error("--from-ref and --to-ref are required unless --validate is used")
|
|
356
|
+
|
|
357
|
+
try:
|
|
358
|
+
generate_release_notes(
|
|
359
|
+
repo=args.repo,
|
|
360
|
+
from_ref=args.from_ref,
|
|
361
|
+
to_ref=args.to_ref,
|
|
362
|
+
output=args.output,
|
|
363
|
+
model=args.model or os.environ.get("COPILOT_MODEL"),
|
|
364
|
+
maintenance_only=args.maintenance_only,
|
|
365
|
+
)
|
|
366
|
+
except (OSError, RuntimeError, ValueError) as error:
|
|
367
|
+
print(f"error: {error}", file=sys.stderr)
|
|
368
|
+
return 1
|
|
369
|
+
return 0
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
if __name__ == "__main__":
|
|
373
|
+
raise SystemExit(main())
|