netprotocols 2.2.1__tar.gz → 2.4.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 (205) hide show
  1. netprotocols-2.4.0/.claude/skills/steward/SKILL.md +26 -0
  2. netprotocols-2.4.0/.gitattributes +3 -0
  3. netprotocols-2.4.0/.github/CODEOWNERS +1 -0
  4. netprotocols-2.4.0/.github/dependabot.yml +65 -0
  5. netprotocols-2.4.0/.github/workflows/ci.yml +173 -0
  6. netprotocols-2.4.0/.github/workflows/codeql.yml +50 -0
  7. netprotocols-2.4.0/.github/workflows/docs-lint.yml +75 -0
  8. {netprotocols-2.2.1 → netprotocols-2.4.0}/.github/workflows/fuzz.yml +45 -4
  9. netprotocols-2.4.0/.github/workflows/mutation.yml +115 -0
  10. netprotocols-2.4.0/.github/workflows/pr-title-lint.yml +33 -0
  11. {netprotocols-2.2.1 → netprotocols-2.4.0}/.github/workflows/release.yml +65 -5
  12. netprotocols-2.4.0/.github/workflows/scorecard.yml +61 -0
  13. {netprotocols-2.2.1 → netprotocols-2.4.0}/.gitignore +6 -0
  14. {netprotocols-2.2.1 → netprotocols-2.4.0}/ARCHITECTURE.md +234 -32
  15. {netprotocols-2.2.1 → netprotocols-2.4.0}/CHANGELOG.md +56 -0
  16. {netprotocols-2.2.1 → netprotocols-2.4.0}/CONTRIBUTING.md +62 -7
  17. {netprotocols-2.2.1 → netprotocols-2.4.0}/PKG-INFO +124 -34
  18. {netprotocols-2.2.1 → netprotocols-2.4.0}/README.md +122 -32
  19. netprotocols-2.4.0/SECURITY.md +59 -0
  20. {netprotocols-2.2.1 → netprotocols-2.4.0}/docs/CLAIMS.md +189 -27
  21. netprotocols-2.4.0/pyproject.toml +151 -0
  22. {netprotocols-2.2.1 → netprotocols-2.4.0}/scripts/_pcap.py +2 -1
  23. netprotocols-2.4.0/scripts/capture_fixtures_http.sh +202 -0
  24. netprotocols-2.4.0/scripts/capture_fixtures_ipip.sh +161 -0
  25. netprotocols-2.4.0/scripts/capture_fixtures_ipsec.sh +124 -0
  26. netprotocols-2.4.0/scripts/capture_fixtures_sctp.sh +206 -0
  27. netprotocols-2.4.0/scripts/capture_fixtures_vxlan.sh +224 -0
  28. netprotocols-2.4.0/scripts/check_fixtures.py +753 -0
  29. netprotocols-2.4.0/scripts/check_mutmut_survivors.py +158 -0
  30. netprotocols-2.4.0/scripts/extract_http_streams.py +138 -0
  31. netprotocols-2.4.0/scripts/ipsec_fixture_packets.py +418 -0
  32. netprotocols-2.4.0/scripts/mutmut_survivors.txt +27 -0
  33. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/__init__.py +27 -2
  34. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/_base.py +42 -3
  35. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/_defaults.py +37 -0
  36. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/_enums.py +10 -0
  37. netprotocols-2.4.0/src/netprotocols/_tlv.py +125 -0
  38. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/checksum.py +83 -17
  39. netprotocols-2.4.0/src/netprotocols/flow.py +135 -0
  40. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/layer2/arp.py +5 -9
  41. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/layer2/ethernet.py +10 -4
  42. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/layer2/vlan.py +6 -5
  43. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/layer3/__init__.py +5 -0
  44. netprotocols-2.4.0/src/netprotocols/layer3/ah.py +245 -0
  45. netprotocols-2.4.0/src/netprotocols/layer3/esp.py +116 -0
  46. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/layer3/gre.py +8 -7
  47. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/layer3/icmp.py +2 -2
  48. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/layer3/igmp.py +2 -2
  49. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/layer3/ip.py +39 -56
  50. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/layer3/ipv6_ext.py +22 -45
  51. netprotocols-2.4.0/src/netprotocols/layer4/__init__.py +5 -0
  52. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/layer4/_ports.py +4 -3
  53. netprotocols-2.4.0/src/netprotocols/layer4/sctp.py +378 -0
  54. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/layer4/tcp.py +17 -42
  55. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/layer4/udp.py +2 -2
  56. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/layer7/__init__.py +16 -0
  57. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/layer7/dns.py +2 -2
  58. netprotocols-2.4.0/src/netprotocols/layer7/http.py +1426 -0
  59. netprotocols-2.4.0/src/netprotocols/layer7/vxlan.py +190 -0
  60. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/packet.py +29 -19
  61. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/registry.py +15 -7
  62. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/utils/exceptions.py +6 -1
  63. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/walk.py +9 -4
  64. netprotocols-2.4.0/tests/conftest.py +204 -0
  65. netprotocols-2.4.0/tests/fixtures/MANIFEST.md +260 -0
  66. netprotocols-2.4.0/tests/fixtures/ah.pcap +0 -0
  67. netprotocols-2.4.0/tests/fixtures/esp.pcap +0 -0
  68. netprotocols-2.4.0/tests/fixtures/http/01.request +5 -0
  69. netprotocols-2.4.0/tests/fixtures/http/01.response +8 -0
  70. netprotocols-2.4.0/tests/fixtures/http/02.request +5 -0
  71. netprotocols-2.4.0/tests/fixtures/http/02.response +7 -0
  72. netprotocols-2.4.0/tests/fixtures/http/03.request +6 -0
  73. netprotocols-2.4.0/tests/fixtures/http/03.response +5 -0
  74. netprotocols-2.4.0/tests/fixtures/http/04.request +8 -0
  75. netprotocols-2.4.0/tests/fixtures/http/04.response +7 -0
  76. netprotocols-2.4.0/tests/fixtures/http/05.request +5 -0
  77. netprotocols-2.4.0/tests/fixtures/http/05.response +17 -0
  78. netprotocols-2.4.0/tests/fixtures/http/06.request +5 -0
  79. netprotocols-2.4.0/tests/fixtures/http/06.response +20 -0
  80. netprotocols-2.4.0/tests/fixtures/http/07.request +10 -0
  81. netprotocols-2.4.0/tests/fixtures/http/07.response +32 -0
  82. netprotocols-2.4.0/tests/fixtures/http/http.pcap +0 -0
  83. netprotocols-2.4.0/tests/fixtures/ipip.pcap +0 -0
  84. netprotocols-2.4.0/tests/fixtures/sctp.pcap +0 -0
  85. netprotocols-2.4.0/tests/fixtures/staging/ah.pcap +0 -0
  86. netprotocols-2.4.0/tests/fixtures/staging/esp.pcap +0 -0
  87. netprotocols-2.4.0/tests/fixtures/staging/http.pcap +0 -0
  88. netprotocols-2.4.0/tests/fixtures/staging/ipip.pcap +0 -0
  89. netprotocols-2.4.0/tests/fixtures/staging/sctp.pcap +0 -0
  90. netprotocols-2.4.0/tests/fixtures/staging/vxlan.pcap +0 -0
  91. netprotocols-2.4.0/tests/fixtures/vxlan.pcap +0 -0
  92. netprotocols-2.4.0/tests/fuzz/fuzz_decode_frame.py +77 -0
  93. netprotocols-2.4.0/tests/fuzz/fuzz_http_decode.py +121 -0
  94. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/strategies.py +220 -2
  95. netprotocols-2.4.0/tests/test_ah.py +545 -0
  96. netprotocols-2.4.0/tests/test_checksum.py +602 -0
  97. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_contract.py +59 -0
  98. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_corpus.py +17 -5
  99. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_diagnostics.py +44 -1
  100. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_dns.py +188 -14
  101. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_docs.py +87 -4
  102. netprotocols-2.4.0/tests/test_enums.py +22 -0
  103. netprotocols-2.4.0/tests/test_esp.py +307 -0
  104. netprotocols-2.4.0/tests/test_extract_http_streams.py +178 -0
  105. netprotocols-2.4.0/tests/test_flow.py +356 -0
  106. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_fuzz.py +262 -15
  107. netprotocols-2.4.0/tests/test_http.py +2059 -0
  108. netprotocols-2.4.0/tests/test_ipip.py +250 -0
  109. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_ipv6_ext.py +12 -18
  110. netprotocols-2.4.0/tests/test_mutmut_gate.py +184 -0
  111. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_registry.py +43 -13
  112. netprotocols-2.4.0/tests/test_sctp.py +573 -0
  113. netprotocols-2.4.0/tests/test_tlv.py +259 -0
  114. netprotocols-2.4.0/tests/test_vxlan.py +562 -0
  115. netprotocols-2.4.0/tests/test_workflows.py +797 -0
  116. netprotocols-2.4.0/uv.lock +886 -0
  117. netprotocols-2.2.1/.github/workflows/ci.yml +0 -114
  118. netprotocols-2.2.1/pyproject.toml +0 -85
  119. netprotocols-2.2.1/scripts/check_fixtures.py +0 -332
  120. netprotocols-2.2.1/src/netprotocols/flow.py +0 -96
  121. netprotocols-2.2.1/src/netprotocols/layer4/__init__.py +0 -4
  122. netprotocols-2.2.1/tests/conftest.py +0 -103
  123. netprotocols-2.2.1/tests/fixtures/MANIFEST.md +0 -96
  124. netprotocols-2.2.1/tests/test_checksum.py +0 -322
  125. netprotocols-2.2.1/tests/test_flow.py +0 -174
  126. netprotocols-2.2.1/tests/test_workflows.py +0 -118
  127. netprotocols-2.2.1/uv.lock +0 -584
  128. {netprotocols-2.2.1 → netprotocols-2.4.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  129. {netprotocols-2.2.1 → netprotocols-2.4.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  130. {netprotocols-2.2.1 → netprotocols-2.4.0}/.github/ISSUE_TEMPLATE/protocol_request.yml +0 -0
  131. {netprotocols-2.2.1 → netprotocols-2.4.0}/.github/pull_request_template.md +0 -0
  132. {netprotocols-2.2.1 → netprotocols-2.4.0}/LICENSE +0 -0
  133. {netprotocols-2.2.1 → netprotocols-2.4.0}/benchmarks/baseline.json +0 -0
  134. {netprotocols-2.2.1 → netprotocols-2.4.0}/scripts/benchmark.py +0 -0
  135. {netprotocols-2.2.1 → netprotocols-2.4.0}/scripts/benchmark_encode.py +0 -0
  136. {netprotocols-2.2.1 → netprotocols-2.4.0}/scripts/benchmark_import.py +0 -0
  137. {netprotocols-2.2.1 → netprotocols-2.4.0}/scripts/capture_fixtures.sh +0 -0
  138. {netprotocols-2.2.1 → netprotocols-2.4.0}/scripts/capture_fixtures_dhcp.sh +0 -0
  139. {netprotocols-2.2.1 → netprotocols-2.4.0}/scripts/capture_fixtures_dns_tcp.sh +0 -0
  140. {netprotocols-2.2.1 → netprotocols-2.4.0}/scripts/capture_fixtures_gre.sh +0 -0
  141. {netprotocols-2.2.1 → netprotocols-2.4.0}/scripts/capture_fixtures_igmp.sh +0 -0
  142. {netprotocols-2.2.1 → netprotocols-2.4.0}/scripts/capture_fixtures_supplement.sh +0 -0
  143. {netprotocols-2.2.1 → netprotocols-2.4.0}/scripts/capture_fixtures_vlan.sh +0 -0
  144. {netprotocols-2.2.1 → netprotocols-2.4.0}/scripts/pyodide/check_in_pyodide.py +0 -0
  145. {netprotocols-2.2.1 → netprotocols-2.4.0}/scripts/pyodide/package-lock.json +0 -0
  146. {netprotocols-2.2.1 → netprotocols-2.4.0}/scripts/pyodide/package.json +0 -0
  147. {netprotocols-2.2.1 → netprotocols-2.4.0}/scripts/pyodide/run_in_pyodide.mjs +0 -0
  148. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/layer2/__init__.py +0 -0
  149. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/layer7/dhcp.py +0 -0
  150. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/pcap.py +0 -0
  151. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/py.typed +0 -0
  152. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/utils/__init__.py +0 -0
  153. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/utils/ipv4.py +0 -0
  154. {netprotocols-2.2.1 → netprotocols-2.4.0}/src/netprotocols/utils/mac.py +0 -0
  155. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/arp_exchange.pcap +0 -0
  156. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/dhcp.pcap +0 -0
  157. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/dns_tcp.pcap +0 -0
  158. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/gre.pcap +0 -0
  159. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/icmpv4_echo_lo.pcap +0 -0
  160. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/icmpv4_external.pcap +0 -0
  161. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/icmpv4_ttl_exceeded.pcap +0 -0
  162. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/icmpv6_echo_lo.pcap +0 -0
  163. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/igmp.pcap +0 -0
  164. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/ipv4_fragments.pcap +0 -0
  165. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/ipv6_fragments.pcap +0 -0
  166. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/ipv6_mld.pcap +0 -0
  167. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/ipv6_ndp_mld.pcap +0 -0
  168. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/staging/arp_exchange.pcap +0 -0
  169. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/staging/dhcp.pcap +0 -0
  170. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/staging/dns_tcp.pcap +0 -0
  171. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/staging/gre.pcap +0 -0
  172. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/staging/icmpv4_echo_lo.pcap +0 -0
  173. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/staging/icmpv4_external.pcap +0 -0
  174. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/staging/icmpv4_ttl_exceeded.pcap +0 -0
  175. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/staging/icmpv6_echo_lo.pcap +0 -0
  176. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/staging/igmp.pcap +0 -0
  177. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/staging/ipv4_fragments.pcap +0 -0
  178. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/staging/ipv6_fragments.pcap +0 -0
  179. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/staging/ipv6_mld.pcap +0 -0
  180. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/staging/ipv6_ndp_mld.pcap +0 -0
  181. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/staging/tcp_http.pcap +0 -0
  182. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/staging/tcp_https.pcap +0 -0
  183. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/staging/udp_dns.pcap +0 -0
  184. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/staging/vlan_icmp.pcap +0 -0
  185. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/tcp_http.pcap +0 -0
  186. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/tcp_https.pcap +0 -0
  187. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/udp_dns.pcap +0 -0
  188. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/fixtures/vlan_icmp.pcap +0 -0
  189. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_arp.py +0 -0
  190. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_dhcp.py +0 -0
  191. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_ethernet.py +0 -0
  192. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_exports.py +0 -0
  193. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_gre.py +0 -0
  194. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_icmp.py +0 -0
  195. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_igmp.py +0 -0
  196. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_ip.py +0 -0
  197. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_packet.py +0 -0
  198. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_pattern_matching.py +0 -0
  199. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_pcap.py +0 -0
  200. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_tcp.py +0 -0
  201. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_udp.py +0 -0
  202. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_utils.py +0 -0
  203. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_version.py +0 -0
  204. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_vlan.py +0 -0
  205. {netprotocols-2.2.1 → netprotocols-2.4.0}/tests/test_walk.py +0 -0
@@ -0,0 +1,26 @@
1
+ # Steward: driving PRs to green in this repo
2
+
3
+ Repo-specific guidance for any Claude Code session babysitting, driving-to-green, or otherwise acting on an open pull request in this repository. This takes precedence over generic PR-babysitting conventions on the points below; it does not expand access or let a session skip anything those generic rules mark as "never."
4
+
5
+ ## Independent implementation and review for CI/CodeRabbit-reported bugs
6
+
7
+ **Rule 1 — independent implementation.** A fix to a bug reported by CI (a failing check, a CodeRabbit finding, a bot review comment) must be implemented by an independent session or sub-agent — never by the same session/agent that authored the code the bug was found in.
8
+
9
+ **Rule 2 — independent review.** Each such fix, once implemented, must be reviewed by another independent agent before proceeding (pushing, resolving the finding's thread, moving to the next item). This review is a separate pass from whichever agent wrote the fix.
10
+
11
+ ### Why this exists
12
+
13
+ Observed directly on [PR #158](https://github.com/EONRaider/NETProtocols/pull/158): the same session that authored a refactor (source dedup, test hardening, new CI workflows) also implemented every fix for what CodeRabbit found in that code. This repeated the exact failure mode being fixed at least once — a regression test written to guard `mutation.yml`'s DNS mutation-testing scope checked `pattern in text` against the whole file rather than the actual executed command block, the same "checks vocabulary, not behavior" bug already fixed twice earlier in the same review pass (a `uses:` line-matching regex, and a reproducible-build test that checked for the word "reproducible" rather than an actual hash comparison). The author's own blind spots — the same assumptions and habits that produced the original bug — carried straight into the fix, and an external reviewer had to catch it again on the next round.
14
+
15
+ An agent fixing its own bug tends to re-apply the same mental model that produced it. A fresh session or sub-agent, with no investment in the original approach, is more likely to notice when a fix is narrower than the problem, or repeats the original mistake in a new shape.
16
+
17
+ ### How to apply this in practice
18
+
19
+ When a CI check fails or CodeRabbit (or any other bot/reviewer) reports a finding on a PR in this repo:
20
+
21
+ 1. **Diagnose** in the current session — reading the failure, the diff, and the relevant code is fine and doesn't need to be delegated.
22
+ 2. **Delegate the fix itself** to an independent sub-agent (e.g. via the Agent tool), giving it the finding and enough context to act, but without it inheriting the current session's authorship of the original code — it should approach the fix fresh, not defend or extend its own prior work.
23
+ 3. **Delegate review of that fix** to a second, separate sub-agent before pushing — distinct from the agent that implemented the fix. This agent should verify the fix actually closes the gap (ideally by constructing a case that would have slipped past the old code and confirming the new code catches it, not just that the reported symptom is gone), and check it didn't introduce the same class of bug elsewhere.
24
+ 4. Only after both steps pass does the fix get pushed and the finding marked resolved.
25
+
26
+ This applies to fixes for bugs reported by CI, CodeRabbit, another bot, or a human reviewer — the fix-a-reported-bug loop, where the risk of an author's blind spots recurring is highest. It does not require every commit or every piece of new feature work in this repo to go through this two-step delegation.
@@ -0,0 +1,3 @@
1
+ # The HTTP fixture streams are wire bytes: their CRLFs are the grammar
2
+ # under test, so no checkout may convert them.
3
+ tests/fixtures/http/* -text
@@ -0,0 +1 @@
1
+ * @EONRaider
@@ -0,0 +1,65 @@
1
+ version: 2
2
+
3
+ updates:
4
+ # --- Python tooling, managed by uv --------------------------------
5
+ #
6
+ # This project's dependencies are declared exclusively via PEP 735
7
+ # [dependency-groups] in pyproject.toml (see uv.lock), not the older
8
+ # [project.optional-dependencies] table. Dependabot's "pip" ecosystem
9
+ # does not parse [dependency-groups]; only the native "uv" ecosystem
10
+ # does (GA since March 2025, with dependency-groups parsing landing
11
+ # in dependabot/dependabot-core#11796), so "uv" is used here rather
12
+ # than "pip".
13
+ - package-ecosystem: "uv"
14
+ directory: "/"
15
+ schedule:
16
+ interval: "weekly"
17
+ commit-message:
18
+ prefix: "chore"
19
+ groups:
20
+ # netprotocols ships zero runtime dependencies today, so every
21
+ # update currently comes from the "dev" dependency-group (pytest,
22
+ # mypy, ruff, etc.) -- kept current here as its own group rather
23
+ # than left to open one PR per package. Matched by explicit
24
+ # package name instead of `dependency-type: development`: as of
25
+ # this writing Dependabot's dependency-type classifier does not
26
+ # work correctly for the uv ecosystem and reports every
27
+ # dependency as "production" regardless of where it's declared
28
+ # (see dependabot/dependabot-core#13202), which would leave a
29
+ # `dependency-type: development` group empty -- or, used the
30
+ # other way around, wrongly sweep this same dev tooling into a
31
+ # "runtime" group. A future runtime dependency added under
32
+ # [project.dependencies] is intentionally left out of this list,
33
+ # so it keeps updating on its own outside this group rather than
34
+ # being bundled in with dev tooling.
35
+ dev-dependencies:
36
+ patterns:
37
+ - "hypothesis"
38
+ - "mypy"
39
+ - "pytest"
40
+ - "pytest-cov"
41
+ - "ruff"
42
+
43
+ # --- GitHub Actions -------------------------------------------------
44
+ #
45
+ # Keeps the SHA-pinned `uses:` references in .github/workflows current
46
+ # (Dependabot updates both the pinned SHA and its trailing `# vX.Y.Z`
47
+ # comment).
48
+ - package-ecosystem: "github-actions"
49
+ directory: "/"
50
+ schedule:
51
+ interval: "weekly"
52
+ commit-message:
53
+ prefix: "ci"
54
+ groups:
55
+ # github/codeql-action ships init, analyze, and upload-sarif as
56
+ # subdirectories of one repository, but Dependabot tracks each
57
+ # `uses:` path as its own dependency and opens one PR per path.
58
+ # init and analyze must run the same release -- analyze refuses a
59
+ # config file written by a different init version ("Loaded a
60
+ # configuration file for version '4.37.9', but running version
61
+ # '4.38.1'") -- so every split PR fails CodeQL on its own and
62
+ # none can merge. Grouping them bumps all three together.
63
+ codeql-action:
64
+ patterns:
65
+ - "github/codeql-action*"
@@ -0,0 +1,173 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+ branches: [master]
8
+ # Callable so the release workflow can run this exact ladder against a
9
+ # tagged commit before publishing (see release.yml). Keeping one
10
+ # definition means the release gate cannot drift from the PR gate.
11
+ workflow_call:
12
+
13
+ # Cancel a stale run the moment a newer one starts for the same branch
14
+ # or pull request, so a quick follow-up push doesn't leave two ladders
15
+ # racing for runners. Keyed on the PR number when this is a pull_request
16
+ # run and on the ref otherwise. The group name is the literal "ci"
17
+ # prefix, not ${{ github.workflow }}: inside a workflow_call, that
18
+ # expression is the CALLER's workflow name, not this one's. Using it
19
+ # here made this group collide with release.yml's own top-level
20
+ # "Release-<ref>" group on a tag push, and GitHub cancelled the whole
21
+ # run as a self-deadlock.
22
+ concurrency:
23
+ group: ci-${{ github.event.pull_request.number || github.ref }}
24
+ cancel-in-progress: true
25
+
26
+ # No job here needs to write anything -- lint/typecheck/test/build/pyodide/
27
+ # benchmark all read the checked-out tree and call out to local tools.
28
+ permissions:
29
+ contents: read
30
+
31
+ jobs:
32
+ lint:
33
+ runs-on: ubuntu-latest
34
+ steps:
35
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
36
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
37
+ with:
38
+ python-version: "3.12"
39
+ - run: uv run ruff check
40
+ - run: uv run ruff format --check
41
+
42
+ typecheck:
43
+ runs-on: ubuntu-latest
44
+ steps:
45
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
46
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
47
+ with:
48
+ python-version: "3.12"
49
+ - run: uv run mypy
50
+
51
+ test:
52
+ runs-on: ubuntu-latest
53
+ strategy:
54
+ matrix:
55
+ python-version: ["3.12", "3.13", "3.14"]
56
+ steps:
57
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
58
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
59
+ with:
60
+ python-version: ${{ matrix.python-version }}
61
+ - run: uv run --frozen pytest --cov=netprotocols --cov-report=term
62
+
63
+ # Scans the dev dependency group (the only third-party code CI itself
64
+ # pulls in) for known vulnerabilities. Exported with --no-emit-project
65
+ # so the audit sees pinned, hashed third-party requirements only --
66
+ # not an unhashable `-e .` reference to this repo's own in-tree
67
+ # package, which pip-audit cannot install under --require-hashes
68
+ # (implied automatically once any requirement carries a hash).
69
+ dependency-scan:
70
+ runs-on: ubuntu-latest
71
+ steps:
72
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
73
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
74
+ with:
75
+ python-version: "3.12"
76
+ - name: Export dev dependencies
77
+ run: >-
78
+ uv export --group dev --format requirements-txt --no-emit-project
79
+ -o requirements-dev.txt
80
+ - name: Audit dev dependencies for known vulnerabilities
81
+ run: >-
82
+ uv run --with pip-audit --no-project
83
+ pip-audit -r requirements-dev.txt
84
+
85
+ build:
86
+ runs-on: ubuntu-latest
87
+ steps:
88
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
89
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
90
+ - run: uv build
91
+ - run: uv run --isolated --no-project --with dist/*.whl python -c "import netprotocols; print(netprotocols.__version__)"
92
+ # Builds are supposed to be a pure function of the source tree, so
93
+ # two runs back-to-back must emit byte-identical wheels. Diffing
94
+ # their sha256 (rather than the files directly) catches drift
95
+ # from things like embedded timestamps or non-deterministic
96
+ # archive member ordering without needing a full binary diff.
97
+ - name: Verify the wheel build is reproducible
98
+ run: |
99
+ set -euo pipefail
100
+ uv build -o dist-repro-1
101
+ uv build -o dist-repro-2
102
+ hash1=$(sha256sum dist-repro-1/*.whl | cut -d ' ' -f1)
103
+ hash2=$(sha256sum dist-repro-2/*.whl | cut -d ' ' -f1)
104
+ echo "Build 1 sha256: $hash1"
105
+ echo "Build 2 sha256: $hash2"
106
+ if [ "$hash1" != "$hash2" ]; then
107
+ echo "::error::Wheel build is not reproducible: sha256 differs between two successive 'uv build' runs."
108
+ exit 1
109
+ fi
110
+ echo "Wheel build is reproducible."
111
+
112
+ # Proves the README's browser/WASM claim (#99) against the real
113
+ # thing, not a simulation of it: boots an actual Pyodide runtime
114
+ # (scripts/pyodide/run_in_pyodide.mjs, via Node — no browser needed,
115
+ # same WebAssembly build either way) and decodes the whole
116
+ # real-capture corpus with the wheel this job just built. See
117
+ # scripts/pyodide/check_in_pyodide.py for what it checks and why.
118
+ pyodide:
119
+ runs-on: ubuntu-latest
120
+ steps:
121
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
122
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
123
+ with:
124
+ python-version: "3.12"
125
+ - run: uv build
126
+ - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
127
+ with:
128
+ node-version: "22"
129
+ - run: npm ci
130
+ working-directory: scripts/pyodide
131
+ - run: >-
132
+ node scripts/pyodide/run_in_pyodide.mjs
133
+ "$GITHUB_WORKSPACE" scripts/pyodide/check_in_pyodide.py
134
+
135
+ # Blocking, at 15% below the committed baseline. The threshold has
136
+ # runner data behind it: this job's first run measured 3.7% away from
137
+ # a baseline recorded on entirely different hardware, and run-to-run
138
+ # spread on one machine is ~2%. 15% is therefore roughly four times
139
+ # the noise it has to tolerate, which leaves real regressions (Tier 1
140
+ # moved this number by 30-90% a step) nowhere to hide.
141
+ #
142
+ # A failure re-measures once before failing, because a shared runner
143
+ # can lose a slice of CPU to a neighbour: a genuine regression fails
144
+ # both attempts, a noisy one usually does not.
145
+ #
146
+ # Pinned to the baseline's interpreter on purpose: normalization
147
+ # cancels how fast the machine is, not which CPython it runs (see the
148
+ # module docstring in scripts/benchmark.py). If a change is a
149
+ # deliberate trade, re-record with --update-baseline and say why.
150
+ benchmark:
151
+ runs-on: ubuntu-latest
152
+ steps:
153
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
154
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
155
+ with:
156
+ python-version: "3.12"
157
+ - name: Corpus decode throughput
158
+ shell: bash
159
+ run: |
160
+ # Keep the benchmark's own exit status: piping the whole thing
161
+ # into tee would report tee's, and wrapping it in a { } group
162
+ # would report the trailing echo's -- either way the gate
163
+ # would pass while the check failed.
164
+ status=0
165
+ uv run --frozen --group bench python scripts/benchmark.py \
166
+ --check --threshold 15 --compare >benchmark.out 2>&1 || status=$?
167
+ {
168
+ echo '```'
169
+ cat benchmark.out
170
+ echo '```'
171
+ } >>"$GITHUB_STEP_SUMMARY"
172
+ cat benchmark.out
173
+ exit "$status"
@@ -0,0 +1,50 @@
1
+ name: CodeQL
2
+
3
+ # Advanced-setup workflow (a checked-in file) rather than GitHub's
4
+ # built-in "default setup", so this analysis lives in version control
5
+ # alongside the rest of this repository's CI (ci.yml, fuzz.yml,
6
+ # release.yml, mutation.yml, scorecard.yml) and its action pins are
7
+ # reviewable the same way theirs are. Default setup is configured
8
+ # through repository settings instead of a file, so it isn't available
9
+ # to author from here.
10
+ on:
11
+ push:
12
+ branches: [master]
13
+ pull_request:
14
+ branches: [master]
15
+ schedule:
16
+ # Arbitrary; weekly on Sundays, clear of fuzz.yml's and mutation.yml's
17
+ # daily 03:00/04:00 UTC slots and scorecard.yml's own Sunday 05:00
18
+ # UTC slot.
19
+ - cron: "0 6 * * 0"
20
+
21
+ permissions:
22
+ contents: read
23
+
24
+ concurrency:
25
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
26
+ cancel-in-progress: true
27
+
28
+ jobs:
29
+ analyze:
30
+ name: Analyze (python)
31
+ runs-on: ubuntu-latest
32
+ permissions:
33
+ actions: read
34
+ contents: read
35
+ security-events: write
36
+ steps:
37
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
38
+
39
+ # Python is interpreted, so there is nothing for autobuild (or a
40
+ # manual build step) to do: build-mode: none has CodeQL extract
41
+ # the database straight from source instead. This is the build
42
+ # mode the CodeQL Action's own docs recommend for interpreted
43
+ # languages, and it sidesteps the question of an autobuild step
44
+ # fitting this project's uv-based build at all.
45
+ - uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
46
+ with:
47
+ languages: python
48
+ build-mode: none
49
+
50
+ - uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2
@@ -0,0 +1,75 @@
1
+ name: Docs lint
2
+
3
+ # Runs lycheeverse/lychee (a link checker) over the small set of
4
+ # hand-maintained docs that carry the project's external claims and
5
+ # cross-references: README.md, ARCHITECTURE.md, CONTRIBUTING.md,
6
+ # CHANGELOG.md, and docs/CLAIMS.md. Two triggers, for two different
7
+ # failure modes: the pull_request path filter catches a broken link
8
+ # introduced *by* a docs edit, and the weekly schedule catches a link
9
+ # that was fine when written but has since gone dead out from under an
10
+ # otherwise-unchanged file (a renamed issue, a retired badge service, a
11
+ # moved external page) -- which no docs PR would ever surface.
12
+ on:
13
+ pull_request:
14
+ branches: [master]
15
+ paths:
16
+ - "README.md"
17
+ - "ARCHITECTURE.md"
18
+ - "CONTRIBUTING.md"
19
+ - "CHANGELOG.md"
20
+ - "docs/CLAIMS.md"
21
+ schedule:
22
+ # Arbitrary; weekly on Mondays, clear of fuzz.yml's/mutation.yml's
23
+ # daily 03:00/04:00 UTC slots and scorecard.yml's Sunday 05:00 slot.
24
+ - cron: "0 6 * * 1"
25
+ workflow_dispatch:
26
+
27
+ concurrency:
28
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
29
+ cancel-in-progress: true
30
+
31
+ permissions:
32
+ contents: read
33
+
34
+ jobs:
35
+ lychee:
36
+ runs-on: ubuntu-latest
37
+ steps:
38
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
39
+
40
+ # lychee-action's `token` input defaults to `${{ github.token }}`,
41
+ # which it exports as GITHUB_TOKEN; lychee reads that itself to
42
+ # check github.com links through the API instead of scraping the
43
+ # page. That's what keeps the ~15 github.com/EONRaider/NETProtocols
44
+ # issue/release links in these docs from tripping an anonymous
45
+ # rate limit -- most relevant on the weekly schedule run, which
46
+ # has no pull request author's own browsing to blend in with.
47
+ #
48
+ # --timeout/--max-retries/--retry-wait-time are more generous
49
+ # than lychee's own defaults (20s/3/1s) to absorb a slow or
50
+ # momentarily rate-limited external host without failing the
51
+ # build outright.
52
+ #
53
+ # --exclude covers img.shields.io, which serves this project's
54
+ # three static README badges: a shared, heavily-trafficked CDN
55
+ # known to intermittently rate-limit or time out under bulk/CI
56
+ # traffic across many unrelated repos, independent of whether
57
+ # any given badge URL is actually broken. Every other file was
58
+ # checked by hand for placeholder/example domains (example.com,
59
+ # localhost, and the like) before this workflow was added; none
60
+ # were found, so no other entries are excluded here.
61
+ - name: Check links in the project's hand-maintained docs
62
+ uses: lycheeverse/lychee-action@e7477775783ea5526144ba13e8db5eec57747ce8 # v2.9.0
63
+ with:
64
+ args: >-
65
+ --no-progress
66
+ --verbose
67
+ --timeout 30
68
+ --max-retries 3
69
+ --retry-wait-time 10
70
+ --exclude '^https://img\.shields\.io'
71
+ README.md
72
+ ARCHITECTURE.md
73
+ CONTRIBUTING.md
74
+ CHANGELOG.md
75
+ docs/CLAIMS.md
@@ -12,6 +12,14 @@ on:
12
12
  - cron: "0 3 * * *"
13
13
  workflow_dispatch:
14
14
 
15
+ # Keyed on the triggering event too, so the nightly cron can never
16
+ # cancel a manually dispatched run on the same ref (the release
17
+ # checklist in CONTRIBUTING.md relies on one) -- runs of the same kind
18
+ # still supersede each other.
19
+ concurrency:
20
+ group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
21
+ cancel-in-progress: true
22
+
15
23
  permissions:
16
24
  contents: read
17
25
 
@@ -29,8 +37,8 @@ jobs:
29
37
  fuzz:
30
38
  runs-on: ubuntu-latest
31
39
  steps:
32
- - uses: actions/checkout@v5
33
- - uses: astral-sh/setup-uv@v6
40
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
41
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
34
42
  with:
35
43
  python-version: "3.12"
36
44
  # Keyed by run id with a prefix restore-key: a fixed key would
@@ -40,7 +48,7 @@ jobs:
40
48
  # today's as a new entry, so Hypothesis's example database grows
41
49
  # night over night instead of starting cold every time.
42
50
  - name: Restore accumulated Hypothesis examples
43
- uses: actions/cache@v4
51
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
44
52
  with:
45
53
  path: .hypothesis
46
54
  key: hypothesis-nightly-${{ github.run_id }}
@@ -57,8 +65,41 @@ jobs:
57
65
  # repo write access to re-run the job locally.
58
66
  - name: Upload the failing example database
59
67
  if: failure()
60
- uses: actions/upload-artifact@v4
68
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
61
69
  with:
62
70
  name: hypothesis-nightly-failure-${{ github.run_id }}
63
71
  path: .hypothesis
64
72
  retention-days: 30
73
+
74
+ # Coverage-guided fuzzing of the same target, via Atheris rather than
75
+ # Hypothesis's example strategies above -- it drives decode_frame with
76
+ # libFuzzer's mutation engine instead of Hypothesis's, so it explores
77
+ # a different part of the input space. HTTP.decode gets a harness of
78
+ # its own because decode_frame never reaches it: HTTP is not
79
+ # port-dispatched. Pinned to Python 3.12 only,
80
+ # like the "fuzz" job above: Atheris hooks CPython internals directly
81
+ # and tends to lag new interpreter releases, so it isn't given the
82
+ # full 3.12/3.13/3.14 matrix the compatibility-gate "test" job in
83
+ # ci.yml runs.
84
+ atheris:
85
+ runs-on: ubuntu-latest
86
+ steps:
87
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
88
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
89
+ with:
90
+ python-version: "3.12"
91
+ - name: Run Atheris against decode_frame
92
+ run: |
93
+ uv run --frozen --with atheris \
94
+ python tests/fuzz/fuzz_decode_frame.py -max_total_time=300
95
+ - name: Run Atheris against HTTP.decode
96
+ run: |
97
+ uv run --frozen --with atheris \
98
+ python tests/fuzz/fuzz_http_decode.py -max_total_time=300
99
+ - name: Upload crash artifacts
100
+ if: failure()
101
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
102
+ with:
103
+ name: atheris-crash-${{ github.run_id }}
104
+ path: crash-*
105
+ retention-days: 30
@@ -0,0 +1,115 @@
1
+ name: Nightly mutation testing
2
+
3
+ # Separate from ci.yml on purpose, same reasoning as fuzz.yml: ci.yml is
4
+ # reused via workflow_call by release.yml, so folding a schedule trigger
5
+ # into it would fire mutation testing on every release too, not just
6
+ # once a night.
7
+ on:
8
+ schedule:
9
+ # Arbitrary; offset an hour past fuzz.yml's 03:00 UTC slot so the
10
+ # two nightly jobs don't contend for the same runner pool.
11
+ - cron: "0 4 * * *"
12
+ workflow_dispatch:
13
+
14
+ # Keyed on the triggering event too, so the nightly cron can never
15
+ # cancel a manually dispatched run on the same ref (the release
16
+ # checklist in CONTRIBUTING.md relies on one) -- runs of the same kind
17
+ # still supersede each other.
18
+ concurrency:
19
+ group: ${{ github.workflow }}-${{ github.ref }}-${{ github.event_name }}
20
+ cancel-in-progress: true
21
+
22
+ permissions:
23
+ contents: read
24
+
25
+ jobs:
26
+ # Mutation testing itself is exploratory/maintenance, not a
27
+ # compatibility gate, so this is pinned to one interpreter — same
28
+ # reasoning as fuzz.yml's "fuzz" job.
29
+ mutation:
30
+ runs-on: ubuntu-latest
31
+ steps:
32
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
33
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
34
+ with:
35
+ python-version: "3.12"
36
+ # mutmut hashes each mutated file plus the tests that cover it and
37
+ # skips re-running mutants whose inputs haven't changed since the
38
+ # last recorded result, so carrying its cache forward night over
39
+ # night saves real work — same accumulation pattern as fuzz.yml's
40
+ # Hypothesis example database. Keyed by run id with a prefix
41
+ # restore-key for the same reason: a fixed key would restore
42
+ # forever but never save a new entry.
43
+ - name: Restore accumulated mutmut cache
44
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
45
+ with:
46
+ path: |
47
+ .mutmut-cache
48
+ mutants
49
+ key: mutmut-nightly-${{ github.run_id }}
50
+ restore-keys: |
51
+ mutmut-nightly-
52
+ # The restored cache also carries mutants/mutmut-stats.json, the
53
+ # map from each function to the tests that call it. When that map
54
+ # exists, mutmut 3.8 re-traces only tests it has never seen, so a
55
+ # function added since the cached run (a new helper called only
56
+ # by existing tests) maps to no tests and every one of its
57
+ # mutants reads "no tests"; so does an existing test edited to
58
+ # call a function it didn't before. Without the map, mutmut
59
+ # traces the configured test selection in full, which costs one
60
+ # traced pytest pass. Nothing else is lost: with the positional
61
+ # filters below, mutmut re-runs every in-scope mutant regardless
62
+ # of a cached result.
63
+ - name: Drop the cached function-to-test map
64
+ run: rm -f mutants/mutmut-stats.json
65
+ - name: Install the project with dev dependencies
66
+ run: uv sync --group dev --frozen
67
+ # File-level scope (src/netprotocols/checksum.py, _tlv.py,
68
+ # layer7/dns.py and layer7/http.py) comes from the project's mutmut config (pyproject.toml's
69
+ # [tool.mutmut] table) via only_mutate, not hardcoded here.
70
+ # dns.py's in-scope surface is narrower still -- only the
71
+ # name-compression byte walk, not the record/RDATA/dataclass code
72
+ # the rest of the file holds -- and mutmut has no config-level way
73
+ # to express that, so these positional filters (kept in lockstep
74
+ # with the ones documented next to only_mutate in pyproject.toml)
75
+ # are what actually narrow it; a filter added or renamed there
76
+ # without a matching update here is exactly the kind of drift this
77
+ # comment can't prevent on its own.
78
+ - name: Run mutmut against the scoped targets
79
+ run: |
80
+ uv run --frozen mutmut run \
81
+ 'netprotocols.checksum.*' \
82
+ 'netprotocols._tlv.*' \
83
+ 'netprotocols.layer7.dns.x__read_name*' \
84
+ 'netprotocols.layer7.dns.x__labels*' \
85
+ 'netprotocols.layer7.http.x__*'
86
+ # `mutmut run` exits 0 however many mutants survive, so it can
87
+ # never turn this job red on its own. This step is what does: it
88
+ # fails on any undetected mutant not in the triage record
89
+ # (scripts/mutmut_survivors.txt), on a stale entry there, and on
90
+ # a scoped file with no checked mutant at all.
91
+ - name: Fail on survivors outside the triage record
92
+ run: |
93
+ uv run --frozen mutmut results --all true > mutmut-results.txt
94
+ uv run --frozen python scripts/check_mutmut_survivors.py \
95
+ mutmut-results.txt
96
+ # A failed scheduled run already notifies repo watchers by
97
+ # default (GitHub's standard behavior), and with the step above a
98
+ # red run means an untriaged survivor or a broken run; nothing
99
+ # here files an issue on top of it. This makes the results
100
+ # recoverable without needing repo write access to re-run mutmut
101
+ # locally. Covers the status listing plus both the sqlite-file
102
+ # cache and the mutated-copies directory, since either of the
103
+ # latter may be what a given mutmut version writes; whatever
104
+ # doesn't exist is silently skipped.
105
+ - name: Upload mutmut results on failure
106
+ if: failure()
107
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
108
+ with:
109
+ name: mutmut-nightly-failure-${{ github.run_id }}
110
+ path: |
111
+ mutmut-results.txt
112
+ .mutmut-cache
113
+ mutants
114
+ if-no-files-found: ignore
115
+ retention-days: 30
@@ -0,0 +1,33 @@
1
+ name: PR Title Lint
2
+
3
+ # Enforces Conventional Commits shape (https://www.conventionalcommits.org/)
4
+ # on the *pull request title* only — e.g. `feat: ...`, `fix: ...`,
5
+ # `docs: ...`, `chore: ...`. This is unrelated to this repo's guidance on
6
+ # individual commit messages within a PR (see CONTRIBUTING.md); nothing
7
+ # here checks or rewrites those.
8
+ #
9
+ # Runs on `pull_request` (not `pull_request_target`) so it only ever sees
10
+ # the PR's own metadata with a read-only, non-elevated token — no need for
11
+ # the elevated-permissions trigger the action's own docs suggest for
12
+ # repos that also want its optional "[WIP]" title support, which this
13
+ # repo doesn't use.
14
+ on:
15
+ pull_request:
16
+ branches: [master]
17
+ types: [opened, edited, synchronize]
18
+
19
+ permissions:
20
+ pull-requests: read
21
+
22
+ concurrency:
23
+ group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
24
+ cancel-in-progress: true
25
+
26
+ jobs:
27
+ lint-pr-title:
28
+ name: Validate PR title
29
+ runs-on: ubuntu-latest
30
+ steps:
31
+ - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1
32
+ env:
33
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}