Skip to content

chore(release): post-§5 ruff fix + format + Naskh-tracked hash litera… #25

chore(release): post-§5 ruff fix + format + Naskh-tracked hash litera…

chore(release): post-§5 ruff fix + format + Naskh-tracked hash litera… #25

Workflow file for this run

name: Release
on:
push:
tags: ['v*']
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# Need full history (or at least the merge-base) for the
# ancestry check below.
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install build deps
run: python -m pip install --upgrade pip build
- name: Build sdist + wheel
run: python -m build
- name: Verify build
run: |
ls -lh dist/
# furqan-lint is pure Python; expect exactly one wheel
# (py3-none-any) and one sdist (.tar.gz).
test "$(ls dist/*.whl | wc -l)" -eq 1
test "$(ls dist/*.tar.gz | wc -l)" -eq 1
# Wheel must be py3-none-any (we are pure Python).
ls dist/*.whl | grep -q "py3-none-any.whl"
- name: Verify tag is an ancestor of origin/main
run: |
# Defends against tags pushed at local-only commits that
# never made it to main. Release artifacts must reflect
# what is actually on the published branch.
git fetch origin main --depth=50
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/main; then
echo "Tag commit $GITHUB_SHA is not an ancestor of origin/main"
exit 1
fi
echo "Tag commit is on origin/main: ok"
- name: Verify version-tag-CHANGELOG sync
run: |
TAG_VERSION="${GITHUB_REF#refs/tags/v}"
# Workflow uses Python 3.12; tomllib is stdlib since 3.11.
# No tomli shim needed.
PYPROJECT_VERSION=$(python -c "
import tomllib
with open('pyproject.toml', 'rb') as f:
print(tomllib.load(f)['project']['version'])
")
if [ "$TAG_VERSION" != "$PYPROJECT_VERSION" ]; then
echo "Tag version $TAG_VERSION != pyproject.toml version $PYPROJECT_VERSION"
exit 1
fi
if ! grep -q "^## \[${TAG_VERSION}\]" CHANGELOG.md; then
echo "CHANGELOG.md has no entry for [${TAG_VERSION}]"
exit 1
fi
echo "Tag/pyproject/CHANGELOG version sync: $TAG_VERSION"
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
publish:
needs: build
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write
contents: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
- name: Verify PyPI publication
# Phase G10.5 (al-Mubin) T01: closes F7 by verifying that
# the just-published version is reachable through PyPI's
# index. Twelve polls at ten-second intervals (120s total)
# per the S5 calibration. PyPI CDN propagation is typically
# 1-15 seconds; tail observations of 90-180 seconds exist;
# the 60-second window we considered earlier risks false-
# failure on a successful publish.
run: |
set -euo pipefail
VERSION="${GITHUB_REF#refs/tags/v}"
echo "Verifying furqan-lint==${VERSION} is installable from PyPI..."
for i in 1 2 3 4 5 6 7 8 9 10 11 12; do
if pip index versions furqan-lint 2>/dev/null | grep -q "Available versions:.*${VERSION}"; then
echo "furqan-lint ${VERSION} is now installable."
exit 0
fi
echo "Attempt ${i}/12: not yet visible, sleeping 10s..."
sleep 10
done
echo "ERROR: furqan-lint ${VERSION} did not become visible on PyPI within 120 seconds."
exit 1
- name: Create GitHub Release
# Phase G10.5 (al-Mubin) T02: closes F8 going forward by
# automatically creating a Release object with notes
# extracted from CHANGELOG.md whenever a v* tag is pushed.
# Notes are extracted by scripts/extract_changelog_section.py
# which slices between the version's ## [X.Y.Z] header and
# the next ## [ header. --verify-tag asserts the tag exists
# at the workflow's GITHUB_SHA, defending against re-runs
# that point at a different SHA.
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
VERSION="${GITHUB_REF#refs/tags/v}"
NOTES_FILE="$(mktemp)"
python scripts/extract_changelog_section.py "${VERSION}" > "${NOTES_FILE}"
if [ ! -s "${NOTES_FILE}" ]; then
echo "ERROR: No CHANGELOG section found for v${VERSION}."
exit 1
fi
gh release create "v${VERSION}" \
--title "v${VERSION}" \
--notes-file "${NOTES_FILE}" \
--verify-tag
- name: Sign and upload self-attestation manifest (al-Basirah T06)
# Phase G12.0 (al-Basirah / v1.0.0+) T03: generates a gate11
# self-manifest for the released furqan-lint version, signs it
# via Sigstore (ambient OIDC; id-token: write permission
# already granted to this job for the gh release step + PyPI
# Trusted Publishing), and uploads both the manifest JSON and
# the Sigstore bundle as GitHub Release assets attached to the
# release object created in the previous step.
#
# The published assets are discoverable via convention-based
# URL (per al-Basirah T04):
# https://github.com/BayyinahEnterprise/furqan-lint/releases/download/v${VERSION}/self_manifest.json
# https://github.com/BayyinahEnterprise/furqan-lint/releases/download/v${VERSION}/self_manifest.bundle
#
# The furqan-lint manifest verify-self subcommand (al-Basirah
# T05) derives the URLs from the installed package version,
# downloads, and verifies via the function-local
# _LANGUAGE_DISPATCH -> _verify_python path per
# al-Mursalat T04 + an-Naziat F-NA-3 substrate-actual.
#
# §5.1 step 4 failure mode #1 (OIDC token timing): if this
# step fails due to OIDC token expiration between the PyPI
# publish + gh release create steps and this signing step,
# the workflow exits non-zero. The wheel is already on PyPI
# (Trusted Publishing happens earlier); operator manual
# remediation is to re-run the workflow (idempotent on
# existing-version-on-PyPI; signing step retries with fresh
# OIDC token).
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
VERSION="${GITHUB_REF#refs/tags/v}"
# Install furqan-lint from the just-published wheel so the
# self-manifest substrate matches what PyPI shipped (NOT the
# source checkout). gate11 + sigstore extras for signing.
pip install --force-reinstall "furqan-lint[gate11]==${VERSION}"
# Generate the self-manifest:
python -m furqan_lint.gate11.self_manifest \
--version "${VERSION}" \
--output self_manifest.json
# Sign via Sigstore (ambient OIDC):
pip install sigstore
python -m sigstore sign \
--bundle self_manifest.bundle \
self_manifest.json
# Upload both as Release assets:
gh release upload "v${VERSION}" \
self_manifest.json self_manifest.bundle \
--clobber
echo "Self-manifest signed + uploaded as Release assets for v${VERSION}"