Skip to content

ci: gate releases on main and green CI, ship LICENSE in the wheel (#615) - #622

Closed
lesnik512 wants to merge 2 commits into
mainfrom
issue-615
Closed

lesnik512 wants to merge 2 commits into
mainfrom
issue-615

Conversation

@lesnik512

@lesnik512 lesnik512 commented Oct 7, 2026 •

Copy link
Copy Markdown
Member

Closes #615.

Summary

  • pyproject.toml gets license-files = ["LICENSE"]. The wheel now has dist-info/licenses/LICENSE and License-File: LICENSE in its metadata, and the sdist has LICENSE. A new test in tests/test_packaging.py runs uv build, checks both archives for the file, and imports modern_di from the built wheel. It failed before the pyproject.toml change.
  • release.yml is split into three jobs: gate, publish, github-release. Each has its own minimal permissions, and the workflow default is permissions: {}.
    • gate (contents: read, actions: read) checks out full history and runs git merge-base --is-ancestor "$GITHUB_SHA" origin/main. It then asks the API for push-event runs of ci.yml on the tagged commit (gh api .../actions/workflows/ci.yml/runs?head_sha=...&event=push), takes the latest by run_number, and fails unless it is completed with success.
    • publish (contents: read, id-token: write, environment pypi) runs just publish.
    • github-release (contents: write) creates the Release after publish succeeds.
  • Third-party actions in release.yml are pinned by SHA: extractions/setup-just v4.0.0, astral-sh/setup-uv v8.2.0, softprops/action-gh-release v3.0.3. actions/checkout is pinned the same way (v6.1.0, which is what v6 points at today). I resolved the SHAs through git/matching-refs and dereferenced the annotated tags.
  • docs/agents/release.md and the workflow header describe the gate. The rationale comments in release.yml moved here and into that doc.

Design decisions

  • The gate does not re-run _checks.yml, as decided in the issue.
  • The gate looks at one workflow run. The first version read every check run on the commit, and review found two problems with that:
    • The script paginated inside a command substitution, so a failed request for page 2 was swallowed. The list was cut short, and the gate could pass without seeing a failure on that page.
    • Other workflows report check runs on the same SHA: the nightly scheduled-dep-check, Dependabot's update-pip-graph, and the docs build and deploy jobs. A failure in any of them blocked a release of code whose CI was green.
  • The push run's workflow conclusion already accounts for the continue-on-error prerelease pytest jobs, so the gate needs no list of jobs that may fail. It also needs no pagination and no logic to skip its own runs.
  • gh api exits non-zero on any HTTP error, and the step runs under set -euo pipefail, so an API failure fails the gate.
  • The query uses git rev-parse "$GITHUB_SHA^{commit}", so an annotated tag object can never be passed in place of the commit.
  • The publish job keeps contents: read next to id-token: write so that actions/checkout has a token. It has no write access to the repository.

Test plan

  • New packaging test fails without license-files and passes with it.
  • uv build into a scratch dir: the wheel lists modern_di-0.dist-info/licenses/LICENSE, METADATA has License-File: LICENSE, and the sdist has modern_di-0/LICENSE.
  • The two gate steps, extracted from release.yml and run locally against real SHAs:
    • f6e343d passes both steps.
    • d850894 and 76608e6 (main commits whose ci.yml push run failed) fail the CI step.
    • 4c5624a (this PR's first head, which has only pull_request runs) fails with "no run found" and also fails the ancestry step.
    • A bad repository name makes gh api return 404, and the step exits 1.
  • actionlint (with shellcheck) clean, and shellcheck clean on the extracted step scripts.
  • just lint-ci, just test-ci (100% coverage), mkdocs build --strict.
  • The release workflow itself only runs on the next tag push.

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Details
Benchmark suite Current: e782497 Previous: 68d4685 Ratio
benchmarks/test_guard_by_type.py::test_g16_resolve_by_type 2912800.1429394204 iter/sec (stddev: 6.501803128181719e-8) 2909479.2346332557 iter/sec (stddev: 1.2209540774992632e-8) 1.00
benchmarks/test_guard_by_type.py::test_g17_resolve_by_type_large_registry 3035084.8524776767 iter/sec (stddev: 1.0540207597295734e-8) 2877933.459589614 iter/sec (stddev: 1.1266063666075538e-8) 0.95
benchmarks/test_guard_cold.py::test_g8_cold_first_resolve 21554.14734073323 iter/sec (stddev: 0.000043945776284059495) 21624.408994070352 iter/sec (stddev: 0.00004353392440193875) 1.00
benchmarks/test_guard_cold.py::test_g8b_cold_first_resolve_cached 15729.421267502772 iter/sec (stddev: 0.00020821109954288118) 16062.33555433443 iter/sec (stddev: 0.0001171580280804952) 1.02
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[1] 428.1391992972049 iter/sec (stddev: 0.00006046282136786154) 372.41718647604426 iter/sec (stddev: 0.00004103336503727921) 0.87
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[2] 414.2529237296105 iter/sec (stddev: 0.00008618372799051245) 356.7992411536997 iter/sec (stddev: 0.00010414956223461677) 0.86
benchmarks/test_guard_concurrency.py::test_g14_concurrent_cached_hit[4] 402.38363089788635 iter/sec (stddev: 0.00012524174515213504) 349.3945837398324 iter/sec (stddev: 0.00008169499576812023) 0.87
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[1] 12739.725809262807 iter/sec (stddev: 0.000007439070802531619) 11812.232748260863 iter/sec (stddev: 0.000008706869035475766) 0.93
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[2] 6134.636875513147 iter/sec (stddev: 0.000021607476373339015) 6162.507479098245 iter/sec (stddev: 0.000025125312663039706) 1.00
benchmarks/test_guard_concurrency.py::test_g15_concurrent_first_resolve[4] 3338.962545938946 iter/sec (stddev: 0.000041181602106447394) 3297.539096035311 iter/sec (stddev: 0.00004497466493849032) 0.99
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[1] 8418.979494833342 iter/sec (stddev: 0.0000097683637404935) 8346.554507511004 iter/sec (stddev: 0.000011356964731263443) 0.99
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[2] 3677.3710033372317 iter/sec (stddev: 0.000024872032618358425) 3539.135539602003 iter/sec (stddev: 0.00002735460345472286) 0.96
benchmarks/test_guard_concurrency.py::test_g15b_concurrent_first_resolve_sibling_children[4] 1708.7958901405198 iter/sec (stddev: 0.000041749668935108324) 1671.7936631028952 iter/sec (stddev: 0.00005203837697615841) 0.98
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[1] 41574.1065385913 iter/sec (stddev: 0.0000034641841993496922) 39542.66274335896 iter/sec (stddev: 0.000005463304072352007) 0.95
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[2] 15621.169965756393 iter/sec (stddev: 0.000010375224689176874) 12754.883765290642 iter/sec (stddev: 0.000014849567578399732) 0.82
benchmarks/test_guard_concurrency.py::test_g15c_worker_pool_floor_control[4] 5959.264553265116 iter/sec (stddev: 0.00003288954453000436) 5954.360422798371 iter/sec (stddev: 0.000034390868545997847) 1.00
benchmarks/test_guard_lifecycle.py::test_g6_build_child_container 1541063.5056522097 iter/sec (stddev: 3.0473732198775795e-8) 1611961.9830068543 iter/sec (stddev: 3.411756829069379e-8) 1.05
benchmarks/test_guard_lifecycle.py::test_g6b_build_child_container_auto_scope 1676907.9628249519 iter/sec (stddev: 2.7679548712390527e-8) 1668358.1733119658 iter/sec (stddev: 1.9225815213946484e-8) 0.99
benchmarks/test_guard_lifecycle.py::test_g7_request_lifecycle_batch 2888.0780056851654 iter/sec (stddev: 0.000015159174440926923) 2839.2156896899733 iter/sec (stddev: 0.000013756369070727859) 0.98
benchmarks/test_guard_lifecycle.py::test_g7c_event_loop_floor_control 58357.4930292669 iter/sec (stddev: 0.0000020100132696184768) 58283.47481855502 iter/sec (stddev: 0.0000021182512433194013) 1.00
benchmarks/test_guard_lifecycle.py::test_g7b_request_cycle_sync 333968.2124713538 iter/sec (stddev: 1.50369104698206e-7) 345822.7268781275 iter/sec (stddev: 9.286792412299475e-8) 1.04
benchmarks/test_guard_lifecycle.py::test_g13_teardown_at_scale 49492.03524199116 iter/sec (stddev: 0.0000020113461100953906) 49603.6366139206 iter/sec (stddev: 0.000002253219725869423) 1.00
benchmarks/test_guard_lifecycle.py::test_g13b_teardown_at_scale_async_no_finalizers 473.22309620020445 iter/sec (stddev: 0.0010174957839167527) 467.50081550218664 iter/sec (stddev: 0.0011356040431617868) 0.99
benchmarks/test_guard_resolve.py::test_g1_transient_resolve 2018743.9365114935 iter/sec (stddev: 4.0537200815586305e-8) 2007530.2459533864 iter/sec (stddev: 3.634368716167373e-8) 0.99
benchmarks/test_guard_resolve.py::test_g2_cached_resolve 3004091.7832897976 iter/sec (stddev: 8.864087856376386e-9) 2826321.578077196 iter/sec (stddev: 1.0206665323835546e-8) 0.94
benchmarks/test_guard_resolve.py::test_g3_deep_chain 714091.2136911361 iter/sec (stddev: 3.901061733577981e-8) 570436.8588003246 iter/sec (stddev: 3.036574585356185e-7) 0.80
benchmarks/test_guard_resolve.py::test_g4_wide_resolve 435948.6766340519 iter/sec (stddev: 3.7741858521043396e-7) 362138.9192055074 iter/sec (stddev: 8.162549886939354e-7) 0.83
benchmarks/test_guard_resolve.py::test_g5_cross_scope 1736558.0637756023 iter/sec (stddev: 4.249373288374624e-8) 1846794.371263989 iter/sec (stddev: 4.1151240163164876e-8) 1.06
benchmarks/test_guard_resolve.py::test_g9_context_resolve 1320075.5941263775 iter/sec (stddev: 1.5368942116973345e-7) 1323622.1821050942 iter/sec (stddev: 1.6394004416591575e-7) 1.00
benchmarks/test_guard_resolve.py::test_g12_override_active_resolve 696780.0123358928 iter/sec (stddev: 5.4662285091813195e-8) 726452.5755240317 iter/sec (stddev: 6.983069484850424e-8) 1.04
benchmarks/test_guard_resolve.py::test_g18_alias_hop 3016504.351079027 iter/sec (stddev: 9.597972345668654e-9) 3043187.9804295627 iter/sec (stddev: 1.3990043802709667e-8) 1.01
benchmarks/test_guard_validate.py::test_g10_validate_deep_chain 28342.738036439947 iter/sec (stddev: 0.00002135547205436896) 27066.48777162898 iter/sec (stddev: 0.00002303542056745669) 0.95
benchmarks/test_guard_validate.py::test_g11_validate_wide 16862.701757604296 iter/sec (stddev: 0.00002620319037480283) 16256.564834377594 iter/sec (stddev: 0.000027449817603891526) 0.96

This comment was automatically generated by workflow using github-action-benchmark.

@lesnik512

Copy link
Copy Markdown
Member Author

This was generated by AI.

Closing without merging. Every modern-python repo shares the same release.yml, and this would make modern-di's workflow the only one with a gate, split jobs and SHA-pinned actions. Release hardening, if wanted, belongs in the shared workflow for all repos. The LICENSE fix moved to #624.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ship LICENSE in the wheel and gate the release workflow

1 participant