Skip to content
Merged
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
3 changes: 2 additions & 1 deletion .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ merge), so keep it conventional: type(scope): summary -->
- [ ] `./venv/bin/ruff check .` and `./venv/bin/ruff format .` pass
- [ ] `./venv/bin/pytest -q` passes
- [ ] Docs updated in this PR, if this changes a config option, an importer,
or an emissions provider
or an emissions provider
- [ ] Tests added or updated, if this changes behaviour
- [ ] Skimmed `gh issue list` for open issues this change touches

Expand All @@ -25,6 +25,7 @@ merge), so keep it conventional: type(scope): summary -->
spend the time. On a PR ready for review: what you tested and what you saw. -->

<!-- This file opens with a comment, not a heading, by design. -->

<!-- markdownlint-configure-file {
"MD041": false
} -->
33 changes: 19 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,23 +57,28 @@ jobs:

docs:
name: markdown
# `npx` fetches Prettier from the registry on every run, and a stalled
# fetch hangs rather than failing. The same line in atdr/contrail-gh has
# sat silent for exactly 301 seconds, npm's fetch-timeout expiring and the
# retry succeeding, in a job that takes twelve seconds when the fetch is
# clean. Ten minutes bounds a stall that never recovers.
# Ten minutes against a job that takes seconds. The point is a `pip` fetch
# that stalls rather than fails, which would otherwise sit until GitHub's
# six-hour default.
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# An action rather than `npx`, so the github-actions dependabot ecosystem
# already configured for this repo keeps it current. Prettier has no
# first-party action, so its pin below is a hand edit — the same deal as
# the `rev:` pins in .pre-commit-config.yaml.
# A JavaScript action, so GitHub runs it on the runner's own Node: no
# toolchain to install, nothing fetched from a registry, and the
# github-actions dependabot ecosystem already configured for this repo
# keeps the pin current. That is why there is no setup-node step here.
- uses: DavidAnson/markdownlint-cli2-action@v24
- uses: actions/setup-node@v7
# 3.13 rather than the 3.12 the other jobs use, and not an oversight:
# .mdformat.toml sets `exclude`, which errors below 3.13. It says why.
- uses: actions/setup-python@v7
with:
node-version: "24"
# Prettier owns table alignment and whitespace; markdownlint owns the
# rules it can't express. Neither is redundant.
- run: npx prettier@3.9.6 --check "**/*.md"
python-version: "3.13"
# mdformat's pin lives in pyproject.toml's dev extra, where the pip
# dependabot ecosystem bumps it. Prettier's had to be a hand edit in this
# file, because it has no first-party action and `npx` fetched it fresh
# on every run — a fetch that sat silent for exactly 301 seconds often
# enough to dominate a job that otherwise takes twelve.
- run: pip install -e ".[dev]"
# No filter: .mdformat.toml's `exclude` covers CHANGELOG.md and the rest.
- run: mdformat --check .
15 changes: 8 additions & 7 deletions .markdownlint-cli2.yaml
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# Prettier owns whitespace and table alignment. These are the rules it cannot
# mdformat owns whitespace and table alignment. These are the rules it cannot
# express. Kept in step by hand across contrail, contrail-gh and any instance
# created from it — the three files are copies, not a shared package.
# created from it — the files are copies, not a shared package.
#
# .prettierrc.json carries two settings and no room to explain them:
# `proseWrap: preserve`, because the prose here is wrapped by hand at
# deliberate points and `always` would rewrap every paragraph in the repo; and
# `embeddedLanguageFormatting: off`, because the code inside a fence is a
# worked example or a command someone will paste, not source to be tidied.
# .mdformat.toml carries the formatter's settings and the shared ignore list,
# and explains both. Two things it does not have to say, because mdformat's
# defaults already do what this repo wants: prose is never rewrapped (`wrap`
# defaults to keep, and the prose here is wrapped by hand at deliberate
# points), and the code inside a fence is left alone unless a codeformatter
# plugin is installed, which is why pyproject.toml says never to add one.
gitignore: true

