pyattacker 0.1.0__tar.gz → 0.2.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 (102) hide show
  1. pyattacker-0.2.0/.github/workflows/ci.yml +115 -0
  2. pyattacker-0.2.0/.github/workflows/release.yml +288 -0
  3. pyattacker-0.2.0/CHANGELOG.md +359 -0
  4. {pyattacker-0.1.0 → pyattacker-0.2.0}/PKG-INFO +143 -29
  5. {pyattacker-0.1.0 → pyattacker-0.2.0}/README.md +140 -27
  6. pyattacker-0.2.0/assets/logo/icon.png +0 -0
  7. pyattacker-0.2.0/assets/logo/title.png +0 -0
  8. pyattacker-0.2.0/docs/benchmark.md +418 -0
  9. {pyattacker-0.1.0 → pyattacker-0.2.0}/docs/cli.md +108 -7
  10. {pyattacker-0.1.0 → pyattacker-0.2.0}/docs/design.md +90 -7
  11. {pyattacker-0.1.0 → pyattacker-0.2.0}/docs/reference.md +255 -10
  12. pyattacker-0.2.0/docs/releasing.md +144 -0
  13. {pyattacker-0.1.0 → pyattacker-0.2.0}/docs/tutorial.md +13 -2
  14. {pyattacker-0.1.0 → pyattacker-0.2.0}/pyproject.toml +23 -5
  15. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/__init__.py +5 -2
  16. pyattacker-0.2.0/src/pyattacker/benchmark/__init__.py +66 -0
  17. pyattacker-0.2.0/src/pyattacker/benchmark/clock.py +259 -0
  18. pyattacker-0.2.0/src/pyattacker/benchmark/harness.py +391 -0
  19. pyattacker-0.2.0/src/pyattacker/benchmark/metrics.py +245 -0
  20. pyattacker-0.2.0/src/pyattacker/benchmark/provider.py +296 -0
  21. pyattacker-0.2.0/src/pyattacker/benchmark/report.py +370 -0
  22. pyattacker-0.2.0/src/pyattacker/benchmark/scenario.py +296 -0
  23. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/cli.py +143 -18
  24. pyattacker-0.2.0/src/pyattacker/declarative.py +782 -0
  25. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/errors.py +5 -0
  26. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/export.py +71 -22
  27. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/pipeline.py +2 -5
  28. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/resource.py +75 -0
  29. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/runner.py +78 -43
  30. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/store/__init__.py +14 -0
  31. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/store/base.py +157 -0
  32. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/store/memory.py +84 -1
  33. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/store/sqlite.py +172 -2
  34. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/store/writebehind.py +44 -1
  35. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/task.py +302 -11
  36. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/tasks/__init__.py +194 -12
  37. {pyattacker-0.1.0 → pyattacker-0.2.0}/tests/test_backends.py +5 -0
  38. pyattacker-0.2.0/tests/test_benchmark_cli.py +129 -0
  39. pyattacker-0.2.0/tests/test_benchmark_clock.py +263 -0
  40. pyattacker-0.2.0/tests/test_benchmark_harness.py +510 -0
  41. pyattacker-0.2.0/tests/test_benchmark_provider.py +346 -0
  42. pyattacker-0.2.0/tests/test_benchmark_report.py +426 -0
  43. pyattacker-0.2.0/tests/test_benchmark_scenario.py +128 -0
  44. pyattacker-0.2.0/tests/test_cli.py +785 -0
  45. {pyattacker-0.1.0 → pyattacker-0.2.0}/tests/test_declarative.py +117 -1
  46. pyattacker-0.2.0/tests/test_docs_examples.py +109 -0
  47. {pyattacker-0.1.0 → pyattacker-0.2.0}/tests/test_export.py +543 -1
  48. {pyattacker-0.1.0 → pyattacker-0.2.0}/tests/test_lease_safety.py +84 -0
  49. pyattacker-0.2.0/tests/test_optional_yaml.py +180 -0
  50. {pyattacker-0.1.0 → pyattacker-0.2.0}/tests/test_packaging.py +21 -3
  51. pyattacker-0.2.0/tests/test_resume_identity.py +393 -0
  52. pyattacker-0.2.0/tests/test_retry_policy.py +144 -0
  53. {pyattacker-0.1.0 → pyattacker-0.2.0}/tests/test_runner.py +1 -0
  54. {pyattacker-0.1.0 → pyattacker-0.2.0}/tests/test_scheduler.py +19 -1
  55. {pyattacker-0.1.0 → pyattacker-0.2.0}/tests/test_shard.py +43 -0
  56. {pyattacker-0.1.0 → pyattacker-0.2.0}/tests/test_store.py +114 -0
  57. pyattacker-0.2.0/tests/test_subprocess_lifecycle.py +599 -0
  58. pyattacker-0.2.0/tests/test_task_overrides.py +96 -0
  59. {pyattacker-0.1.0 → pyattacker-0.2.0}/tests/test_tutorial.py +6 -0
  60. {pyattacker-0.1.0 → pyattacker-0.2.0}/uv.lock +8 -3
  61. pyattacker-0.1.0/.github/workflows/ci.yml +0 -53
  62. pyattacker-0.1.0/CHANGELOG.md +0 -91
  63. pyattacker-0.1.0/src/pyattacker/declarative.py +0 -327
  64. pyattacker-0.1.0/tests/test_cli.py +0 -398
  65. {pyattacker-0.1.0 → pyattacker-0.2.0}/.gitignore +0 -0
  66. {pyattacker-0.1.0 → pyattacker-0.2.0}/LICENSE +0 -0
  67. {pyattacker-0.1.0 → pyattacker-0.2.0}/examples/data.jsonl +0 -0
  68. {pyattacker-0.1.0 → pyattacker-0.2.0}/examples/llm_eval/README.md +0 -0
  69. {pyattacker-0.1.0 → pyattacker-0.2.0}/examples/llm_eval/__init__.py +0 -0
  70. {pyattacker-0.1.0 → pyattacker-0.2.0}/examples/llm_eval/backend.py +0 -0
  71. {pyattacker-0.1.0 → pyattacker-0.2.0}/examples/llm_eval/demo.py +0 -0
  72. {pyattacker-0.1.0 → pyattacker-0.2.0}/examples/llm_eval/pipelines.py +0 -0
  73. {pyattacker-0.1.0 → pyattacker-0.2.0}/examples/plugin_package/README.md +0 -0
  74. {pyattacker-0.1.0 → pyattacker-0.2.0}/examples/plugin_package/pa_demo_plugin/__init__.py +0 -0
  75. {pyattacker-0.1.0 → pyattacker-0.2.0}/examples/plugin_package/pa_demo_plugin/algorithms.py +0 -0
  76. {pyattacker-0.1.0 → pyattacker-0.2.0}/examples/plugin_package/pa_demo_plugin/codecs.py +0 -0
  77. {pyattacker-0.1.0 → pyattacker-0.2.0}/examples/plugin_package/pa_demo_plugin/tasks.py +0 -0
  78. {pyattacker-0.1.0 → pyattacker-0.2.0}/examples/plugin_package/pyproject.toml +0 -0
  79. {pyattacker-0.1.0 → pyattacker-0.2.0}/examples/plugin_tasks.yaml +0 -0
  80. {pyattacker-0.1.0 → pyattacker-0.2.0}/examples/qa_eval.yaml +0 -0
  81. {pyattacker-0.1.0 → pyattacker-0.2.0}/examples/quickstart.py +0 -0
  82. {pyattacker-0.1.0 → pyattacker-0.2.0}/examples/sharded.py +0 -0
  83. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/__main__.py +0 -0
  84. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/algorithm.py +0 -0
  85. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/artifact.py +0 -0
  86. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/backends.py +0 -0
  87. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/merge.py +0 -0
  88. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/monitor.py +0 -0
  89. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/plugins.py +0 -0
  90. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/scheduler.py +0 -0
  91. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/server.py +0 -0
  92. {pyattacker-0.1.0 → pyattacker-0.2.0}/src/pyattacker/shard.py +0 -0
  93. {pyattacker-0.1.0 → pyattacker-0.2.0}/tests/helpers.py +0 -0
  94. {pyattacker-0.1.0 → pyattacker-0.2.0}/tests/test_algorithms.py +0 -0
  95. {pyattacker-0.1.0 → pyattacker-0.2.0}/tests/test_artifact.py +0 -0
  96. {pyattacker-0.1.0 → pyattacker-0.2.0}/tests/test_errors.py +0 -0
  97. {pyattacker-0.1.0 → pyattacker-0.2.0}/tests/test_m2.py +0 -0
  98. {pyattacker-0.1.0 → pyattacker-0.2.0}/tests/test_monitor.py +0 -0
  99. {pyattacker-0.1.0 → pyattacker-0.2.0}/tests/test_pipeline.py +0 -0
  100. {pyattacker-0.1.0 → pyattacker-0.2.0}/tests/test_plugins.py +0 -0
  101. {pyattacker-0.1.0 → pyattacker-0.2.0}/tests/test_server.py +0 -0
  102. {pyattacker-0.1.0 → pyattacker-0.2.0}/tests/test_tasks.py +0 -0
