mdcompose 0.1.0__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 (101) hide show
  1. mdcompose-0.1.0/.gitattributes +11 -0
  2. mdcompose-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +51 -0
  3. mdcompose-0.1.0/.github/ISSUE_TEMPLATE/config.yml +5 -0
  4. mdcompose-0.1.0/.github/ISSUE_TEMPLATE/feature_request.yml +28 -0
  5. mdcompose-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +11 -0
  6. mdcompose-0.1.0/.github/workflows/bump.yml +97 -0
  7. mdcompose-0.1.0/.github/workflows/ci.yml +318 -0
  8. mdcompose-0.1.0/.github/workflows/codeql.yml +39 -0
  9. mdcompose-0.1.0/.github/workflows/dependency-review.yml +45 -0
  10. mdcompose-0.1.0/.github/workflows/mutation.yml +55 -0
  11. mdcompose-0.1.0/.github/workflows/python-eol.yml +74 -0
  12. mdcompose-0.1.0/.github/workflows/release.yml +181 -0
  13. mdcompose-0.1.0/.github/workflows/scorecard.yml +47 -0
  14. mdcompose-0.1.0/.github/workflows/supply-chain.yml +151 -0
  15. mdcompose-0.1.0/.github/zizmor.yml +11 -0
  16. mdcompose-0.1.0/.gitignore +58 -0
  17. mdcompose-0.1.0/.pre-commit-config.yaml +78 -0
  18. mdcompose-0.1.0/CHANGELOG.md +20 -0
  19. mdcompose-0.1.0/CODE_OF_CONDUCT.md +85 -0
  20. mdcompose-0.1.0/CONTRIBUTING.md +107 -0
  21. mdcompose-0.1.0/LICENSE +21 -0
  22. mdcompose-0.1.0/PKG-INFO +234 -0
  23. mdcompose-0.1.0/README.md +211 -0
  24. mdcompose-0.1.0/SECURITY.md +27 -0
  25. mdcompose-0.1.0/TODO.md +536 -0
  26. mdcompose-0.1.0/docs/benchmarks.md +46 -0
  27. mdcompose-0.1.0/docs/commit-and-release-tooling.md +196 -0
  28. mdcompose-0.1.0/docs/concepts.md +154 -0
  29. mdcompose-0.1.0/docs/core-contract.md +914 -0
  30. mdcompose-0.1.0/docs/hardening-review.md +53 -0
  31. mdcompose-0.1.0/docs/optional-tooling.md +79 -0
  32. mdcompose-0.1.0/docs/publishing.md +136 -0
  33. mdcompose-0.1.0/docs/scorecard.md +113 -0
  34. mdcompose-0.1.0/docs/versioning-explained.md +173 -0
  35. mdcompose-0.1.0/mdcompose/__init__.py +0 -0
  36. mdcompose-0.1.0/mdcompose/cli.py +219 -0
  37. mdcompose-0.1.0/mdcompose/commands/__init__.py +0 -0
  38. mdcompose-0.1.0/mdcompose/commands/config_cmd.py +172 -0
  39. mdcompose-0.1.0/mdcompose/commands/convert_cmd.py +299 -0
  40. mdcompose-0.1.0/mdcompose/commands/eject_cmd.py +180 -0
  41. mdcompose-0.1.0/mdcompose/commands/import_cmd.py +351 -0
  42. mdcompose-0.1.0/mdcompose/commands/init_cmd.py +668 -0
  43. mdcompose-0.1.0/mdcompose/commands/snippet_cmd.py +312 -0
  44. mdcompose-0.1.0/mdcompose/commands/target_cmd.py +134 -0
  45. mdcompose-0.1.0/mdcompose/core/__init__.py +0 -0
  46. mdcompose-0.1.0/mdcompose/core/composition.py +96 -0
  47. mdcompose-0.1.0/mdcompose/core/config.py +454 -0
  48. mdcompose-0.1.0/mdcompose/core/convert_ops.py +193 -0
  49. mdcompose-0.1.0/mdcompose/core/eject.py +168 -0
  50. mdcompose-0.1.0/mdcompose/core/exit_codes.py +28 -0
  51. mdcompose-0.1.0/mdcompose/core/files.py +194 -0
  52. mdcompose-0.1.0/mdcompose/core/import_ops.py +175 -0
  53. mdcompose-0.1.0/mdcompose/core/init_ops.py +436 -0
  54. mdcompose-0.1.0/mdcompose/core/library_ops.py +199 -0
  55. mdcompose-0.1.0/mdcompose/core/managed_block.py +380 -0
  56. mdcompose-0.1.0/mdcompose/core/manifest.py +404 -0
  57. mdcompose-0.1.0/mdcompose/core/output.py +208 -0
  58. mdcompose-0.1.0/mdcompose/core/platform.py +368 -0
  59. mdcompose-0.1.0/mdcompose/core/report.py +278 -0
  60. mdcompose-0.1.0/mdcompose/core/sections.py +215 -0
  61. mdcompose-0.1.0/mdcompose/core/snippets.py +359 -0
  62. mdcompose-0.1.0/mdcompose/core/targets.py +200 -0
  63. mdcompose-0.1.0/mdcompose/prompts.py +102 -0
  64. mdcompose-0.1.0/mdcompose/version.py +31 -0
  65. mdcompose-0.1.0/pyproject.toml +192 -0
  66. mdcompose-0.1.0/renovate.json +39 -0
  67. mdcompose-0.1.0/scripts/check_ascii.py +89 -0
  68. mdcompose-0.1.0/scripts/check_licenses.py +156 -0
  69. mdcompose-0.1.0/tests/benchmarks/conftest.py +100 -0
  70. mdcompose-0.1.0/tests/benchmarks/test_core_ops.py +87 -0
  71. mdcompose-0.1.0/tests/conftest.py +91 -0
  72. mdcompose-0.1.0/tests/fixtures/plain.bom.md +4 -0
  73. mdcompose-0.1.0/tests/fixtures/plain.crlf.md +4 -0
  74. mdcompose-0.1.0/tests/fixtures/plain.lf.md +4 -0
  75. mdcompose-0.1.0/tests/fixtures/prose.cp1252.md +3 -0
  76. mdcompose-0.1.0/tests/test_cli.py +377 -0
  77. mdcompose-0.1.0/tests/test_config.py +366 -0
  78. mdcompose-0.1.0/tests/test_config_cmd.py +382 -0
  79. mdcompose-0.1.0/tests/test_convert.py +484 -0
  80. mdcompose-0.1.0/tests/test_doctor_drift.py +224 -0
  81. mdcompose-0.1.0/tests/test_e2e_import_convert.py +181 -0
  82. mdcompose-0.1.0/tests/test_eject.py +435 -0
  83. mdcompose-0.1.0/tests/test_files.py +236 -0
  84. mdcompose-0.1.0/tests/test_fuzz.py +82 -0
  85. mdcompose-0.1.0/tests/test_import_cmd.py +610 -0
  86. mdcompose-0.1.0/tests/test_import_ops.py +265 -0
  87. mdcompose-0.1.0/tests/test_init.py +657 -0
  88. mdcompose-0.1.0/tests/test_init_ops.py +299 -0
  89. mdcompose-0.1.0/tests/test_layering.py +82 -0
  90. mdcompose-0.1.0/tests/test_managed_block.py +306 -0
  91. mdcompose-0.1.0/tests/test_manifest.py +356 -0
  92. mdcompose-0.1.0/tests/test_onedrive.py +137 -0
  93. mdcompose-0.1.0/tests/test_output.py +192 -0
  94. mdcompose-0.1.0/tests/test_platform.py +220 -0
  95. mdcompose-0.1.0/tests/test_prompts.py +93 -0
  96. mdcompose-0.1.0/tests/test_report.py +213 -0
  97. mdcompose-0.1.0/tests/test_sections.py +230 -0
  98. mdcompose-0.1.0/tests/test_snippet_commands.py +493 -0
  99. mdcompose-0.1.0/tests/test_snippets.py +349 -0
  100. mdcompose-0.1.0/tests/test_targets.py +562 -0
  101. mdcompose-0.1.0/uv.lock +1773 -0