# Without this, a bare `markdownlint-cli2` finds nothing and reports success.
Expand Down
39 changes: 39 additions & 0 deletions .mdformat.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Markdown formatting. mdformat owns table padding and whitespace;
# markdownlint-cli2 owns line length and the rest, in .markdownlint-cli2.yaml.
# Neither is redundant: markdownlint has no rule that pads a table cell, and
# mdformat has no opinion on line length.
#
# Kept in step by hand across contrail, contrail-gh and any instance created
# from it — the files are copies, not a shared package.

# Without this, every ordered list is renumbered to `1.` on the next run.
number = true

# The successor to .prettierignore. `exclude` applies to paths passed
# explicitly, not just to a directory walk, so this one list also covers the
# files pre-commit hands over — which is why the hook carries no `exclude:` of
# its own and ci.yml needs no filter.
#
# It is also why both call sites pin Python 3.13: this key errors outright
# below it, and mdformat says so rather than ignoring it. That is a deliberate
# trade. Writing these three entries twice instead, in a shell filter and a
# pre-commit regex, would buy nothing but the ability to format Markdown on an
# interpreter nobody here runs. `requires-python` is unaffected — the matrix
# still tests 3.11.
# Unlike Prettier and markdownlint-cli2, mdformat does not read .gitignore, so
# anything ignored that holds Markdown has to be named here or a full-tree run
# walks into it. site-packages alone carries hundreds of other projects'
# READMEs. CI never has a venv; a dev machine always does.
exclude = [
"venv/**",
".venv/**",
# Regenerated by release-please from commit subjects. A reformat here is
# undone on the next release and can desync the manifest.
"CHANGELOG.md",
# A symlink to AGENTS.md. Formatting it writes the same file twice.
"CLAUDE.md",
# Local agent scratch, kept out of this public repo via .git/info/exclude,
# which mdformat does not read. CI never sees it; a full-tree run on a dev
# machine does.
".claude/**",
]
23 changes: 18 additions & 5 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,22 @@ repos:
# them twice.
pass_filenames: false

# The maintained fork; pre-commit/mirrors-prettier is archived.
- repo: https://github.com/rbubley/mirrors-prettier
rev: v3.9.6
- repo: https://github.com/hukkin/mdformat
rev: 1.0.0
hooks:
- id: prettier
types_or: [markdown]
- id: mdformat
# Both are extensions, not core CommonMark. Without mdformat-gfm every
# padded table in the repo is reflowed into unpadded pipes; without
# mdformat-frontmatter a YAML block at the top of a file is rewritten
# into a horizontal rule and a heading. Keep in step with the floors in
# pyproject.toml, which explain them.
additional_dependencies:
- mdformat-gfm==1.0.0
- mdformat-frontmatter==2.1.2
# .mdformat.toml sets `exclude`, which errors below 3.13. Pinning the
# hook's own env means that holds whatever the developer's default
# interpreter is.
language_version: python3.13
# No `exclude:` here on purpose. .mdformat.toml owns that list, and it
# applies to the paths pre-commit passes as well as to a directory
# walk, so a second copy here could only drift from it.
7 changes: 0 additions & 7 deletions .prettierignore

This file was deleted.

4 changes: 0 additions & 4 deletions .prettierrc.json

This file was deleted.

37 changes: 24 additions & 13 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,19 @@ Model, writes a CSV.
## Commands

```bash
python3.12 -m venv venv && ./venv/bin/pip install -e ".[dev]"
python3.13 -m venv venv && ./venv/bin/pip install -e ".[dev]"
./venv/bin/pytest -q
./venv/bin/ruff check . && ./venv/bin/ruff format .
TRIPIT_ICAL_URL=tests/fixtures/sample_feed.ics \
FLIGHTY_CSV_PATH=tests/fixtures/sample_flighty.csv ./venv/bin/contrail sync --dry-run
./venv/bin/python scripts/refresh_airline_codes.py # needs network; run by hand
```

Python 3.11+. The default `python3` on this machine is
3.7 — use `/usr/local/bin/python3.12` explicitly.
Python 3.11+ to run, 3.13 to develop: `.mdformat.toml` sets `exclude`, which
errors below 3.13, so a 3.12 venv would put an `mdformat` on `PATH` that cannot
read its own config. The published floor is unchanged and the matrix still
tests 3.11. The default `python3` on this machine is 3.7 — use
`/usr/local/bin/python3.13` explicitly.

