utec-client 0.5.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 (57) hide show
  1. utec_client-0.5.0/.github/dependabot.yml +15 -0
  2. utec_client-0.5.0/.github/workflows/manual-release.yml +88 -0
  3. utec_client-0.5.0/.github/workflows/publish.yml +263 -0
  4. utec_client-0.5.0/.github/workflows/test.yml +57 -0
  5. utec_client-0.5.0/.github/workflows/weekly-release.yml +107 -0
  6. utec_client-0.5.0/.gitignore +508 -0
  7. utec_client-0.5.0/.vscode/launch.json +15 -0
  8. utec_client-0.5.0/.vscode/settings.json +14 -0
  9. utec_client-0.5.0/LICENSE +21 -0
  10. utec_client-0.5.0/MIGRATION.md +46 -0
  11. utec_client-0.5.0/PKG-INFO +99 -0
  12. utec_client-0.5.0/README.md +66 -0
  13. utec_client-0.5.0/pyproject.toml +108 -0
  14. utec_client-0.5.0/scripts/check_artifact.py +150 -0
  15. utec_client-0.5.0/scripts/find_validated_build.py +181 -0
  16. utec_client-0.5.0/scripts/prepare_release.py +282 -0
  17. utec_client-0.5.0/scripts/pypi_preflight.py +267 -0
  18. utec_client-0.5.0/scripts/release_logic.py +172 -0
  19. utec_client-0.5.0/scripts/test_api_behavior.py +311 -0
  20. utec_client-0.5.0/scripts/test_webhook.py +657 -0
  21. utec_client-0.5.0/scripts/verify_assets.py +75 -0
  22. utec_client-0.5.0/scripts/verify_validated_build.py +128 -0
  23. utec_client-0.5.0/setup.cfg +4 -0
  24. utec_client-0.5.0/src/utec_client/__init__.py +0 -0
  25. utec_client-0.5.0/src/utec_client/api.py +146 -0
  26. utec_client-0.5.0/src/utec_client/auth.py +45 -0
  27. utec_client-0.5.0/src/utec_client/const.py +38 -0
  28. utec_client-0.5.0/src/utec_client/devices/__init__.py +0 -0
  29. utec_client-0.5.0/src/utec_client/devices/device.py +274 -0
  30. utec_client-0.5.0/src/utec_client/devices/device_const.py +222 -0
  31. utec_client-0.5.0/src/utec_client/devices/light.py +128 -0
  32. utec_client-0.5.0/src/utec_client/devices/lock.py +108 -0
  33. utec_client-0.5.0/src/utec_client/devices/sensor.py +10 -0
  34. utec_client-0.5.0/src/utec_client/devices/switch.py +46 -0
  35. utec_client-0.5.0/src/utec_client/exceptions.py +40 -0
  36. utec_client-0.5.0/src/utec_client.egg-info/PKG-INFO +99 -0
  37. utec_client-0.5.0/src/utec_client.egg-info/SOURCES.txt +55 -0
  38. utec_client-0.5.0/src/utec_client.egg-info/dependency_links.txt +1 -0
  39. utec_client-0.5.0/src/utec_client.egg-info/requires.txt +13 -0
  40. utec_client-0.5.0/src/utec_client.egg-info/scm_file_list.json +52 -0
  41. utec_client-0.5.0/src/utec_client.egg-info/scm_version.json +8 -0
  42. utec_client-0.5.0/src/utec_client.egg-info/top_level.txt +1 -0
  43. utec_client-0.5.0/tests/__init__.py +0 -0
  44. utec_client-0.5.0/tests/conftest.py +92 -0
  45. utec_client-0.5.0/tests/test_api.py +272 -0
  46. utec_client-0.5.0/tests/test_auth.py +54 -0
  47. utec_client-0.5.0/tests/test_base_device.py +231 -0
  48. utec_client-0.5.0/tests/test_device_const.py +45 -0
  49. utec_client-0.5.0/tests/test_device_info.py +34 -0
  50. utec_client-0.5.0/tests/test_exceptions.py +47 -0
  51. utec_client-0.5.0/tests/test_light.py +201 -0
  52. utec_client-0.5.0/tests/test_lock.py +261 -0
  53. utec_client-0.5.0/tests/test_publish_pipeline.py +650 -0
  54. utec_client-0.5.0/tests/test_release_logic.py +115 -0
  55. utec_client-0.5.0/tests/test_release_plumbing.py +462 -0
  56. utec_client-0.5.0/tests/test_smoke.py +21 -0
  57. utec_client-0.5.0/tests/test_switch.py +50 -0
