Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/build-constraints.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# CI builds on x86_64 Linux. Only accept the published wheel for that platform.
maturin==1.15.0 --hash=sha256:653020a63525bb224e5ab0adf02e17a2e08bc86dbea7fc1399c9a56d7529b99e
20 changes: 20 additions & 0 deletions .github/install-rust.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail

version=$1
case "$version" in
1.97.0) sha256=1cf17e4905b841d4c8e3f76467ac148d55fb3f54bf213c86f0d287a36471d904 ;;
1.99.1) sha256=231fcae47ee950621374725b5fc3b5441d2fbda2955ac45dd7dc816d8876cfe7 ;;
*) echo "No pinned Rust distribution for $version" >&2; exit 1 ;;
esac

# The standalone distribution includes cargo, clippy and rustfmt under one digest.
target=x86_64-unknown-linux-gnu
dist="rust-$version-$target"
cd "$RUNNER_TEMP"
curl --fail --silent --show-error --location --retry 3 "https://static.rust-lang.org/dist/$dist.tar.xz" -o "$dist.tar.xz"
echo "$sha256 $dist.tar.xz" | sha256sum --check
tar -xf "$dist.tar.xz"
"$dist/install.sh" --prefix="$RUNNER_TEMP/rust" --disable-ldconfig \
--components="rustc,cargo,rust-std-$target,rustfmt-preview,clippy-preview"
echo "$RUNNER_TEMP/rust/bin" >> "$GITHUB_PATH"
97 changes: 58 additions & 39 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,36 @@ name: CI
on:
push:
pull_request:
workflow_call:

permissions:
contents: read

env:
# Explicit installs use the Python archive hashes embedded in the pinned uv binary.
UV_PYTHON_DOWNLOADS: manual
UV_MANAGED_PYTHON: 'true'