## Architecture

Expand Down Expand Up @@ -49,11 +52,14 @@ Two keys, and the difference matters:

- Conventional commits. release-please owns versions, tags and `CHANGELOG.md` —
never hand-edit the changelog or the version in `pyproject.toml`.

- `__version__` is read from installed package metadata, so `pyproject.toml` is
the single source of truth. Code that needs the version imports `__version__`
rather than spelling it out — see the gotcha on version pins for the two
literals that remain.

- **No test may make a real network call.** Mock `requests` in both directions.

- **Every workflow is named after its own file**, and the description goes on
the job. GitHub labels a check `<workflow name> / <job name>` and never shows
the filename, so `pr-title / conventional title` names both the file to open
Expand All @@ -65,19 +71,24 @@ Two keys, and the difference matters:
`Analyze (actions)` checks are the exception and always will be: CodeQL runs
from default setup, which is a repo setting rather than a file, so its name
and its timeout are GitHub's to choose.

- **Every job that can carry `timeout-minutes` sets one.** GitHub's default is
six hours, and the failure that matters is a stall rather than an error: a
`pip` or `npx` fetch that hangs never fails on its own. Ten minutes
everywhere except `pr-title.yml`, which gets five. A job whose only key is
`uses:` cannot carry it, which is why the setting lives inside a reusable
workflow rather than on its callers.
- **Markdown is formatted, not hand-aligned.** Prettier owns table padding and

- **Markdown is formatted, not hand-aligned.** mdformat owns table padding and
whitespace; markdownlint-cli2 owns line length and the rest. Run
`npx prettier@3.9.6 --write "**/*.md"` rather than lining a table up by
hand. Prose wraps at 80, except `README.md`, which wraps at 100 and says so
in a `markdownlint-configure-file` comment at its foot.
`./venv/bin/mdformat .` rather than lining a table up by hand. Prose wraps at
80, except `README.md`, which wraps at 100 and says so in a
`markdownlint-configure-file` comment at its foot — a per-file override only
markdownlint honours, and one reason it is still here.

- This repo is public. Never commit a real CSV, a raw log, or `config.json` —
all are gitignored.

