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.
- charmlint-0.1.1/.github/check-conventional-pr-title.py +87 -0
- charmlint-0.1.1/.github/dependabot.yaml +19 -0
- charmlint-0.1.1/.github/workflows/ci.yaml +43 -0
- charmlint-0.1.1/.github/workflows/dependency-review.yaml +22 -0
- charmlint-0.1.1/.github/workflows/publish.yaml +104 -0
- charmlint-0.1.1/.github/workflows/validate-pr-title.yaml +21 -0
- charmlint-0.1.1/.github/workflows/zizmor.yaml +34 -0
- charmlint-0.1.1/.github/zizmor.yaml +8 -0
- charmlint-0.1.1/.gitignore +38 -0
- charmlint-0.1.1/.pre-commit-config.yaml +61 -0
- charmlint-0.1.1/AGENTS.md +37 -0
- charmlint-0.1.1/CODE_OF_CONDUCT.md +10 -0
- charmlint-0.1.1/CONTRIBUTING.md +85 -0
- charmlint-0.1.1/LICENSE +190 -0
- charmlint-0.1.1/Makefile +21 -0
- charmlint-0.1.1/PKG-INFO +124 -0
- charmlint-0.1.1/README.md +99 -0
- charmlint-0.1.1/RULES_TRACKER.md +67 -0
- charmlint-0.1.1/SECURITY.md +40 -0
- charmlint-0.1.1/docs/id-scheme.md +48 -0
- charmlint-0.1.1/docs/versioning.md +86 -0
- charmlint-0.1.1/pyproject.toml +90 -0
- charmlint-0.1.1/src/charmlint/__init__.py +11 -0
- charmlint-0.1.1/src/charmlint/__main__.py +6 -0
- charmlint-0.1.1/src/charmlint/_ast.py +421 -0
- charmlint-0.1.1/src/charmlint/_cli.py +237 -0
- charmlint-0.1.1/src/charmlint/_config.py +189 -0
- charmlint-0.1.1/src/charmlint/_linter.py +331 -0
- charmlint-0.1.1/src/charmlint/_models.py +328 -0
- charmlint-0.1.1/src/charmlint/_noqa.py +103 -0
- charmlint-0.1.1/src/charmlint/_pypi_attest.py +238 -0
- charmlint-0.1.1/src/charmlint/_rules/__init__.py +17 -0
- charmlint-0.1.1/src/charmlint/_rules/_base.py +91 -0
- charmlint-0.1.1/src/charmlint/_rules/actions.py +175 -0
- charmlint-0.1.1/src/charmlint/_rules/charmcraft_compat.py +489 -0
- charmlint-0.1.1/src/charmlint/_rules/config_quality.py +81 -0
- charmlint-0.1.1/src/charmlint/_rules/documentation.py +24 -0
- charmlint-0.1.1/src/charmlint/_rules/libraries.py +105 -0
- charmlint-0.1.1/src/charmlint/_rules/metadata.py +279 -0
- charmlint-0.1.1/src/charmlint/_rules/pebble.py +139 -0
- charmlint-0.1.1/src/charmlint/_rules/security.py +44 -0
- charmlint-0.1.1/src/charmlint/_rules/structure.py +46 -0
- charmlint-0.1.1/src/charmlint/_rules/testing.py +20 -0
- charmlint-0.1.1/src/charmlint/_yaml.py +142 -0
- charmlint-0.1.1/tests/__init__.py +0 -0
- charmlint-0.1.1/tests/conftest.py +116 -0
- charmlint-0.1.1/tests/test_actions.py +433 -0
- charmlint-0.1.1/tests/test_ast.py +461 -0
- charmlint-0.1.1/tests/test_charmcraft_compat.py +800 -0
- charmlint-0.1.1/tests/test_cli.py +68 -0
- charmlint-0.1.1/tests/test_config.py +147 -0
- charmlint-0.1.1/tests/test_config_quality.py +226 -0
- charmlint-0.1.1/tests/test_linter.py +228 -0
- charmlint-0.1.1/tests/test_metadata.py +265 -0
- charmlint-0.1.1/tests/test_models.py +94 -0
- charmlint-0.1.1/tests/test_noqa.py +150 -0
- charmlint-0.1.1/tests/test_pebble.py +240 -0
- charmlint-0.1.1/tests/test_properties.py +205 -0
- charmlint-0.1.1/tests/test_rules.py +409 -0
- charmlint-0.1.1/tests/test_security.py +85 -0
- charmlint-0.1.1/tests/test_yaml.py +234 -0
- charmlint-0.1.1/tools/refresh_charmlibs_map.py +91 -0
- 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,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
|
+
-->
|