charmlint 0.1.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.
Files changed (63) hide show
  1. charmlint-0.1.1/.github/check-conventional-pr-title.py +87 -0
  2. charmlint-0.1.1/.github/dependabot.yaml +19 -0
  3. charmlint-0.1.1/.github/workflows/ci.yaml +43 -0
  4. charmlint-0.1.1/.github/workflows/dependency-review.yaml +22 -0
  5. charmlint-0.1.1/.github/workflows/publish.yaml +104 -0
  6. charmlint-0.1.1/.github/workflows/validate-pr-title.yaml +21 -0
  7. charmlint-0.1.1/.github/workflows/zizmor.yaml +34 -0
  8. charmlint-0.1.1/.github/zizmor.yaml +8 -0
  9. charmlint-0.1.1/.gitignore +38 -0
  10. charmlint-0.1.1/.pre-commit-config.yaml +61 -0
  11. charmlint-0.1.1/AGENTS.md +37 -0
  12. charmlint-0.1.1/CODE_OF_CONDUCT.md +10 -0
  13. charmlint-0.1.1/CONTRIBUTING.md +85 -0
  14. charmlint-0.1.1/LICENSE +190 -0
  15. charmlint-0.1.1/Makefile +21 -0
  16. charmlint-0.1.1/PKG-INFO +124 -0
  17. charmlint-0.1.1/README.md +99 -0
  18. charmlint-0.1.1/RULES_TRACKER.md +67 -0
  19. charmlint-0.1.1/SECURITY.md +40 -0
  20. charmlint-0.1.1/docs/id-scheme.md +48 -0
  21. charmlint-0.1.1/docs/versioning.md +86 -0
  22. charmlint-0.1.1/pyproject.toml +90 -0
  23. charmlint-0.1.1/src/charmlint/__init__.py +11 -0
  24. charmlint-0.1.1/src/charmlint/__main__.py +6 -0
  25. charmlint-0.1.1/src/charmlint/_ast.py +421 -0
  26. charmlint-0.1.1/src/charmlint/_cli.py +237 -0
  27. charmlint-0.1.1/src/charmlint/_config.py +189 -0
  28. charmlint-0.1.1/src/charmlint/_linter.py +331 -0
  29. charmlint-0.1.1/src/charmlint/_models.py +328 -0
  30. charmlint-0.1.1/src/charmlint/_noqa.py +103 -0
  31. charmlint-0.1.1/src/charmlint/_pypi_attest.py +238 -0
  32. charmlint-0.1.1/src/charmlint/_rules/__init__.py +17 -0
  33. charmlint-0.1.1/src/charmlint/_rules/_base.py +91 -0
  34. charmlint-0.1.1/src/charmlint/_rules/actions.py +175 -0
  35. charmlint-0.1.1/src/charmlint/_rules/charmcraft_compat.py +489 -0
  36. charmlint-0.1.1/src/charmlint/_rules/config_quality.py +81 -0
  37. charmlint-0.1.1/src/charmlint/_rules/documentation.py +24 -0
  38. charmlint-0.1.1/src/charmlint/_rules/libraries.py +105 -0
  39. charmlint-0.1.1/src/charmlint/_rules/metadata.py +279 -0
  40. charmlint-0.1.1/src/charmlint/_rules/pebble.py +139 -0
  41. charmlint-0.1.1/src/charmlint/_rules/security.py +44 -0
  42. charmlint-0.1.1/src/charmlint/_rules/structure.py +46 -0
  43. charmlint-0.1.1/src/charmlint/_rules/testing.py +20 -0
  44. charmlint-0.1.1/src/charmlint/_yaml.py +142 -0
  45. charmlint-0.1.1/tests/__init__.py +0 -0
  46. charmlint-0.1.1/tests/conftest.py +116 -0
  47. charmlint-0.1.1/tests/test_actions.py +433 -0
  48. charmlint-0.1.1/tests/test_ast.py +461 -0
  49. charmlint-0.1.1/tests/test_charmcraft_compat.py +800 -0
  50. charmlint-0.1.1/tests/test_cli.py +68 -0
  51. charmlint-0.1.1/tests/test_config.py +147 -0
  52. charmlint-0.1.1/tests/test_config_quality.py +226 -0
  53. charmlint-0.1.1/tests/test_linter.py +228 -0
  54. charmlint-0.1.1/tests/test_metadata.py +265 -0
  55. charmlint-0.1.1/tests/test_models.py +94 -0
  56. charmlint-0.1.1/tests/test_noqa.py +150 -0
  57. charmlint-0.1.1/tests/test_pebble.py +240 -0
  58. charmlint-0.1.1/tests/test_properties.py +205 -0
  59. charmlint-0.1.1/tests/test_rules.py +409 -0
  60. charmlint-0.1.1/tests/test_security.py +85 -0
  61. charmlint-0.1.1/tests/test_yaml.py +234 -0
  62. charmlint-0.1.1/tools/refresh_charmlibs_map.py +91 -0
  63. charmlint-0.1.1/uv.lock +315 -0
