chore(release): post-§5 ruff fix + format + Naskh-tracked hash litera… #25
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}" |