Loading files
This 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
97c6f2bbeThis file is pinned to the selected release. Download the original to open it in your design tool.