jobs:
build:
runs-on: ubuntu-latest
steps:
- &checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- &setup-uv
uses: &setup-uv-action astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: &uv-version '0.12.23'
- uses: &setup-python-action actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
python-version: '3.14'
- uses: &rust-toolchain dtolnay/rust-toolchain@7e38f4b43b4db5c8dd498af069a4f6196df1d067
with:
toolchain: '1.97.0'
version: '0.13.6'
checksum: f29c3e94ffabd7e6f3ba521dba169a9b7ec12100bf3257087f0f24f6123f3645
# A restored dependency cache can bypass archive verification on a fresh runner.
enable-cache: false
- run: uv python install 3.14.10
- run: bash .github/install-rust.sh 1.97.0
# Build abi3 once so both interpreters exercise the same binary.
- name: Build the source distribution and stable ABI wheel
env:
UV_BUILD_CONSTRAINT: ${{ github.workspace }}/.github/build-constraints.txt
UV_REQUIRE_HASHES: 'true'
run: |
dist="$RUNNER_TEMP/dist"
uv build --sdist --no-sources --out-dir "$dist" --python 3.14
Expand All @@ -42,25 +48,26 @@ jobs:
test:
needs: build
runs-on: ubuntu-latest
strategy: &toolchains
strategy:
matrix:
include:
- python: '3.14'
- python: '3.14.10'
rust: '1.97.0'
- python: '3.15'
rust: stable
- python: '3.15.1'
rust: '1.99.1'
steps:
- *checkout
- *setup-uv
- &setup-python
uses: *setup-python-action
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
python-version: ${{ matrix.python }}
- uses: *rust-toolchain
persist-credentials: false
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
toolchain: ${{ matrix.rust }}
version: '0.13.6'
checksum: f29c3e94ffabd7e6f3ba521dba169a9b7ec12100bf3257087f0f24f6123f3645
enable-cache: false
- run: uv python install ${{ matrix.python }}
- run: bash .github/install-rust.sh ${{ matrix.rust }}
- name: Install locked test dependencies
run: uv sync --locked --no-install-project --python ${{ matrix.python }}
run: uv sync --locked --no-build --no-install-project --python ${{ matrix.python }}
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: sdist-and-abi3
Expand All @@ -69,36 +76,45 @@ jobs:
# Exercise selector limits with abort in the native wheels and the default strategy in abi3.
env:
CARGO_PROFILE_RELEASE_PANIC: abort
UV_BUILD_CONSTRAINT: ${{ github.workspace }}/.github/build-constraints.txt
UV_REQUIRE_HASHES: 'true'
run: >-
uv build "$RUNNER_TEMP"/dist/*.tar.gz --wheel --no-sources
--out-dir "$RUNNER_TEMP/dist" --python .venv/bin/python
--config-setting 'build-args=--compatibility pypi --auditwheel check'
# Both wheels have the same version. Force each install and keep uv from
# syncing the editable project back into the test environment.
- name: Test the native and stable ABI wheels
env:
PYTHON_VERSION: ${{ matrix.python }}
run: |
dist="$RUNNER_TEMP/dist"
cp="cp${PYTHON_VERSION//./}"
cp=$(.venv/bin/python -c 'import sys; print("cp%d%d" % sys.version_info[:2])')
for tag in "$cp-$cp" cp314-abi3; do
uv pip install --python .venv/bin/python --no-deps --strict --reinstall "$dist"/*-"$tag"-*.whl
uv run --no-sync python -I -m pytest --import-mode=importlib
done

lint:
runs-on: ubuntu-latest
strategy: *toolchains
strategy:
matrix:
include:
- python: '3.14.10'
rust: '1.97.0'
- python: '3.15.1'
rust: '1.99.1'
steps:
- *checkout
- *setup-uv
- *setup-python
- uses: *rust-toolchain
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
toolchain: ${{ matrix.rust }}
components: rustfmt, clippy
version: '0.13.6'
checksum: f29c3e94ffabd7e6f3ba521dba169a9b7ec12100bf3257087f0f24f6123f3645
enable-cache: false
- run: uv python install ${{ matrix.python }}
- run: bash .github/install-rust.sh ${{ matrix.rust }}
- name: Install locked lint dependencies
run: uv sync --locked --no-install-project --group benchmark --python ${{ matrix.python }}
run: uv sync --locked --no-build --no-install-project --group benchmark --python ${{ matrix.python }}
- run: uv run --no-sync ruff check .
- run: uv run --no-sync ruff format --check .
- run: uv run --no-sync ty check
Expand All @@ -108,14 +124,17 @@ jobs:
zizmor:
runs-on: ubuntu-latest
steps:
- *checkout
- uses: *setup-uv-action
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: *uv-version
# Leave the shared uv cache to jobs that install project dependencies.
version: '0.13.6'
checksum: f29c3e94ffabd7e6f3ba521dba169a9b7ec12100bf3257087f0f24f6123f3645
enable-cache: false
- run: uv python install 3.14.10
- name: Install the locked zizmor wheel
run: uv sync --locked --only-group zizmor --no-build
run: uv sync --locked --only-group zizmor --no-build --python 3.14.10
- name: Audit GitHub Actions
env:
GH_TOKEN: ${{ github.token }}
Expand Down
84 changes: 84 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: Publish release

on:
# The pypi job checks the dispatch, run identity, and main ancestry before consuming the artifact.
workflow_run: # zizmor: ignore[dangerous-triggers]
workflows: [Start release]
types: [completed]
branches: [main]

permissions: {}

concurrency:
group: publish-release
cancel-in-progress: false

jobs:
pypi:
# workflow_run has publishing privileges. Only consume a successful manual run in this repository.
if: >-
github.repository == 'astral-experiments/astral-html-python' &&
github.event.workflow_run.head_repository.full_name == github.repository &&
github.event.workflow_run.path == '.github/workflows/start-release.yml' &&
github.event.workflow_run.event == 'workflow_dispatch' &&
github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/astral-html/
permissions:
actions: read
contents: read
id-token: write
steps:
- name: Verify the source commit is on main
env:
GH_TOKEN: ${{ github.token }}
RELEASE_SHA: ${{ github.event.workflow_run.head_sha }}
# workflow_run reports a short branch name, which a same-named tag could also have.
run: |
main_sha=$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/main" --jq .object.sha)
gh api "repos/$GITHUB_REPOSITORY/compare/$RELEASE_SHA...$main_sha" --jq .status | grep -Ex 'ahead|identical'
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: release-${{ github.event.workflow_run.run_attempt }}
path: release
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: '0.13.6'
checksum: f29c3e94ffabd7e6f3ba521dba169a9b7ec12100bf3257087f0f24f6123f3645
enable-cache: false
- run: uv publish --trusted-publishing always --check-url https://pypi.org/simple release/*.tar.gz

github:
needs: pypi
runs-on: ubuntu-latest
permissions:
actions: read
contents: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
# workflow_run's GITHUB_SHA can be newer than the commit CI tested.
ref: ${{ github.event.workflow_run.head_sha }}
persist-credentials: false
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: release-${{ github.event.workflow_run.run_attempt }}
path: release
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ github.token }}
- name: Tag the published commit and release its changelog entry
env:
GH_TOKEN: ${{ github.token }}
run: |
tag=$(cat release/tag)
gh auth setup-git
git push origin "HEAD:refs/tags/$tag"
if gh release view "$tag" --json isDraft --jq '.isDraft == false' | grep -qx true; then
exit 0
fi
gh release create "$tag" --verify-tag --title "$tag" --notes-file release/notes.md \
--prerelease="$(cat release/prerelease)"
76 changes: 76 additions & 0 deletions .github/workflows/start-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
name: Start release

on:
workflow_dispatch:
inputs:
version:
description: Version to publish from main, without the leading v
required: true
type: string

permissions:
contents: read

env:
# Explicit installs use the Python archive hashes embedded in the pinned uv binary.
UV_PYTHON_DOWNLOADS: manual
UV_MANAGED_PYTHON: 'true'

jobs:
ci:
if: github.repository == 'astral-experiments/astral-html-python' && github.ref == 'refs/heads/main'
uses: $/.github/workflows/ci.yml

prepare:
needs: ci
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: sdist-and-abi3
path: dist
- uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
version: '0.13.6'
checksum: f29c3e94ffabd7e6f3ba521dba169a9b7ec12100bf3257087f0f24f6123f3645
enable-cache: false
- run: uv python install 3.14.10
- name: Select the tested sdist and its changelog entry
env:
VERSION: ${{ inputs.version }}
run: |
sdists=(dist/*.tar.gz)
if [[ ${#sdists[@]} != 1 || ${sdists[0]} != "dist/astral_html-$VERSION.tar.gz" ]]; then
echo "::error::Requested version $VERSION does not match the built sdist"
exit 1
fi
if git show-ref --verify --quiet "refs/tags/v$VERSION"; then
test "$(git rev-parse "refs/tags/v$VERSION^{commit}")" = "$GITHUB_SHA"
fi
mkdir release
awk -v heading="## $VERSION" '
/^## / { selected = ($0 == heading); next }
selected { print }
' CHANGELOG.md > release/notes.md
if ! grep -q '[^[:space:]]' release/notes.md; then
echo "::error::Missing changelog entry for $VERSION"
exit 1
fi
mv "${sdists[0]}" release/
printf 'v%s\n' "$VERSION" > release/tag
uv run --locked --no-build --only-group dev --python 3.14.10 python - <<'PY' > release/prerelease
import os
from packaging.version import Version

print(str(Version(os.environ["VERSION"]).is_prerelease).lower())
PY
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
# A rerun may rebuild the sdist. Bind publication to this attempt's bytes.
name: release-${{ github.run_attempt }}
path: release/
if-no-files-found: error
3 changes: 2 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
* The Readme is concise and only contains deep information. Do not add random information there. Keep source-build instructions in the development guide so the quick start opens with Python usage.
* Consider if you need really new sections in the documentation, or if you can avoid it.
* When the reason for a fix or improvement isn't clear from the code, add or update a comment beside the relevant logic. Explain the constraint it satisfies or the failure it prevents.
* If we undo something previously done, document that, so we don't do it again.
* If we undo something previously done, document that, so we don't do it again.
* CI may only download actions, tools, and dependencies pinned by hash. Pin actions to full commit SHAs and verify downloaded artifacts against trusted, pinned digests before use.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Changelog

## 0.0.1

- Parse HTML from Python and query elements with CSS selectors.
- Read element attributes, extract descendant text, and iterate over mixed text and child elements in source order.
5 changes: 3 additions & 2 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,6 @@ done

The `-I` flag prevents Python from adding the checkout to the import path.

Set the package version in `pyproject.toml` and `Cargo.toml`.
[Cargo.toml](../Cargo.toml) · [CI workflow](../.github/workflows/ci.yml)
For a release, update the version in `pyproject.toml` and `Cargo.toml`, refresh
both lockfiles, and add a matching entry to [CHANGELOG.md](../CHANGELOG.md).
After merging, run **Start release** on `main` with that version.
Loading