Skip to content

Bump to 1.0.12 and drop the README PyPI version badge. #31

Bump to 1.0.12 and drop the README PyPI version badge.

Bump to 1.0.12 and drop the README PyPI version badge. #31

Workflow file for this run

name: CI
on:
pull_request:
branches:
- main
push:
branches:
- main
workflow_dispatch:
inputs:
force:
description: Overwrite an existing GitHub release tag vX.Y.Z
type: boolean
default: false
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
permissions:
contents: read
jobs:
quality:
name: Static analysis and formatting
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: "3.14"
cache: pip
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
- name: Run quality checks
run: |
python scripts/generate_spec_data.py --check
python -m black --check src tests scripts
python -m isort --check-only src tests scripts
python -m autopep8 -r --diff --exit-code --select=W291,W293 src tests scripts
python -m mypy src/battest tests scripts
python -m pylint src/battest scripts
python -m pyproject_fmt --check pyproject.toml
python -m bandit -r src/battest -c pyproject.toml -ll -q
python -m blinter scripts --no-recursive --config scripts/blinter.ini
python -m blinter examples
python -m pip_audit
test-unit:
name: pytest unit (${{ matrix.os }}, Python ${{ matrix.python-version }})
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
python-version: ["3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
cache: pip
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
- name: Copy spec catalogs
run: python scripts/generate_spec_data.py
- name: Run unit tests
run: python -m pytest -m "not windows" --cov-fail-under=0
test-windows:
name: pytest windows (${{ matrix.python-version }})
runs-on: windows-latest
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
python-version: ["3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
persist-credentials: false
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: "1.97.1"
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}
cache: pip
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e ".[dev]"
- name: Copy spec catalogs
run: python scripts/generate_spec_data.py
- name: Run example suites with committed stub
run: python -m battest run examples --safe-defaults
- name: Rebuild PATH-mock stub
run: python scripts/build_stub.py
- name: Run full tests
run: python -m pytest
- name: Check line, branch, function, and class coverage
run: python scripts/check_coverage.py
- name: Run example suites with rebuilt stub
run: python -m battest run examples --safe-defaults
action-windows:
name: Composite action dogfood
runs-on: windows-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
persist-credentials: false
- id: battest
uses: ./
with:
path: examples
extra-args: '["--jobs","1"]'
safe-defaults: "true"
- name: Assert JUnit report exists
shell: pwsh
env:
BATTEST_JUNIT: ${{ steps.battest.outputs.junit-xml }}
run: |
if (-not (Test-Path -LiteralPath $env:BATTEST_JUNIT)) {
throw "junit-xml output is missing: $env:BATTEST_JUNIT"
}
if ((Get-Item -LiteralPath $env:BATTEST_JUNIT).Length -lt 1) {
throw "junit-xml is empty: $env:BATTEST_JUNIT"
}
- name: Dogfood missing discovery path
id: battest-fail
shell: pwsh
env:
BATTEST_PATH: missing-battest-fixtures
BATTEST_EXTRA_ARGS: '["--jobs","1"]'
BATTEST_SAFE_DEFAULTS: "true"
run: |
# Dot-source so a non-zero battest exit is a return value, not
# a step failure / ##[error] annotation.
$PSNativeCommandUseErrorActionPreference = $false
. "$env:GITHUB_WORKSPACE/scripts/run-battest-action.ps1"
$code = Invoke-BattestAction
if ($code -eq 0) {
throw "expected battest to fail when the discovery path is missing"
}
# pwsh -Command exits with the last native command's code otherwise.
exit 0
- name: Assert JUnit written on discovery failure
shell: pwsh
env:
BATTEST_JUNIT: ${{ steps.battest-fail.outputs.junit-xml }}
run: |
if (-not (Test-Path -LiteralPath $env:BATTEST_JUNIT)) {
throw "junit-xml output is missing: $env:BATTEST_JUNIT"
}
if ((Get-Item -LiteralPath $env:BATTEST_JUNIT).Length -lt 1) {
throw "junit-xml is empty: $env:BATTEST_JUNIT"
}
$text = Get-Content -LiteralPath $env:BATTEST_JUNIT -Raw
if ($text -notmatch 'does not exist') {
throw "junit-xml does not record the missing discovery path"
}
- name: Analyze PowerShell action script
shell: pwsh
run: |
& ./scripts/check_action_ps1.ps1
codeql:
name: CodeQL (${{ matrix.language }})
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
actions: read
contents: read
packages: read
security-events: write
strategy:
fail-fast: false
matrix:
include:
- language: python
build-mode: none
- language: actions
build-mode: none
- language: rust
build-mode: none
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
persist-credentials: false
fetch-depth: 2
- name: Set up Rust
if: matrix.language == 'rust'
uses: dtolnay/rust-toolchain@stable
with:
toolchain: "1.97.1"
- name: Fetch Rust crate graph
if: matrix.language == 'rust'
run: cargo fetch --locked --manifest-path stub/Cargo.toml
- name: Initialize CodeQL
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@v4
with:
category: "/language:${{ matrix.language }}"
rust-quality:
name: Rust format, check, clippy, coverage, audit
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: "1.97.1"
components: rustfmt, clippy, llvm-tools-preview
- uses: Swatinem/rust-cache@v2
with:
workspaces: stub
- uses: taiki-e/install-action@v2
with:
tool: cargo-llvm-cov,cargo-audit
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: "3.14"
- name: Install coverage gate dependencies
run: python -m pip install pydantic
- name: rustfmt
run: cargo fmt --all --check --manifest-path stub/Cargo.toml
- name: cargo check
run: cargo check --manifest-path stub/Cargo.toml --all-targets --locked
- name: clippy
run: >
cargo clippy --manifest-path stub/Cargo.toml --all-targets --locked
-- -D warnings
- name: coverage
run: python scripts/check_rust_coverage.py
- name: cargo audit
run: cargo audit --file stub/Cargo.lock
rust-test:
name: cargo test (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: "1.97.1"
- uses: Swatinem/rust-cache@v2
with:
workspaces: stub
- name: cargo check
run: cargo check --manifest-path stub/Cargo.toml --all-targets --locked
- name: cargo test
run: cargo test --manifest-path stub/Cargo.toml --locked --all-targets
test-windows-exe:
name: Windows executable smoke tests
runs-on: windows-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v7
with:
submodules: recursive
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: "3.14"
cache: pip
- name: Create virtual environment
run: python -m venv venv
- name: Install dependencies in virtual environment
run: |
.\venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install --upgrade setuptools
python -m pip install --upgrade wheel
python -m pip install --upgrade pyinstaller
python -m pip install --upgrade -r requirements.txt
shell: pwsh
- name: Generate version info file
run: |
.\venv\Scripts\Activate.ps1
python scripts/generate_file_version_info.py
shell: pwsh
- name: Build executable
run: |
.\venv\Scripts\Activate.ps1
pyinstaller battest.spec
shell: pwsh
- name: Verify executable exists
run: |
if (-not (Test-Path -Path "dist\battest.exe" -PathType Leaf)) {
throw "dist\battest.exe was not produced by PyInstaller"
}
shell: pwsh
- name: Install battest for parity checks
run: |
.\venv\Scripts\Activate.ps1
python -m pip install -e .
shell: pwsh
- name: Smoke test executable
run: .\scripts\test_exe_smoke.ps1 -ExePath .\dist\battest.exe
shell: pwsh
package-smoke:
name: sdist and wheel
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: "3.14"
cache: pip
- name: Install build tools
run: |
python -m pip install --upgrade pip
python -m pip install --upgrade build twine
- name: Build sdist and wheel
run: python -m build
- name: Check dist
run: python -m twine check dist/*
- name: Install wheel and import battest
run: |
python -m pip install dist/*.whl
python -c "
import importlib.metadata
from importlib.resources import files
import battest
from battest.mocks import stub_executable
assert battest.__version__
scripts = importlib.metadata.entry_points(group='console_scripts')
assert any(item.name == 'battest' for item in scripts)
assert files('battest').joinpath('data/battest_stub.exe').is_file()
assert files('battest').joinpath('data/commands.yaml').is_file()
assert files('battest').joinpath('data/expansion.yaml').is_file()
assert files('battest').joinpath('data/battest-expect.schema.json').is_file()
assert stub_executable().is_file()
"
dependency-graph:
name: Dependency Graph
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Submit dependency snapshot
uses: advanced-security/component-detection-dependency-submission-action@v0.1.4
with:
detectorArgs: Pip=EnableIfDefaultOff
check-version:
name: Check pyproject.toml version bump
needs:
- quality
- test-unit
- test-windows
- action-windows
- codeql
- rust-quality
- rust-test
- test-windows-exe
- package-smoke
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
runs-on: ubuntu-latest
timeout-minutes: 10
concurrency:
group: battest-release
cancel-in-progress: false
outputs:
should_release: ${{ steps.check.outputs.should_release }}
should_publish_pypi: ${{ steps.check.outputs.should_publish_pypi }}
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 2
fetch-tags: true
persist-credentials: false
- name: Read version from pyproject.toml
id: version
run: |
VERSION=$(python -c "import pathlib, tomllib; print(tomllib.loads(pathlib.Path('pyproject.toml').read_text(encoding='utf-8'))['project']['version'])")
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Project version: $VERSION"
- name: Decide whether to release
id: check
run: |
NEW_VERSION="${{ steps.version.outputs.version }}"
FORCE="${{ github.event.inputs.force }}"
TAG_EXISTS=false
if git rev-parse --verify --quiet "refs/tags/v${NEW_VERSION}"; then
TAG_EXISTS=true
fi
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
if [ "$TAG_EXISTS" = true ] && [ "$FORCE" != "true" ]; then
echo "Manual dispatch without force; tag v${NEW_VERSION} exists."
echo "Skipping GitHub release rewrite and PyPI."
echo "should_release=false" >> "$GITHUB_OUTPUT"
echo "should_publish_pypi=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Manual workflow_dispatch; releasing version ${NEW_VERSION}."
echo "should_release=true" >> "$GITHUB_OUTPUT"
echo "should_publish_pypi=true" >> "$GITHUB_OUTPUT"
exit 0
fi
OLD_VERSION=$(python scripts/read_git_pyproject_version.py HEAD^)
if [ "$OLD_VERSION" != "$NEW_VERSION" ]; then
echo "Version bumped: $OLD_VERSION -> $NEW_VERSION"
echo "should_release=true" >> "$GITHUB_OUTPUT"
echo "should_publish_pypi=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ "$TAG_EXISTS" = true ]; then
echo "Version unchanged ($NEW_VERSION) and tag v${NEW_VERSION} exists."
echo "Skipping GitHub assets and PyPI."
echo "should_release=false" >> "$GITHUB_OUTPUT"
echo "should_publish_pypi=false" >> "$GITHUB_OUTPUT"
else
echo "Version unchanged ($NEW_VERSION) but tag v${NEW_VERSION} is missing; releasing."
echo "should_release=true" >> "$GITHUB_OUTPUT"
echo "should_publish_pypi=true" >> "$GITHUB_OUTPUT"
fi
build-windows:
needs: [check-version]
if: needs.check-version.outputs.should_release == 'true'
runs-on: windows-latest
timeout-minutes: 45
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Read version from pyproject.toml
id: version
shell: pwsh
run: |
$match = Select-String -Path "pyproject.toml" -Pattern '^version = "(.+)"'
if (-not $match) { throw "Could not read version from pyproject.toml" }
$version = $match.Matches[0].Groups[1].Value
"version=$version" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: "3.14"
cache: pip
- name: Create virtual environment
run: python -m venv venv
- name: Install dependencies in virtual environment
run: |
.\venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
python -m pip install --upgrade setuptools
python -m pip install --upgrade wheel
python -m pip install --upgrade pyinstaller
python -m pip install --upgrade -r requirements.txt
shell: pwsh
- name: Generate version info file
run: |
.\venv\Scripts\Activate.ps1
python scripts/generate_file_version_info.py
shell: pwsh
- name: Build executable
run: |
.\venv\Scripts\Activate.ps1
pyinstaller battest.spec
shell: pwsh
- name: Install battest for parity checks
run: |
.\venv\Scripts\Activate.ps1
python -m pip install -e .
shell: pwsh
- name: Smoke test executable
run: .\scripts\test_exe_smoke.ps1 -ExePath .\dist\battest.exe
shell: pwsh
- name: Package executable for release
run: |
$version = "${{ steps.version.outputs.version }}"
$folder = "dist/Battest-v$version"
New-Item -ItemType Directory -Path $folder -Force
Move-Item -Path dist/battest.exe -Destination "$folder/battest.exe"
Compress-Archive -Path $folder -DestinationPath "dist/Battest-v$version.zip"
shell: pwsh
- name: Upload artifact
uses: actions/upload-artifact@v7
with:
name: windows-build
path: dist/Battest-v${{ steps.version.outputs.version }}.zip
build-wheels:
name: Build Python wheels
needs: [check-version]
if: needs.check-version.outputs.should_publish_pypi == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: "3.14"
cache: pip
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install --upgrade build
- name: Build pure Python wheel and sdist
run: python -m build
- name: Store artifacts
uses: actions/upload-artifact@v7
with:
name: python-dist
path: dist/
create-release:
needs: [check-version, build-windows, build-wheels]
if: needs.check-version.outputs.should_release == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: write
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- name: Download Windows build
uses: actions/download-artifact@v8
with:
name: windows-build
path: ./build
- name: Extract release notes
id: release_notes
run: |
NOTES_FILE="${RUNNER_TEMP}/release-notes.md"
python scripts/extract_release_notes.py > "$NOTES_FILE"
echo "path=$NOTES_FILE" >> "$GITHUB_OUTPUT"
- name: Create GitHub Release
uses: ncipollo/release-action@v1
with:
artifacts: ./build/Battest-v${{ needs.check-version.outputs.version }}.zip
tag: v${{ needs.check-version.outputs.version }}
bodyFile: ${{ steps.release_notes.outputs.path }}
token: ${{ secrets.GITHUB_TOKEN }}
allowUpdates: ${{ github.event.inputs.force == 'true' }}
removeArtifacts: ${{ github.event.inputs.force == 'true' }}
- name: Move major version tag
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.check-version.outputs.version }}
run: |
set -euo pipefail
if ! printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+([.+-].*)?$'; then
echo "unexpected version: $VERSION" >&2
exit 1
fi
MAJOR="${VERSION%%.*}"
SHA="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/v${VERSION}" --jq '.object.sha')"
TYPE="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/v${VERSION}" --jq '.object.type')"
if [ "$TYPE" = "tag" ]; then
SHA="$(gh api "repos/${GITHUB_REPOSITORY}/git/tags/${SHA}" --jq '.object.sha')"
fi
echo "Pointing v${MAJOR} at v${VERSION} (${SHA})"
if gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/v${MAJOR}" >/dev/null 2>&1; then
gh api --method PATCH "repos/${GITHUB_REPOSITORY}/git/refs/tags/v${MAJOR}" \
-f sha="$SHA" -F force=true
else
gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" \
-f ref="refs/tags/v${MAJOR}" -f sha="$SHA"
fi
publish-pypi:
name: Publish wheels to PyPI
needs: [check-version, build-wheels, create-release]
if: needs.create-release.result == 'success'
environment: pypi
permissions:
id-token: write
contents: read
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Download artifacts
uses: actions/download-artifact@v8
with:
name: python-dist
path: dist
- name: Set up Python
uses: actions/setup-python@v7
with:
python-version: "3.14"
- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_BATTEST }}
run: |
python -m pip install --upgrade twine
python -m twine check dist/*
python -m twine upload dist/*