From 965a8d64d0916128eb71e29eff9a51d6f3e85789 Mon Sep 17 00:00:00 2001 From: squidfunk Date: Wed, 17 Dec 2025 19:16:18 +0100 Subject: [PATCH] chore: set up new build workflow Signed-off-by: squidfunk --- .gitattributes | 1 - .githooks/commit-msg | 26 ---- .github/workflows/build.yml | 267 +++++++++++------------------------ .github/workflows/check.yml | 80 ----------- .github/workflows/commit.yml | 61 -------- scripts/commit.py | 243 ------------------------------- scripts/prepare.py | 2 + 7 files changed, 86 insertions(+), 594 deletions(-) delete mode 100755 .githooks/commit-msg delete mode 100644 .github/workflows/check.yml delete mode 100644 .github/workflows/commit.yml delete mode 100755 scripts/commit.py diff --git a/.gitattributes b/.gitattributes index 35ddd6d..2f90568 100644 --- a/.gitattributes +++ b/.gitattributes @@ -21,5 +21,4 @@ # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. -.githooks/* linguist-vendored scripts/* linguist-vendored diff --git a/.githooks/commit-msg b/.githooks/commit-msg deleted file mode 100755 index ece1314..0000000 --- a/.githooks/commit-msg +++ /dev/null @@ -1,26 +0,0 @@ -#!/usr/bin/env bash - -# Copyright (c) 2025 Zensical and contributors - -# SPDX-License-Identifier: MIT -# Third-party contributions licensed under DCO - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -exec python3 "$(git rev-parse --show-toplevel)/scripts/commit.py" "$@" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8512d88..01fbc66 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,140 +24,91 @@ name: Build on: - pull_request: push: branches: - master - tags: - - '*' + pull_request: + branches: + - master + workflow_call: + inputs: + ref: + required: true + type: string permissions: - contents: read + attestations: write # Required for attestations + id-token: write # Required for OIDC jobs: - linux: - name: Build wheels (Linux, ${{ matrix.platform.target }}) - runs-on: ubuntu-latest + build: + name: Wheel (${{ matrix.name }}, ${{ matrix.target }}) + runs-on: ${{ matrix.os }} strategy: matrix: - platform: - - target: x86_64 - - target: x86 - - target: aarch64 - - target: armv7 - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3.x - - - name: Prepare build - run: python scripts/prepare.py - - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.platform.target }} - args: --release --out dist - sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - manylinux: auto - - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: wheels-linux-${{ matrix.platform.target }} - path: dist - - musllinux: - name: Build wheels (Linux Musl, ${{ matrix.platform.target }}) - runs-on: ubuntu-latest - strategy: - matrix: - platform: - - target: x86_64 - - target: x86 - - target: aarch64 - - target: armv7 - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3.x - - - name: Prepare build - run: python scripts/prepare.py - - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.platform.target }} - args: --release --out dist - sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - manylinux: musllinux_1_2 - - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: wheels-musllinux-${{ matrix.platform.target }} - path: dist - - windows: - name: Build wheels (Windows, ${{ matrix.platform.target }}) - runs-on: windows-latest - strategy: - matrix: - platform: - - target: x64 - - target: x86 - - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3.x - architecture: ${{ matrix.platform.target }} - - - name: Prepare build - run: python scripts/prepare.py - - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.platform.target }} - args: --release --out dist - sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} - - - name: Upload wheels - uses: actions/upload-artifact@v4 - with: - name: wheels-windows-${{ matrix.platform.target }} - path: dist - - macos: - name: Build wheels (macOS, ${{ matrix.platform.target }}) - runs-on: ${{ matrix.platform.runner }} - strategy: - matrix: - platform: - - runner: macos-15-intel + include: + - name: Linux + platform: linux + os: ubuntu-latest target: x86_64 - - runner: macos-15 + manylinux: auto + - name: Linux + platform: linux + os: ubuntu-latest + target: x86 + manylinux: auto + - name: Linux + platform: linux + os: ubuntu-latest + target: aarch64 + manylinux: auto + - name: Linux + platform: linux + os: ubuntu-latest + target: armv7 + manylinux: auto + - name: Linux Musl + platform: musllinux + os: ubuntu-latest + target: x86_64 + manylinux: musllinux_1_2 + - name: Linux Musl + platform: musllinux + os: ubuntu-latest + target: x86 + manylinux: musllinux_1_2 + - name: Linux Musl + platform: musllinux + os: ubuntu-latest + target: aarch64 + manylinux: musllinux_1_2 + - name: Linux Musl + platform: musllinux + os: ubuntu-latest + target: armv7 + manylinux: musllinux_1_2 + - name: Windows + platform: windows + os: windows-latest + target: x64 + - name: Windows + platform: windows + os: windows-latest + target: x86 + - name: macOS + platform: macos + os: macos-15-intel + target: x86_64 + - name: macOS + platform: macos + os: macos-15 target: aarch64 steps: - name: Checkout repository uses: actions/checkout@v5 + with: + ref: ${{ inputs.ref || github.sha }} - name: Set up Python uses: actions/setup-python@v5 @@ -167,72 +118,22 @@ jobs: - name: Prepare build run: python scripts/prepare.py - - name: Build wheels + - name: Build artifact uses: PyO3/maturin-action@v1 with: - target: ${{ matrix.platform.target }} - args: --release --out dist - sccache: ${{ !startsWith(github.ref, 'refs/tags/') }} + target: ${{ matrix.target }} + args: --release --out artifacts + sccache: > + ${{ !startsWith(github.event.pull_request.head.ref, 'release/') }} - - name: Upload wheels + - name: Create artifact attestation + uses: actions/attest-build-provenance@v1 + with: + show-summary: false + subject-path: artifacts/* + + - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: wheels-macos-${{ matrix.platform.target }} - path: dist - - sdist: - name: Build source distribution - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3.x - - - name: Prepare build - run: python scripts/prepare.py - - - name: Build sdist - uses: PyO3/maturin-action@v1 - with: - command: sdist - args: --out dist - - - name: Upload sdist - uses: actions/upload-artifact@v4 - with: - name: wheels-sdist - path: dist - - release: - name: Release - runs-on: ubuntu-latest - if: >- - ${{ - startsWith(github.ref, 'refs/tags/') || - github.event_name == 'workflow_dispatch' - }} - needs: [linux, musllinux, windows, macos, sdist] - permissions: - id-token: write # Sign the release artifacts - contents: write # Upload release artifacts - attestations: write # Generate artifact attestation - - steps: - - name: Download wheels and sdist - uses: actions/download-artifact@v4 - - - name: Generate artifact attestation - uses: actions/attest-build-provenance@v2 - with: - subject-path: 'wheels-*/*' - - - name: Publish to PyPI - if: ${{ startsWith(github.ref, 'refs/tags/') }} - uses: PyO3/maturin-action@v1 - with: - command: upload - args: --non-interactive --skip-existing wheels-*/* + name: wheel-${{ matrix.platform }}-${{ matrix.target }} + path: artifacts/* diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml deleted file mode 100644 index cc2badc..0000000 --- a/.github/workflows/check.yml +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright (c) 2025 Zensical and contributors - -# SPDX-License-Identifier: MIT -# Third-party contributions licensed under DCO - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -name: Check - -on: - pull_request: - push: - branches: - - master - -jobs: - checks: - name: Lint and format - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up Rust - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable - - - name: Run clippy - run: cargo clippy - - - name: Run rustfmt - run: find crates -name "*.rs" -exec rustfmt --check {} \; - - - name: Set up Python - uses: astral-sh/setup-uv@v7 - with: - python-version: "3.12" - - - name: Install dependencies - run: uv sync --all-groups - - - name: Run ruff - run: uv run ruff check --no-fix-only . - - - name: Run mypy - run: uv run mypy . - - security: - name: Security audit - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v5 - - - name: Set up Rust cache - uses: Swatinem/rust-cache@v2 - - - name: Install cargo-audit - run: cargo install cargo-audit - - - name: Audit dependencies - run: cargo audit - diff --git a/.github/workflows/commit.yml b/.github/workflows/commit.yml deleted file mode 100644 index 72736d6..0000000 --- a/.github/workflows/commit.yml +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (c) 2025 Zensical and contributors - -# SPDX-License-Identifier: MIT -# Third-party contributions licensed under DCO - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -name: Commit - -on: - pull_request: - types: - - opened - - synchronize - - reopened - -jobs: - lint: - name: Lint commit messages - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: 3.x - - - name: Get commit messages - run: | - base="${{ github.event.pull_request.base.sha }}" - head="${{ github.event.pull_request.head.sha }}" - - echo "Getting commits from ${base}..${head}" - git log "${base}..${head}" --pretty=format:"%s" > commits.txt - - # - name: Run commit message linter - # run: | - # while IFS= read -r commit; do - # echo "Linting commit message: $commit" - # scripts/commit.py "$commit" - # done < commits.txt diff --git a/scripts/commit.py b/scripts/commit.py deleted file mode 100755 index 2110329..0000000 --- a/scripts/commit.py +++ /dev/null @@ -1,243 +0,0 @@ -#!/usr/bin/env python - -# ----------------------------------------------------------------------------- - -# Copyright (c) 2025 Zensical and contributors - -# SPDX-License-Identifier: MIT -# Third-party contributions licensed under DCO - -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to -# deal in the Software without restriction, including without limitation the -# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or -# sell copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: - -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. - -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS -# IN THE SOFTWARE. - -import os -import re -import sys -from dataclasses import dataclass -from glob import glob - -import tomllib - -# ---------------------------------------------------------------------------- -# Classes -# ---------------------------------------------------------------------------- - - -class ScopeError(ValueError): - """Invalid commit scope error.""" - - -class TypeError(ValueError): - """Invalid commit type error.""" - - -# ---------------------------------------------------------------------------- - - -@dataclass -class Message: - """Commit message. - - This class represents a commit message with a scope, type, and description. - It provides methods to parse and validate commit messages according to our - format, which is slightly different from the Conventional Commits standard, - improving readability and consistency. - """ - - @classmethod - def parse(cls, message: str) -> "Message": - """Parse a commit message string into an object.""" - match = re.match(r"^([^:]+):([^\s]+) - (.+)$", message) - if not match: - raise ValueError("Required format: : - ") - - # Extract components and return commit message - scope, type, description = match.groups() - return cls(scope=scope, type=type, description=description) - - def validate(self, scopes: dict[str, str]) -> None: - """Validate the commit message against the given scopes and types.""" - if self.scope not in scopes: - raise ScopeError(f"Invalid scope: {self.scope}") - - # Validate type - if self.type not in TYPES: - raise TypeError(f"Invalid type: {self.type}") - - # Validate description - if self.description[0] != self.description[0].lower(): - raise ValueError("Commit message must be lowercased.") - - # Retrieve staged files - with os.popen("git diff --cached --name-only") as p: - output = p.read() - - # Validate if files are within scope - for file in output.strip().split("\n"): - if not f"./{file}".startswith(scopes[self.scope]): - raise ValueError( - f"Invalid scope for file: " - f"{file} not in {scopes[self.scope]}" - ) - - scope: str - """ - Commit scope. - """ - - type: str - """ - Commit type. - """ - - description: str - """ - Commit description. - """ - - -# ---------------------------------------------------------------------------- -# Functions -# ---------------------------------------------------------------------------- - - -def resolve(directory: str) -> dict[str, str] | None: - """Return commit scopes for a cargo project. - - This function checks if the given directory contains a `Cargo.toml` file, - and if so, parses it to extract the workspace members. It then resolves the - valid scopes, which are the names of the crates defined in the respective - `Cargo.toml` files. - """ - path = os.path.join(directory, "Cargo.toml") - if not os.path.isfile(path): - return None - - # Open and parse the Cargo.toml file - with open(path, "rb") as f: - content = tomllib.load(f) - - # Return workspace members - if "workspace" in content: - scopes: dict[str, str] = {} - - # Get the list of member crates - for member in content["workspace"].get("members", []): - path = os.path.join(directory, member) - for match in glob(path): - nested = resolve(match) - if nested: - scopes.update(nested) - - # Return commit scopes - return scopes - - # Return crate - package = content.get("package") - if package and "name" in package: - return {package["name"]: directory} - - return None - - -# ---------------------------------------------------------------------------- -# Constants -# ---------------------------------------------------------------------------- - - -TYPES = { - "feature", - "fix", - "refactor", - "docs", - "perf", - "test", - "build", - "style", - "chore", - "release", -} -""" -Commit types. -""" - -# ---------------------------------------------------------------------------- - -BG_RED = "\033[41m" -""" -ANSI escape code for red background. -""" - -FG_RED = "\033[31m" -""" -ANSI escape code for red foreground. -""" - -RESET = "\033[0m" -""" -ANSI escape code to reset formatting. -""" - -# ---------------------------------------------------------------------------- -# Program -# ---------------------------------------------------------------------------- - - -def main() -> int: - """Commit message linter.""" - if len(sys.argv) < 2: - print("No commit message provided.") - return 1 - - # Commit message might be passed as string, or in a file - commit = sys.argv[1] - if os.path.isfile(commit): - with open(sys.argv[1]) as f: - message = f.read().strip() - else: - message = commit.strip() - - # Skip merge commits - if message.startswith("Merge branch"): - return 0 - - # Resolve cargo workspace members and parse commit message - scopes = resolve(os.path.curdir) - if scopes: - scopes["workspace"] = "." - try: - msg = Message.parse(message) - msg.validate(scopes) - - # If an error happened, print it - except ValueError as e: - print(f"{FG_RED}✘{RESET} {BG_RED} Error {RESET} {e}") - print() - print(" Commit rejected.") - print() - - # Exit with error - return 1 - - return 0 - - -# ---------------------------------------------------------------------------- - -if __name__ == "__main__": - sys.exit(main()) diff --git a/scripts/prepare.py b/scripts/prepare.py index 7b716bc..6a63894 100755 --- a/scripts/prepare.py +++ b/scripts/prepare.py @@ -39,9 +39,11 @@ def main() -> int: # Clone UI repository into tmp directory repo_url = "https://github.com/zensical/ui.git" + repo_tag = "v0.0.1" dest_dir = os.path.join("tmp", "ui") if not os.path.exists(dest_dir): subprocess.run(["git", "clone", repo_url, dest_dir], check=True) + subprocess.run(["git", "checkout", repo_tag], cwd=dest_dir, check=True) # Determine base and dist directories base_dir = os.path.join("python", "zensical")