@@ -0,0 +1,15 @@
1
+ version: 2
2
+ updates:
3
+ # Python dependencies (from pyproject.toml), weekly, no auto-merge.
4
+ - package-ecosystem: "pip"
5
+ directory: "/"
6
+ schedule:
7
+ interval: "weekly"
8
+ labels: ["dependencies"]
9
+ # GitHub Actions, weekly, no auto-merge. Dependabot may update the pinned
10
+ # action revisions in workflows when new versions are released.
11
+ - package-ecosystem: "github-actions"
12
+ directory: "/"
13
+ schedule:
14
+ interval: "weekly"
15
+ labels: ["dependencies"]
@@ -0,0 +1,88 @@
1
+ name: Manual release
2
+
3
+ # Manual dispatch accepting a custom stable version (X.Y.Z / vX.Y.Z). Always
4
+ # validates from and releases at the tip of main (dispatching from another
5
+ # ref is rejected: the workflow checks out main and compares the pinned HEAD
6
+ # against the fetched origin/main tip). Can create the first fork release,
7
+ # permits intentional minor/major choices, and refuses reused, lower, or
8
+ # non-stable versions. After creating the tag and release it dispatches the
9
+ # common validated publishing path (publish.yml), which builds and checks
10
+ # the exact tagged source.
11
+
12
+ on:
13
+ workflow_dispatch:
14
+ inputs:
15
+ version:
16
+ description: "Stable version to release, e.g. 1.2.3 or v1.2.3"
17
+ required: true
18
+ type: string
19
+
20
+ permissions:
21
+ contents: read
22
+
23
+ concurrency:
24
+ group: utec-client-release
25
+ cancel-in-progress: false
26
+
27
+ jobs:
28
+ release:
29
+ runs-on: ubuntu-latest
30
+ permissions:
31
+ contents: write # create the tag and release
32
+ actions: write # explicitly dispatch the publish workflow
33
+ env:
34
+ GH_TOKEN: ${{ github.token }}
35
+ REQUESTED_VERSION: ${{ inputs.version }}
36
+ steps:
37
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
38
+ with:
39
+ ref: main
40
+ fetch-depth: 0
41
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
42
+ with:
43
+ python-version: "3.13"
44
+ - name: Validate requested version at the tip of main
45
+ id: pin
46
+ run: |
47
+ git remote set-branches origin main
48
+ git fetch origin main
49
+ git checkout --detach origin/main
50
+ OUT=$(python scripts/prepare_release.py manual "$REQUESTED_VERSION")
51
+ echo "$OUT"
52
+ echo "tag=$(echo "$OUT" | python -c "import json,sys; print(json.load(sys.stdin)['tag'])")" >> "$GITHUB_OUTPUT"
53
+ echo "version=$(echo "$OUT" | python -c "import json,sys; print(json.load(sys.stdin)['version'])")" >> "$GITHUB_OUTPUT"
54
+ echo "sha=$(echo "$OUT" | python -c "import json,sys; print(json.load(sys.stdin)['sha'])")" >> "$GITHUB_OUTPUT"
55
+ - name: Create tag at the pinned main commit
56
+ env:
57
+ RELEASE_TAG: ${{ steps.pin.outputs.tag }}
58
+ PINNED_SHA: ${{ steps.pin.outputs.sha }}
59
+ run: |
60
+ git tag "${RELEASE_TAG}" "${PINNED_SHA}"
61
+ git push origin "refs/tags/${RELEASE_TAG}"
62
+ - name: Generate release notes since the previous fork release
63
+ env:
64
+ RELEASE_TAG: ${{ steps.pin.outputs.tag }}
65
+ run: python scripts/prepare_release.py notes --tag "${RELEASE_TAG}" > notes-body.md
66
+ - name: Create the release with the fork marker
67
+ env:
68
+ RELEASE_TAG: ${{ steps.pin.outputs.tag }}
69
+ RELEASE_VERSION: ${{ steps.pin.outputs.version }}
70
+ run: |
71
+ # The marker line identifies this as a fork release for weekly
72
+ # selection; eligibility additionally requires fork-base ancestry.
73
+ printf 'utec-client-fork-release: true\n\n' > release-body.md
74
+ cat notes-body.md >> release-body.md
75
+ gh release create "${RELEASE_TAG}" \
76
+ --verify-tag \
77
+ --target "${{ steps.pin.outputs.sha }}" \
78
+ --title "utec-client ${RELEASE_VERSION}" \
79
+ --notes-file release-body.md
80
+ - name: Dispatch the common validated publishing path
81
+ env:
82
+ RELEASE_TAG: ${{ steps.pin.outputs.tag }}
83
+ RELEASE_VERSION: ${{ steps.pin.outputs.version }}
84
+ run: |
85
+ gh workflow run publish.yml \
86
+ --ref main \
87
+ -f tag="${RELEASE_TAG}" \
88
+ -f version="${RELEASE_VERSION}"
@@ -0,0 +1,263 @@
1
+ name: Publish to PyPI
2
+
3
+ # The single common validated publishing path for human-created, manual, and
4
+ # weekly releases. Flow:
5
+ #
6
+ # 1. validate-source (trusted main checkout, before any target-tag use):
7
+ # strict stable tag/version pair, single remote tag ref, tag commit an
8
+ # ancestor of origin/main, release carries the fork marker, tag commit a
9
+ # strict descendant of the fork base. Inputs only ever reach scripts via
10
+ # env vars.
11
+ # 2. build: if a prior run of this trusted workflow already built the exact
12
+ # source commit (its BUILD job succeeded even if the run later failed at
13
+ # publish time; located via the GitHub API, scripts/find_validated_build.py),
14
+ # its immutable GitHub Actions artifact release-dists-<source sha> is
15
+ # reused — retries never rebuild. Otherwise, when PyPI has NO release
16
+ # files yet, the selected source is built and fully validated (lint,
17
+ # tests, build, clean-environment artifact checks); when PyPI already has
18
+ # files, a missing/unusable validated artifact fails closed instead of
19
+ # rebuilding originals. The artifact carries PROVENANCE.json (tag,
20
+ # version, source commit, building run) + SHA256SUMS. Release assets are
21
+ # intentionally NOT uploaded: trust rests on the immutable workflow
22
+ # artifact plus its provenance binding.
23
+ # 3. pypi-publish (OIDC): re-downloads the same-run immutable artifact,
24
+ # re-verifies provenance + manifest + producer run + version, classifies
25
+ # against PyPI (read-only), uploads only missing distribution files, then
26
+ # performs a post-upload readback verification.
27
+
28
+ on:
29
+ workflow_call:
30
+ inputs:
31
+ tag:
32
+ required: true
33
+ type: string
34
+ version:
35
+ required: true
36
+ type: string
37
+ workflow_dispatch:
38
+ inputs:
39
+ tag:
40
+ description: "Release tag to publish (must already exist)"
41
+ required: true
42
+ type: string
43
+ version:
44
+ description: "Stable version matching the tag, e.g. 1.2.3"
45
+ required: true
46
+ type: string
47
+ release:
48
+ types: [published]
49
+
50
+ concurrency:
51
+ group: utec-client-release
52
+ cancel-in-progress: false
53
+
54
+ permissions:
55
+ contents: read
56
+
57
+ jobs:
58
+ validate-source:
59
+ runs-on: ubuntu-latest
60
+ outputs:
61
+ tag: ${{ steps.set.outputs.tag }}
62
+ version: ${{ steps.set.outputs.version }}
63
+ env:
64
+ GH_TOKEN: ${{ github.token }}
65
+ steps:
66
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
67
+ with:
68
+ ref: main
69
+ fetch-depth: 0
70
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
71
+ with:
72
+ python-version: "3.13"
73
+ - id: set
74
+ run: |
75
+ if [ -n "$DISPATCH_TAG" ]; then
76
+ echo "tag=$DISPATCH_TAG" >> "$GITHUB_OUTPUT"
77
+ echo "version=$DISPATCH_VERSION" >> "$GITHUB_OUTPUT"
78
+ else
79
+ echo "tag=$RELEASE_TAG" >> "$GITHUB_OUTPUT"
80
+ echo "version=${RELEASE_TAG#v}" >> "$GITHUB_OUTPUT"
81
+ fi
82
+ env:
83
+ DISPATCH_TAG: ${{ inputs.tag }}
84
+ DISPATCH_VERSION: ${{ inputs.version }}
85
+ RELEASE_TAG: ${{ github.event.release.tag_name }}
86
+ - name: Validate source eligibility before any target-tag use
87
+ env:
88
+ PUBLISH_TAG: ${{ steps.set.outputs.tag }}
89
+ PUBLISH_VERSION: ${{ steps.set.outputs.version }}
90
+ run: |
91
+ git remote set-branches origin main
92
+ git fetch origin main
93
+ python scripts/prepare_release.py prepublish --tag "$PUBLISH_TAG" --version "$PUBLISH_VERSION"
94
+
95
+ build:
96
+ runs-on: ubuntu-latest
97
+ needs: validate-source
98
+ permissions:
99
+ contents: read
100
+ actions: read # locate/download the prior validated build artifact
101
+ env:
102
+ GH_TOKEN: ${{ github.token }}
103
+ RELEASE_TAG: ${{ needs.validate-source.outputs.tag }}
104
+ RELEASE_VERSION: ${{ needs.validate-source.outputs.version }}
105
+ outputs:
106
+ source_sha: ${{ steps.prepub.outputs.sha }}
107
+ built_by_run: ${{ steps.decide.outputs.built_by_run }}
108
+ steps:
109
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
110
+ with:
111
+ ref: main
112
+ fetch-depth: 0
113
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
114
+ with:
115
+ python-version: "3.13"
116
+ - run: pip install -e .[dev]
117
+ - name: Resolve and re-verify source eligibility
118
+ id: prepub
119
+ run: |
120
+ git remote set-branches origin main
121
+ git fetch origin main
122
+ OUT=$(python scripts/prepare_release.py prepublish --tag "$RELEASE_TAG" --version "$RELEASE_VERSION")
123
+ echo "$OUT"
124
+ SHA=$(echo "$OUT" | python -c "import json,sys; print(json.load(sys.stdin)['sha'])")
125
+ echo "sha=$SHA" >> "$GITHUB_OUTPUT"
126
+ - name: Look for a prior validated build of this exact source
127
+ id: decide
128
+ env:
129
+ SOURCE_SHA: ${{ steps.prepub.outputs.sha }}
130
+ CURRENT_RUN: ${{ github.run_id }}
131
+ run: |
132
+ set +e
133
+ OUT=$(python scripts/find_validated_build.py --sha "$SOURCE_SHA" --tag "$RELEASE_TAG" --exclude-run "$CURRENT_RUN")
134
+ code=$?
135
+ set -e
136
+ if [ "$code" = 2 ]; then
137
+ # Trusted run found but artifact missing/expired: fail closed here
138
+ # too (before any rebuild decision) with the actionable guidance
139
+ # the finder already printed.
140
+ exit 1
141
+ fi
142
+ if [ "$code" = 0 ]; then
143
+ REUSED_RUN=$(echo "$OUT" | awk '{print $1}')
144
+ ARTIFACT=$(echo "$OUT" | awk '{print $2}')
145
+ echo "reused=true" >> "$GITHUB_OUTPUT"
146
+ echo "reused_run_id=$REUSED_RUN" >> "$GITHUB_OUTPUT"
147
+ echo "artifact_name=$ARTIFACT" >> "$GITHUB_OUTPUT"
148
+ echo "built_by_run=$REUSED_RUN" >> "$GITHUB_OUTPUT"
149
+ else
150
+ echo "reused=false" >> "$GITHUB_OUTPUT"
151
+ echo "built_by_run=$CURRENT_RUN" >> "$GITHUB_OUTPUT"
152
+ fi
153
+ - name: Reuse prior validated build artifact
154
+ if: steps.decide.outputs.reused == 'true'
155
+ env:
156
+ REUSED_RUN: ${{ steps.decide.outputs.reused_run_id }}
157
+ ARTIFACT_NAME: ${{ steps.decide.outputs.artifact_name }}
158
+ SOURCE_SHA: ${{ steps.prepub.outputs.sha }}
159
+ run: |
160
+ mkdir -p dist
161
+ if ! gh run download "$REUSED_RUN" -n "$ARTIFACT_NAME" -D dist; then
162
+ echo "::error::failed to download validated artifact $ARTIFACT_NAME from run $REUSED_RUN."
163
+ echo "::error::Recovery: re-run the failed jobs of run $REUSED_RUN (its build job rebuilds from the same source commit), then re-dispatch this publish run. Do NOT rebuild from a different source after files may be on PyPI."
164
+ exit 1
165
+ fi
166
+ python scripts/verify_validated_build.py --dir dist --tag "$RELEASE_TAG" \
167
+ --version "$RELEASE_VERSION" --sha "$SOURCE_SHA" --built-by-run "$REUSED_RUN"
168
+ - name: Rebuilding this version is only allowed while it has no PyPI files
169
+ if: steps.decide.outputs.reused != 'true'
170
+ env:
171
+ RELEASE_VERSION: ${{ needs.validate-source.outputs.version }}
172
+ run: |
173
+ set +e
174
+ # N6: version-scoped gate — older published versions never block a
175
+ # new release; exit 4 (unknown PyPI state) also fails closed.
176
+ python scripts/pypi_preflight.py --check-files-exist --version "${RELEASE_VERSION}"
177
+ code=$?
178
+ set -e
179
+ if [ "$code" = 0 ]; then
180
+ echo "::error::PyPI already has files for version ${RELEASE_VERSION} but no validated artifact for source ${{ steps.prepub.outputs.sha }} was found; refusing to rebuild."
181
+ echo "::error::Recovery: re-run the failed PUBLISH job of the original run (its retained artifact is reused), or re-dispatch publish with the same tag/version to reuse a validated artifact from another trusted run. If the original artifact is irretrievably lost, manual owner intervention is required — never delete or reuse published filenames."
182
+ exit 1
183
+ fi
184
+ if [ "$code" != 1 ]; then
185
+ echo "::error::could not determine PyPI state for ${RELEASE_VERSION} (exit $code); refusing to rebuild on unknown state."
186
+ exit 1
187
+ fi
188
+ - name: Build and fully validate the selected source
189
+ if: steps.decide.outputs.reused != 'true'
190
+ env:
191
+ SOURCE_SHA: ${{ steps.prepub.outputs.sha }}
192
+ CURRENT_RUN: ${{ github.run_id }}
193
+ run: |
194
+ git fetch --force origin "refs/tags/${RELEASE_TAG}:refs/tags/${RELEASE_TAG}"
195
+ git checkout --detach "refs/tags/${RELEASE_TAG}"
196
+ pip install -e .[dev]
197
+ ruff check . && ruff format --check .
198
+ pytest
199
+ python -m build
200
+ V=$(ls dist | grep '\.whl$'); V=${V#utec_client-}; V=${V%-py3-none-any.whl}
201
+ if [ "$V" != "${RELEASE_VERSION}" ]; then
202
+ echo "::error::built version $V != release version ${RELEASE_VERSION}"
203
+ exit 1
204
+ fi
205
+ python -c "import json, os; json.dump({'tag': os.environ['RELEASE_TAG'], 'version': os.environ['RELEASE_VERSION'], 'source_sha': os.environ['SOURCE_SHA'], 'built_by_run': int(os.environ['CURRENT_RUN'])}, open('dist/PROVENANCE.json', 'w'))"
206
+ sha256sum dist/*.whl dist/*.tar.gz > dist/SHA256SUMS
207
+ python scripts/check_artifact.py --dist-dir dist --version "${RELEASE_VERSION}"
208
+ - name: Check actual bytes against the selected version (all publish attempts)
209
+ run: python scripts/check_artifact.py --dist-dir dist --version "${RELEASE_VERSION}"
210
+ - name: Upload immutable validated artifact (this run)
211
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
212
+ with:
213
+ name: release-dists-${{ steps.prepub.outputs.sha }}
214
+ path: dist/
215
+ if-no-files-found: error
216
+ retention-days: 90 # the supported maximum; retain offline originals for longer needs
217
+
218
+ pypi-publish:
219
+ runs-on: ubuntu-latest
220
+ needs: build
221
+ permissions:
222
+ id-token: write # the only job with publishing authority (OIDC)
223
+ contents: read
224
+ environment:
225
+ name: pypi-publish
226
+ url: https://pypi.org/p/utec-client/
227
+ env:
228
+ GH_TOKEN: ${{ github.token }}
229
+ PUBLISH_TAG: ${{ needs.validate-source.outputs.tag }}
230
+ PUBLISH_VERSION: ${{ needs.validate-source.outputs.version }}
231
+ SOURCE_SHA: ${{ needs.build.outputs.source_sha }}
232
+ BUILT_BY_RUN: ${{ needs.build.outputs.built_by_run }}
233
+ steps:
234
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
235
+ with:
236
+ ref: main
237
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
238
+ with:
239
+ python-version: "3.13"
240
+ - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
241
+ with:
242
+ name: release-dists-${{ needs.build.outputs.source_sha }}
243
+ path: dist/
244
+ - name: Re-verify provenance, producer run, and manifest of the exact bytes
245
+ run: |
246
+ python scripts/verify_validated_build.py --dir dist --tag "$PUBLISH_TAG" \
247
+ --version "$PUBLISH_VERSION" --sha "$SOURCE_SHA" --built-by-run "$BUILT_BY_RUN"
248
+ - name: Read-only PyPI preflight (new / identical / conflict) and staging of new files
249
+ id: preflight
250
+ run: |
251
+ set +e
252
+ python scripts/pypi_preflight.py --dist-dir dist --staging-dir staging
253
+ code=$?
254
+ if [ "$code" = 0 ]; then echo "proceed=true" >> "$GITHUB_OUTPUT"
255
+ elif [ "$code" = 3 ]; then echo "proceed=false" >> "$GITHUB_OUTPUT"
256
+ else exit 1; fi
257
+ - name: Publish missing files to PyPI
258
+ if: steps.preflight.outputs.proceed == 'true'
259
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
260
+ with:
261
+ packages-dir: staging/
262
+ - name: Post-upload readback verification
263
+ run: python scripts/pypi_preflight.py --dist-dir dist --verify-published
@@ -0,0 +1,57 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ # PR checks are read-only and run no publishing credentials.
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ lint:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
17
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
18
+ with:
19
+ python-version: "3.13"
20
+ - run: pip install -e .[dev]
21
+ - run: ruff check .
22
+ - run: ruff format --check .
23
+
24
+ test:
25
+ runs-on: ubuntu-latest
26
+ strategy:
27
+ fail-fast: false
28
+ matrix:
29
+ python-version: ["3.11", "3.12", "3.13", "3.14"]
30
+ steps:
31
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
32
+ with:
33
+ fetch-depth: 0 # setuptools-scm derives the version from tags
34
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
35
+ with:
36
+ python-version: ${{ matrix.python-version }}
37
+ - run: pip install -e .[dev]
38
+ # Coverage floor (90%) and flags are centralized in pyproject.toml.
39
+ - run: pytest
40
+
41
+ package:
42
+ # Verify wheel/sdist metadata and installed-package behavior outside the
43
+ # source checkout, including a wheel rebuilt from the sdist.
44
+ runs-on: ubuntu-latest
45
+ steps:
46
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
47
+ with:
48
+ fetch-depth: 0
49
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
50
+ with:
51
+ python-version: "3.13"
52
+ - run: pip install -e .[dev]
53
+ - run: python -m build
54
+ - name: Verify artifacts from a clean environment
55
+ run: |
56
+ V=$(basename dist/*.whl .whl); V=${V#utec_client-}; V=${V%-py3-none-any}
57
+ python scripts/check_artifact.py --dist-dir dist --version "$V"
@@ -0,0 +1,107 @@
1
+ name: Weekly release
2
+
3
+ # Mondays 13:00 UTC. Selects the highest eligible fork release, verifies its
4
+ # tag is an ancestor of main, and — only when main has newer commits — cuts
5
+ # the next patch release at the pinned main commit, then dispatches the
6
+ # common validated publishing path (publish.yml), which builds and checks
7
+ # the exact tagged source. Skips with a notice when no fork baseline release
8
+ # exists yet or main is unchanged; never bootstraps from inherited upstream
9
+ # releases/tags.
10
+
11
+ on:
12
+ schedule:
13
+ - cron: "0 13 * * 1"
14
+ workflow_dispatch: {}
15
+
16
+ permissions:
17
+ contents: read
18
+
19
+ concurrency:
20
+ group: utec-client-release
21
+ cancel-in-progress: false
22
+
23
+ jobs:
24
+ decide:
25
+ runs-on: ubuntu-latest
26
+ outputs:
27
+ action: ${{ steps.decide.outputs.action }}
28
+ tag: ${{ steps.decide.outputs.tag }}
29
+ version: ${{ steps.decide.outputs.version }}
30
+ env:
31
+ GH_TOKEN: ${{ github.token }}
32
+ steps:
33
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
34
+ with:
35
+ fetch-depth: 0
36
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
37
+ with:
38
+ python-version: "3.13"
39
+ - id: decide
40
+ run: |
41
+ git remote set-branches origin main
42
+ git fetch origin main
43
+ DECISION=$(python scripts/prepare_release.py weekly)
44
+ echo "$DECISION"
45
+ ACTION=$(echo "$DECISION" | python -c "import json,sys; print(json.load(sys.stdin)['action'])")
46
+ echo "action=$ACTION" >> "$GITHUB_OUTPUT"
47
+ if [ "$ACTION" = release ]; then
48
+ echo "tag=$(echo "$DECISION" | python -c "import json,sys; print(json.load(sys.stdin)['tag'])")" >> "$GITHUB_OUTPUT"
49
+ echo "version=$(echo "$DECISION" | python -c "import json,sys; print(json.load(sys.stdin)['version'])")" >> "$GITHUB_OUTPUT"
50
+ fi
51
+
52
+ release:
53
+ if: needs.decide.outputs.action == 'release'
54
+ needs: decide
55
+ runs-on: ubuntu-latest
56
+ permissions:
57
+ contents: write # create the tag and release
58
+ actions: write # explicitly dispatch the publish workflow
59
+ env:
60
+ GH_TOKEN: ${{ github.token }}
61
+ RELEASE_TAG: ${{ needs.decide.outputs.tag }}
62
+ RELEASE_VERSION: ${{ needs.decide.outputs.version }}
63
+ steps:
64
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
65
+ with:
66
+ ref: main
67
+ fetch-depth: 0
68
+ - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
69
+ with:
70
+ python-version: "3.13"
71
+ - name: Re-validate version at the pinned main tip
72
+ id: pin
73
+ run: |
74
+ git remote set-branches origin main
75
+ git fetch origin main
76
+ git checkout --detach origin/main
77
+ OUT=$(python scripts/prepare_release.py manual "$RELEASE_VERSION")
78
+ echo "$OUT"
79
+ echo "sha=$(echo "$OUT" | python -c "import json,sys; print(json.load(sys.stdin)['sha'])")" >> "$GITHUB_OUTPUT"
80
+ - name: Check tag does not already exist
81
+ run: |
82
+ if git rev-parse -q --verify "refs/tags/${RELEASE_TAG}" >/dev/null; then
83
+ echo "::error::tag ${RELEASE_TAG} already exists; tags are never moved"
84
+ exit 1
85
+ fi
86
+ - name: Create tag at the pinned main commit
87
+ run: |
88
+ git tag "${RELEASE_TAG}" "${{ steps.pin.outputs.sha }}"
89
+ git push origin "refs/tags/${RELEASE_TAG}"
90
+ - name: Generate release notes since the previous fork release
91
+ run: python scripts/prepare_release.py notes --tag "${RELEASE_TAG}" > notes-body.md
92
+ - name: Create the release with the fork marker
93
+ run: |
94
+ # The marker line identifies this as a fork release for weekly
95
+ # selection; eligibility additionally requires fork-base ancestry.
96
+ printf 'utec-client-fork-release: true\n\n' > release-body.md
97
+ cat notes-body.md >> release-body.md
98
+ gh release create "${RELEASE_TAG}" \
99
+ --verify-tag \
100
+ --title "utec-client ${RELEASE_VERSION}" \
101
+ --notes-file release-body.md
102
+ - name: Dispatch the common validated publishing path
103
+ run: |
104
+ gh workflow run publish.yml \
105
+ --ref main \
106
+ -f tag="${RELEASE_TAG}" \
107
+ -f version="${RELEASE_VERSION}"