aiorainpoint 0.1.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.
- aiorainpoint-0.1.0/.gitattributes +2 -0
- aiorainpoint-0.1.0/.github/dependabot.yml +44 -0
- aiorainpoint-0.1.0/.github/rulesets/main.json +64 -0
- aiorainpoint-0.1.0/.github/workflows/catalog-freshness.yml +204 -0
- aiorainpoint-0.1.0/.github/workflows/constraints.yml +186 -0
- aiorainpoint-0.1.0/.github/workflows/release-please.yml +121 -0
- aiorainpoint-0.1.0/.github/workflows/tests.yml +229 -0
- aiorainpoint-0.1.0/.gitignore +15 -0
- aiorainpoint-0.1.0/.pre-commit-config.yaml +28 -0
- aiorainpoint-0.1.0/.release-please-manifest.json +3 -0
- aiorainpoint-0.1.0/CHANGELOG.md +10 -0
- aiorainpoint-0.1.0/LICENSE +22 -0
- aiorainpoint-0.1.0/PKG-INFO +57 -0
- aiorainpoint-0.1.0/README.md +45 -0
- aiorainpoint-0.1.0/codecov.yml +9 -0
- aiorainpoint-0.1.0/pyproject.toml +70 -0
- aiorainpoint-0.1.0/release-please-config.json +27 -0
- aiorainpoint-0.1.0/requirements-test.txt +5 -0
- aiorainpoint-0.1.0/scripts/check_dist.py +83 -0
- aiorainpoint-0.1.0/scripts/dump_push_envelopes.py +391 -0
- aiorainpoint-0.1.0/scripts/probe_app_endpoints.py +114 -0
- aiorainpoint-0.1.0/scripts/refresh_product_catalog.py +493 -0
- aiorainpoint-0.1.0/scripts/smoke_installed.py +37 -0
- aiorainpoint-0.1.0/scripts/trial_control_work_mode.py +299 -0
- aiorainpoint-0.1.0/sonar-project.properties +12 -0
- aiorainpoint-0.1.0/src/aiorainpoint/__init__.py +157 -0
- aiorainpoint-0.1.0/src/aiorainpoint/certs/ali_iot_ca.crt +24 -0
- aiorainpoint-0.1.0/src/aiorainpoint/client.py +930 -0
- aiorainpoint-0.1.0/src/aiorainpoint/data/product_catalog.json +6411 -0
- aiorainpoint-0.1.0/src/aiorainpoint/decoders.py +1775 -0
- aiorainpoint-0.1.0/src/aiorainpoint/generic_decoder.py +475 -0
- aiorainpoint-0.1.0/src/aiorainpoint/models.py +103 -0
- aiorainpoint-0.1.0/src/aiorainpoint/mqtt.py +1155 -0
- aiorainpoint-0.1.0/src/aiorainpoint/product_catalog.py +268 -0
- aiorainpoint-0.1.0/src/aiorainpoint/py.typed +0 -0
- aiorainpoint-0.1.0/src/aiorainpoint/trust.py +66 -0
- aiorainpoint-0.1.0/src/aiorainpoint/utils.py +705 -0
- aiorainpoint-0.1.0/src/aiorainpoint/validators.py +98 -0
- aiorainpoint-0.1.0/tests/__init__.py +1 -0
- aiorainpoint-0.1.0/tests/helpers.py +40 -0
- aiorainpoint-0.1.0/tests/payload_samples.py +494 -0
- aiorainpoint-0.1.0/tests/test_client.py +3650 -0
- aiorainpoint-0.1.0/tests/test_decoders.py +3176 -0
- aiorainpoint-0.1.0/tests/test_generic_decoder.py +848 -0
- aiorainpoint-0.1.0/tests/test_mqtt.py +2748 -0
- aiorainpoint-0.1.0/tests/test_product_catalog.py +457 -0
- aiorainpoint-0.1.0/tests/test_trust.py +231 -0
- aiorainpoint-0.1.0/tests/test_utils.py +1053 -0
- aiorainpoint-0.1.0/tests/test_validators.py +203 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
|
|
2
|
+
|
|
3
|
+
version: 2
|
|
4
|
+
updates:
|
|
5
|
+
# Monitor GitHub Actions if you add workflows
|
|
6
|
+
- package-ecosystem: "github-actions"
|
|
7
|
+
directory: "/"
|
|
8
|
+
schedule:
|
|
9
|
+
interval: "weekly"
|
|
10
|
+
# Hold new releases a few days so a hijacked publish is pulled before it is
|
|
11
|
+
# proposed. Security updates bypass this.
|
|
12
|
+
cooldown:
|
|
13
|
+
default-days: 5
|
|
14
|
+
groups:
|
|
15
|
+
actions:
|
|
16
|
+
patterns:
|
|
17
|
+
- "*"
|
|
18
|
+
commit-message:
|
|
19
|
+
prefix: "ci"
|
|
20
|
+
labels:
|
|
21
|
+
- "dependencies"
|
|
22
|
+
- "github-actions"
|
|
23
|
+
|
|
24
|
+
# Monitor Python (pip) dependencies
|
|
25
|
+
- package-ecosystem: "pip"
|
|
26
|
+
directory: "/"
|
|
27
|
+
schedule:
|
|
28
|
+
interval: "weekly"
|
|
29
|
+
cooldown:
|
|
30
|
+
default-days: 5
|
|
31
|
+
semver-major-days: 14
|
|
32
|
+
# One PR for minor and patch bumps; majors still arrive one PR each.
|
|
33
|
+
groups:
|
|
34
|
+
pip:
|
|
35
|
+
patterns:
|
|
36
|
+
- "*"
|
|
37
|
+
update-types:
|
|
38
|
+
- "minor"
|
|
39
|
+
- "patch"
|
|
40
|
+
commit-message:
|
|
41
|
+
prefix: "chore"
|
|
42
|
+
labels:
|
|
43
|
+
- "dependencies"
|
|
44
|
+
- "python"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "main",
|
|
3
|
+
"target": "branch",
|
|
4
|
+
"enforcement": "active",
|
|
5
|
+
"bypass_actors": [],
|
|
6
|
+
"conditions": {
|
|
7
|
+
"ref_name": {
|
|
8
|
+
"include": [
|
|
9
|
+
"~DEFAULT_BRANCH"
|
|
10
|
+
],
|
|
11
|
+
"exclude": []
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"rules": [
|
|
15
|
+
{
|
|
16
|
+
"type": "deletion"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"type": "non_fast_forward"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"type": "pull_request",
|
|
23
|
+
"parameters": {
|
|
24
|
+
"required_approving_review_count": 0,
|
|
25
|
+
"dismiss_stale_reviews_on_push": false,
|
|
26
|
+
"require_code_owner_review": false,
|
|
27
|
+
"require_last_push_approval": false,
|
|
28
|
+
"required_review_thread_resolution": false
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"type": "required_status_checks",
|
|
33
|
+
"parameters": {
|
|
34
|
+
"strict_required_status_checks_policy": false,
|
|
35
|
+
"required_status_checks": [
|
|
36
|
+
{
|
|
37
|
+
"context": "ruff",
|
|
38
|
+
"integration_id": 15368
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
"context": "mypy",
|
|
42
|
+
"integration_id": 15368
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"context": "test",
|
|
46
|
+
"integration_id": 15368
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"context": "test-min",
|
|
50
|
+
"integration_id": 15368
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"context": "wheel",
|
|
54
|
+
"integration_id": 15368
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"context": "ha-constraints-min",
|
|
58
|
+
"integration_id": 15368
|
|
59
|
+
}
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
]
|
|
64
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
name: Catalog Freshness
|
|
2
|
+
|
|
3
|
+
# Writes are done with a GitHub App token rather than GITHUB_TOKEN. A branch
|
|
4
|
+
# pushed with GITHUB_TOKEN does not raise workflow events, so the PR this job
|
|
5
|
+
# opens would sit with no checks on it forever. An App-authored push raises
|
|
6
|
+
# them normally, so the catalog PR gets the same checks as a hand-pushed one.
|
|
7
|
+
# The App and its two secrets are the shared release bot's.
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
on:
|
|
12
|
+
schedule:
|
|
13
|
+
# An hour after ha-rainpoint's run, which uses the same RainPoint account.
|
|
14
|
+
- cron: "0 7 * * 1"
|
|
15
|
+
workflow_dispatch:
|
|
16
|
+
|
|
17
|
+
# A manual dispatch that overlaps the weekly cron would otherwise race two
|
|
18
|
+
# runs into the same branch and the same PR-creation probe.
|
|
19
|
+
concurrency:
|
|
20
|
+
group: ${{ github.workflow }}
|
|
21
|
+
cancel-in-progress: true
|
|
22
|
+
|
|
23
|
+
env:
|
|
24
|
+
BRANCH: automated/product-catalog-sync
|
|
25
|
+
|
|
26
|
+
jobs:
|
|
27
|
+
freshness-check:
|
|
28
|
+
runs-on: ubuntu-latest
|
|
29
|
+
timeout-minutes: 10
|
|
30
|
+
|
|
31
|
+
steps:
|
|
32
|
+
- name: Generate app token
|
|
33
|
+
id: app-token
|
|
34
|
+
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
|
|
35
|
+
with:
|
|
36
|
+
client-id: ${{ secrets.RELEASE_PLEASE_APP_ID }}
|
|
37
|
+
private-key: ${{ secrets.RELEASE_PLEASE_APP_PRIVATE_KEY }}
|
|
38
|
+
# Scoped to exactly what the push and the PR need. Requesting a
|
|
39
|
+
# permission the installation lacks fails the token request, so a run
|
|
40
|
+
# that starts erroring here wants the specific scope added back rather
|
|
41
|
+
# than this block removed.
|
|
42
|
+
permission-contents: write
|
|
43
|
+
permission-pull-requests: write
|
|
44
|
+
|
|
45
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
46
|
+
with:
|
|
47
|
+
# Pinned to main rather than left to default, because a dispatch run
|
|
48
|
+
# takes the ref picked under "Use workflow from". Off main, the branch
|
|
49
|
+
# the PR step creates would be based on that ref, so the catalog PR
|
|
50
|
+
# would carry its unrelated commits while claiming to be a snapshot
|
|
51
|
+
# refresh. The scheduled run is already on main; this makes the
|
|
52
|
+
# dispatch behave the same.
|
|
53
|
+
ref: main
|
|
54
|
+
# Persisted on purpose, unlike the other workflows here: the drift
|
|
55
|
+
# step pushes with it.
|
|
56
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
57
|
+
|
|
58
|
+
- name: Set up Python 3.13
|
|
59
|
+
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
60
|
+
with:
|
|
61
|
+
python-version: "3.13"
|
|
62
|
+
|
|
63
|
+
- name: Set up uv
|
|
64
|
+
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
|
65
|
+
with:
|
|
66
|
+
enable-cache: false
|
|
67
|
+
|
|
68
|
+
- name: Install dependencies
|
|
69
|
+
run: uv pip install --system .
|
|
70
|
+
|
|
71
|
+
- name: Check RainPoint credentials are present
|
|
72
|
+
id: creds
|
|
73
|
+
env:
|
|
74
|
+
RAINPOINT_EMAIL: ${{ secrets.RAINPOINT_EMAIL }}
|
|
75
|
+
RAINPOINT_PASSWORD: ${{ secrets.RAINPOINT_PASSWORD }}
|
|
76
|
+
run: |
|
|
77
|
+
if [ -z "$RAINPOINT_EMAIL" ] || [ -z "$RAINPOINT_PASSWORD" ]; then
|
|
78
|
+
echo "::warning::RainPoint credentials are not set; the catalog was not checked."
|
|
79
|
+
echo "skip=true" >> "$GITHUB_OUTPUT"
|
|
80
|
+
else
|
|
81
|
+
echo "skip=false" >> "$GITHUB_OUTPUT"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# RAINPOINT_AREA_CODE is a repository variable, not a secret. It is a phone
|
|
85
|
+
# dial code (1 for the US), which is not sensitive, and holding it as a secret
|
|
86
|
+
# made the runner redact every occurrence of that digit in the log: the drift
|
|
87
|
+
# report came back reading "Fetched ***04 model entries" and listing models as
|
|
88
|
+
# "HTV2***3FRF", which is ambiguous and defeats the point of the report. An
|
|
89
|
+
# unset variable is an empty string, and the script already falls back to 1.
|
|
90
|
+
- name: Run catalog freshness check
|
|
91
|
+
id: check
|
|
92
|
+
if: steps.creds.outputs.skip == 'false'
|
|
93
|
+
env:
|
|
94
|
+
RAINPOINT_EMAIL: ${{ secrets.RAINPOINT_EMAIL }}
|
|
95
|
+
RAINPOINT_PASSWORD: ${{ secrets.RAINPOINT_PASSWORD }}
|
|
96
|
+
RAINPOINT_AREA_CODE: ${{ vars.RAINPOINT_AREA_CODE }}
|
|
97
|
+
run: |
|
|
98
|
+
# The default step shell is "bash -e", so drift (a deliberate exit 1)
|
|
99
|
+
# would abort the step before the status could be read. -e goes off
|
|
100
|
+
# around the call and back on straight after.
|
|
101
|
+
set +e
|
|
102
|
+
# --check writes its drift summary to stdout and its progress lines to
|
|
103
|
+
# stderr, so redirecting stdout alone captures the summary cleanly. It
|
|
104
|
+
# is worth capturing rather than leaving in the log: it splits changed
|
|
105
|
+
# models into datapoint changes and metadata-only changes, which is the
|
|
106
|
+
# difference between "commit this" and "read this carefully", and that
|
|
107
|
+
# split is what the PR body is for.
|
|
108
|
+
python scripts/refresh_product_catalog.py --check > drift.txt
|
|
109
|
+
status=$?
|
|
110
|
+
set -e
|
|
111
|
+
cat drift.txt
|
|
112
|
+
if [ "$status" -eq 0 ]; then
|
|
113
|
+
echo "drift=false" >> "$GITHUB_OUTPUT"
|
|
114
|
+
exit 0
|
|
115
|
+
fi
|
|
116
|
+
# Exit 3 is the server's rate limiter refusing every login attempt,
|
|
117
|
+
# after the script has already waited out the cooldown and retried.
|
|
118
|
+
# It says nothing about the snapshot, so it must not be reported as
|
|
119
|
+
# drift and must not reach the regenerate step. Left red rather than
|
|
120
|
+
# silently green, so a week with no check is visible, but named so
|
|
121
|
+
# nobody has to read the log to find out why.
|
|
122
|
+
if [ "$status" -eq 3 ]; then
|
|
123
|
+
echo "::error::RainPoint rate-limited every login attempt; the catalog was not checked. Re-run this workflow later."
|
|
124
|
+
exit 1
|
|
125
|
+
fi
|
|
126
|
+
# --check also exits 1 when the live pull comes back empty, which is a
|
|
127
|
+
# fetch failure rather than drift. It reports that on stderr, leaving
|
|
128
|
+
# stdout empty, so an empty summary here is the failure case and has to
|
|
129
|
+
# stay a red run.
|
|
130
|
+
if ! grep -q '[^[:space:]]' drift.txt; then
|
|
131
|
+
echo "::error::Catalog check failed without producing a drift summary."
|
|
132
|
+
exit 1
|
|
133
|
+
fi
|
|
134
|
+
echo "drift=true" >> "$GITHUB_OUTPUT"
|
|
135
|
+
|
|
136
|
+
# The write is a second live pull rather than a reuse of the one above
|
|
137
|
+
# because write mode is where the guards live: it refuses to overwrite the
|
|
138
|
+
# committed catalog with an empty pull, or with one holding under half the
|
|
139
|
+
# models. Fetching twice costs a login and about half a megabyte, once a
|
|
140
|
+
# week.
|
|
141
|
+
- name: Regenerate catalog
|
|
142
|
+
if: steps.check.outputs.drift == 'true'
|
|
143
|
+
env:
|
|
144
|
+
RAINPOINT_EMAIL: ${{ secrets.RAINPOINT_EMAIL }}
|
|
145
|
+
RAINPOINT_PASSWORD: ${{ secrets.RAINPOINT_PASSWORD }}
|
|
146
|
+
RAINPOINT_AREA_CODE: ${{ vars.RAINPOINT_AREA_CODE }}
|
|
147
|
+
run: python scripts/refresh_product_catalog.py
|
|
148
|
+
|
|
149
|
+
- name: Open or update the catalog PR
|
|
150
|
+
if: steps.check.outputs.drift == 'true'
|
|
151
|
+
env:
|
|
152
|
+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
|
|
153
|
+
APP_SLUG: ${{ steps.app-token.outputs.app-slug }}
|
|
154
|
+
CATALOG: src/aiorainpoint/data/product_catalog.json
|
|
155
|
+
run: |
|
|
156
|
+
set -euo pipefail
|
|
157
|
+
# The drift report and the written file are two separate pulls, so a
|
|
158
|
+
# report with nothing behind it is possible and must not become an
|
|
159
|
+
# empty commit.
|
|
160
|
+
if git diff --quiet -- "$CATALOG"; then
|
|
161
|
+
echo "Drift was reported but the regenerated catalog is unchanged; nothing to open."
|
|
162
|
+
exit 0
|
|
163
|
+
fi
|
|
164
|
+
git config user.name "${APP_SLUG}[bot]"
|
|
165
|
+
git config user.email "${APP_SLUG}[bot]@users.noreply.github.com"
|
|
166
|
+
# -C resets the branch onto main every run (the checkout is pinned to
|
|
167
|
+
# it), so it carries exactly one commit and never accumulates stale
|
|
168
|
+
# ones.
|
|
169
|
+
git switch -C "$BRANCH"
|
|
170
|
+
git add "$CATALOG"
|
|
171
|
+
git commit -m "fix(catalog): refresh the product catalog snapshot"
|
|
172
|
+
# The checkout is single-branch, so nothing here has a remote-tracking
|
|
173
|
+
# ref for this branch, and a bare --force-with-lease refuses with
|
|
174
|
+
# "stale info" from the second push onwards. Fetching the branch is
|
|
175
|
+
# necessary but not sufficient: the bare form still refuses, because it
|
|
176
|
+
# wants a value it observed rather than one it just created. Stating
|
|
177
|
+
# the expected value explicitly is the documented, non-experimental
|
|
178
|
+
# form of the lease, and it still refuses if anyone else moved the
|
|
179
|
+
# branch. An absent branch leaves the expectation empty, which git
|
|
180
|
+
# reads as "must not exist yet", so the first run is covered too.
|
|
181
|
+
git fetch --no-tags origin "+refs/heads/${BRANCH}:refs/remotes/origin/${BRANCH}" 2>/dev/null || true
|
|
182
|
+
expected="$(git rev-parse --verify --quiet "refs/remotes/origin/${BRANCH}" || true)"
|
|
183
|
+
git push --force-with-lease="refs/heads/${BRANCH}:${expected}" origin "$BRANCH"
|
|
184
|
+
|
|
185
|
+
{
|
|
186
|
+
echo "The bundled snapshot of RainPoint's product list no longer matches a live pull, so it has been regenerated."
|
|
187
|
+
echo
|
|
188
|
+
echo '```'
|
|
189
|
+
cat drift.txt
|
|
190
|
+
echo '```'
|
|
191
|
+
} > pr-body.md
|
|
192
|
+
|
|
193
|
+
number="$(gh pr list --head "$BRANCH" --state open --json number --jq '.[].number')"
|
|
194
|
+
if [ -z "$number" ]; then
|
|
195
|
+
gh pr create --draft --base main --head "$BRANCH" \
|
|
196
|
+
--title "fix(catalog): refresh the product catalog snapshot" \
|
|
197
|
+
--body-file pr-body.md
|
|
198
|
+
else
|
|
199
|
+
# gh pr edit --body fails on the maintainer's repos with a Projects-classic
|
|
200
|
+
# GraphQL error and changes nothing, so the body is patched through
|
|
201
|
+
# the REST API instead.
|
|
202
|
+
gh api -X PATCH "repos/${GITHUB_REPOSITORY}/pulls/${number}" -F body=@pr-body.md > /dev/null
|
|
203
|
+
echo "PR #${number} for $BRANCH already open; force-push and body update applied."
|
|
204
|
+
fi
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
name: HA Constraints
|
|
2
|
+
permissions:
|
|
3
|
+
contents: read
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
pull_request:
|
|
9
|
+
branches: [main]
|
|
10
|
+
schedule:
|
|
11
|
+
- cron: "0 8 * * 1"
|
|
12
|
+
|
|
13
|
+
# Cancel a superseded pull request run. Every other run gets its own group,
|
|
14
|
+
# since a pending run in a shared group is replaced by the next one queued.
|
|
15
|
+
concurrency:
|
|
16
|
+
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.ref || github.run_id }}
|
|
17
|
+
cancel-in-progress: true
|
|
18
|
+
|
|
19
|
+
# Checks that the built wheel installs under Home Assistant's pinned dependencies,
|
|
20
|
+
# so a release here cannot make the integration uninstallable.
|
|
21
|
+
jobs:
|
|
22
|
+
# Required check.
|
|
23
|
+
ha-constraints-min:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
timeout-minutes: 10
|
|
26
|
+
env:
|
|
27
|
+
# The oldest Home Assistant ha-rainpoint supports (its hacs.json minimum); moves when that does.
|
|
28
|
+
HA_REF: "2026.2.0"
|
|
29
|
+
|
|
30
|
+
steps:
|
|
31
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
32
|
+
with:
|
|
33
|
+
persist-credentials: false
|
|
34
|
+
|
|
35
|
+
- name: Set up Python 3.13
|
|
36
|
+
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
37
|
+
with:
|
|
38
|
+
python-version: "3.13"
|
|
39
|
+
|
|
40
|
+
- name: Set up uv
|
|
41
|
+
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
|
42
|
+
with:
|
|
43
|
+
enable-cache: false
|
|
44
|
+
|
|
45
|
+
- name: Build the wheel
|
|
46
|
+
run: uv build --wheel
|
|
47
|
+
|
|
48
|
+
- name: Fetch Home Assistant's package constraints
|
|
49
|
+
run: |
|
|
50
|
+
url="https://raw.githubusercontent.com/home-assistant/core/${HA_REF}/homeassistant/package_constraints.txt"
|
|
51
|
+
if ! curl -fsSL --retry 3 --retry-delay 2 --retry-all-errors --max-time 30 "$url" -o package_constraints.txt; then
|
|
52
|
+
echo "::error::Could not download package_constraints.txt for Home Assistant ${HA_REF}"
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
if [ ! -s package_constraints.txt ]; then
|
|
56
|
+
echo "::error::package_constraints.txt for Home Assistant ${HA_REF} is empty"
|
|
57
|
+
exit 1
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# Home Assistant's own install flags, minus --quiet because the output is read below,
|
|
61
|
+
# plus --dry-run. A venv because uv refuses a bare interpreter.
|
|
62
|
+
- name: Resolve the wheel under those constraints
|
|
63
|
+
run: |
|
|
64
|
+
uv venv "$RUNNER_TEMP/resolve" --python 3.13
|
|
65
|
+
status=0
|
|
66
|
+
uv pip install --dry-run --python "$RUNNER_TEMP/resolve/bin/python" \
|
|
67
|
+
--index-strategy unsafe-first-match --upgrade \
|
|
68
|
+
--constraint package_constraints.txt dist/*.whl > resolve.txt 2>&1 || status=$?
|
|
69
|
+
cat resolve.txt
|
|
70
|
+
if [ "$status" -ne 0 ]; then
|
|
71
|
+
echo "::error::The wheel does not resolve under Home Assistant ${HA_REF} constraints"
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
- name: Check the resolved pins match Home Assistant's
|
|
76
|
+
run: |
|
|
77
|
+
for pkg in aiohttp paho-mqtt; do
|
|
78
|
+
pin=$(grep -m1 -iE "^${pkg}==" package_constraints.txt || true)
|
|
79
|
+
if [ -z "$pin" ]; then
|
|
80
|
+
echo "::error::Home Assistant ${HA_REF} no longer pins $pkg"
|
|
81
|
+
exit 1
|
|
82
|
+
fi
|
|
83
|
+
if ! grep -qxF " + $pin" resolve.txt; then
|
|
84
|
+
echo "::error::$pkg did not resolve to $pin, the pin in Home Assistant ${HA_REF}"
|
|
85
|
+
exit 1
|
|
86
|
+
fi
|
|
87
|
+
done
|
|
88
|
+
|
|
89
|
+
# Not a required check: dev moves daily.
|
|
90
|
+
ha-constraints-dev:
|
|
91
|
+
runs-on: ubuntu-latest
|
|
92
|
+
timeout-minutes: 10
|
|
93
|
+
env:
|
|
94
|
+
HA_REF: dev
|
|
95
|
+
|
|
96
|
+
steps:
|
|
97
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
98
|
+
with:
|
|
99
|
+
persist-credentials: false
|
|
100
|
+
|
|
101
|
+
- name: Set up Python 3.14
|
|
102
|
+
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
103
|
+
with:
|
|
104
|
+
python-version: "3.14"
|
|
105
|
+
|
|
106
|
+
- name: Set up uv
|
|
107
|
+
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
|
108
|
+
with:
|
|
109
|
+
enable-cache: false
|
|
110
|
+
|
|
111
|
+
- name: Build the wheel
|
|
112
|
+
run: uv build --wheel
|
|
113
|
+
|
|
114
|
+
- name: Fetch Home Assistant's package constraints
|
|
115
|
+
run: |
|
|
116
|
+
url="https://raw.githubusercontent.com/home-assistant/core/${HA_REF}/homeassistant/package_constraints.txt"
|
|
117
|
+
if ! curl -fsSL --retry 3 --retry-delay 2 --retry-all-errors --max-time 30 "$url" -o package_constraints.txt; then
|
|
118
|
+
echo "::error::Could not download package_constraints.txt for Home Assistant ${HA_REF}"
|
|
119
|
+
exit 1
|
|
120
|
+
fi
|
|
121
|
+
if [ ! -s package_constraints.txt ]; then
|
|
122
|
+
echo "::error::package_constraints.txt for Home Assistant ${HA_REF} is empty"
|
|
123
|
+
exit 1
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
# Home Assistant's own install flags, minus --quiet because the output is read below,
|
|
127
|
+
# plus --dry-run. A venv because uv refuses a bare interpreter.
|
|
128
|
+
- name: Resolve the wheel under those constraints
|
|
129
|
+
run: |
|
|
130
|
+
uv venv "$RUNNER_TEMP/resolve" --python 3.14
|
|
131
|
+
status=0
|
|
132
|
+
uv pip install --dry-run --python "$RUNNER_TEMP/resolve/bin/python" \
|
|
133
|
+
--index-strategy unsafe-first-match --upgrade \
|
|
134
|
+
--constraint package_constraints.txt dist/*.whl > resolve.txt 2>&1 || status=$?
|
|
135
|
+
cat resolve.txt
|
|
136
|
+
if [ "$status" -ne 0 ]; then
|
|
137
|
+
echo "::error::The wheel does not resolve under Home Assistant ${HA_REF} constraints"
|
|
138
|
+
exit 1
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
- name: Check the resolved pins match Home Assistant's
|
|
142
|
+
run: |
|
|
143
|
+
for pkg in aiohttp paho-mqtt; do
|
|
144
|
+
pin=$(grep -m1 -iE "^${pkg}==" package_constraints.txt || true)
|
|
145
|
+
if [ -z "$pin" ]; then
|
|
146
|
+
echo "::error::Home Assistant ${HA_REF} no longer pins $pkg"
|
|
147
|
+
exit 1
|
|
148
|
+
fi
|
|
149
|
+
if ! grep -qxF " + $pin" resolve.txt; then
|
|
150
|
+
echo "::error::$pkg did not resolve to $pin, the pin in Home Assistant ${HA_REF}"
|
|
151
|
+
exit 1
|
|
152
|
+
fi
|
|
153
|
+
done
|
|
154
|
+
|
|
155
|
+
# The only job with a write token. It runs on the schedule alone and checks out nothing.
|
|
156
|
+
report-scheduled-failure:
|
|
157
|
+
needs: [ha-constraints-min, ha-constraints-dev]
|
|
158
|
+
if: ${{ !cancelled() && github.event_name == 'schedule' && contains(needs.*.result, 'failure') }}
|
|
159
|
+
runs-on: ubuntu-latest
|
|
160
|
+
timeout-minutes: 5
|
|
161
|
+
permissions:
|
|
162
|
+
issues: write
|
|
163
|
+
env:
|
|
164
|
+
GH_TOKEN: ${{ github.token }}
|
|
165
|
+
GH_REPO: ${{ github.repository }}
|
|
166
|
+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
167
|
+
MIN_RESULT: ${{ needs.ha-constraints-min.result }}
|
|
168
|
+
DEV_RESULT: ${{ needs.ha-constraints-dev.result }}
|
|
169
|
+
|
|
170
|
+
steps:
|
|
171
|
+
- name: Open or update the failure issue
|
|
172
|
+
run: |
|
|
173
|
+
title="Weekly Home Assistant constraints check failing"
|
|
174
|
+
targets=""
|
|
175
|
+
[ "$MIN_RESULT" = "failure" ] && targets="ha-constraints-min"
|
|
176
|
+
[ "$DEV_RESULT" = "failure" ] && targets="${targets:+$targets, }ha-constraints-dev"
|
|
177
|
+
[ -n "$targets" ] || exit 0
|
|
178
|
+
body="Target: $targets
|
|
179
|
+
Run: $RUN_URL"
|
|
180
|
+
number=$(gh issue list --state open --limit 100 --json number,title \
|
|
181
|
+
| jq -r --arg t "$title" 'map(select(.title == $t))[0].number // empty')
|
|
182
|
+
if [ -n "$number" ]; then
|
|
183
|
+
gh issue comment "$number" --body "$body"
|
|
184
|
+
else
|
|
185
|
+
gh issue create --title "$title" --body "$body"
|
|
186
|
+
fi
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
name: Release Please
|
|
2
|
+
|
|
3
|
+
# One file on purpose: the PyPI and TestPyPI publisher records bind to this
|
|
4
|
+
# filename and to the pypi environment, and neither accepts a reusable workflow.
|
|
5
|
+
# There is no concurrency group, so a release run waiting on the pypi reviewer
|
|
6
|
+
# never cancels or queues a TestPyPI rehearsal dispatch.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
branches:
|
|
11
|
+
- main
|
|
12
|
+
# A push event is not guaranteed to arrive. A dispatch with an empty ref
|
|
13
|
+
# re-runs release-please alone and is the recovery path.
|
|
14
|
+
workflow_dispatch:
|
|
15
|
+
inputs:
|
|
16
|
+
ref:
|
|
17
|
+
description: Commit, branch or tag to build and upload to TestPyPI. Leave empty for a normal release run.
|
|
18
|
+
type: string
|
|
19
|
+
required: false
|
|
20
|
+
default: ""
|
|
21
|
+
|
|
22
|
+
permissions:
|
|
23
|
+
contents: read
|
|
24
|
+
|
|
25
|
+
jobs:
|
|
26
|
+
release-please:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
outputs:
|
|
29
|
+
release_created: ${{ steps.release.outputs.release_created }}
|
|
30
|
+
tag_name: ${{ steps.release.outputs.tag_name }}
|
|
31
|
+
version: ${{ steps.release.outputs.version }}
|
|
32
|
+
steps:
|
|
33
|
+
- name: Generate app token
|
|
34
|
+
id: app-token
|
|
35
|
+
uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
|
|
36
|
+
with:
|
|
37
|
+
client-id: ${{ secrets.RELEASE_PLEASE_APP_ID }}
|
|
38
|
+
private-key: ${{ secrets.RELEASE_PLEASE_APP_PRIVATE_KEY }}
|
|
39
|
+
# Scoped to what release-please uses: contents for the version bump,
|
|
40
|
+
# tag and release; pull-requests for the release PR and its labels.
|
|
41
|
+
permission-contents: write
|
|
42
|
+
permission-pull-requests: write
|
|
43
|
+
|
|
44
|
+
- uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
|
|
45
|
+
id: release
|
|
46
|
+
with:
|
|
47
|
+
token: ${{ steps.app-token.outputs.token }}
|
|
48
|
+
|
|
49
|
+
# Runs the repository's code and suite, so it holds no id-token permission.
|
|
50
|
+
build:
|
|
51
|
+
needs: release-please
|
|
52
|
+
if: >-
|
|
53
|
+
needs.release-please.outputs.release_created == 'true'
|
|
54
|
+
|| (github.event_name == 'workflow_dispatch' && inputs.ref != '')
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
|
|
57
|
+
steps:
|
|
58
|
+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
59
|
+
with:
|
|
60
|
+
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || needs.release-please.outputs.tag_name }}
|
|
61
|
+
persist-credentials: false
|
|
62
|
+
|
|
63
|
+
- name: Set up Python 3.13
|
|
64
|
+
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
|
|
65
|
+
with:
|
|
66
|
+
python-version: "3.13"
|
|
67
|
+
|
|
68
|
+
- name: Set up uv
|
|
69
|
+
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
|
|
70
|
+
with:
|
|
71
|
+
enable-cache: false
|
|
72
|
+
|
|
73
|
+
- name: Build the sdist and the wheel
|
|
74
|
+
run: uv build
|
|
75
|
+
|
|
76
|
+
- name: Check the archives
|
|
77
|
+
run: python scripts/check_dist.py dist
|
|
78
|
+
|
|
79
|
+
- name: Install the wheel into a clean venv
|
|
80
|
+
run: |
|
|
81
|
+
uv venv "$RUNNER_TEMP/wheel-venv" --python 3.13
|
|
82
|
+
uv pip install --python "$RUNNER_TEMP/wheel-venv/bin/python" dist/*.whl -r requirements-test.txt
|
|
83
|
+
|
|
84
|
+
# EXPECTED_VERSION is empty on a dispatch, so only a release asserts the version.
|
|
85
|
+
- name: Smoke-test the installed wheel
|
|
86
|
+
working-directory: ${{ runner.temp }}
|
|
87
|
+
env:
|
|
88
|
+
EXPECTED_VERSION: ${{ needs.release-please.outputs.version }}
|
|
89
|
+
run: |
|
|
90
|
+
"$RUNNER_TEMP/wheel-venv/bin/python" -I "$GITHUB_WORKSPACE/scripts/smoke_installed.py" ${EXPECTED_VERSION:+"$EXPECTED_VERSION"}
|
|
91
|
+
|
|
92
|
+
- name: Run the whole suite against the wheel
|
|
93
|
+
run: |
|
|
94
|
+
mkdir "$RUNNER_TEMP/wheel-run"
|
|
95
|
+
cp -r tests scripts pyproject.toml "$RUNNER_TEMP/wheel-run/"
|
|
96
|
+
cd "$RUNNER_TEMP/wheel-run"
|
|
97
|
+
"$RUNNER_TEMP/wheel-venv/bin/python" -m pytest --no-cov --tb=short
|
|
98
|
+
|
|
99
|
+
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
100
|
+
with:
|
|
101
|
+
name: dist
|
|
102
|
+
path: dist/
|
|
103
|
+
if-no-files-found: error
|
|
104
|
+
|
|
105
|
+
# Only the job that mints the OIDC token, and it runs no repository code.
|
|
106
|
+
publish:
|
|
107
|
+
needs: build
|
|
108
|
+
runs-on: ubuntu-latest
|
|
109
|
+
environment: pypi
|
|
110
|
+
permissions:
|
|
111
|
+
id-token: write
|
|
112
|
+
|
|
113
|
+
steps:
|
|
114
|
+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
115
|
+
with:
|
|
116
|
+
name: dist
|
|
117
|
+
path: dist/
|
|
118
|
+
|
|
119
|
+
- uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # v1.14.3
|
|
120
|
+
with:
|
|
121
|
+
repository-url: ${{ github.event_name == 'workflow_dispatch' && inputs.ref != '' && 'https://test.pypi.org/legacy/' || 'https://upload.pypi.org/legacy/' }}
|