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.
- utec_client-0.5.0/.github/dependabot.yml +15 -0
- utec_client-0.5.0/.github/workflows/manual-release.yml +88 -0
- utec_client-0.5.0/.github/workflows/publish.yml +263 -0
- utec_client-0.5.0/.github/workflows/test.yml +57 -0
- utec_client-0.5.0/.github/workflows/weekly-release.yml +107 -0
- utec_client-0.5.0/.gitignore +508 -0
- utec_client-0.5.0/.vscode/launch.json +15 -0
- utec_client-0.5.0/.vscode/settings.json +14 -0
- utec_client-0.5.0/LICENSE +21 -0
- utec_client-0.5.0/MIGRATION.md +46 -0
- utec_client-0.5.0/PKG-INFO +99 -0
- utec_client-0.5.0/README.md +66 -0
- utec_client-0.5.0/pyproject.toml +108 -0
- utec_client-0.5.0/scripts/check_artifact.py +150 -0
- utec_client-0.5.0/scripts/find_validated_build.py +181 -0
- utec_client-0.5.0/scripts/prepare_release.py +282 -0
- utec_client-0.5.0/scripts/pypi_preflight.py +267 -0
- utec_client-0.5.0/scripts/release_logic.py +172 -0
- utec_client-0.5.0/scripts/test_api_behavior.py +311 -0
- utec_client-0.5.0/scripts/test_webhook.py +657 -0
- utec_client-0.5.0/scripts/verify_assets.py +75 -0
- utec_client-0.5.0/scripts/verify_validated_build.py +128 -0
- utec_client-0.5.0/setup.cfg +4 -0
- utec_client-0.5.0/src/utec_client/__init__.py +0 -0
- utec_client-0.5.0/src/utec_client/api.py +146 -0
- utec_client-0.5.0/src/utec_client/auth.py +45 -0
- utec_client-0.5.0/src/utec_client/const.py +38 -0
- utec_client-0.5.0/src/utec_client/devices/__init__.py +0 -0
- utec_client-0.5.0/src/utec_client/devices/device.py +274 -0
- utec_client-0.5.0/src/utec_client/devices/device_const.py +222 -0
- utec_client-0.5.0/src/utec_client/devices/light.py +128 -0
- utec_client-0.5.0/src/utec_client/devices/lock.py +108 -0
- utec_client-0.5.0/src/utec_client/devices/sensor.py +10 -0
- utec_client-0.5.0/src/utec_client/devices/switch.py +46 -0
- utec_client-0.5.0/src/utec_client/exceptions.py +40 -0
- utec_client-0.5.0/src/utec_client.egg-info/PKG-INFO +99 -0
- utec_client-0.5.0/src/utec_client.egg-info/SOURCES.txt +55 -0
- utec_client-0.5.0/src/utec_client.egg-info/dependency_links.txt +1 -0
- utec_client-0.5.0/src/utec_client.egg-info/requires.txt +13 -0
- utec_client-0.5.0/src/utec_client.egg-info/scm_file_list.json +52 -0
- utec_client-0.5.0/src/utec_client.egg-info/scm_version.json +8 -0
- utec_client-0.5.0/src/utec_client.egg-info/top_level.txt +1 -0
- utec_client-0.5.0/tests/__init__.py +0 -0
- utec_client-0.5.0/tests/conftest.py +92 -0
- utec_client-0.5.0/tests/test_api.py +272 -0
- utec_client-0.5.0/tests/test_auth.py +54 -0
- utec_client-0.5.0/tests/test_base_device.py +231 -0
- utec_client-0.5.0/tests/test_device_const.py +45 -0
- utec_client-0.5.0/tests/test_device_info.py +34 -0
- utec_client-0.5.0/tests/test_exceptions.py +47 -0
- utec_client-0.5.0/tests/test_light.py +201 -0
- utec_client-0.5.0/tests/test_lock.py +261 -0
- utec_client-0.5.0/tests/test_publish_pipeline.py +650 -0
- utec_client-0.5.0/tests/test_release_logic.py +115 -0
- utec_client-0.5.0/tests/test_release_plumbing.py +462 -0
- utec_client-0.5.0/tests/test_smoke.py +21 -0
- 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}"
|