@@ -0,0 +1,115 @@
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
+ jobs:
14
+ test:
15
+ name: python ${{ matrix.python-version }}
16
+ runs-on: ubuntu-latest
17
+ strategy:
18
+ fail-fast: false
19
+ matrix:
20
+ # The two versions declared in pyproject.toml classifiers.
21
+ python-version: ["3.11", "3.12"]
22
+ steps:
23
+ - uses: actions/checkout@v7
24
+
25
+ - name: Install uv
26
+ # The only third-party action in the pipeline, so it carries a commit pin: setup-uv stopped
27
+ # publishing floating major tags at v8, and pinning to the commit is what its README now
28
+ # recommends. The actions/* above and below are GitHub's own and track a major tag.
29
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
30
+ with:
31
+ enable-cache: true
32
+ python-version: ${{ matrix.python-version }}
33
+
34
+ - name: Install dependencies
35
+ run: uv sync --locked
36
+
37
+ - name: Lint
38
+ run: uv run ruff check
39
+
40
+ - name: Test
41
+ # The whole suite is offline; network access is never needed.
42
+ run: uv run pytest
43
+
44
+ - name: CLI smoke test
45
+ run: uv run pyattacker demo --pipelines 25 --store runs/demo.db
46
+
47
+ - name: Example smoke tests
48
+ # The examples are documentation, and documentation rots: the llm_eval demo also asserts
49
+ # its own claim about checkpoint granularity, so this catches recovery regressions.
50
+ run: |
51
+ uv run python examples/quickstart.py
52
+ uv run python examples/sharded.py
53
+ uv run python -m examples.llm_eval.demo
54
+
55
+ - name: Build sdist and wheel
56
+ run: uv build
57
+
58
+ no-extra:
59
+ name: base install, no yaml extra
60
+ runs-on: ubuntu-latest
61
+ steps:
62
+ - uses: actions/checkout@v7
63
+
64
+ - name: Install uv
65
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
66
+ with:
67
+ enable-cache: true
68
+ # Both 3.11 jobs read the same lockfile, so without a suffix they compute the same cache
69
+ # key and setup-uv warns that one of them could not upload the cache it just built. The
70
+ # suffix is also honest: this job installs a different set of packages (pytest and the
71
+ # build backend, rather than the dev group).
72
+ cache-suffix: no-extra
73
+ python-version: "3.11"
74
+
75
+ - name: Install the library and nothing else
76
+ # Deliberately not `uv sync`: the dev group carries the yaml extra, and doing without it is
77
+ # the entire point of this job. Installing into /tmp keeps the venv out of the checkout.
78
+ run: |
79
+ uv venv /tmp/no-extra --python 3.11
80
+ uv pip install --python /tmp/no-extra/bin/python . pytest
81
+
82
+ - name: PyYAML really is absent, and importing pyattacker does not want it
83
+ run: |
84
+ /tmp/no-extra/bin/python -c "
85
+ import importlib.util, sys
86
+ assert importlib.util.find_spec('yaml') is None, 'pyyaml leaked into the base install'
87
+ import pyattacker
88
+ assert 'yaml' not in sys.modules, 'importing pyattacker pulled in pyyaml'
89
+ print('pyattacker imports cleanly with no pyyaml installed')
90
+ "
91
+
92
+ - name: The suite passes without it
93
+ # The `requires_yaml` tests are deselected rather than skipped, so an accidental loss of the
94
+ # extra in the *development* environment fails the ordinary suite instead of quietly skipping
95
+ # a third of it. The optional-dependency tests above are not marked, and run here for real.
96
+ run: /tmp/no-extra/bin/python -m pytest -q -m "not requires_yaml"
97
+
98
+ - name: A YAML config asks for the extra, a JSON one does not
99
+ run: |
100
+ /tmp/no-extra/bin/pyattacker --version
101
+ mkdir -p /tmp/no-extra-check
102
+ /tmp/no-extra/bin/pyattacker demo --pipelines 10 --store /tmp/no-extra-check/demo.db
103
+ printf 'pipeline:\n name: x\n tasks:\n - {use: echo}\n' > /tmp/no-extra-check/x.yaml
104
+ printf '{"pipeline": {"name": "x", "tasks": [{"use": "echo"}]}}' > /tmp/no-extra-check/x.json
105
+ if /tmp/no-extra/bin/pyattacker validate -c /tmp/no-extra-check/x.yaml 2>/tmp/no-extra-check/err.txt; then
106
+ echo "::error::a YAML config was accepted without the extra installed"
107
+ exit 1
108
+ fi
109
+ grep -q 'pyattacker\[yaml\]' /tmp/no-extra-check/err.txt || {
110
+ echo "::error::the YAML error does not name the extra to install"
111
+ cat /tmp/no-extra-check/err.txt
112
+ exit 1
113
+ }
114
+ /tmp/no-extra/bin/pyattacker validate -c /tmp/no-extra-check/x.json > /dev/null
115
+ echo "yaml asked for the extra; json validated"
@@ -0,0 +1,288 @@
1
+ name: Release
2
+
3
+ # Publishing is driven by a tag, so the artifact is always traceable to an exact commit.
4
+ # Pushing v0.2.0 builds, verifies and publishes to PyPI, then attaches the files to a GitHub
5
+ # Release. `workflow_dispatch` runs everything except the PyPI upload, which is how you rehearse
6
+ # a release without spending a version number: PyPI refuses a re-upload, and a version, once
7
+ # published, can be deleted but never reused.
8
+ on:
9
+ push:
10
+ tags: ["v*"]
11
+ workflow_dispatch:
12
+ inputs:
13
+ publish_to_testpypi:
14
+ description: "Publish to TestPyPI (a dry run needs no version number)"
15
+ type: boolean
16
+ default: false
17
+
18
+ concurrency:
19
+ group: release-${{ github.ref }}
20
+ cancel-in-progress: false # never interrupt an upload in progress
21
+
22
+ permissions: {}
23
+
24
+ jobs:
25
+ # The gate. A tag can point at any commit, including one that never passed CI, so the release
26
+ # re-runs the full suite instead of trusting that it did.
27
+ verify:
28
+ name: verify python ${{ matrix.python-version }}
29
+ runs-on: ubuntu-latest
30
+ strategy:
31
+ fail-fast: true # a release is all-or-nothing; stop at the first failure
32
+ matrix:
33
+ python-version: ["3.11", "3.12"]
34
+ steps:
35
+ - uses: actions/checkout@v7
36
+
37
+ - name: Install uv
38
+ # The only third-party action in the pipeline, so it carries a commit pin: setup-uv stopped
39
+ # publishing floating major tags at v8, and pinning to the commit is what its README now
40
+ # recommends. The actions/* elsewhere in this file are GitHub's own and track a major tag.
41
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
42
+ with:
43
+ enable-cache: true
44
+ python-version: ${{ matrix.python-version }}
45
+
46
+ - name: Install dependencies
47
+ run: uv sync --locked
48
+
49
+ - name: Lint
50
+ run: uv run ruff check
51
+
52
+ - name: Test
53
+ run: uv run pytest
54
+
55
+ - name: CLI smoke test
56
+ run: uv run pyattacker demo --pipelines 25 --store runs/demo.db
57
+
58
+ - name: Example smoke tests
59
+ run: |
60
+ uv run python examples/quickstart.py
61
+ uv run python examples/sharded.py
62
+ uv run python -m examples.llm_eval.demo
63
+
64
+ build:
65
+ name: build and check artifacts
66
+ needs: verify
67
+ runs-on: ubuntu-latest
68
+ outputs:
69
+ version: ${{ steps.version.outputs.version }}
70
+ steps:
71
+ - uses: actions/checkout@v7
72
+
73
+ - name: Install uv
74
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
75
+ with:
76
+ enable-cache: true
77
+ python-version: "3.11"
78
+
79
+ - name: Build sdist and wheel
80
+ run: |
81
+ rm -rf dist
82
+ uv build
83
+
84
+ - name: Read the version being released
85
+ id: version
86
+ run: |
87
+ version="$(uv run --no-project --with ./dist/*.whl \
88
+ python -c 'import pyattacker; print(pyattacker.__version__)')"
89
+ echo "version=$version" >> "$GITHUB_OUTPUT"
90
+ echo "building version $version"
91
+
92
+ # A tag that disagrees with the package version produces a release nobody can find again.
93
+ - name: Check the tag matches the package version
94
+ if: startsWith(github.ref, 'refs/tags/v')
95
+ env:
96
+ VERSION: ${{ steps.version.outputs.version }}
97
+ run: |
98
+ tag="${GITHUB_REF#refs/tags/v}"
99
+ if [ "$tag" != "$VERSION" ]; then
100
+ echo "::error::tag v$tag does not match __version__ $VERSION — fix one of them and re-tag"
101
+ exit 1
102
+ fi
103
+ echo "tag v$tag matches the package version"
104
+
105
+ # Metadata has to render on PyPI, and the sdist has to be self-sufficient: these two checks
106
+ # are what caught a leaked local config file and 16 README links that break off GitHub.
107
+ - name: Check metadata renders
108
+ run: uvx twine check --strict dist/*
109
+
110
+ - name: Check the sdist carries no local tooling state
111
+ run: |
112
+ if tar tzf dist/*.tar.gz | grep -E '(^|/)\.(claude|venv|env|idea|vscode)/|\.pyc$'; then
113
+ echo "::error::the sdist contains local tooling state; tighten the include list in pyproject.toml"
114
+ exit 1
115
+ fi
116
+ echo "sdist contains no local tooling state"
117
+
118
+ - name: Check the sdist rebuilds and passes its own tests
119
+ run: |
120
+ mkdir -p /tmp/sdist && tar xzf dist/*.tar.gz -C /tmp/sdist --strip-components=1
121
+ cd /tmp/sdist
122
+ uv venv .venv --python 3.11
123
+ # The extra is requested on purpose here: the whole suite is the point of this check, and
124
+ # the configuration without PyYAML is the `no-extra` job in ci.yml plus the wheel check below.
125
+ uv pip install --python .venv/bin/python -e ".[yaml]" pytest
126
+ .venv/bin/python -m pytest -q
127
+
128
+ - name: Check the wheel installs and runs
129
+ run: |
130
+ uv venv /tmp/wheel-check --python 3.11
131
+ uv pip install --python /tmp/wheel-check/bin/python dist/*.whl
132
+ /tmp/wheel-check/bin/pyattacker --version
133
+ /tmp/wheel-check/bin/pyattacker demo --pipelines 10 --store /tmp/wheel-check/demo.db
134
+
135
+ - name: Check the extra really is optional
136
+ # A plain install of the wheel must bring in nothing, so this is the one place where the
137
+ # published metadata is checked rather than the source tree: PyYAML must be absent, importing
138
+ # pyattacker must not change that, YAML must ask for the extra, and JSON must still work.
139
+ run: |
140
+ /tmp/wheel-check/bin/python -c "
141
+ import importlib.util, sys
142
+ assert importlib.util.find_spec('yaml') is None, 'pyyaml leaked into the base install'
143
+ import pyattacker
144
+ assert 'yaml' not in sys.modules, 'importing pyattacker pulled in pyyaml'
145
+ print('the wheel installs with no dependencies')
146
+ "
147
+ mkdir -p /tmp/wheel-check/cfg
148
+ printf 'pipeline:\n name: x\n tasks:\n - {use: echo}\n' > /tmp/wheel-check/cfg/x.yaml
149
+ printf '{"pipeline": {"name": "x", "tasks": [{"use": "echo"}]}}' > /tmp/wheel-check/cfg/x.json
150
+ if /tmp/wheel-check/bin/pyattacker validate -c /tmp/wheel-check/cfg/x.yaml 2>/tmp/wheel-check/err.txt; then
151
+ echo "::error::a YAML config was accepted without the extra installed"
152
+ exit 1
153
+ fi
154
+ grep -q 'pyattacker\[yaml\]' /tmp/wheel-check/err.txt || {
155
+ echo "::error::the YAML error does not name the extra to install"
156
+ cat /tmp/wheel-check/err.txt
157
+ exit 1
158
+ }
159
+ /tmp/wheel-check/bin/pyattacker validate -c /tmp/wheel-check/cfg/x.json > /dev/null
160
+ echo "yaml asked for the extra; json validated"
161
+
162
+ - name: Check the wheel's yaml extra installs and works
163
+ # The other direction of the same metadata, and the one no source-level test can cover: the
164
+ # extra has to make `pip install "pyattacker[yaml]"` resolve PyYAML *from the built wheel*, and
165
+ # that has to be enough to read a real config. A wrong Provides-Extra, a missing marker on
166
+ # Requires-Dist or a build-backend change would leave the suite green and break this.
167
+ run: |
168
+ uv venv /tmp/wheel-yaml-check --python 3.11
169
+ wheel="$(ls dist/*.whl)" # exactly one; `dist/` is rebuilt from scratch above
170
+ uv pip install --python /tmp/wheel-yaml-check/bin/python "${wheel}[yaml]"
171
+ /tmp/wheel-yaml-check/bin/python -c "
172
+ import importlib.metadata as md, yaml
173
+ print('PyYAML', yaml.__version__, 'from', md.requires('pyattacker'))
174
+ "
175
+ mkdir -p /tmp/wheel-yaml-check/cfg
176
+ printf 'pipeline:\n name: x\n tasks:\n - {use: echo}\n' > /tmp/wheel-yaml-check/cfg/x.yaml
177
+ /tmp/wheel-yaml-check/bin/pyattacker validate -c /tmp/wheel-yaml-check/cfg/x.yaml > /dev/null
178
+ echo "the extra resolved pyyaml and the config validated"
179
+
180
+ - uses: actions/upload-artifact@v7
181
+ with:
182
+ name: dist
183
+ path: dist/
184
+ if-no-files-found: error
185
+
186
+ # Publishing lives in its own job so the OIDC token is scoped to it alone: no test or build step
187
+ # ever holds a credential that can upload a release.
188
+ testpypi:
189
+ name: publish to TestPyPI
190
+ if: github.event_name == 'workflow_dispatch' && inputs.publish_to_testpypi
191
+ needs: build
192
+ runs-on: ubuntu-latest
193
+ environment: testpypi
194
+ permissions:
195
+ id-token: write # mint the OIDC token trusted publishing exchanges for an upload token
196
+ steps:
197
+ - uses: actions/download-artifact@v8
198
+ with:
199
+ name: dist
200
+ path: dist/
201
+
202
+ - name: Install uv
203
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
204
+ with:
205
+ # This job never checks out the repository, so there is no uv.lock to key a cache on:
206
+ # caching here can only ever warn that the cache will never get invalidated.
207
+ enable-cache: false
208
+
209
+ # --check-url lets a re-run skip files already uploaded instead of failing the job.
210
+ - name: Publish
211
+ run: |
212
+ uv publish \
213
+ --trusted-publishing always \
214
+ --publish-url https://test.pypi.org/legacy/ \
215
+ --check-url https://test.pypi.org/simple/ \
216
+ dist/*
217
+
218
+ pypi:
219
+ name: publish to PyPI
220
+ if: startsWith(github.ref, 'refs/tags/v')
221
+ needs: build
222
+ runs-on: ubuntu-latest
223
+ environment: pypi
224
+ permissions:
225
+ id-token: write
226
+ steps:
227
+ - uses: actions/download-artifact@v8
228
+ with:
229
+ name: dist
230
+ path: dist/
231
+
232
+ - name: Install uv
233
+ uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
234
+ with:
235
+ # No checkout in this job, so no lockfile to key a cache on — see the note in `testpypi`.
236
+ enable-cache: false
237
+
238
+ - name: Publish
239
+ run: |
240
+ uv publish \
241
+ --trusted-publishing always \
242
+ --check-url https://pypi.org/simple/ \
243
+ dist/*
244
+
245
+ github_release:
246
+ name: create the GitHub release
247
+ if: startsWith(github.ref, 'refs/tags/v')
248
+ # `pypi` gates this on the upload having succeeded; `build` is also required because
249
+ # needs.build.outputs is only readable from a job that lists build as a dependency.
250
+ needs: [build, pypi]
251
+ runs-on: ubuntu-latest
252
+ permissions:
253
+ contents: write # create the release and upload the artifacts to it
254
+ steps:
255
+ - uses: actions/checkout@v7
256
+
257
+ - uses: actions/download-artifact@v8
258
+ with:
259
+ name: dist
260
+ path: dist/
261
+
262
+ # The section for this version in CHANGELOG.md, so the release notes are the notes we wrote
263
+ # rather than a list of commit subjects.
264
+ - name: Extract the changelog section
265
+ env:
266
+ VERSION: ${{ needs.build.outputs.version }}
267
+ run: |
268
+ if awk -v v="$VERSION" '
269
+ $0 ~ "^## \\[" v "\\]" {found=1; next}
270
+ found && /^## \[/ {exit}
271
+ found {print}
272
+ ' CHANGELOG.md > notes.md && [ -s notes.md ]; then
273
+ echo "using the CHANGELOG.md section for $VERSION"
274
+ else
275
+ echo "No changelog section for $VERSION." > notes.md
276
+ echo "::warning::CHANGELOG.md has no section for $VERSION; publishing with a placeholder"
277
+ fi
278
+
279
+ - name: Create the release
280
+ env:
281
+ GH_TOKEN: ${{ github.token }}
282
+ VERSION: ${{ needs.build.outputs.version }}
283
+ run: |
284
+ gh release create "${GITHUB_REF#refs/tags/}" \
285
+ --title "pyattacker $VERSION" \
286
+ --notes-file notes.md \
287
+ --verify-tag \
288
+ dist/*