rhiza-task 0.1.1__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.
- rhiza_task-0.1.1/.github/workflows/ci.yml +72 -0
- rhiza_task-0.1.1/.github/workflows/rhiza_release.yml +528 -0
- rhiza_task-0.1.1/.gitignore +11 -0
- rhiza_task-0.1.1/.python-version +1 -0
- rhiza_task-0.1.1/CHANGELOG.md +32 -0
- rhiza_task-0.1.1/LICENSE +21 -0
- rhiza_task-0.1.1/PKG-INFO +209 -0
- rhiza_task-0.1.1/README.md +185 -0
- rhiza_task-0.1.1/pyproject.toml +85 -0
- rhiza_task-0.1.1/pytest.ini +5 -0
- rhiza_task-0.1.1/ruff.toml +175 -0
- rhiza_task-0.1.1/src/rhiza_task/__init__.py +26 -0
- rhiza_task-0.1.1/src/rhiza_task/__main__.py +6 -0
- rhiza_task-0.1.1/src/rhiza_task/cli.py +163 -0
- rhiza_task-0.1.1/src/rhiza_task/config.py +248 -0
- rhiza_task-0.1.1/src/rhiza_task/runner.py +149 -0
- rhiza_task-0.1.1/src/rhiza_task/spec.py +185 -0
- rhiza_task-0.1.1/src/rhiza_task/tasks/__init__.py +7 -0
- rhiza_task-0.1.1/src/rhiza_task/tasks/book.py +195 -0
- rhiza_task-0.1.1/src/rhiza_task/tasks/doctor.py +148 -0
- rhiza_task-0.1.1/src/rhiza_task/tasks/extras.py +164 -0
- rhiza_task-0.1.1/src/rhiza_task/tasks/python.py +315 -0
- rhiza_task-0.1.1/src/rhiza_task/tasks/quality.py +236 -0
- rhiza_task-0.1.1/src/rhiza_task/templates/Makefile +36 -0
- rhiza_task-0.1.1/src/rhiza_task/uv.py +178 -0
- rhiza_task-0.1.1/tests/__init__.py +1 -0
- rhiza_task-0.1.1/tests/conftest.py +197 -0
- rhiza_task-0.1.1/tests/test_cli.py +235 -0
- rhiza_task-0.1.1/tests/test_config.py +151 -0
- rhiza_task-0.1.1/tests/test_dev_tasks.py +418 -0
- rhiza_task-0.1.1/tests/test_runner.py +219 -0
- rhiza_task-0.1.1/tests/test_spec.py +100 -0
- rhiza_task-0.1.1/tests/test_tasks.py +466 -0
- rhiza_task-0.1.1/tests/test_uv.py +169 -0
- rhiza_task-0.1.1/uv.lock +356 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
# Least privilege: the gates only need to read the code.
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
# A new push to a PR makes the in-flight run irrelevant.
|
|
13
|
+
concurrency:
|
|
14
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
15
|
+
cancel-in-progress: true
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
test:
|
|
19
|
+
name: pytest (${{ matrix.os }}, ${{ matrix.python-version }})
|
|
20
|
+
runs-on: ${{ matrix.os }}
|
|
21
|
+
strategy:
|
|
22
|
+
fail-fast: false
|
|
23
|
+
matrix:
|
|
24
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
25
|
+
# Mirrors the Programming Language :: Python :: X.Y classifiers in pyproject.toml.
|
|
26
|
+
python-version: ["3.11", "3.12", "3.13", "3.14"]
|
|
27
|
+
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v7
|
|
30
|
+
|
|
31
|
+
# Exact version, not a floating major: astral-sh/setup-uv publishes no `v8`, `v9` or
|
|
32
|
+
# `v10` alias tag, so `@v10` fails to resolve at job start.
|
|
33
|
+
#
|
|
34
|
+
# This step is also what replaces `install-uv`. bootstrap.mk curls the uv installer
|
|
35
|
+
# into ./bin because make cannot assume uv exists; a workflow can just say so.
|
|
36
|
+
#
|
|
37
|
+
# `version` pins the uv binary itself, not just the action, to the same uv that
|
|
38
|
+
# rhiza_release.yml builds and publishes with. A gate that passes here on one uv and
|
|
39
|
+
# a release built on another is a resolver difference nothing would catch.
|
|
40
|
+
- uses: astral-sh/setup-uv@v10.0.1
|
|
41
|
+
with:
|
|
42
|
+
version: "0.11.16"
|
|
43
|
+
python-version: ${{ matrix.python-version }}
|
|
44
|
+
enable-cache: true
|
|
45
|
+
|
|
46
|
+
# Windows is in the matrix deliberately. The make layer needed a 40-line probe to
|
|
47
|
+
# detect make falling back to cmd.exe, because its recipes were POSIX shell; every
|
|
48
|
+
# command here is an argument vector, so this job is what asserts that the shell
|
|
49
|
+
# dependency is really gone.
|
|
50
|
+
- name: Run tests
|
|
51
|
+
run: uv run --group test --python ${{ matrix.python-version }} pytest -ra
|
|
52
|
+
|
|
53
|
+
gates:
|
|
54
|
+
name: the gates this package applies to others
|
|
55
|
+
runs-on: ubuntu-latest
|
|
56
|
+
steps:
|
|
57
|
+
- uses: actions/checkout@v7
|
|
58
|
+
- uses: astral-sh/setup-uv@v10.0.1
|
|
59
|
+
with:
|
|
60
|
+
version: "0.11.16"
|
|
61
|
+
enable-cache: true
|
|
62
|
+
|
|
63
|
+
# Dogfooding, and not for its own sake: `all` is the aggregate consumers run, so a
|
|
64
|
+
# break in prerequisite resolution or outcome reporting fails here first.
|
|
65
|
+
#
|
|
66
|
+
# Deliberately without --strict. This repository is not rhiza-managed -- it ships no
|
|
67
|
+
# .pre-commit-config.yaml and no .rhiza/semgrep.yml, exactly as pytest-rhiza does not
|
|
68
|
+
# -- so `fmt` and `semgrep` have genuinely nothing to measure here and skip. --strict
|
|
69
|
+
# is the right setting in a *consumer* repository, where a skip means a gate lost its
|
|
70
|
+
# subject; asserting it here would only assert that this repo is something it is not.
|
|
71
|
+
- name: rhiza-task all
|
|
72
|
+
run: uv run rhiza-task all
|
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
# This file is part of the jebel-quant/rhiza repository
|
|
2
|
+
# (https://github.com/jebel-quant/rhiza).
|
|
3
|
+
#
|
|
4
|
+
# ⚠️ WHY THIS WORKFLOW CANNOT USE A STUB
|
|
5
|
+
#
|
|
6
|
+
# Rhiza normally distributes workflows as thin stubs that delegate to a reusable
|
|
7
|
+
# workflow in this repository via `workflow_call`. That pattern cannot be used here
|
|
8
|
+
# because PyPI Trusted Publishing (OIDC) validates the *exact* workflow file path
|
|
9
|
+
# and repository that invokes `pypa/gh-action-pypi-publish`. When a stub delegates
|
|
10
|
+
# to a reusable workflow in jebel-quant/rhiza, PyPI sees rhiza's repository as the
|
|
11
|
+
# publisher — not the downstream project's — and rejects the OIDC token. This file
|
|
12
|
+
# must therefore be synced directly into each downstream project so that the publish
|
|
13
|
+
# step runs under the correct repository identity.
|
|
14
|
+
#
|
|
15
|
+
# Release Workflow for Python Packages
|
|
16
|
+
#
|
|
17
|
+
# This workflow implements a secure, maintainable release pipeline with distinct phases:
|
|
18
|
+
#
|
|
19
|
+
# 📋 Pipeline Phases:
|
|
20
|
+
# 1. 🔍 Validate Tag - Check tag format and ensure release doesn't already exist
|
|
21
|
+
# 2. 🏗️ Build - Build Python package with Hatch (if [build-system] is defined in pyproject.toml)
|
|
22
|
+
# 3. 📦 Generate SBOM - Create Software Bill of Materials (CycloneDX format)
|
|
23
|
+
# 4. 📝 Draft Release - Create draft GitHub release with build artifacts and SBOM
|
|
24
|
+
# 5. 🚀 Publish to PyPI - Publish package using OIDC or custom feed
|
|
25
|
+
# 6. ✅ Finalize Release - Publish the GitHub release with links
|
|
26
|
+
#
|
|
27
|
+
# 📄 CHANGELOG: not updated here. The release process (rhiza-claude `/release`)
|
|
28
|
+
# folds a freshly generated CHANGELOG.md into the version-bump commit before the
|
|
29
|
+
# tag is pushed, so the tagged commit already carries the changelog — no separate
|
|
30
|
+
# post-tag commit.
|
|
31
|
+
#
|
|
32
|
+
# 📦 SBOM Generation:
|
|
33
|
+
# - Generated using CycloneDX format (industry standard for software supply chain security)
|
|
34
|
+
# - Creates both JSON and XML formats for maximum compatibility
|
|
35
|
+
# - SBOM attestations are created and stored (public repos only)
|
|
36
|
+
# - Attached to GitHub releases for transparency and compliance
|
|
37
|
+
# - Skipped if pyproject.toml doesn't exist
|
|
38
|
+
#
|
|
39
|
+
# 🚀 PyPI Publishing:
|
|
40
|
+
# - Skipped if no dist/ artifacts exist
|
|
41
|
+
# - Skipped if pyproject.toml contains "Private :: Do Not Upload"
|
|
42
|
+
# - Uses Trusted Publishing (OIDC) for PyPI (no stored credentials)
|
|
43
|
+
# - For custom feeds, use PYPI_REPOSITORY_URL and PYPI_TOKEN secrets
|
|
44
|
+
# - Adds PyPI/custom feed link to GitHub release notes
|
|
45
|
+
#
|
|
46
|
+
# 🔐 Security:
|
|
47
|
+
# - No PyPI credentials stored; relies on Trusted Publishing via GitHub OIDC
|
|
48
|
+
# - For custom feeds, PYPI_TOKEN secret is used with default username __token__
|
|
49
|
+
# - SLSA provenance attestations generated for build artifacts (public repos only)
|
|
50
|
+
# - SBOM attestations generated for supply chain transparency (public repos only)
|
|
51
|
+
#
|
|
52
|
+
# 📄 Requirements:
|
|
53
|
+
# - pyproject.toml with top-level version field (for Python packages)
|
|
54
|
+
# - Package registered on PyPI as Trusted Publisher (for PyPI publishing)
|
|
55
|
+
#
|
|
56
|
+
# ✅ To Trigger:
|
|
57
|
+
# Create and push a version tag:
|
|
58
|
+
# git tag v1.2.3
|
|
59
|
+
# git push origin v1.2.3
|
|
60
|
+
#
|
|
61
|
+
# 🎯 For repos without Python packages:
|
|
62
|
+
# The workflow gracefully skips Python-related steps if pyproject.toml doesn't exist.
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
name: (RHIZA) RELEASE
|
|
66
|
+
|
|
67
|
+
on:
|
|
68
|
+
push:
|
|
69
|
+
tags:
|
|
70
|
+
- 'v*'
|
|
71
|
+
workflow_call:
|
|
72
|
+
inputs:
|
|
73
|
+
tag:
|
|
74
|
+
description: 'Version tag to release (e.g. v1.2.3). Required when called via workflow_call.'
|
|
75
|
+
required: true
|
|
76
|
+
type: string
|
|
77
|
+
secrets:
|
|
78
|
+
GH_PAT:
|
|
79
|
+
required: false
|
|
80
|
+
UV_EXTRA_INDEX_URL:
|
|
81
|
+
required: false
|
|
82
|
+
PYPI_TOKEN:
|
|
83
|
+
required: false
|
|
84
|
+
|
|
85
|
+
# Queue runs instead of cancelling: a release or sync must never be
|
|
86
|
+
# interrupted mid-publish or mid-push.
|
|
87
|
+
concurrency:
|
|
88
|
+
group: ${{ github.workflow }}-${{ github.ref }}
|
|
89
|
+
cancel-in-progress: false
|
|
90
|
+
|
|
91
|
+
# Least-privilege default: every job below re-declares exactly the write
|
|
92
|
+
# scopes it needs (Scorecard Token-Permissions). Top-level stays read-only.
|
|
93
|
+
permissions:
|
|
94
|
+
contents: read
|
|
95
|
+
|
|
96
|
+
jobs:
|
|
97
|
+
tag:
|
|
98
|
+
name: Validate Tag
|
|
99
|
+
runs-on: ubuntu-latest
|
|
100
|
+
permissions:
|
|
101
|
+
contents: read # Validation only: reads tags and release state
|
|
102
|
+
outputs:
|
|
103
|
+
tag: ${{ steps.set_tag.outputs.tag }}
|
|
104
|
+
steps:
|
|
105
|
+
- name: Checkout Code
|
|
106
|
+
uses: actions/checkout@v6.1.0
|
|
107
|
+
with:
|
|
108
|
+
fetch-depth: 0
|
|
109
|
+
|
|
110
|
+
- name: Set Tag Variable
|
|
111
|
+
id: set_tag
|
|
112
|
+
run: |
|
|
113
|
+
if [[ -n "${{ inputs.tag }}" ]]; then
|
|
114
|
+
TAG="${{ inputs.tag }}"
|
|
115
|
+
else
|
|
116
|
+
TAG="${GITHUB_REF#refs/tags/}"
|
|
117
|
+
fi
|
|
118
|
+
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
|
119
|
+
|
|
120
|
+
- name: Validate Release
|
|
121
|
+
env:
|
|
122
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
123
|
+
TAG: ${{ steps.set_tag.outputs.tag }}
|
|
124
|
+
run: |
|
|
125
|
+
if gh release view "$TAG" >/dev/null 2>&1; then
|
|
126
|
+
DRAFT_STATUS=$(gh release view "$TAG" --json isDraft --jq '.isDraft')
|
|
127
|
+
if [ "$DRAFT_STATUS" != "true" ]; then
|
|
128
|
+
echo "::error::Release '$TAG' already exists and is not a draft. Please use a new tag."
|
|
129
|
+
exit 1
|
|
130
|
+
else
|
|
131
|
+
echo "::warning::Release '$TAG' exists as a draft. Will proceed to update it."
|
|
132
|
+
fi
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
- name: Ensure the tagged commit is reachable from a branch
|
|
136
|
+
env:
|
|
137
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
138
|
+
TAG: ${{ steps.set_tag.outputs.tag }}
|
|
139
|
+
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
|
140
|
+
run: |
|
|
141
|
+
# Backstop for issue #1454: a release cut on a branch that is then
|
|
142
|
+
# squash-merged leaves the tag on the pre-squash commit, and the squash
|
|
143
|
+
# puts the same content on the default branch under a new SHA. The tag is
|
|
144
|
+
# then permanently orphaned — no branch contains it, `git describe` skips
|
|
145
|
+
# the release, and a git-cliff regeneration silently deletes that version's
|
|
146
|
+
# CHANGELOG section because it cannot place a boundary at an unreachable
|
|
147
|
+
# tag. Publishing from such a tag is never intended, so refuse it.
|
|
148
|
+
# The checkout above uses fetch-depth: 0; this fetch only adds the remote
|
|
149
|
+
# branch refs, which a tag-push checkout does not need otherwise.
|
|
150
|
+
git fetch --no-tags --quiet origin '+refs/heads/*:refs/remotes/origin/*'
|
|
151
|
+
|
|
152
|
+
if [ -z "$DEFAULT_BRANCH" ]; then
|
|
153
|
+
DEFAULT_BRANCH=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name')
|
|
154
|
+
fi
|
|
155
|
+
if ! git rev-parse --verify --quiet "refs/remotes/origin/$DEFAULT_BRANCH" >/dev/null; then
|
|
156
|
+
echo "::error::Cannot resolve the default branch 'origin/$DEFAULT_BRANCH' — refusing to release without a reachability check."
|
|
157
|
+
exit 1
|
|
158
|
+
fi
|
|
159
|
+
|
|
160
|
+
COMMIT=$(git rev-parse "$TAG^{commit}")
|
|
161
|
+
if git merge-base --is-ancestor "$COMMIT" "refs/remotes/origin/$DEFAULT_BRANCH"; then
|
|
162
|
+
echo "✅ $TAG ($COMMIT) is an ancestor of $DEFAULT_BRANCH"
|
|
163
|
+
exit 0
|
|
164
|
+
fi
|
|
165
|
+
|
|
166
|
+
# Reachable from some other branch: a maintenance/hotfix release. Legitimate,
|
|
167
|
+
# but a changelog regenerated from the default branch still cannot see it.
|
|
168
|
+
BRANCHES=$(git branch -r --contains "$COMMIT" --format='%(refname:short)')
|
|
169
|
+
if [ -n "$BRANCHES" ]; then
|
|
170
|
+
echo "::warning::Tag $TAG is not an ancestor of $DEFAULT_BRANCH; it is contained in: $(echo "$BRANCHES" | tr '\n' ' '). A CHANGELOG regenerated from $DEFAULT_BRANCH will not include this release."
|
|
171
|
+
exit 0
|
|
172
|
+
fi
|
|
173
|
+
|
|
174
|
+
echo "::error::Tag $TAG points at $COMMIT, which no branch contains. It is most likely a pre-squash commit from a squash-merged release branch: re-tag the merged commit on $DEFAULT_BRANCH and delete this tag (issue #1454)."
|
|
175
|
+
exit 1
|
|
176
|
+
|
|
177
|
+
- name: Install uv
|
|
178
|
+
uses: astral-sh/setup-uv@v7.6.0
|
|
179
|
+
|
|
180
|
+
- name: Ensure release version is newer than latest published
|
|
181
|
+
env:
|
|
182
|
+
TAG: ${{ steps.set_tag.outputs.tag }}
|
|
183
|
+
run: |
|
|
184
|
+
# Backstop for issue #1126: refuse a tag that is not strictly newer than the
|
|
185
|
+
# highest version already released. A diverged branch with a stale pyproject.toml
|
|
186
|
+
# can otherwise publish an older version (e.g. v0.3.3 after v0.4.0). The bump
|
|
187
|
+
# tooling enforces the same rule; this guards manually-pushed tags too.
|
|
188
|
+
git tag -l 'v*' | uv run --with packaging --no-project python3 -c '
|
|
189
|
+
import sys
|
|
190
|
+
from packaging.version import Version, InvalidVersion
|
|
191
|
+
|
|
192
|
+
new = sys.argv[1].lstrip("v")
|
|
193
|
+
try:
|
|
194
|
+
new_v = Version(new)
|
|
195
|
+
except InvalidVersion:
|
|
196
|
+
print(f"::error::Tag {sys.argv[1]} is not a valid version")
|
|
197
|
+
sys.exit(1)
|
|
198
|
+
|
|
199
|
+
latest = None
|
|
200
|
+
for line in sys.stdin:
|
|
201
|
+
tag = line.strip().lstrip("v")
|
|
202
|
+
if not tag:
|
|
203
|
+
continue
|
|
204
|
+
try:
|
|
205
|
+
candidate = Version(tag)
|
|
206
|
+
except InvalidVersion:
|
|
207
|
+
continue
|
|
208
|
+
if candidate == new_v:
|
|
209
|
+
continue
|
|
210
|
+
if latest is None or candidate > latest:
|
|
211
|
+
latest = candidate
|
|
212
|
+
|
|
213
|
+
if latest is not None and new_v <= latest:
|
|
214
|
+
print(
|
|
215
|
+
f"::error::Refusing to release v{new_v}: it is not newer than the latest "
|
|
216
|
+
f"released version v{latest} (issue #1126). Sync with the default branch and bump again."
|
|
217
|
+
)
|
|
218
|
+
sys.exit(1)
|
|
219
|
+
print(f"v{new_v} is newer than the latest released version v{latest}" if latest
|
|
220
|
+
else f"v{new_v} is the first release")
|
|
221
|
+
' "$TAG"
|
|
222
|
+
|
|
223
|
+
build:
|
|
224
|
+
name: Build
|
|
225
|
+
runs-on: ubuntu-latest
|
|
226
|
+
needs: tag
|
|
227
|
+
permissions:
|
|
228
|
+
contents: read
|
|
229
|
+
id-token: write # OIDC for attestation signing
|
|
230
|
+
attestations: write # SLSA provenance + SBOM attestations (public repos only)
|
|
231
|
+
steps:
|
|
232
|
+
- name: Checkout Code
|
|
233
|
+
uses: actions/checkout@v6.1.0
|
|
234
|
+
with:
|
|
235
|
+
fetch-depth: 0
|
|
236
|
+
|
|
237
|
+
- name: Install uv
|
|
238
|
+
uses: astral-sh/setup-uv@v7.6.0
|
|
239
|
+
with:
|
|
240
|
+
version: "0.11.16"
|
|
241
|
+
|
|
242
|
+
- name: Configure git auth for private packages
|
|
243
|
+
uses: jebel-quant/actions/configure-git-auth@6d52725ca371d609489c5b50236965ad70bbe528 # v1
|
|
244
|
+
with:
|
|
245
|
+
token: ${{ secrets.GH_PAT }}
|
|
246
|
+
|
|
247
|
+
- name: Verify version matches tag
|
|
248
|
+
if: hashFiles('pyproject.toml') != ''
|
|
249
|
+
run: |
|
|
250
|
+
TAG_VERSION="${{ needs.tag.outputs.tag }}"
|
|
251
|
+
TAG_VERSION=${TAG_VERSION#v}
|
|
252
|
+
PROJECT_VERSION=$(uv version --short)
|
|
253
|
+
|
|
254
|
+
# Normalize tag version to PEP 440 format for comparison.
|
|
255
|
+
# Tags use semver format (e.g., 0.11.1-beta.1) while uv version --short
|
|
256
|
+
# returns PEP 440 normalized format (e.g., 0.11.1b1).
|
|
257
|
+
NORMALIZED_TAG=$(uv run --with packaging --no-project python3 -c "from packaging.version import Version; print(Version('$TAG_VERSION'))")
|
|
258
|
+
|
|
259
|
+
if [[ "$PROJECT_VERSION" != "$NORMALIZED_TAG" ]]; then
|
|
260
|
+
echo "::error::Version mismatch: pyproject.toml has '$PROJECT_VERSION' but tag is '$NORMALIZED_TAG' (from tag '$TAG_VERSION')"
|
|
261
|
+
exit 1
|
|
262
|
+
fi
|
|
263
|
+
echo "Version verified: $PROJECT_VERSION matches tag (normalized: $NORMALIZED_TAG)"
|
|
264
|
+
|
|
265
|
+
- name: Detect buildable Python package
|
|
266
|
+
id: buildable
|
|
267
|
+
run: |
|
|
268
|
+
if [[ -f pyproject.toml ]] && grep -q '^\[build-system\]' pyproject.toml; then
|
|
269
|
+
echo "buildable=true" >> "$GITHUB_OUTPUT"
|
|
270
|
+
else
|
|
271
|
+
echo "buildable=false" >> "$GITHUB_OUTPUT"
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
- name: Build
|
|
275
|
+
if: steps.buildable.outputs.buildable == 'true'
|
|
276
|
+
run: |
|
|
277
|
+
printf "[INFO] Building package...\n"
|
|
278
|
+
uv build
|
|
279
|
+
|
|
280
|
+
- name: Install Python for SBOM generation
|
|
281
|
+
if: hashFiles('pyproject.toml') != ''
|
|
282
|
+
uses: actions/setup-python@v6.3.0
|
|
283
|
+
with:
|
|
284
|
+
python-version-file: .python-version
|
|
285
|
+
|
|
286
|
+
- name: Sync environment for SBOM generation
|
|
287
|
+
if: hashFiles('pyproject.toml') != ''
|
|
288
|
+
run: |
|
|
289
|
+
export UV_EXTRA_INDEX_URL="${{ secrets.UV_EXTRA_INDEX_URL }}"
|
|
290
|
+
uv sync --all-extras --all-groups --frozen
|
|
291
|
+
|
|
292
|
+
- name: Generate SBOM (CycloneDX)
|
|
293
|
+
if: hashFiles('pyproject.toml') != ''
|
|
294
|
+
run: |
|
|
295
|
+
printf "[INFO] Generating SBOM in CycloneDX format...\n"
|
|
296
|
+
# Note: uvx caches the tool environment, so the second call is fast
|
|
297
|
+
uvx --from 'cyclonedx-bom>=7.0.0' cyclonedx-py environment --pyproject pyproject.toml --of JSON -o sbom.cdx.json
|
|
298
|
+
uvx --from 'cyclonedx-bom>=7.0.0' cyclonedx-py environment --pyproject pyproject.toml --of XML -o sbom.cdx.xml
|
|
299
|
+
printf "[INFO] SBOM generation complete\n"
|
|
300
|
+
printf "Generated files:\n"
|
|
301
|
+
ls -lh sbom.cdx.*
|
|
302
|
+
|
|
303
|
+
- name: Attest SBOM
|
|
304
|
+
# Attest only the JSON format as it's the canonical machine-readable format.
|
|
305
|
+
# The XML format is provided for compatibility but doesn't need separate attestation.
|
|
306
|
+
id: attest-sbom
|
|
307
|
+
if: hashFiles('pyproject.toml') != '' && github.event.repository.private == false
|
|
308
|
+
uses: actions/attest@v4.2.2
|
|
309
|
+
with:
|
|
310
|
+
subject-path: sbom.cdx.json
|
|
311
|
+
sbom-path: sbom.cdx.json
|
|
312
|
+
|
|
313
|
+
- name: Stage SBOM attestation for the GitHub release
|
|
314
|
+
# Attach the SBOM's Sigstore attestation bundle to the release as a
|
|
315
|
+
# recognised signature asset (*.sigstore.json). Non-buildable repos
|
|
316
|
+
# (no [build-system], so no dist/*.intoto.jsonl provenance) would
|
|
317
|
+
# otherwise ship releases without any signature asset, which fails
|
|
318
|
+
# OpenSSF Scorecard's Signed-Releases check.
|
|
319
|
+
if: hashFiles('pyproject.toml') != '' && github.event.repository.private == false
|
|
320
|
+
run: cp "${{ steps.attest-sbom.outputs.bundle-path }}" sbom.cdx.json.sigstore.json
|
|
321
|
+
|
|
322
|
+
- name: Upload SBOM artifacts
|
|
323
|
+
if: hashFiles('pyproject.toml') != ''
|
|
324
|
+
uses: actions/upload-artifact@v7.0.1
|
|
325
|
+
with:
|
|
326
|
+
name: sbom
|
|
327
|
+
path: |
|
|
328
|
+
sbom.cdx.json
|
|
329
|
+
sbom.cdx.xml
|
|
330
|
+
sbom.cdx.json.sigstore.json
|
|
331
|
+
|
|
332
|
+
- name: Generate SLSA provenance attestations
|
|
333
|
+
id: provenance
|
|
334
|
+
if: steps.buildable.outputs.buildable == 'true' && github.event.repository.private == false
|
|
335
|
+
uses: actions/attest-build-provenance@v4.2.2
|
|
336
|
+
with:
|
|
337
|
+
subject-path: dist/*
|
|
338
|
+
|
|
339
|
+
- name: Stage provenance bundle for the GitHub release
|
|
340
|
+
# Attach the SLSA provenance bundle to the release as a recognised
|
|
341
|
+
# signature asset (*.intoto.jsonl) so consumers — and OpenSSF
|
|
342
|
+
# Scorecard's Signed-Releases check — can verify the artifacts.
|
|
343
|
+
if: steps.buildable.outputs.buildable == 'true' && github.event.repository.private == false
|
|
344
|
+
run: cp "${{ steps.provenance.outputs.bundle-path }}" dist/provenance.intoto.jsonl
|
|
345
|
+
|
|
346
|
+
- name: Upload dist artifact
|
|
347
|
+
if: steps.buildable.outputs.buildable == 'true'
|
|
348
|
+
uses: actions/upload-artifact@v7.0.1
|
|
349
|
+
with:
|
|
350
|
+
name: dist
|
|
351
|
+
path: dist
|
|
352
|
+
|
|
353
|
+
# Don't try to upload artifacts to GitHub Release at all
|
|
354
|
+
draft-release:
|
|
355
|
+
name: Draft GitHub Release
|
|
356
|
+
runs-on: ubuntu-latest
|
|
357
|
+
needs: [tag, build]
|
|
358
|
+
permissions:
|
|
359
|
+
contents: write # Needed to create the GitHub release and upload artifacts
|
|
360
|
+
|
|
361
|
+
steps:
|
|
362
|
+
- name: Checkout Code
|
|
363
|
+
uses: actions/checkout@v6.1.0
|
|
364
|
+
with:
|
|
365
|
+
fetch-depth: 0
|
|
366
|
+
|
|
367
|
+
- name: Install uv
|
|
368
|
+
uses: astral-sh/setup-uv@v7.6.0
|
|
369
|
+
|
|
370
|
+
- name: Generate release notes with git-cliff
|
|
371
|
+
run: uvx git-cliff --latest --output RELEASE_NOTES.md
|
|
372
|
+
|
|
373
|
+
- name: Download SBOM artifact
|
|
374
|
+
# Downloads sbom.cdx.json and sbom.cdx.xml into sbom/ directory
|
|
375
|
+
uses: actions/download-artifact@v8.0.1
|
|
376
|
+
with:
|
|
377
|
+
name: sbom
|
|
378
|
+
path: sbom
|
|
379
|
+
continue-on-error: true
|
|
380
|
+
|
|
381
|
+
- name: Download dist artifact
|
|
382
|
+
# Brings in provenance.intoto.jsonl (and the built distributions) staged
|
|
383
|
+
# by the build job, so the SLSA provenance ships as a release asset.
|
|
384
|
+
uses: actions/download-artifact@v8.0.1
|
|
385
|
+
with:
|
|
386
|
+
name: dist
|
|
387
|
+
path: dist
|
|
388
|
+
continue-on-error: true
|
|
389
|
+
|
|
390
|
+
- name: Create GitHub Release with artifacts
|
|
391
|
+
uses: ncipollo/release-action@v1.21.0
|
|
392
|
+
with:
|
|
393
|
+
tag: ${{ needs.tag.outputs.tag }}
|
|
394
|
+
name: ${{ needs.tag.outputs.tag }}
|
|
395
|
+
bodyFile: RELEASE_NOTES.md
|
|
396
|
+
draft: true
|
|
397
|
+
allowUpdates: true
|
|
398
|
+
artifacts: "sbom/*,dist/provenance.intoto.jsonl"
|
|
399
|
+
|
|
400
|
+
# Decide at step-level whether to publish
|
|
401
|
+
pypi:
|
|
402
|
+
name: Publish to PyPI
|
|
403
|
+
runs-on: ubuntu-latest
|
|
404
|
+
environment: release
|
|
405
|
+
needs: [tag, build, draft-release]
|
|
406
|
+
permissions:
|
|
407
|
+
contents: read
|
|
408
|
+
id-token: write # OIDC Trusted Publishing to PyPI (no stored credentials)
|
|
409
|
+
outputs:
|
|
410
|
+
should_publish: ${{ steps.check_dist.outputs.should_publish }}
|
|
411
|
+
|
|
412
|
+
steps:
|
|
413
|
+
- name: Checkout Code
|
|
414
|
+
uses: actions/checkout@v6.1.0
|
|
415
|
+
with:
|
|
416
|
+
fetch-depth: 0
|
|
417
|
+
|
|
418
|
+
- name: Download dist artifact
|
|
419
|
+
uses: actions/download-artifact@v8.0.1
|
|
420
|
+
with:
|
|
421
|
+
name: dist
|
|
422
|
+
path: dist
|
|
423
|
+
# Continue if dist folder is not found. Don't fail the job.
|
|
424
|
+
continue-on-error: true
|
|
425
|
+
|
|
426
|
+
- name: Check if dist contains artifacts and not marked as private
|
|
427
|
+
id: check_dist
|
|
428
|
+
run: |
|
|
429
|
+
if [[ ! -d dist ]]; then
|
|
430
|
+
echo "::warning::No folder dist/. Skipping PyPI publish."
|
|
431
|
+
echo "should_publish=false" >> "$GITHUB_OUTPUT"
|
|
432
|
+
else
|
|
433
|
+
if grep -R "Private :: Do Not Upload" pyproject.toml; then
|
|
434
|
+
echo "should_publish=false" >> "$GITHUB_OUTPUT"
|
|
435
|
+
else
|
|
436
|
+
echo "should_publish=true" >> "$GITHUB_OUTPUT"
|
|
437
|
+
fi
|
|
438
|
+
fi
|
|
439
|
+
cat "$GITHUB_OUTPUT"
|
|
440
|
+
|
|
441
|
+
- name: Remove non-distribution files before publish
|
|
442
|
+
# The dist artifact also carries the SLSA provenance bundle (staged for
|
|
443
|
+
# the GitHub release). twine/pypi-publish rejects it as an unknown
|
|
444
|
+
# distribution format, so strip it here before publishing.
|
|
445
|
+
if: ${{ steps.check_dist.outputs.should_publish == 'true' }}
|
|
446
|
+
run: rm -f dist/*.intoto.jsonl
|
|
447
|
+
|
|
448
|
+
# this should not take place, as "Private :: Do Not Upload" set in pyproject.toml
|
|
449
|
+
# repository-url and password only used for custom feeds, not for PyPI with OIDC
|
|
450
|
+
- name: Publish to PyPI
|
|
451
|
+
if: ${{ steps.check_dist.outputs.should_publish == 'true' }}
|
|
452
|
+
uses: pypa/gh-action-pypi-publish@v1.14.2
|
|
453
|
+
with:
|
|
454
|
+
packages-dir: dist/
|
|
455
|
+
skip-existing: true
|
|
456
|
+
repository-url: ${{ vars.PYPI_REPOSITORY_URL }}
|
|
457
|
+
password: ${{ secrets.PYPI_TOKEN }}
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
finalise-release:
|
|
461
|
+
name: Finalise Release
|
|
462
|
+
runs-on: ubuntu-latest
|
|
463
|
+
needs: [tag, pypi]
|
|
464
|
+
if: needs.pypi.result == 'success'
|
|
465
|
+
permissions:
|
|
466
|
+
contents: write # Needed to undraft/publish the GitHub release
|
|
467
|
+
steps:
|
|
468
|
+
- name: Checkout Code
|
|
469
|
+
uses: actions/checkout@v6.1.0
|
|
470
|
+
with:
|
|
471
|
+
fetch-depth: 0
|
|
472
|
+
|
|
473
|
+
- name: Install uv
|
|
474
|
+
uses: astral-sh/setup-uv@v7.6.0
|
|
475
|
+
with:
|
|
476
|
+
version: "0.11.16"
|
|
477
|
+
|
|
478
|
+
- name: "Sync the virtual environment for ${{ github.repository }}"
|
|
479
|
+
shell: bash
|
|
480
|
+
run: |
|
|
481
|
+
export UV_EXTRA_INDEX_URL="${{ secrets.uv_extra_index_url }}"
|
|
482
|
+
# will just use .python-version?
|
|
483
|
+
uv sync --all-extras --all-groups --frozen
|
|
484
|
+
|
|
485
|
+
- name: Set up Python
|
|
486
|
+
uses: actions/setup-python@v6.3.0
|
|
487
|
+
|
|
488
|
+
- name: Generate PyPI Link
|
|
489
|
+
id: pypi_link
|
|
490
|
+
if: needs.pypi.outputs.should_publish == 'true' && needs.pypi.result == 'success'
|
|
491
|
+
run: |
|
|
492
|
+
PACKAGE_NAME=$(uv run python -c "import tomllib; print(tomllib.load(open('pyproject.toml', 'rb'))['project']['name'])")
|
|
493
|
+
VERSION="${{ needs.tag.outputs.tag }}"
|
|
494
|
+
VERSION=${VERSION#v}
|
|
495
|
+
|
|
496
|
+
REPO_URL="${{ vars.PYPI_REPOSITORY_URL || '' }}"
|
|
497
|
+
if [ -z "$REPO_URL" ]; then
|
|
498
|
+
LINK="https://pypi.org/project/$PACKAGE_NAME/$VERSION/"
|
|
499
|
+
NAME="PyPI Package"
|
|
500
|
+
else
|
|
501
|
+
LINK="$REPO_URL"
|
|
502
|
+
NAME="Custom Feed Package"
|
|
503
|
+
fi
|
|
504
|
+
|
|
505
|
+
{
|
|
506
|
+
echo "message<<EOF"
|
|
507
|
+
echo "### $NAME"
|
|
508
|
+
echo ""
|
|
509
|
+
echo "[$PACKAGE_NAME]($LINK)"
|
|
510
|
+
echo "EOF"
|
|
511
|
+
} >> "$GITHUB_OUTPUT"
|
|
512
|
+
|
|
513
|
+
- name: Publish Release
|
|
514
|
+
env:
|
|
515
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
516
|
+
TAG: ${{ needs.tag.outputs.tag }}
|
|
517
|
+
PYPI_MSG: ${{ steps.pypi_link.outputs.message }}
|
|
518
|
+
run: |
|
|
519
|
+
# Get existing auto-generated release notes (gracefully handle missing release)
|
|
520
|
+
EXISTING=$(gh release view "$TAG" --json body --jq '.body // ""' 2>/dev/null || echo "")
|
|
521
|
+
|
|
522
|
+
# Append links to release body
|
|
523
|
+
{
|
|
524
|
+
printf '%s' "$EXISTING"
|
|
525
|
+
[ -n "$PYPI_MSG" ] && printf '\n\n%s' "$PYPI_MSG"
|
|
526
|
+
} > /tmp/notes.md
|
|
527
|
+
|
|
528
|
+
gh release edit "$TAG" --draft=false --notes-file /tmp/notes.md
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.11
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.1.1] - 2026-08-18
|
|
4
|
+
|
|
5
|
+
## [0.1.0] - unreleased
|
|
6
|
+
|
|
7
|
+
Initial extraction. Replaces `.rhiza/rhiza.mk` and `.rhiza/make.d/*.mk` (1023 synced
|
|
8
|
+
lines) with a pinned CLI, on the same principle as `pytest-rhiza` replacing
|
|
9
|
+
`.rhiza/tests`.
|
|
10
|
+
|
|
11
|
+
- `core` + `python-core` gate set: install, test, typecheck, security, deps, license,
|
|
12
|
+
docs-coverage, fmt, semgrep, rhiza-test, todos, clean, doctor, all
|
|
13
|
+
- `tests` extras: benchmark, hypothesis-test, stress, mutation
|
|
14
|
+
- `book` / `marimo`: book, serve, marimo, marimo-validate
|
|
15
|
+
- Not ported: `github.mk`'s seven `gh` wrappers (use `gh` directly), `install-uv`
|
|
16
|
+
(running under `uvx` means uv is already present)
|
|
17
|
+
|
|
18
|
+
### Deliberate differences from the make layer
|
|
19
|
+
|
|
20
|
+
Four places where the port does not reproduce the recipe, each found by running the gates
|
|
21
|
+
against this package:
|
|
22
|
+
|
|
23
|
+
- **`security` passes `--ini .bandit` only when that file exists.** python.mk passes it
|
|
24
|
+
unconditionally, and bandit treats a missing ini as a usage error — so a project without
|
|
25
|
+
one got a red security gate reporting a configuration problem.
|
|
26
|
+
- **`doctor` treats GNU make as optional.** doctor.mk requires it, which was honest while
|
|
27
|
+
the task layer *was* make. Only the shim needs it now.
|
|
28
|
+
- **The shim declares `local.mk: ;`.** An included makefile is a target make tries to remake
|
|
29
|
+
first, and the catch-all routed that attempt to the CLI — so every invocation began with
|
|
30
|
+
`unknown task: local.mk`.
|
|
31
|
+
- **`shim` writes to stdout, not through rich.** Console output word-wraps and expands
|
|
32
|
+
tabs; a Makefile survives neither, so `rhiza-task shim > Makefile` produced a broken file.
|
rhiza_task-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Jebel Quant Research
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|