Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
fail-fast: false
matrix:
# One entry per tool. Add a directory here when you add a package.
package: [ai-failure-notifier]
package: [ai-failure-notifier, charm-tech-baseline]
python-version: ['3.10', '3.12', '3.14']
defaults:
run:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Each tool is its own package in its own top-level directory, with its own `pypro
| directory | what it does |
|---|---|
| [`ai-failure-notifier`](ai-failure-notifier) | Triages and enriches the issue opened when a scheduled workflow fails. |
| [`charm-tech-baseline`](charm-tech-baseline) | Audits a repository against the Charm Tech baseline, and applies the mechanical fixes. |

Code here is consumed by workflow YAML in the repository that runs it, pinned by commit SHA:

Expand Down
61 changes: 61 additions & 0 deletions charm-tech-baseline/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# charm-tech-baseline

Audits a repository against the Canonical Charm Tech baseline that the 26.10
cycle distilled from SSDLC (SEC0023-SEC0061), the Astral OSS-security review,
and per-tool measurement work. It emits a JSON report of findings and can
apply the mechanical fixes.

This is the deterministic half of a pair. The other half is the
`charm-tech-baseline` skill in
[`canonical/charm-tech`](https://github.com/canonical/charm-tech), which is
what an agent reads: when a check applies, what a finding means, which
decisions are already settled, and which tools were measured and skipped.
The split is deliberate. Prose that an agent reads belongs next to the other
skills; code that has to be run, tested and linted belongs here, where it
gets a lockfile and CI.

## Use

```shell
uvx --from "git+https://github.com/canonical/charm-tech-code@<40-char-sha>#subdirectory=charm-tech-baseline" \
charm-tech-baseline check --tier=product
```

- `check` runs every check that applies to the tier and prints one JSON
report. `--only=security-md,dependabot` narrows it; `--format=markdown`
is for reading rather than for parsing.
- `detect-tier` prints `product`, `canonical`, `personal` or `unknown`,
which is what `check` does for itself when `--tier` is not given.
- `fix <name>` applies one mechanical fix, for example
`fix add-code-of-conduct`.
- `list` prints the check and fix names.

Every check reports one of `pass`, `fail`, `na` or `unknown`. `unknown` means
the answer lives somewhere this cannot see - a Drive sheet, a spreadsheet -
and needs a person to look; it is not a quieter `pass`.

## Layout

| path | what it is |
|---|---|
| `checks/` | one module per control, each with a `CHECK_ID` and a `main()` |
| `fixes/` | one module per mechanical remediation |
| `assets/` | templates the fixes copy, and the per-repo AGENTS.md question batteries |
| `common.py` | exit codes, tier matching, result emission |
| `tier.py` | tier detection from the origin remote, resolving forks to upstream |

Checks are imported and called in process by the runner rather than being
shelled out to, so the report is assembled without a round trip through JSON.
A check invoked on its own still prints its own single-line result, which is
how the tests drive them.

Adding a check means adding a module to `checks/` with a `CHECK_ID` and a
`main()` that calls `emit_check` exactly once. The runner finds it, and no
registry needs updating.

## Developing

```shell
uv sync --group unit
uv run pytest
```
36 changes: 36 additions & 0 deletions charm-tech-baseline/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[project]
name = "charm-tech-code-charm-tech-baseline"
version = "0.1.0"
description = "Audit a repository against the Canonical Charm Tech baseline."
readme = "README.md"
requires-python = ">=3.10"
authors = [
{name = "The Charm Tech team at Canonical Ltd."},
]
license = "Apache-2.0"
# PyYAML only. Three checks parse YAML that a regex cannot read honestly -
# dependabot cooldowns, workflow env scoping, and the question batteries - and
# each of those carried its own `# /// script` dependency block when they were
# standalone scripts. Everything else is stdlib, and `gh` and `git` are called
# as subprocesses rather than through a library.
dependencies = ["pyyaml"]

[project.scripts]
charm-tech-baseline = "charm_tech_code.charm_tech_baseline:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src/charm_tech_code"]

