Repository navigation
317 lines (283 loc) · 12.5 KB
/
Copy pathrelease.yml
File metadata and controls
317 lines (283 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
name: Release
on:
workflow_dispatch:
inputs:
version:
description: Stable package version without the v prefix
required: true
type: string
candidate-commit:
description: Full main-branch commit to qualify and publish
required: true
type: string
permissions:
contents: read
concurrency:
group: release-${{ inputs.version }}
cancel-in-progress: false
jobs:
preflight:
name: Verify unpublished main candidate
runs-on: ubuntu-latest
outputs:
commit: ${{ steps.request.outputs.commit }}
tag: ${{ steps.request.outputs.tag }}
version: ${{ steps.request.outputs.version }}
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
ref: ${{ github.sha }}
- name: Fetch current main and tags
run: git fetch origin main --tags
- name: Verify requested version and source state
env:
GITHUB_TOKEN: ${{ github.token }}
run: >-
python validation/release_preflight.py
--version "${{ inputs.version }}"
--candidate-commit "${{ inputs.candidate-commit }}"
--workflow-commit "${{ github.sha }}"
--repository "${{ github.repository }}"
--pypi-name ml4t-backtest
- name: Record the accepted request
id: request
run: |
{
echo "commit=${{ inputs.candidate-commit }}"
echo "tag=v${{ inputs.version }}"
echo "version=${{ inputs.version }}"
} >> "$GITHUB_OUTPUT"
ecosystem-qualification:
name: Ecosystem Qualification
needs: preflight
permissions:
contents: read
uses: ml4t/ecosystem/.github/workflows/qualify-library.yml@da2cbb9a9d0b4d2f166ad630470ff7d7a1aa2643 # 2026-09-19 policy snapshot
with:
import-package: ml4t.backtest
qualification:
name: Qualify Release Candidate
needs: preflight
uses: ./.github/workflows/ci.yml
with:
release-version: ${{ needs.preflight.outputs.version }}
private-comparisons:
name: Qualify Licensed and Container Comparisons
needs: preflight
uses: ./.github/workflows/private-comparisons.yml
secrets: inherit
deploy-documentation:
name: Deploy and verify documentation
runs-on: ubuntu-latest
needs: [preflight, ecosystem-qualification, private-comparisons, qualification]
environment: documentation
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
- name: Download exact documentation candidate
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: docs-${{ needs.preflight.outputs.commit }}
path: candidate-site/
- name: Verify documentation candidate identity
run: >-
python validation/check_documentation_identity.py
--site candidate-site
--expected-library backtest
--expected-version "${{ needs.preflight.outputs.version }}"
--expected-commit "${{ needs.preflight.outputs.commit }}"
- name: Require the documentation deployment credential
env:
SSH_DEPLOY_KEY: ${{ secrets.DOCS_DEPLOY_KEY }}
run: test -n "$SSH_DEPLOY_KEY"
- name: Deploy the immutable candidate to the canonical route
uses: cpina/github-action-push-to-another-repository@55306faa4ed53b815ae49e564af8cfb359d32ae2 # v1.7.3
env:
SSH_DEPLOY_KEY: ${{ secrets.DOCS_DEPLOY_KEY }}
with:
source-directory: candidate-site/
destination-github-username: ml4t
destination-repository-name: website
target-directory: static/docs/backtest/
target-branch: main
commit-message: "docs(backtest): publish ${{ needs.preflight.outputs.version }} from ${{ needs.preflight.outputs.commit }}"
user-name: ml4t-bot
user-email: bot@ml4trading.io
- name: Verify the deployed canonical and representative pages
run: >-
python validation/check_documentation_identity.py
--url https://www.ml4trading.io/docs/backtest/
--url https://www.ml4trading.io/docs/backtest/getting-started/quickstart/
--url https://www.ml4trading.io/docs/backtest/api/
--expected-library backtest
--expected-version "${{ needs.preflight.outputs.version }}"
--expected-commit "${{ needs.preflight.outputs.commit }}"
--attempts 24
--delay 10
publish:
name: Publish exact candidate to PyPI
runs-on: ubuntu-latest
needs: [preflight, deploy-documentation]
environment: pypi
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
- name: Download exact release candidate
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-candidate-${{ needs.preflight.outputs.commit }}
path: candidate/
- name: Verify candidate version, source, and SHA256 manifest
run: >-
python validation/release_candidate.py verify
--dist candidate/dist
--manifest candidate/release-candidate.json
--expected-commit "${{ needs.preflight.outputs.commit }}"
--expected-repository "${{ github.repository }}"
--expected-version "${{ needs.preflight.outputs.version }}"
--expected-tag "${{ needs.preflight.outputs.tag }}"
- name: Publish to PyPI with trusted publishing
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
packages-dir: candidate/dist/
verify-metadata: true
print-hash: true
attestations: true
tag-and-release:
name: Create tag and GitHub release
runs-on: ubuntu-latest
needs: [preflight, publish]
permissions:
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
ref: ${{ github.sha }}
- name: Download exact release candidate
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-candidate-${{ needs.preflight.outputs.commit }}
path: candidate/
- name: Verify PyPI published the candidate digests
run: >-
python validation/release_candidate.py verify-index
--manifest candidate/release-candidate.json
- name: Create or verify the commit-bound tag
env:
TAG: ${{ needs.preflight.outputs.tag }}
COMMIT: ${{ needs.preflight.outputs.commit }}
run: |
remote_commit="$(git ls-remote origin "refs/tags/$TAG^{}" | cut -f1)"
if test -n "$remote_commit"; then
test "$remote_commit" = "$COMMIT"
else
git config user.name "ml4t-bot"
git config user.email "bot@ml4trading.io"
git tag -a "$TAG" "$COMMIT" -m "Release $TAG"
git push origin "$TAG"
fi
- name: Create or complete the GitHub release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.preflight.outputs.tag }}
run: |
if ! gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then
gh release create "$TAG" --repo "${{ github.repository }}" --target "${{ needs.preflight.outputs.commit }}" --title "$TAG" --generate-notes
fi
gh release upload "$TAG" candidate/dist/* candidate/release-candidate.json --clobber --repo "${{ github.repository }}"
post-release:
name: Verify published release
runs-on: ubuntu-latest
needs: [preflight, tag-and-release]
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
- uses: astral-sh/setup-uv@bec219d24cd3e171d82865faccec33120bb574f4 # v10.1.0
with:
version: "latest"
python-version: "3.12"
- name: Download exact release candidate
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: release-candidate-${{ needs.preflight.outputs.commit }}
path: candidate/
- name: Verify PyPI identity and digests
run: >-
python validation/release_candidate.py verify-index
--manifest candidate/release-candidate.json
- name: Install and exercise the published wheel
run: |
uv venv --clear --python 3.12 "$RUNNER_TEMP/published-venv"
# The step above reads https://pypi.org/pypi/<name>/<version>/json; this one reads
# https://pypi.org/simple/. The two endpoints propagate independently, so the JSON
# API can already serve a version the simple index has not listed yet. On 0.1.7 the
# gap was under four minutes and turned a correct publication into a red release run.
attempt=1
until uv pip install --python "$RUNNER_TEMP/published-venv/bin/python" --index-url https://pypi.org/simple --refresh-package ml4t-backtest "ml4t-backtest==${{ needs.preflight.outputs.version }}"; do
if [ "$attempt" -ge 12 ]; then
echo "::error::ml4t-backtest==${{ needs.preflight.outputs.version }} is still absent from the simple index after $attempt attempts over 120s."
exit 1
fi
echo "Simple index has not listed ${{ needs.preflight.outputs.version }} yet (attempt $attempt); retrying in 10s."
attempt=$((attempt + 1))
sleep 10
done
"$RUNNER_TEMP/published-venv/bin/python" -I -c "import ml4t.backtest as package; assert package.__version__ == '${{ needs.preflight.outputs.version }}'"
"$RUNNER_TEMP/published-venv/bin/python" validation/check_documentation_examples.py
uv pip install --python "$RUNNER_TEMP/published-venv/bin/python" "ml4t-diagnostic[viz]==0.1.4"
"$RUNNER_TEMP/published-venv/bin/python" validation/check_documentation_examples.py docs/tutorials/diagnostic-handoff.md
"$RUNNER_TEMP/published-venv/bin/python" validation/check_documentation_examples.py docs/user-guide/results.md
- name: Download and verify GitHub release assets
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p published-release
gh release download "${{ needs.preflight.outputs.tag }}" --repo "${{ github.repository }}" --dir published-release
python validation/release_candidate.py verify --dist published-release --manifest published-release/release-candidate.json --expected-commit "${{ needs.preflight.outputs.commit }}" --expected-repository "${{ github.repository }}" --expected-version "${{ needs.preflight.outputs.version }}" --expected-tag "${{ needs.preflight.outputs.tag }}"
- name: Recheck deployed documentation identity
run: >-
python validation/check_documentation_identity.py
--url https://www.ml4trading.io/docs/backtest/
--url https://www.ml4trading.io/docs/backtest/api/
--expected-library backtest
--expected-version "${{ needs.preflight.outputs.version }}"
--expected-commit "${{ needs.preflight.outputs.commit }}"
--attempts 12
--delay 10
record-recovery:
name: Record post-publication recovery
if: ${{ always() && needs.publish.result == 'success' && needs.post-release.result != 'success' }}
runs-on: ubuntu-latest
needs: [preflight, publish, tag-and-release, post-release]
permissions:
contents: read
issues: write
steps:
- name: Create the recovery issue
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ needs.preflight.outputs.tag }}
COMMIT: ${{ needs.preflight.outputs.commit }}
run: >-
gh issue create
--repo "${{ github.repository }}"
--title "Recover incomplete $TAG publication"
--label "type: bug"
--label "priority: high"
--label "status: accepted"
--label "compatibility: affected"
--body "PyPI accepted $TAG from $COMMIT, but a later release step failed. Rerun only the failed jobs in this workflow so the retained candidate is reused. Do not rebuild or reuse this version for different bytes."