Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
2126c3284This file is pinned to the selected release. Download the original to open it in your design tool.
# Release pipeline — builds the package, publishes a GitHub Release with the
# sdist + wheel attached, and (only if configured) publishes to PyPI via
# trusted publishing. See docs/releasing.md for the operator runbook.
#
# PyPI publishing (job `publish-pypi`) is gated on the **repository** variable
# `PYPI_TRUSTED_PUBLISHING` being set to "true" (Settings > Secrets and
# variables > Actions > Variables). It must NOT live on the `pypi` Environment:
# a job's `if:` is evaluated before its environment resolves, so an
# environment-scoped variable is invisible there and the job would be silently
# skipped. The `pypi` Environment must still exist for OIDC to bind to.
# Repos that haven't set that up are skipped gracefully — the release job's
# success never depends on PyPI being configured.
#
# The `ci-green` job below refuses to build/publish a tag whose commit has not
# gone fully green on CI (the historical release-breaker: tags pushed before
# windows-latest had ever run the commit). Its logic lives in
# .github/scripts/ci_green_gate.sh — it asks the CI *workflow-runs* API for the
# tagged SHA, so "CI never ran this commit" and "CI ran and went red" are two
# distinct, both-fatal states. Escape hatch: run this workflow via
# workflow_dispatch with `force: true`.
name: Release
on:
push:
tags:
- "v*.*.*"
workflow_dispatch:
inputs:
force:
description: "Skip the CI-green gate on the tagged commit (emergency use)"
type: boolean
default: false
permissions:
contents: write # create the GitHub Release + upload assets
actions: read # read CI workflow-run + job conclusions for the tagged commit
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
# ---------------------------------------------------------------------------
# Gate: the CI workflow must have RUN on the tagged commit and concluded
# success, with the windows-latest matrix legs individually asserted. A tag
# is public the moment it is pushed, so this is the last place the matrix can
# still stop a bad release. Logic + rationale: .github/scripts/ci_green_gate.sh
# (unit-tested by tests/test_release_gate.py).
# ---------------------------------------------------------------------------
ci-green:
name: CI green on tagged commit
runs-on: ubuntu-latest
steps:
- name: Forced bypass notice
if: ${{ inputs.force }}
run: echo "::warning::CI-green gate bypassed via workflow_dispatch force=true."
- uses: actions/checkout@v7
if: ${{ !inputs.force }}
- name: Verify the CI workflow ran and went green on ${{ github.sha }}
if: ${{ !inputs.force }}
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
SHA: ${{ github.sha }}
run: bash .github/scripts/ci_green_gate.sh
release:
name: build + GitHub Release
needs: ci-green
runs-on: ubuntu-latest
outputs:
version: ${{ steps.ver.outputs.version }}
steps:
- name: Guard — must run on a tag
if: ${{ github.ref_type != 'tag' }}
run: |
echo "::error::This workflow must be triggered by a v* tag (ref_type=${{ github.ref_type }})."
exit 1
- uses: actions/checkout@v7
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Derive + verify version (tag must match pyproject)
id: ver
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
ver="${tag#v}"
py="$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")"
echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "version=$ver" >> "$GITHUB_OUTPUT"
if [ "$ver" != "$py" ]; then
echo "::error::Tag $tag (version $ver) != pyproject version $py. Bump pyproject + manifests, then re-tag."
exit 1
fi
if printf '%s' "$tag" | grep -q '-'; then
echo "prerelease=true" >> "$GITHUB_OUTPUT"
else
echo "prerelease=false" >> "$GITHUB_OUTPUT"
fi
- name: Manifest version-sync check
if: ${{ hashFiles('tools/sync_version.py') != '' }}
run: python tools/sync_version.py --check
- name: Build sdist + wheel
run: |
python -m pip install --upgrade build twine
python -m build
twine check dist/*
# --strict: no silent fallback to [Unreleased]. Forgetting to move the
# section would otherwise ship "the next version's notes" as this
# release's body, with exit 0 and nothing in the log.
- name: Extract release notes from CHANGELOG
run: python tools/changelog_extract.py --strict "${{ steps.ver.outputs.version }}" > release-notes.md
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.ver.outputs.tag }}
name: ${{ steps.ver.outputs.tag }}
body_path: release-notes.md
prerelease: ${{ steps.ver.outputs.prerelease }}
fail_on_unmatched_files: true
files: |
dist/*.whl
dist/*.tar.gz
- uses: actions/upload-artifact@v7
with:
name: dist
path: dist/
retention-days: 7
# `publish-pypi` skipping is a silent no-op in the run summary (a skipped
# job looks the same as a job that was never meant to run), so say it out
# loud on the job that always runs.
- name: Note when PyPI publishing is disabled
if: ${{ vars.PYPI_TRUSTED_PUBLISHING != 'true' }}
run: |
echo "::warning::PyPI publishing disabled (repository variable PYPI_TRUSTED_PUBLISHING is not 'true') — this release ships to GitHub Releases only. See docs/releasing.md."
# ---------------------------------------------------------------------------
# PyPI publish via trusted publishing (OIDC, no long-lived token). Skipped
# gracefully when the repo hasn't opted in — set the **repository** variable
# `PYPI_TRUSTED_PUBLISHING` to "true" (Settings > Secrets and variables >
# Actions > Variables) once PyPI trusted publishing is configured. Do not put
# it on the `pypi` Environment: the `if:` below is evaluated before the job's
# environment resolves, so an environment variable there is never seen and
# the job is silently skipped. The `pypi` Environment itself must still exist
# for the OIDC trusted-publishing binding.
# ---------------------------------------------------------------------------
publish-pypi:
name: publish to PyPI
needs: release
if: ${{ vars.PYPI_TRUSTED_PUBLISHING == 'true' }}
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/akcli-kicad/${{ needs.release.outputs.version }}/
permissions:
id-token: write # OIDC token for trusted publishing; no API token/secret needed
steps:
- uses: actions/download-artifact@v8
with:
name: dist
path: dist/
# Pinned to a commit SHA, not the mutable `release/v1` branch ref: this
# job holds `id-token: write`, so a compromised/retargeted branch would
# hand an attacker the PyPI trusted-publishing identity. Bump it by hand
# (rewrite both the SHA and the trailing version comment together).
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1
with:
packages-dir: dist/
skip-existing: true