[dependency-groups]
unit = ["pytest"]

[tool.pytest.ini_options]
testpaths = ["tests"]

# Ruff configuration is at the root of the monorepo, deliberately not repeated
# here: ruff uses the closest config it finds rather than merging, so a
# [tool.ruff] block in this file would silently override the shared one.
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"""Audit a repository against the Canonical Charm Tech baseline.

The agent-facing half of this lives in the `charm-tech-baseline` skill in
`canonical/charm-tech`; this package is the deterministic half it drives.
"""

from .cli import main

__all__ = ['main']
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# AGENTS.md

<!--
Keep this file SHORT. AGENTS.md is for the first 90 seconds of an
agent's interaction with the repo: how to run tests, how to lint, how
to build, where the conventions live. Depth belongs in CONTRIBUTING.md or
docs/. Under ~50 lines is the target.
-->

## What this repo is

{{REPO_DESCRIPTION_ONE_SENTENCE}}

## Dev setup

```bash
{{SETUP_COMMANDS}}
```

## Tests

```bash
{{TEST_COMMANDS}}
```

## Lint

```bash
{{LINT_COMMANDS}}
```

## Conventions

- Commits follow [Conventional Commits](https://www.conventionalcommits.org/).
- PRs are reviewed before merge; CI must pass.
- For deeper guidance see [{{DEPTH_LINK_TITLE}}]({{DEPTH_LINK}}).

## Security

See [SECURITY.md](SECURITY.md).
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Code of Conduct

This project follows the [Ubuntu Code of Conduct](https://ubuntu.com/community/ethos/code-of-conduct).

Concerns and reports go to the [Ubuntu Community Council](https://wiki.ubuntu.com/CommunityCouncil), which administers the CoC's reporting and enforcement process. See the [Ubuntu Code of Conduct](https://ubuntu.com/community/ethos/code-of-conduct) for details.
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
We welcome contributions to this project!

Before working on changes, please consider [opening an issue](https://github.com/REPLACE_WITH_OWNER/REPLACE_WITH_REPO/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/).

<!--
For detailed dev-environment setup, build, and test instructions, link here to
the substantive doc if one exists (HACKING.md, docs/contributing.md, etc.).
Most Charm Tech repos keep this section inline rather than redirecting.
-->

# AI

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.

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.

# Pull requests

Changes are proposed as [pull requests on GitHub](https://github.com/REPLACE_WITH_OWNER/REPLACE_WITH_REPO/pulls).

- Work on a branch in your own fork.
- Sequence your commits logically if possible. But don't worry too much -- we'll squash to `main` after review.
- Don't force-push after review has started.
- Follow [conventional commit style](https://www.conventionalcommits.org/en/) for the PR title (not required for individual commits).

The allowed PR-title types — enforced by `.github/workflows/validate-pr-title.yaml` — are:

`chore`, `ci`, `docs`, `feat`, `fix`, `perf`, `refactor`, `revert`, `test`

Examples:

- feat: add support for X
- fix!: correct the type hinting for config data
- docs: clarify how to use Y
- ci: tighten the publish workflow

We consider this project too small to use scopes, so we don't use them.

## Branch updates

Before you ask for review, please rebase your branch onto `main` so that your changes will merge cleanly.

If you need to bring in the latest changes from `main` after the review has started, please use a merge commit.

# Releasing

<!--
Most Charm Tech repos that produce a release artefact include a section
describing how to cut one. The shape depends on what the repo produces:

- PyPI package (uv build → Trusted Publishing): tag → GitHub Release →
release workflow publishes via pypa/gh-action-pypi-publish (OIDC).
- snap (snapcraft / launchpad build recipe): document the release channel
promotion flow (edge → beta → candidate → stable).
- Go binary (goreleaser): document `git tag vX.Y.Z && git push --tags`
and which workflow goreleaser runs from.
- Charm on Charmhub (charmcraft): document the track/channel and the
upload-resource / promote-charm flow.
- Library shipped via canonical/charmlibs: document the version-bump and
publish-library flow.

Replace this comment with the actual procedure. Repos that don't produce a
discrete release artefact (demos, tutorials, specs, registries) can drop the
whole section.
-->

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Security policy

## Supported versions

<!--
Replace this section with the project's supported-version statement.
For tools & frameworks: list major versions under support and link the
SECURITY.md matrix. For products mirroring Ubuntu release lifecycle,
state that explicitly.
-->

## Reporting a vulnerability

Please provide a description of the issue, the steps you took to
create the issue, affected versions, and, if known, mitigations for
the issue.

The easiest way to report a security issue is through [GitHub's
security advisories for this project](https://github.com/{{REPO}}/security/advisories/new).
See [Privately reporting a security vulnerability](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing/privately-reporting-a-security-vulnerability)
for instructions on using the feature.

You may also send email to {{CONTACT}}. If you want to encrypt your
email, follow [Canonical's reporting instructions](https://ubuntu.com/security/disclosure-policy#contact-us).

If you have a deadline for public disclosure, please let us know. Our
vulnerability management team intends to respond within 3 working days
of your report. This project aims to resolve all vulnerabilities
within 90 days.

The [Ubuntu Security disclosure and embargo policy](https://ubuntu.com/security/disclosure-policy)
contains more information about what you can expect when you contact
us, and what we expect from you.

To stay informed about vulnerabilities, watch:

- The [GitHub Security Advisories for `{{REPO}}`](https://github.com/{{REPO}}/security/advisories).
- The project's release history.
- Relevant [Ubuntu Security Notices](https://ubuntu.com/security/notices) when a vulnerability
also affects an Ubuntu-packaged component.
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Copyright 2025 Canonical Ltd.
# See LICENSE file for licensing details.

"""Check that a PR title follows the Conventional Commits specification.

Reads the PR title from the PR_TITLE environment variable.
Exits with a non-zero status and prints an error message if the title is invalid.

Reference: https://www.conventionalcommits.org/en/v1.0.0/

This repo defines a restricted set of commit types and disallows scopes in PR titles.
"""

from __future__ import annotations

import os
import re
import sys

_TYPES = frozenset({
'chore',
'ci',
'docs',
'feat',
'fix',
'perf',
'refactor',
'revert',
'test',
})

# <type>[optional scope][optional !]: <description>
_PATTERN = re.compile(
r'^(?P<type>[A-Za-z]+)' # lower-case only, but let this be validated by _TYPES
r'(?:\((?P<scope>[^()]+)\))?'
r'(?P<breaking>!)?'
r': '
r'(?P<description>.+)$'
)

# Adjust this URL when copying into a new repo — point at <this repo>/CONTRIBUTING.md#pull-requests.
_HELP_URL = 'https://github.com/REPLACE_WITH_OWNER/REPLACE_WITH_REPO/blob/main/CONTRIBUTING.md#pull-requests'


def _main() -> None:
title = os.environ.get('PR_TITLE', '').strip()
if not title:
print('PR_TITLE environment variable is not set or empty.', file=sys.stderr)
sys.exit(1)

match = _PATTERN.match(title)
if not match:
print(
f'PR title does not follow Conventional Commits format.\n'
f'Expected: <type>[!]: <description>\n'
f'Got: {title!r}\n'
f'Read more: {_HELP_URL}',
file=sys.stderr,
)
sys.exit(1)

scope = match.group('scope')
if scope is not None:
print(
f'Scopes must not be used in PR titles.\n'
f'Got: {title!r}\n'
f'Read more: {_HELP_URL}',
file=sys.stderr,
)
sys.exit(1)

commit_type = match.group('type')
if commit_type not in _TYPES:
print(
f'Invalid type {commit_type!r} in PR title.\n'
f'Valid types: {", ".join(sorted(_TYPES))}\n'
f'Got: {title!r}\n'
f'Read more: {_HELP_URL}',
file=sys.stderr,
)
sys.exit(1)

print(f'OK: {title!r}')


if __name__ == '__main__':
_main()
Loading