- **When updating docs at the end of a change, skim the open issues**
(`gh issue list`) for any the change touched. Cheap, and it catches both
directions: an issue quietly fixed, and one made easier to hit.
Expand Down Expand Up @@ -150,17 +161,17 @@ Two keys, and the difference matters:
drifted before. A _new_ literal version anywhere else is a bug: import
`__version__` in code, or wire the file into `extra-files` if it's not code.
- **release-please needs the repo setting "Allow GitHub Actions to create and
approve pull requests"** (Settings → Actions → General). `permissions:
pull-requests: write` in the workflow is _not_ sufficient on its own, and the
API can report the flag as enabled while it is still blocked. Without it the
release job fails with "GitHub Actions is not permitted to create or approve
pull requests".
approve pull requests"** (Settings → Actions → General). Granting
`pull-requests: write` in the workflow is _not_ sufficient on its own, and
the API can report the flag as enabled while it is still blocked. Without it
the release job fails with "GitHub Actions is not permitted to create or
approve pull requests".
- **`CHANGELOG.md` and `CLAUDE.md` are excluded from both Markdown tools.**
release-please regenerates the changelog from commit subjects, so a
reformat there is undone on the next release and can desync the manifest;
`CLAUDE.md` is a symlink to `AGENTS.md`, so linting it reports every line
twice and formatting it writes the same file twice. Both are named in
`.markdownlint-cli2.yaml` and `.prettierignore`.
`.markdownlint-cli2.yaml` and `.mdformat.toml`.
- **`cli._now()` exists to be monkeypatched.** Tests that use the real clock rot
once the fixture's dates fall into the past.
- **`src/contrail/data/airline_codes.csv` is generated, never hand-edited.** Fix
Expand Down
10 changes: 5 additions & 5 deletions CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,14 +121,14 @@ version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].

Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].
[Mozilla's code of conduct enforcement ladder][mozilla coc].

For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at
[https://www.contributor-covenant.org/faq][faq]. Translations are available at
[https://www.contributor-covenant.org/translations][translations].

[faq]: https://www.contributor-covenant.org/faq
[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq
[mozilla coc]: https://github.com/mozilla/diversity
[translations]: https://www.contributor-covenant.org/translations
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,7 @@ changelog are handled by release-please.
MIT — see [LICENSE](LICENSE).

<!-- This file is wrapped at 100, not the 80 the rest of the repo uses. -->

<!-- markdownlint-configure-file {
"MD013": { "line_length": 100, "tables": false, "code_blocks": false }
} -->
10 changes: 5 additions & 5 deletions docs/contrail-gh.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,11 +163,11 @@ hand, so nothing else would catch it.

## The Markdown config is duplicated, not shared

`.markdownlint-cli2.yaml`, `.prettierrc.json` and `.prettierignore` exist in this
repo, in the template, and in every instance created from it. They are copies:
three separate repositories can't share a config file, and neither tool reads one
from a package. Change a rule here and the same edit has to be made in the
template, or the two repos start disagreeing about what correct Markdown is.
`.markdownlint-cli2.yaml` and `.mdformat.toml` exist in this repo, in the
template, and in every instance created from it. They are copies: three separate
repositories can't share a config file, and neither tool reads one from a
package. Change a rule here and the same edit has to be made in the template, or
the two repos start disagreeing about what correct Markdown is.

The template's copies ship to instances through "Use this template", so its
`README.md` names them in the recipe for pulling template updates — the workflow
Expand Down
2 changes: 2 additions & 0 deletions docs/resync.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ exactly. The cost of the clash is double counting, not a wrong figure.
every stored row changed on the first sync after an upgrade — and `changed` is
what bypasses the no-downgrade guard, which would replace a whole file of exact
figures with route averages and then freeze them that way.

- **Only a source that returned something may cancel its own rows.** With several
sources configured, one silently empty source would otherwise cancel every
flight it owns while the others kept a global guard happy. Granularity is the
Expand All @@ -126,6 +127,7 @@ exactly. The cost of the clash is double counting, not a wrong figure.

- **A feed yielding no flights at all refuses to cancel anything** and exits
non-zero. `--dry-run` is exempt: it can neither write nor cancel.

- **The file is written only when content actually changed.** Re-pricing runs
unconditionally, so `_merge_row` keeps the original `sync_timestamp` on a no-op
and `cmd_sync` compares against the rows as loaded. Otherwise contrail-gh would
Expand Down
19 changes: 19 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,25 @@ dev = [
# tools. Optional to use — CI is still the gate — but declared here so
# `pip install -e ".[dev]"` leaves nothing to install by hand.
"pre-commit>=4.6.2",
# Formats Markdown; markdownlint-cli2 checks what it cannot express. Both
# `docs` in ci.yml and the pre-commit hook run 3.13, because .mdformat.toml
# sets `exclude` and that key errors below it — see the comment there.
#
# Never add an mdformat *codeformatter* plugin (mdformat-black and the
# like). mdformat enables every installed one by default, which would
# reformat the code inside fences — worked examples and commands people
# paste, not source to tidy. Prettier needed embeddedLanguageFormatting
# off for the same reason.
"mdformat>=1.0.0",
# Tables. Core CommonMark has none, so without this every padded table is
# reflowed into unpadded pipes.
"mdformat-gfm>=1.0.0",
# YAML frontmatter. mdformat does not recognise it otherwise and rewrites
# the block into a horizontal rule and a heading — it did exactly that to a
# skill file in contrail-gh. Nothing tracked here carries frontmatter
# today, which is why this is a guard rather than a fix: the failure is
# silent, and the first .md that needs it would not announce itself.
"mdformat-frontmatter>=2.1.2",
# Self-reference rather than repeating the pyyaml floor: declared once, so
# the two can't drift, and Dependabot sees pyyaml for what it is — a runtime
# dependency of the `yaml` extra, not a dev tool.
Expand Down