@@ -0,0 +1,11 @@
1
+ # Normalize every text file to LF in the repository and in working copies.
2
+ # This project's whole design turns on line-ending correctness, so it must not
3
+ # depend on a contributor's core.autocrlf setting.
4
+ * text=auto eol=lf
5
+
6
+ # Test fixtures that deliberately carry a specific encoding or line ending must
7
+ # survive checkout byte for byte, otherwise the tests that prove normalization
8
+ # works would be testing normalized inputs.
9
+ tests/fixtures/**/*.crlf.md -text
10
+ tests/fixtures/**/*.bom.md -text
11
+ tests/fixtures/**/*.cp1252.md -text
@@ -0,0 +1,51 @@
1
+ name: Bug report
2
+ description: Something mdcompose does wrong
3
+ labels: [bug]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ A failing test case is the fastest possible bug report. If you can write
9
+ one, open a pull request instead of an issue.
10
+ - type: textarea
11
+ id: what
12
+ attributes:
13
+ label: What happened
14
+ description: The command you ran and what mdcompose did.
15
+ render: text
16
+ validations:
17
+ required: true
18
+ - type: textarea
19
+ id: expected
20
+ attributes:
21
+ label: What you expected instead
22
+ validations:
23
+ required: true
24
+ - type: textarea
25
+ id: repro
26
+ attributes:
27
+ label: Smallest reproduction
28
+ description: The fewest steps, files, or snippets that still show the problem.
29
+ validations:
30
+ required: true
31
+ - type: textarea
32
+ id: doctor
33
+ attributes:
34
+ label: Output of mdcompose doctor
35
+ render: text
36
+ validations:
37
+ required: true
38
+ - type: input
39
+ id: version
40
+ attributes:
41
+ label: mdcompose version and how you installed it
42
+ placeholder: 0.1.0, pipx
43
+ validations:
44
+ required: true
45
+ - type: input
46
+ id: platform
47
+ attributes:
48
+ label: Operating system and Python version
49
+ placeholder: Windows 11, Python 3.12
50
+ validations:
51
+ required: true
@@ -0,0 +1,5 @@
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Question or discussion
4
+ url: https://github.com/Rovetown/mdcompose/discussions
5
+ about: Usage questions and early ideas that are not yet a bug or a concrete request.
@@ -0,0 +1,28 @@
1
+ name: Feature request
2
+ description: Suggest a capability or a change
3
+ labels: [enhancement]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ Read the "Deliberate non-goals" section of AGENTS.md first: a few things
9
+ are excluded on purpose, with the reasoning.
10
+ - type: textarea
11
+ id: problem
12
+ attributes:
13
+ label: The problem
14
+ description: What are you trying to do that mdcompose makes hard or impossible?
15
+ validations:
16
+ required: true
17
+ - type: textarea
18
+ id: idea
19
+ attributes:
20
+ label: The change you have in mind
21
+ validations:
22
+ required: true
23
+ - type: textarea
24
+ id: alternatives
25
+ attributes:
26
+ label: Alternatives you considered
27
+ validations:
28
+ required: false
@@ -0,0 +1,11 @@
1
+ <!-- What does this change, and why.
2
+ Link the issue if there is one. -->
3
+
4
+ ## Checklist
5
+
6
+ - [ ] `uv run pytest` passes, and so does `uv sync --locked --python 3.11 && uv run --no-sync pytest`
7
+ - [ ] `uv run ruff check .` is clean
8
+ - [ ] `uv run pre-commit run --all-files` is clean
9
+ - [ ] Every commit is a Conventional Commit (`uv run cz check --rev-range origin/main..HEAD`)
10
+ - [ ] Plain ASCII in everything the change adds
11
+ - [ ] Behavior change: [docs/core-contract.md](../docs/core-contract.md) updated in the same commit
@@ -0,0 +1,97 @@
1
+ name: bump
2
+
3
+ # The release button. Run this from the Actions tab and pick a channel:
4
+ #
5
+ # stable -> a stable release (0.3.1 -> 0.3.2 / 0.4.0 / 1.0.0, from the commits)
6
+ # alpha -> 0.4.0a1, then a2, ... feature-incomplete, breakage expected
7
+ # beta -> 0.4.0b1, ... feature-complete, bug hunting
8
+ # rc -> 0.4.0rc1, ... believed shippable
9
+ #
10
+ # Run it again with "stable" to promote the current pre-release to the final
11
+ # version. cz bump derives the number from the Conventional Commits since
12
+ # the last tag, rewrites [project].version and CHANGELOG.md, commits, and pushes
13
+ # the tag that release.yml publishes on. You never type a version.
14
+ #
15
+ # Requires a repository secret RELEASE_TOKEN: a fine-grained PAT (or GitHub App
16
+ # token) with contents:write. The built-in GITHUB_TOKEN cannot be used here,
17
+ # because a tag it pushes does not trigger release.yml and its push is blocked by
18
+ # branch protection. See TODO.md, "The bump.yml PAT wrinkle".
19
+
20
+ on:
21
+ workflow_dispatch:
22
+ inputs:
23
+ prerelease:
24
+ description: Release channel
25
+ type: choice
26
+ default: stable
27
+ options:
28
+ - stable
29
+ - alpha
30
+ - beta
31
+ - rc
32
+
33
+ # Never let two bumps run at once.
34
+ concurrency:
35
+ group: bump
36
+ cancel-in-progress: false
37
+
38
+ permissions:
39
+ contents: read
40
+
41
+ jobs:
42
+ bump:
43
+ name: bump version, changelog and tag
44
+ runs-on: ubuntu-latest
45
+ timeout-minutes: 10
46
+ # A release comes only off the mainline.
47
+ if: github.ref == 'refs/heads/main'
48
+ steps:
49
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
50
+ with:
51
+ # Full history so cz bump can read every commit since the last tag,
52
+ # and the PAT so the pushed commit and tag are not blocked by branch
53
+ # protection and the tag triggers release.yml.
54
+ fetch-depth: 0
55
+ token: ${{ secrets.RELEASE_TOKEN }}
56
+ persist-credentials: true
57
+
58
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
59
+ with:
60
+ enable-cache: true
61
+ cache-dependency-glob: uv.lock
62
+
63
+ - name: Sync the locked environment
64
+ run: uv sync --locked
65
+
66
+ - name: Identify the committer
67
+ run: |
68
+ git config user.name "github-actions[bot]"
69
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
70
+
71
+ - name: Bump
72
+ env:
73
+ PRERELEASE: ${{ inputs.prerelease }}
74
+ run: |
75
+ if [ "$PRERELEASE" = "stable" ]; then
76
+ uv run --no-sync cz bump --yes --changelog
77
+ else
78
+ uv run --no-sync cz bump --yes --changelog --prerelease "$PRERELEASE"
79
+ fi
80
+
81
+ - name: Fold the refreshed lockfile into the bump commit
82
+ # cz bump rewrites [project].version but not uv.lock, which pins the
83
+ # project's own version. Left stale, every `uv sync --locked` downstream
84
+ # (CI, the release build) fails with "the lockfile needs to be updated".
85
+ # The bump commit and tag are still local here, so amend rather than add
86
+ # a follow-up commit, and re-point the annotated tag at the new commit.
87
+ run: |
88
+ uv lock
89
+ if ! git diff --quiet -- uv.lock; then
90
+ tag="$(git describe --tags --exact-match HEAD)"
91
+ git add uv.lock
92
+ git commit --amend --no-edit
93
+ git tag -f -a "$tag" -m "$tag"
94
+ fi
95
+
96
+ - name: Push the bump commit and the tag
97
+ run: git push --follow-tags origin HEAD:main
@@ -0,0 +1,318 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ concurrency:
10
+ group: ${{ github.workflow }}-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ # The default token is read-only. No job here writes to the repository.
14
+ permissions:
15
+ contents: read
16
+
17
+ env:
18
+ # Colour is decided by TTY detection, and a runner is not a TTY. Setting this
19
+ # anyway keeps log output identical to what a piped run produces locally.
20
+ NO_COLOR: "1"
21
+
22
+ jobs:
23
+ test:
24
+ # The full matrix runs on all three operating systems rather than only on
25
+ # Linux. Cross-platform path and encoding handling is what this project
26
+ # claims to do better than its neighbours, so it is the last thing that
27
+ # should be tested on one platform and assumed elsewhere.
28
+ name: test ${{ matrix.python }} on ${{ matrix.os }}
29
+ runs-on: ${{ matrix.os }}
30
+ timeout-minutes: 20
31
+ strategy:
32
+ fail-fast: false
33
+ matrix:
34
+ os: [ubuntu-latest, windows-latest, macos-latest]
35
+ python: ["3.11", "3.12", "3.13", "3.14"]
36
+
37
+ steps:
38
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
39
+ with:
40
+ persist-credentials: false
41
+
42
+ - name: Install uv
43
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
44
+ with:
45
+ enable-cache: true
46
+ cache-dependency-glob: uv.lock
47
+
48
+ - name: Sync the locked environment
49
+ # --locked fails if uv.lock is out of date with pyproject.toml, so a
50
+ # dependency change that was not locked cannot pass CI unnoticed.
51
+ run: uv sync --locked --python ${{ matrix.python }}
52
+
53
+ - name: Run the test suite
54
+ run: uv run --no-sync pytest
55
+
56
+ - name: Smoke test the doctor command
57
+ # The suite covers doctor thoroughly, but only through the in-process
58
+ # entry point. This runs the installed console script the way a user
59
+ # does, which is what would have caught the Path.exists divergence
60
+ # between Python versions.
61
+ shell: bash
62
+ run: |
63
+ uv run --no-sync mdcompose --version
64
+ uv run --no-sync mdcompose doctor
65
+ uv run --no-sync mdcompose doctor --json > report.json
66
+ uv run --no-sync python -c "import json,pathlib; d=json.loads(pathlib.Path('report.json').read_text()); assert d['os'] in {'windows','macos','linux'}, d; assert d['paths'], d; print('doctor json ok on', d['os'])"
67
+
68
+ - name: Verify quiet mode reduces doctor to its exit code
69
+ shell: bash
70
+ run: |
71
+ output=$(uv run --no-sync mdcompose doctor --quiet)
72
+ if [ -n "$output" ]; then
73
+ echo "expected no stdout in quiet mode, got: $output"
74
+ exit 1
75
+ fi
76
+
77
+ lint:
78
+ name: lint
79
+ runs-on: ubuntu-latest
80
+ timeout-minutes: 20
81
+ steps:
82
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
83
+ with:
84
+ persist-credentials: false
85
+
86
+ - name: Install uv
87
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
88
+ with:
89
+ enable-cache: true
90
+ cache-dependency-glob: uv.lock
91
+
92
+ - name: Sync the locked environment
93
+ run: uv sync --locked
94
+
95
+ - name: Run ruff
96
+ run: uv run --no-sync ruff check .
97
+
98
+ - name: Run mypy
99
+ # Strict type checking over mdcompose/. Config in [tool.mypy].
100
+ run: uv run --no-sync mypy
101
+
102
+ - name: Check dependency hygiene
103
+ # Unused, missing, or misplaced dependencies, inferred from the imports.
104
+ run: uv run --no-sync deptry .
105
+
106
+ coverage:
107
+ # A single-platform coverage floor. The full matrix in `test` proves the
108
+ # suite passes everywhere; this one run enforces `fail_under` from
109
+ # [tool.coverage.report] so coverage cannot slide unnoticed between
110
+ # ratchets. It is not the matrix's job because branch coverage differs by
111
+ # platform (winreg, /mnt/) and the floor is set against one of them.
112
+ name: coverage floor
113
+ runs-on: ubuntu-latest
114
+ timeout-minutes: 20
115
+ steps:
116
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
117
+ with:
118
+ persist-credentials: false
119
+
120
+ - name: Install uv
121
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
122
+ with:
123
+ enable-cache: true
124
+ cache-dependency-glob: uv.lock
125
+
126
+ - name: Sync the locked environment
127
+ run: uv sync --locked
128
+
129
+ - name: Run the suite under coverage
130
+ run: uv run --no-sync pytest --cov=mdcompose --cov-report=term-missing
131
+
132
+ ascii:
133
+ # This project requires every character it generates, and every character in
134
+ # its own documents, to be ASCII. The reason is not style: a Windows console
135
+ # on a cp1252 or cp437 code page cannot encode an emoji or an em dash, so
136
+ # emitting one raises an encoding error on a primary target platform. A rule
137
+ # that is only enforced by review stops being enforced. The same script runs
138
+ # as a pre-commit hook, so a violation fails locally before it is pushed.
139
+ name: ascii convention
140
+ runs-on: ubuntu-latest
141
+ timeout-minutes: 20
142
+ steps:
143
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
144
+ with:
145
+ persist-credentials: false
146
+
147
+ - name: Check that sources and documents are plain ASCII
148
+ run: python3 scripts/check_ascii.py
149
+
150
+ build:
151
+ name: build and install
152
+ runs-on: ubuntu-latest
153
+ timeout-minutes: 20
154
+ steps:
155
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
156
+ with:
157
+ persist-credentials: false
158
+
159
+ - name: Install uv
160
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
161
+ with:
162
+ enable-cache: true
163
+ cache-dependency-glob: uv.lock
164
+
165
+ - name: Build the wheel and sdist
166
+ run: uv build
167
+
168
+ - name: Install the built wheel into a clean environment and run it
169
+ # Proves the package works as published, not only as an editable
170
+ # checkout. A missing file in the wheel would pass every other job.
171
+ shell: bash
172
+ run: |
173
+ uv venv --python 3.11 /tmp/fresh
174
+ uv pip install --python /tmp/fresh/bin/python dist/*.whl
175
+ /tmp/fresh/bin/mdcompose --version
176
+ /tmp/fresh/bin/mdcompose doctor
177
+
178
+ workflows:
179
+ # The two workflow-file linters that pre-commit.ci cannot run in its sandbox
180
+ # (actionlint fetches a binary, zizmor ships as a compiled tool), run here
181
+ # from pinned releases instead. Everything else in .pre-commit-config.yaml
182
+ # is left to pre-commit.ci; those two are listed in its `ci.skip`.
183
+ name: workflow lint
184
+ runs-on: ubuntu-latest
185
+ timeout-minutes: 10
186
+ steps:
187
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
188
+ with:
189
+ persist-credentials: false
190
+
191
+ - name: Install uv
192
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
193
+ with:
194
+ enable-cache: true
195
+ cache-dependency-glob: uv.lock
196
+
197
+ - name: Install actionlint
198
+ env:
199
+ # renovate: datasource=github-releases depName=rhysd/actionlint
200
+ ACTIONLINT_VERSION: 1.7.12
201
+ run: |
202
+ curl -sSfL "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" \
203
+ | sudo tar -xz -C /usr/local/bin actionlint
204
+
205
+ - name: actionlint
206
+ run: actionlint -color
207
+
208
+ - name: zizmor
209
+ env:
210
+ # renovate: datasource=pypi depName=zizmor
211
+ ZIZMOR_VERSION: 1.30.1
212
+ run: uv tool run "zizmor==${ZIZMOR_VERSION}" .
213
+
214
+ - name: check-github-workflows
215
+ run: >-
216
+ uv tool run --from check-jsonschema
217
+ check-jsonschema --builtin-schema github-workflows .github/workflows/*.yml
218
+
219
+ commits:
220
+ # Every commit on a pull request branch must be a Conventional Commit, so
221
+ # the release-time version bump derived from those commits is trustworthy.
222
+ name: conventional commits
223
+ if: github.event_name == 'pull_request'
224
+ runs-on: ubuntu-latest
225
+ timeout-minutes: 20
226
+ steps:
227
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
228
+ with:
229
+ fetch-depth: 0
230
+ persist-credentials: false
231
+
232
+ - name: Install uv
233
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
234
+ with:
235
+ enable-cache: true
236
+ cache-dependency-glob: uv.lock
237
+
238
+ - name: Sync the locked environment
239
+ run: uv sync --locked
240
+
241
+ - name: Check every commit in the pull request
242
+ env:
243
+ RANGE: ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}
244
+ run: uv run --no-sync cz check --rev-range "$RANGE"
245
+
246
+ gitleaks:
247
+ # The pre-commit gitleaks hook only sees a working tree. This scans the full
248
+ # history, so a secret committed and then removed in a later commit is still
249
+ # caught. The gitleaks binary is run directly rather than through
250
+ # gitleaks-action: the action diffs only the pushed range, which has no
251
+ # valid base on the first push of a rewritten history.
252
+ name: secret scan
253
+ runs-on: ubuntu-latest
254
+ timeout-minutes: 20
255
+ steps:
256
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
257
+ with:
258
+ fetch-depth: 0
259
+ persist-credentials: false
260
+
261
+ - name: Install gitleaks
262
+ env:
263
+ # renovate: datasource=github-releases depName=gitleaks/gitleaks
264
+ GITLEAKS_VERSION: 8.24.3
265
+ run: |
266
+ curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \
267
+ | sudo tar -xz -C /usr/local/bin gitleaks
268
+
269
+ - name: Scan the full history
270
+ run: gitleaks git --redact --exit-code 1 --verbose .
271
+
272
+ all-green:
273
+ # The single check branch protection requires: it passes only if every job
274
+ # it needs passed or was legitimately skipped (commits is skipped outside a
275
+ # pull request). Adding a job above without adding it here is the one way to
276
+ # weaken the gate, so the list is deliberately explicit.
277
+ name: all green
278
+ if: always()
279
+ needs: [test, lint, coverage, ascii, build, workflows, commits, gitleaks]
280
+ runs-on: ubuntu-latest
281
+ timeout-minutes: 20
282
+ steps:
283
+ - name: Require every needed job to have passed
284
+ env:
285
+ RESULTS: ${{ join(needs.*.result, ' ') }}
286
+ run: |
287
+ echo "job results: $RESULTS"
288
+ for result in $RESULTS; do
289
+ case "$result" in
290
+ success|skipped) ;;
291
+ *) echo "a required job did not pass"; exit 1 ;;
292
+ esac
293
+ done
294
+
295
+ # DORMANT, repo-gated. The suite (tests/benchmarks) and the `benchmark`
296
+ # dependency group both exist; what is missing is a CodSpeed project, which
297
+ # needs the GitHub repo. CodSpeed runs the benchmarks instrumented rather than
298
+ # wall-clock, so the numbers are stable across runners, and it posts the delta
299
+ # as a pull request check rather than failing hard. Free for open source. To
300
+ # enable: connect the repo at codspeed.io, uncomment this job, and leave it
301
+ # out of the all-green needs list (benchmarks never block a merge).
302
+ #
303
+ # benchmarks:
304
+ # name: benchmarks
305
+ # runs-on: ubuntu-latest
306
+ # timeout-minutes: 20
307
+ # steps:
308
+ # - uses: actions/checkout@v4
309
+ # with:
310
+ # persist-credentials: false
311
+ # - uses: astral-sh/setup-uv@v5
312
+ # with:
313
+ # enable-cache: true
314
+ # cache-dependency-glob: uv.lock
315
+ # - run: uv sync --locked --group benchmark
316
+ # - uses: CodSpeedHQ/action@v3
317
+ # with:
318
+ # run: uv run --no-sync pytest tests/benchmarks --codspeed
@@ -0,0 +1,39 @@
1
+ name: codeql
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+ schedule:
9
+ - cron: "41 5 * * 2"
10
+
11
+ concurrency:
12
+ group: ${{ github.workflow }}-${{ github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ permissions:
16
+ contents: read
17
+
18
+ jobs:
19
+ analyze:
20
+ name: analyze python
21
+ runs-on: ubuntu-latest
22
+ timeout-minutes: 30
23
+ permissions:
24
+ security-events: write
25
+ actions: read
26
+ contents: read
27
+ steps:
28
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
29
+ with:
30
+ persist-credentials: false
31
+
32
+ - name: Initialize CodeQL
33
+ uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4
34
+ with:
35
+ languages: python
36
+ queries: security-and-quality
37
+
38
+ - name: Analyze
39
+ uses: github/codeql-action/analyze@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4
@@ -0,0 +1,45 @@
1
+ name: dependency review
2
+
3
+ # On a pull request that changes dependencies, block the merge if it introduces
4
+ # a known-vulnerable package or a copyleft licence. Complements
5
+ # scripts/check_licenses.py (which runs on the resolved tree) by catching the
6
+ # change at review time. Does nothing until the repository is on GitHub and a
7
+ # pull request exists.
8
+
9
+ on:
10
+ pull_request:
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ concurrency:
16
+ group: ${{ github.workflow }}-${{ github.ref }}
17
+ cancel-in-progress: true
18
+
19
+ jobs:
20
+ review:
21
+ name: dependency review
22
+ runs-on: ubuntu-latest
23
+ timeout-minutes: 10
24
+ steps:
25
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
26
+ with:
27
+ persist-credentials: false
28
+
29
+ - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
30
+ with:
31
+ fail-on-severity: moderate
32
+ # The DENY tier from scripts/check_licenses.py: the strong and linking
33
+ # copyleft families (GPL, LGPL, AGPL, EUPL, SSPL), every SPDX spelling
34
+ # of each. `deny-licenses` matches exact ids, so each -only and
35
+ # -or-later variant and each deprecated id is listed. That script is
36
+ # the real gate (deny by default over the resolved tree); this one
37
+ # catches the same families at review time.
38
+ deny-licenses: >-
39
+ GPL-1.0-only, GPL-1.0-or-later, GPL-2.0, GPL-2.0-only, GPL-2.0-or-later,
40
+ GPL-3.0, GPL-3.0-only, GPL-3.0-or-later,
41
+ LGPL-2.0, LGPL-2.0-only, LGPL-2.0-or-later,
42
+ LGPL-2.1, LGPL-2.1-only, LGPL-2.1-or-later,
43
+ LGPL-3.0, LGPL-3.0-only, LGPL-3.0-or-later,
44
+ AGPL-1.0-only, AGPL-1.0-or-later, AGPL-3.0, AGPL-3.0-only, AGPL-3.0-or-later,
45
+ EUPL-1.1, EUPL-1.2, SSPL-1.0
@@ -0,0 +1,55 @@
1
+ name: mutation testing
2
+
3
+ # Mutation testing: mutmut mutates mdcompose/core (flips a comparison, deletes a
4
+ # line, ...) and runs the suite against each mutant. A mutant that survives is
5
+ # behaviour the tests do not actually check. It runs the whole suite once per
6
+ # mutant, so this is a weekly cron and a manual button, never a required check
7
+ # and never on a pull request. Read the job summary to see where freshly written
8
+ # code needs more tests; the survivors feed the TDD coverage review in the
9
+ # roadmap quality pass.
10
+
11
+ on:
12
+ schedule:
13
+ - cron: "0 4 * * 1"
14
+ workflow_dispatch:
15
+
16
+ concurrency:
17
+ group: mutation
18
+ cancel-in-progress: true
19
+
20
+ permissions:
21
+ contents: read
22
+
23
+ jobs:
24
+ mutmut:
25
+ name: mutmut over core
26
+ runs-on: ubuntu-latest
27
+ timeout-minutes: 60
28
+ # Reporting only. Nothing depends on this job and it never blocks a merge.
29
+ continue-on-error: true
30
+ steps:
31
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
32
+ with:
33
+ persist-credentials: false
34
+
35
+ - uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
36
+ with:
37
+ enable-cache: true
38
+ cache-dependency-glob: uv.lock
39
+
40
+ - name: Sync the locked environment with the mutation group
41
+ run: uv sync --locked --group mutation
42
+
43
+ - name: Run mutmut
44
+ continue-on-error: true
45
+ run: uv run --no-sync mutmut run
46
+
47
+ - name: Write the results to the job summary
48
+ if: always()
49
+ run: |
50
+ {
51
+ echo '## mutmut results'
52
+ echo '```'
53
+ uv run --no-sync mutmut results || true
54
+ echo '```'
55
+ } >> "$GITHUB_STEP_SUMMARY"