@@ -0,0 +1,87 @@
1
+ # Copyright 2025 Canonical Ltd.
2
+ # See LICENSE file for licensing details.
3
+
4
+ """Check that a PR title follows the Conventional Commits specification.
5
+
6
+ Reads the PR title from the PR_TITLE environment variable.
7
+ Exits with a non-zero status and prints an error message if the title is invalid.
8
+
9
+ Reference: https://www.conventionalcommits.org/en/v1.0.0/
10
+
11
+ This repo defines a restricted set of commit types and disallows scopes in PR titles.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import os
17
+ import re
18
+ import sys
19
+
20
+ _TYPES = frozenset({
21
+ 'chore',
22
+ 'ci',
23
+ 'docs',
24
+ 'feat',
25
+ 'fix',
26
+ 'perf',
27
+ 'refactor',
28
+ 'revert',
29
+ 'test',
30
+ })
31
+
32
+ # <type>[optional scope][optional !]: <description>
33
+ _PATTERN = re.compile(
34
+ r'^(?P<type>[A-Za-z]+)' # lower-case only, but let this be validated by _TYPES
35
+ r'(?:\((?P<scope>[^()]+)\))?'
36
+ r'(?P<breaking>!)?'
37
+ r': '
38
+ r'(?P<description>.+)$'
39
+ )
40
+
41
+ # Adjust this URL when copying into a new repo — point at <this repo>/CONTRIBUTING.md#pull-requests.
42
+ _HELP_URL = 'https://github.com/canonical/charmlint/blob/main/CONTRIBUTING.md#pull-requests'
43
+
44
+
45
+ def _main() -> None:
46
+ title = os.environ.get('PR_TITLE', '').strip()
47
+ if not title:
48
+ print('PR_TITLE environment variable is not set or empty.', file=sys.stderr)
49
+ sys.exit(1)
50
+
51
+ match = _PATTERN.match(title)
52
+ if not match:
53
+ print(
54
+ f'PR title does not follow Conventional Commits format.\n'
55
+ f'Expected: <type>[!]: <description>\n'
56
+ f'Got: {title!r}\n'
57
+ f'Read more: {_HELP_URL}',
58
+ file=sys.stderr,
59
+ )
60
+ sys.exit(1)
61
+
62
+ scope = match.group('scope')
63
+ if scope is not None:
64
+ print(
65
+ f'Scopes must not be used in PR titles.\n'
66
+ f'Got: {title!r}\n'
67
+ f'Read more: {_HELP_URL}',
68
+ file=sys.stderr,
69
+ )
70
+ sys.exit(1)
71
+
72
+ commit_type = match.group('type')
73
+ if commit_type not in _TYPES:
74
+ print(
75
+ f'Invalid type {commit_type!r} in PR title.\n'
76
+ f'Valid types: {", ".join(sorted(_TYPES))}\n'
77
+ f'Got: {title!r}\n'
78
+ f'Read more: {_HELP_URL}',
79
+ file=sys.stderr,
80
+ )
81
+ sys.exit(1)
82
+
83
+ print(f'OK: {title!r}')
84
+
85
+
86
+ if __name__ == '__main__':
87
+ _main()
@@ -0,0 +1,19 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "monthly" # Hard-coded to the 1st of the month.
7
+ cooldown:
8
+ default-days: 7 # Don't adopt a release until it's 7 days old.
9
+ labels:
10
+ - "dependencies"
11
+ - package-ecosystem: "uv"
12
+ directory: "/"
13
+ schedule:
14
+ interval: "daily"
15
+ cooldown:
16
+ default-days: 7
17
+ open-pull-requests-limit: 0 # Security updates only.
18
+ labels:
19
+ - "dependencies"
@@ -0,0 +1,43 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ pull_request:
7
+ branches: ["**"]
8
+ workflow_call:
9
+
10
+ permissions: {}
11
+
12
+ jobs:
13
+ python-lint:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
17
+ with:
18
+ persist-credentials: false
19
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
20
+ - name: uv lock --check
21
+ run: uv lock --check
22
+ - name: ruff check
23
+ run: uv run --group dev ruff check src tests
24
+ - name: ruff format
25
+ run: uv run --group dev ruff format --check src tests
26
+ - name: ty
27
+ run: uv run --group dev ty check src tests
28
+
29
+ python-unit:
30
+ runs-on: ubuntu-latest
31
+ strategy:
32
+ fail-fast: false
33
+ matrix:
34
+ python-version: ["3.12", "3.13", "3.14"]
35
+ steps:
36
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
37
+ with:
38
+ persist-credentials: false
39
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
40
+ with:
41
+ python-version: ${{ matrix.python-version }}
42
+ - name: Unit tests
43
+ run: uv run --group dev pytest --cov=charmlint --cov-report=term-missing
@@ -0,0 +1,22 @@
1
+ name: Dependency Review
2
+
3
+ on:
4
+ pull_request:
5
+ branches: ["**"]
6
+
7
+ permissions: {}
8
+
9
+ jobs:
10
+ dependency-review:
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ contents: read
14
+ pull-requests: write
15
+ steps:
16
+ - uses: actions/checkout@v7.0.1
17
+ with:
18
+ persist-credentials: false
19
+ - uses: actions/dependency-review-action@v5
20
+ with:
21
+ fail-on-severity: high
22
+ comment-summary-in-pr: on-failure
@@ -0,0 +1,104 @@
1
+ name: Publish
2
+
3
+ # Publish to PyPI on tag push.
4
+ # Publish to TestPyPI on manual dispatch to exercise the same key steps.
5
+
6
+ on:
7
+ push:
8
+ tags: ['v*']
9
+ workflow_dispatch:
10
+
11
+ permissions: {}
12
+
13
+ jobs:
14
+ publish:
15
+ name: Build and publish to ${{ github.event_name == 'push' && 'PyPI' || 'TestPyPI' }} (Trusted Publishing)
16
+ runs-on: ubuntu-latest
17
+ environment:
18
+ # The name must match the environment registered with the trusted
19
+ # publisher on PyPI (and separately on TestPyPI).
20
+ name: ${{ github.event_name == 'push' && 'publish-pypi' || 'publish-testpypi' }}
21
+ # Cosmetic: the link shown against the deployment in the GitHub UI.
22
+ url: ${{ github.event_name == 'push' && 'https://pypi.org/p/charmlint' || 'https://test.pypi.org/p/charmlint' }}
23
+ permissions:
24
+ id-token: write # OIDC to PyPI + sigstore for attestations.
25
+ attestations: write # Write build-provenance + SBOM predicates.
26
+ contents: read
27
+ steps:
28
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
29
+ with:
30
+ persist-credentials: false
31
+
32
+ - name: Check the tag matches the project version
33
+ if: github.event_name == 'push'
34
+ env:
35
+ TAG: ${{ github.ref_name }}
36
+ shell: python
37
+ run: |
38
+ import os, pathlib, sys, tomllib
39
+ tag = os.environ["TAG"]
40
+ version = tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"]
41
+ if tag != f"v{version}":
42
+ print(f"::error::Tag {tag} does not match pyproject.toml version {version} (expected v{version}).")
43
+ sys.exit(1)
44
+
45
+ - name: Give the build a .dev suffix
46
+ if: github.event_name == 'workflow_dispatch'
47
+ env:
48
+ RUN_NUMBER: ${{ github.run_number }}
49
+ shell: python
50
+ run: |
51
+ import os, pathlib, re, sys, tomllib
52
+ run_number = os.environ["RUN_NUMBER"]
53
+ path = pathlib.Path("pyproject.toml")
54
+ # Write dev version.
55
+ path.write_text(re.sub(
56
+ r'^version = "([^"]+)"', rf'version = "\1.dev{run_number}"', path.read_text(),
57
+ count=1, flags=re.MULTILINE,
58
+ ))
59
+ # Ensure we wrote the dev version correctly.
60
+ version = tomllib.loads(path.read_text())["project"]["version"]
61
+ if not version.endswith(f".dev{run_number}"):
62
+ print(f"::error::Failed to add a .dev suffix; version is {version}.")
63
+ sys.exit(1)
64
+ print(f"Building {version}")
65
+
66
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
67
+ with:
68
+ enable-cache: false
69
+
70
+ - name: Build sdist and wheel
71
+ run: uv build
72
+
73
+ - name: Generate CycloneDX SBOM
74
+ run: |
75
+ uv sync --frozen --no-dev
76
+ uv run --with cyclonedx-bom cyclonedx-py environment .venv \
77
+ --output-format JSON \
78
+ --output-file sbom.cdx.json
79
+
80
+ - name: Attest build provenance (SLSA)
81
+ uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
82
+ with:
83
+ subject-path: 'dist/*'
84
+
85
+ - name: Attest SBOM (CycloneDX)
86
+ uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2
87
+ with:
88
+ subject-path: 'dist/*'
89
+ sbom-path: sbom.cdx.json
90
+
91
+ # Only the SBOM is uploaded: the sdist and wheel go to PyPI moments later
92
+ # and stay there, whereas this is the only convenient copy of the SBOM
93
+ # (the other being inside the attestation predicate).
94
+ - name: Upload SBOM artifact
95
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
96
+ with:
97
+ name: sbom
98
+ path: sbom.cdx.json
99
+ if-no-files-found: error
100
+
101
+ - name: Publish
102
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
103
+ with:
104
+ repository-url: ${{ github.event_name == 'push' && 'https://upload.pypi.org/legacy/' || 'https://test.pypi.org/legacy/' }}
@@ -0,0 +1,21 @@
1
+ ---
2
+ name: "Validate PR Title"
3
+ # Ensure that the PR title conforms to the Conventional Commits and our choice of types and scopes, so that library version bumps can be detected automatically
4
+
5
+ on:
6
+ pull_request:
7
+ types: [opened, edited, synchronize]
8
+
9
+ permissions: {}
10
+
11
+ jobs:
12
+ main:
13
+ name: Validate PR title
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v7.0.1
17
+ with:
18
+ persist-credentials: false
19
+ - run: python3 .github/check-conventional-pr-title.py
20
+ env:
21
+ PR_TITLE: ${{ github.event.pull_request.title }}
@@ -0,0 +1,34 @@
1
+ name: Workflow static checks
2
+
3
+ on:
4
+ push:
5
+ branches: ["main"]
6
+ pull_request:
7
+ branches: ["**"]
8
+
9
+ permissions: {}
10
+
11
+ jobs:
12
+ zizmor:
13
+ runs-on: ubuntu-latest
14
+ permissions:
15
+ security-events: write
16
+ steps:
17
+ - name: Checkout repository
18
+ uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
19
+ with:
20
+ persist-credentials: false
21
+
22
+ - name: Install uv
23
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
24
+
25
+ - name: Run zizmor
26
+ run: uv run --group dev zizmor --format=sarif . > results.sarif
27
+ env:
28
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
29
+
30
+ - name: Upload SARIF file
31
+ uses: github/codeql-action/upload-sarif@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
32
+ with:
33
+ sarif_file: results.sarif
34
+ category: zizmor
@@ -0,0 +1,8 @@
1
+ rules:
2
+ unpinned-uses:
3
+ config:
4
+ # SHA-pin everything by default; allow ref/tag pins only for first-party
5
+ # GitHub-owned actions.
6
+ policies:
7
+ "actions/*": ref-pin
8
+ "github/*": ref-pin
@@ -0,0 +1,38 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ *.so
6
+ .Python
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ .eggs/
11
+ .venv/
12
+ venv/
13
+ ENV/
14
+
15
+ # Testing / coverage
16
+ .pytest_cache/
17
+ .coverage
18
+ htmlcov/
19
+ .tox/
20
+ .nox/
21
+ .hypothesis/
22
+
23
+ # Type checker caches
24
+ .ty/
25
+ .mypy_cache/
26
+ .ruff_cache/
27
+
28
+ # IDE
29
+ .idea/
30
+ .vscode/
31
+ *.swp
32
+ *.swo
33
+ *~
34
+
35
+ # Claude Code (personal/local only; commit shared settings explicitly)
36
+ .claude/settings.local.json
37
+ .claude/worktrees/
38
+ .claude/scratch/
@@ -0,0 +1,61 @@
1
+ repos:
2
+ - repo: https://github.com/pre-commit/pre-commit-hooks
3
+ rev: v5.0.0
4
+ hooks:
5
+ - id: trailing-whitespace
6
+ exclude: ^tests/fixtures/
7
+ - id: end-of-file-fixer
8
+ exclude: ^tests/fixtures/
9
+ - id: check-yaml
10
+ - id: check-toml
11
+ - id: check-json
12
+ - id: check-added-large-files
13
+ - id: check-case-conflict
14
+ - id: check-merge-conflict
15
+ - id: debug-statements
16
+ - id: detect-private-key
17
+
18
+ # Local hooks run the tools through `uv run`, so the versions come from
19
+ # pyproject.toml's `dev` dependency group. This keeps pre-commit and CI on
20
+ # exactly the same tool versions.
21
+ - repo: local
22
+ hooks:
23
+ - id: ruff-check
24
+ name: ruff check
25
+ entry: uv run --group dev -- ruff check --fix --exit-non-zero-on-fix src tests
26
+ language: system
27
+ files: \.py$
28
+ pass_filenames: false
29
+ require_serial: true
30
+
31
+ - id: ruff-format
32
+ name: ruff format
33
+ entry: uv run --group dev -- ruff format src tests
34
+ language: system
35
+ files: \.py$
36
+ pass_filenames: false
37
+ require_serial: true
38
+
39
+ - id: ty
40
+ name: ty
41
+ entry: uv run --group dev -- ty check src tests
42
+ language: system
43
+ files: \.py$
44
+ pass_filenames: false
45
+ require_serial: true
46
+
47
+ - id: pytest
48
+ name: pytest
49
+ entry: uv run --group dev -- pytest --maxfail=1 -q
50
+ language: system
51
+ files: \.py$
52
+ pass_filenames: false
53
+ stages: [pre-push]
54
+
55
+ - id: uv-lock-check
56
+ name: uv lock --check
57
+ entry: uv lock --check
58
+ language: system
59
+ files: ^(pyproject\.toml|uv\.lock)$
60
+ pass_filenames: false
61
+ require_serial: true
@@ -0,0 +1,37 @@
1
+ # AGENTS.md
2
+
3
+ ## What this repo is
4
+
5
+ `charmlint` is a charm-aware, model-agnostic linter for [Juju](https://juju.is/) charms — rules check charm source code against Canonical's charm best practices (observability, security, testing, metadata, configuration).
6
+
7
+ ## Dev setup
8
+
9
+ ```bash
10
+ uv sync --dev
11
+ ```
12
+
13
+ ## Tests
14
+
15
+ ```bash
16
+ uv run --group dev pytest
17
+ ```
18
+
19
+ ## Lint
20
+
21
+ ```bash
22
+ uv run --group dev ruff check src tests
23
+ uv run --group dev ruff format --check src tests
24
+ uv run --group dev ty check src tests
25
+ ```
26
+
27
+ `pre-commit` runs the same tools through `uv run` (config in `.pre-commit-config.yaml`); tool versions live in `pyproject.toml`'s `dev` dependency group.
28
+
29
+ ## Conventions
30
+
31
+ - Commits and PR titles follow [Conventional Commits](https://www.conventionalcommits.org/); PR-title types enforced by `.github/workflows/validate-pr-title.yaml` (see [CONTRIBUTING.md](CONTRIBUTING.md#pull-requests) for the list).
32
+ - Rule IDs follow `PREFIX###` — see [docs/id-scheme.md](docs/id-scheme.md) for the prefix catalogue.
33
+ - Runtime dependencies are kept minimal (PyYAML only); the `_pypi_attest/` helper is stdlib-only by design.
34
+
35
+ ## Security
36
+
37
+ See [SECURITY.md](SECURITY.md).
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ This project follows the [Ubuntu Code of
4
+ Conduct](https://ubuntu.com/community/ethos/code-of-conduct).
5
+
6
+ Concerns and reports go to the
7
+ [Ubuntu Community Council](https://wiki.ubuntu.com/CommunityCouncil),
8
+ which administers the CoC's reporting and enforcement process. See
9
+ the [Ubuntu Code of Conduct](https://ubuntu.com/community/ethos/code-of-conduct)
10
+ for details.
@@ -0,0 +1,85 @@
1
+ We welcome contributions to this project!
2
+
3
+ Before working on changes, please consider [opening an issue](https://github.com/canonical/charmlint/issues) explaining your use case. If you would like to chat with us about your use cases or proposed implementation, you can reach us on [Matrix](https://matrix.to/#/#charmhub-charmdev:ubuntu.com) or [Discourse](https://discourse.charmhub.io/).
4
+
5
+ <!--
6
+ For detailed dev-environment setup, build, and test instructions, link here to
7
+ the substantive doc if one exists (HACKING.md, docs/contributing.md, etc.).
8
+ Most Charm Tech repos keep this section inline rather than redirecting.
9
+ -->
10
+
11
+ # Project status
12
+
13
+ charmlint is early work, and the implementation is subject to change (and probably will change). Please don't treat anything under `src/` as settled, or build on it expecting the internals to stay where they are.
14
+
15
+ What we're trying to get right at the moment is the set of rules. That's where the care goes: what each rule is for, what it does and doesn't match, and the tests that pin that behaviour down. The tests themselves might change shape later - we'd like them to be less Python-specific, since charmlint is meant to be model-agnostic - so it's not worth over-investing in the current fixtures.
16
+
17
+ The code is almost entirely agent-generated, and that's likely to continue for a while yet.
18
+
19
+ # Review
20
+
21
+ Review still matters, but the weight isn't spread evenly.
22
+
23
+ Most of the attention belongs on the rule itself:
24
+
25
+ * Is this a good thing to have a rule about at all? A rule that's noisy, or that encodes a personal preference rather than a practice we'd defend, costs more than it's worth.
26
+ * What does it match, and (more importantly) what does it not match? A false positive is worse than a gap.
27
+ * Do the tests actually capture that? A test that only exercises the case the rule was written for isn't telling us much.
28
+
29
+ The implementation gets a lighter pass. We're more lenient about "agent-isms" here than in the other Charm Tech repos: if the code is more verbose or more defensive than a person would have written it, and it's correct and readable, that's ok. That's not a license to let quality slide - we still want code we'd be happy to maintain, and unnecessary comments (especially ones referring to history) should still go. But if you find yourself choosing, spend the time on the rule rather than on the style of the code implementing it.
30
+
31
+ # AI
32
+
33
+ You're welcome to submit pull requests that are partly or entirely generated using generative AI tools. However, you must review the code yourself before moving the PR out of draft -- by submitting the PR, you are claiming personal responsibility for its quality and suitability. If you are not capable of reviewing the PR, please do not submit it (maybe you'd like to open an issue instead). PRs that are clearly (co-)authored by tools will be closed without review unless there is a human author that claims responsibility for the PR.
34
+
35
+ Please do not use tools (such as GitHub Copilot) to provide PR reviews. The Charm Tech team also has access to these tools, and will use them when appropriate.
36
+
37
+ # Pull requests
38
+
39
+ Changes are proposed as [pull requests on GitHub](https://github.com/canonical/charmlint/pulls).
40
+
41
+ - Work on a branch in your own fork.
42
+ - Sequence your commits logically if possible. But don't worry too much -- we'll squash to `main` after review.
43
+ - Don't force-push after review has started.
44
+ - Follow [conventional commit style](https://www.conventionalcommits.org/en/) for the PR title (not required for individual commits).
45
+
46
+ The allowed PR-title types — enforced by `.github/workflows/validate-pr-title.yaml` — are:
47
+
48
+ `chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `revert`, `test`
49
+
50
+ Examples:
51
+
52
+ - feat: add support for X
53
+ - fix!: correct the type hinting for config data
54
+ - docs: clarify how to use Y
55
+ - ci: tighten the publish workflow
56
+
57
+ We consider this project too small to use scopes, so we don't use them.
58
+
59
+ ## Branch updates
60
+
61
+ Before you ask for review, please rebase your branch onto `main` so that your changes will merge cleanly.
62
+
63
+ If you need to bring in the latest changes from `main` after the review has started, please use a merge commit.
64
+
65
+ # Releasing
66
+
67
+ <!--
68
+ Most Charm Tech repos that produce a release artefact include a section
69
+ describing how to cut one. The shape depends on what the repo produces:
70
+
71
+ - PyPI package (uv build → Trusted Publishing): tag → GitHub Release →
72
+ release workflow publishes via pypa/gh-action-pypi-publish (OIDC).
73
+ - snap (snapcraft / launchpad build recipe): document the release channel
74
+ promotion flow (edge → beta → candidate → stable).
75
+ - Go binary (goreleaser): document `git tag vX.Y.Z && git push --tags`
76
+ and which workflow goreleaser runs from.
77
+ - Charm on Charmhub (charmcraft): document the track/channel and the
78
+ upload-resource / promote-charm flow.
79
+ - Library shipped via canonical/charmlibs: document the version-bump and
80
+ publish-library flow.
81
+
82
+ Replace this comment with the actual procedure. Repos that don't produce a
83
+ discrete release artefact (demos, tutorials, specs, registries) can drop the
84
+ whole section.
85
+ -->