Publish to PyPI #33
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: Publish to PyPI | |
| on: | |
| push: | |
| tags: | |
| - "v*.*.*" | |
| workflow_dispatch: | |
| inputs: | |
| release_tag: | |
| description: "Existing tag to repair as a GitHub Release" | |
| required: false | |
| type: string | |
| permissions: | |
| contents: read | |
| jobs: | |
| build: | |
| name: Build distributions | |
| runs-on: ubuntu-latest | |
| if: >- | |
| github.event_name == 'push' || | |
| inputs.release_tag == '' | |
| env: | |
| PIP_CONSTRAINT: ${{ github.workspace }}/.github/release-constraints.txt | |
| PIP_BUILD_CONSTRAINT: ${{ github.workspace }}/.github/release-constraints.txt | |
| steps: | |
| - name: Check out source | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Python | |
| uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.11" | |
| - name: Install release gate and the production dependency set (without SQLCipher) | |
| run: >- | |
| python -m pip install --upgrade | |
| pip setuptools wheel build twine pip-audit ".[all,test]" | |
| - name: Require tag and package version to match | |
| if: github.event_name == 'push' | |
| shell: bash | |
| run: | | |
| expected="${GITHUB_REF_NAME#v}" | |
| actual="$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml", "rb"))["project"]["version"])')" | |
| test "$GITHUB_REF_NAME" = "v$actual" | |
| test "$expected" = "$actual" | |
| - name: Require release tag commit to be on protected main | |
| if: github.event_name == 'push' | |
| shell: bash | |
| run: | | |
| git fetch --no-tags origin main:refs/remotes/origin/main | |
| git merge-base --is-ancestor "$GITHUB_SHA" origin/main | |
| - name: Full release gate | |
| run: | | |
| python scripts/check_commercial_manifest.py | |
| python scripts/externalize_dashboard_assets.py | |
| ruff check . | |
| pyright | |
| python -c "import fastapi, httpx, mcp, multipart, pydantic, uvicorn" | |
| ENGRAPHIS_INDEX_ROOTS="${GITHUB_WORKSPACE}:${RUNNER_TEMP}" python -m pytest -o addopts="" tests/ -q -rs --basetemp="${RUNNER_TEMP}/engraphis-pytest" | |
| ENGRAPHIS_INDEX_ROOTS="${GITHUB_WORKSPACE}:${RUNNER_TEMP}" python -m pytest -o addopts="" tests/test_public_research_boundary.py -q --basetemp="${RUNNER_TEMP}/engraphis-pytest" | |
| ENGRAPHIS_INDEX_ROOTS="${GITHUB_WORKSPACE}:${RUNNER_TEMP}" python -m pytest -o addopts="" tests/test_compact_recall.py tests/test_eval_performance.py -q --basetemp="${RUNNER_TEMP}/engraphis-pytest" | |
| ENGRAPHIS_INDEX_ROOTS="${GITHUB_WORKSPACE}:${RUNNER_TEMP}" python -m pytest -o addopts="" tests/test_eval_harness.py tests/test_benchmark_evidence.py -q --basetemp="${RUNNER_TEMP}/engraphis-pytest" | |
| python -m eval.harness --dataset eval/datasets/sample.jsonl --k 5 | |
| python -m eval.harness --dataset eval/datasets/codemem.jsonl --k 5 | |
| python -m eval.ablation | |
| python -m eval.reinforcement | |
| python -m eval.adversarial_memory_security | |
| python -m pip_audit --local --skip-editable | |
| - name: Build source and universal wheel distributions | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| export SOURCE_DATE_EPOCH="$(git show -s --format=%ct "$GITHUB_SHA")" | |
| python -m build --outdir dist | |
| python scripts/normalize_sdist.py dist/*.tar.gz | |
| python -m build --outdir dist-repeat | |
| python scripts/normalize_sdist.py dist-repeat/*.tar.gz | |
| diff <(cd dist && sha256sum * | sort) <(cd dist-repeat && sha256sum * | sort) | |
| python scripts/verify_distribution_contents.py dist/* | |
| - name: Validate distributions | |
| run: python -m twine check dist/* | |
| - name: Smoke installed wheel and source distribution | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| dist_dir="$PWD/dist" | |
| index=0 | |
| for artifact in "$dist_dir"/*.whl "$dist_dir"/*.tar.gz; do | |
| index=$((index + 1)) | |
| venv="$RUNNER_TEMP/engraphis-artifact-smoke-$index" | |
| python -m venv --system-site-packages "$venv" | |
| "$venv/bin/python" -m pip install --no-deps "$artifact" | |
| ( | |
| cd "$RUNNER_TEMP" | |
| "$venv/bin/python" - <<'PY' | |
| import pathlib | |
| import sys | |
| import engraphis | |
| from engraphis.core.engine import MemoryEngine | |
| package = pathlib.Path(engraphis.__file__).resolve() | |
| assert pathlib.Path(sys.prefix).resolve() in package.parents, package | |
| engine = MemoryEngine.create(":memory:") | |
| workspace_id = engine.store.get_or_create_workspace("artifact-smoke") | |
| memory_id = engine.remember( | |
| "The artifact smoke marker is indigo.", | |
| workspace_id=workspace_id, | |
| resolve_conflicts=False, | |
| ) | |
| result = engine.recall("artifact smoke marker", workspace_id=workspace_id, k=3) | |
| assert any(chunk["id"] == memory_id for chunk in result.chunks) | |
| engine.store.close() | |
| PY | |
| "$venv/bin/python" -m scripts.smoke_entry_points --timeout 20 | |
| ) | |
| done | |
| - name: Store distributions | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: python-package-distributions | |
| path: dist/ | |
| python-matrix: | |
| name: Python ${{ matrix.python-version }} release gate | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ["3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] | |
| if: >- | |
| github.event_name == 'push' || | |
| inputs.release_tag == '' | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Install version-appropriate gate | |
| shell: bash | |
| run: | | |
| python -m pip install --upgrade pip | |
| if [ "${{ matrix.python-version }}" = "3.9" ]; then | |
| python -m pip install numpy "pytest<9" ruff | |
| else | |
| python -m pip install -e ".[test]" | |
| fi | |
| - name: Unit, lint, and retrieval gates | |
| run: | | |
| ruff check . | |
| if [ "${{ matrix.python-version }}" != "3.9" ]; then | |
| python -c "import fastapi, httpx, mcp, multipart, pydantic, uvicorn" | |
| fi | |
| ENGRAPHIS_INDEX_ROOTS="${GITHUB_WORKSPACE}:${RUNNER_TEMP}" python -m pytest -o addopts="" tests/ -q -rs --basetemp="${RUNNER_TEMP}/engraphis-pytest" | |
| python -m eval.harness --dataset eval/datasets/sample.jsonl --k 5 | |
| python -m eval.harness --dataset eval/datasets/codemem.jsonl --k 5 | |
| python -m eval.ablation | |
| python -m eval.reinforcement | |
| python -m eval.adversarial_memory_security | |
| artifact-core-py39: | |
| name: Python 3.9 installed release artifacts | |
| needs: build | |
| if: >- | |
| github.event_name == 'push' || | |
| inputs.release_tag == '' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.9" | |
| - name: Download exact release distributions | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: python-package-distributions | |
| path: dist/ | |
| - name: Install, verify, and smoke wheel and source distribution | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| index=0 | |
| for artifact in dist/*.whl dist/*.tar.gz; do | |
| index=$((index + 1)) | |
| venv="$RUNNER_TEMP/engraphis-release-py39-artifact-$index" | |
| python -m venv "$venv" | |
| "$venv/bin/python" -m pip install --disable-pip-version-check "$artifact" | |
| "$venv/bin/python" -m pip check | |
| ( | |
| cd "$RUNNER_TEMP" | |
| "$venv/bin/python" - <<'PY' | |
| import pathlib | |
| import sys | |
| import engraphis | |
| from engraphis.core.engine import MemoryEngine | |
| package = pathlib.Path(engraphis.__file__).resolve() | |
| assert pathlib.Path(sys.prefix).resolve() in package.parents, package | |
| engine = MemoryEngine.create(":memory:") | |
| workspace_id = engine.store.get_or_create_workspace("release-py39-artifact") | |
| memory_id = engine.remember( | |
| "The release Python 3.9 artifact marker is indigo.", | |
| workspace_id=workspace_id, | |
| resolve_conflicts=False, | |
| ) | |
| result = engine.recall("release Python 3.9 artifact marker", workspace_id=workspace_id, k=3) | |
| assert any(chunk["id"] == memory_id for chunk in result.chunks) | |
| engine.store.close() | |
| PY | |
| "$venv/bin/engraphis" --help | |
| "$venv/bin/engraphis" --version | |
| "$venv/bin/engraphis-cli" --help | |
| ) | |
| done | |
| encryption: | |
| name: Encryption driver release gate (Python ${{ matrix.python-version }}) | |
| runs-on: ubuntu-latest | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] | |
| if: >- | |
| github.event_name == 'push' || | |
| inputs.release_tag == '' | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Install encryption integration gate | |
| run: | | |
| python -m pip install --upgrade pip | |
| pip install -e ".[test,encryption]" | |
| - name: Encryption at-rest integration tests | |
| run: | | |
| python -c "import sqlcipher3; print(sqlcipher3.__file__)" | |
| ENGRAPHIS_INDEX_ROOTS="${GITHUB_WORKSPACE}:${RUNNER_TEMP}" python -m pytest -o addopts="" tests/test_encrypted_store.py -q -rs --basetemp="${RUNNER_TEMP}/engraphis-pytest" | |
| browser-accessibility: | |
| name: Browser accessibility release gate | |
| runs-on: ubuntu-latest | |
| if: >- | |
| github.event_name == 'push' || | |
| inputs.release_tag == '' | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.11" | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: "24" | |
| - name: Install browser gate | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install -e ".[test]" "uvicorn[standard]>=0.29" | |
| npm ci | |
| npx playwright install --with-deps chromium | |
| - name: Playwright desktop/mobile, keyboard, CSP, console, and axe checks | |
| run: npm run test:e2e | |
| pi-extension: | |
| name: Pi extension release gate | |
| runs-on: ubuntu-latest | |
| if: >- | |
| github.event_name == 'push' || | |
| inputs.release_tag == '' | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.11" | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: "24" | |
| cache: npm | |
| cache-dependency-path: integrations/pi/npm-shrinkwrap.json | |
| - name: Install the tagged Smart MCP server | |
| run: | | |
| python -m pip install --upgrade pip | |
| python -m pip install -e ".[test]" | |
| - name: Verify the publishable Pi package and live bridge | |
| working-directory: integrations/pi | |
| env: | |
| ENGRAPHIS_PI_TEST_COMMAND: engraphis-mcp | |
| run: | | |
| npm ci --ignore-scripts | |
| npm run verify | |
| npm run test:integration | |
| npm audit --omit=dev | |
| docker-smoke: | |
| name: Production image release gate | |
| runs-on: ubuntu-latest | |
| if: >- | |
| github.event_name == 'push' || | |
| inputs.release_tag == '' | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - name: Validate Compose configuration | |
| run: docker compose config --quiet | |
| - name: Reject unauthenticated LAN Compose overlay | |
| run: | | |
| if env -u ENGRAPHIS_API_TOKEN docker compose -f docker-compose.yml -f docker-compose.lan.yml config --quiet; then | |
| echo "LAN overlay must require ENGRAPHIS_API_TOKEN" | |
| exit 1 | |
| fi | |
| - name: Validate token-protected LAN Compose overlay | |
| env: | |
| ENGRAPHIS_API_TOKEN: ci-lan-overlay-token | |
| run: docker compose -f docker-compose.yml -f docker-compose.lan.yml config --quiet | |
| - name: Build production image | |
| run: docker build -t engraphis:release . | |
| - name: Verify production image OCR runtime | |
| run: >- | |
| docker run --rm --entrypoint sh engraphis:release -c | |
| 'python -c "import PIL, pytesseract" && command -v tesseract >/dev/null && | |
| tesseract --version | head -n 1' | |
| - name: Audit production image dependencies | |
| # The runtime image deliberately has no pip. Audit its exact installed | |
| # distributions from the runner instead of reintroducing a build tool to the | |
| # production image only for this check. | |
| shell: bash | |
| run: | | |
| audit_dir="$(mktemp -d)" | |
| container="engraphis-release-audit-${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}" | |
| cleanup() { | |
| docker rm -f "$container" >/dev/null 2>&1 || true | |
| rm -rf "$audit_dir" | |
| } | |
| trap cleanup EXIT | |
| python -m pip install --disable-pip-version-check --no-cache-dir pip-audit | |
| docker create --name "$container" engraphis:release >/dev/null | |
| docker cp "$container":/usr/local/lib/python3.11/site-packages/. "$audit_dir" | |
| python -m pip_audit --path "$audit_dir" | |
| - name: Run customer-mode readiness smoke | |
| shell: bash | |
| run: | | |
| docker run -d --name engraphis-release -p 8700:8700 \ | |
| -e ENGRAPHIS_EMBED_MODEL= \ | |
| -e ENGRAPHIS_LOOP_INTERVAL=0 \ | |
| -e ENGRAPHIS_HOST=0.0.0.0 \ | |
| engraphis:release | |
| for i in $(seq 1 60); do | |
| if curl -fsS http://127.0.0.1:8700/api/ready; then | |
| exit 0 | |
| fi | |
| sleep 1 | |
| done | |
| docker logs engraphis-release | |
| exit 1 | |
| - name: Teardown | |
| if: always() | |
| run: docker rm -f engraphis-release || true | |
| code-security: | |
| name: CodeQL ${{ matrix.language }} release gate | |
| if: >- | |
| github.event_name == 'push' || | |
| inputs.release_tag == '' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| env: | |
| CODEQL_ACTION_DIFF_INFORMED_QUERIES: "false" | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| language: ["python", "javascript-typescript"] | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.11" | |
| - name: Initialize CodeQL | |
| uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4 | |
| with: | |
| languages: ${{ matrix.language }} | |
| build-mode: none | |
| - name: Analyze complete source tree | |
| id: analyze | |
| uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4 | |
| with: | |
| output: codeql-results | |
| upload: never | |
| - name: Require clean CodeQL results | |
| run: python scripts/check_codeql_sarif.py "${{ steps.analyze.outputs.sarif-output }}" | |
| release-evidence: | |
| name: Generate public release evidence | |
| needs: [build, python-matrix, artifact-core-py39, encryption, browser-accessibility, pi-extension, docker-smoke, code-security] | |
| if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.11" | |
| - name: Install SBOM generator and project dependencies | |
| run: >- | |
| python -m pip install --upgrade "pip>=26.1.2" "setuptools>=83" | |
| cyclonedx-bom==7.3.0 ".[all,test]" | |
| - name: Download distributions | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: python-package-distributions | |
| path: dist/ | |
| - name: Generate evidence and reproducible SBOM after all release gates | |
| shell: bash | |
| run: | | |
| mkdir release-evidence | |
| sbom="release-evidence/engraphis-${GITHUB_REF_NAME#v}.cdx.json" | |
| cyclonedx-py environment --output-reproducible --of JSON --pyproject pyproject.toml -o "$sbom" | |
| python scripts/release_evidence.py --dist dist --commit "$GITHUB_SHA" \ | |
| --tag "$GITHUB_REF_NAME" \ | |
| --sbom "$sbom" \ | |
| --verified-check ruff \ | |
| --verified-check pyright-core-backends \ | |
| --verified-check codeql \ | |
| --verified-check pytest \ | |
| --verified-check reproducible-distributions \ | |
| --verified-check installed-artifact-smoke \ | |
| --verified-check installed-artifact-smoke-py39 \ | |
| --verified-check privacy-boundary \ | |
| --verified-check token-efficiency \ | |
| --verified-check benchmark-schema-evidence \ | |
| --verified-check encryption-at-rest \ | |
| --verified-check browser-e2e \ | |
| --verified-check pi-extension \ | |
| --verified-check dependency-audit \ | |
| --verified-check container-smoke \ | |
| --verified-check retrieval-sample \ | |
| --verified-check retrieval-codemem \ | |
| --verified-check retrieval-ablation \ | |
| --verified-check reinforcement-state-transition \ | |
| --verified-check adversarial-memory-security \ | |
| --output release-evidence/release-evidence.json | |
| - name: Store public release evidence | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 | |
| with: | |
| name: public-release-evidence | |
| path: release-evidence/ | |
| publish: | |
| name: Publish to PyPI | |
| needs: release-evidence | |
| # Manual dispatch is intentionally build/check-only. Publication requires a pushed | |
| # semver tag, whose value was matched to pyproject.toml in the build job above. | |
| if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| id-token: write | |
| contents: read | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.11" | |
| - name: Download distributions | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: python-package-distributions | |
| path: dist/ | |
| - name: Verify any previously published subset | |
| shell: bash | |
| run: >- | |
| python scripts/verify_release_artifacts.py --dist dist | |
| --version "${GITHUB_REF_NAME#v}" --allow-subset | |
| # The trusted publisher may write a receipt beside the distributions. Preserve | |
| # the exact set that passed validation so the post-publish check cannot be | |
| # affected by that implementation detail. | |
| - name: Freeze verified distribution set | |
| shell: bash | |
| run: | | |
| mkdir verified-dist | |
| cp dist/*.whl dist/*.tar.gz verified-dist/ | |
| - name: Publish distributions to PyPI | |
| uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1 | |
| with: | |
| skip-existing: true | |
| - name: Require the exact complete PyPI file set | |
| shell: bash | |
| run: >- | |
| python scripts/verify_release_artifacts.py --dist verified-dist | |
| --version "${GITHUB_REF_NAME#v}" --retries 18 --delay 10 | |
| github-release: | |
| name: Publish GitHub Release | |
| needs: publish | |
| if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Download distributions | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: python-package-distributions | |
| path: dist/ | |
| - name: Download public release evidence | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 | |
| with: | |
| name: public-release-evidence | |
| path: release-evidence/ | |
| - name: Create GitHub Release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| shell: bash | |
| run: | | |
| if gh release view "$GITHUB_REF_NAME" --repo "$GH_REPO" >/dev/null 2>&1; then | |
| # A previous partial attempt may have created the release before every | |
| # canonical package asset uploaded. Reconcile same-named assets from the | |
| # exact aggregate that passed the publish gate. | |
| gh release upload "$GITHUB_REF_NAME" dist/* release-evidence/release-evidence.json release-evidence/*.cdx.json \ | |
| --repo "$GH_REPO" \ | |
| --clobber | |
| else | |
| gh release create "$GITHUB_REF_NAME" dist/* release-evidence/release-evidence.json release-evidence/*.cdx.json \ | |
| --repo "$GH_REPO" \ | |
| --verify-tag \ | |
| --generate-notes \ | |
| --title "Engraphis ${GITHUB_REF_NAME#v}" \ | |
| --latest | |
| fi | |
| github-release-repair: | |
| name: Repair GitHub Release | |
| if: >- | |
| github.event_name == 'workflow_dispatch' && | |
| github.ref == 'refs/heads/main' && | |
| inputs.release_tag != '' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: read | |
| contents: write | |
| id-token: write | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.11" | |
| - name: Download published distributions | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| RELEASE_TAG: ${{ inputs.release_tag }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| [[ "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] | |
| tag_ref="$(gh api "repos/${GH_REPO}/git/ref/tags/${RELEASE_TAG}")" | |
| object_type="$(jq -r '.object.type' <<<"$tag_ref")" | |
| tag_sha="$(jq -r '.object.sha' <<<"$tag_ref")" | |
| # Annotated tags point at tag objects rather than commits. Peel a bounded | |
| # chain explicitly so a same-named branch can never supply the repair SHA. | |
| for _ in {1..8}; do | |
| if [ "$object_type" = "commit" ]; then | |
| break | |
| fi | |
| test "$object_type" = "tag" | |
| tag_object="$(gh api "repos/${GH_REPO}/git/tags/${tag_sha}")" | |
| object_type="$(jq -r '.object.type' <<<"$tag_object")" | |
| tag_sha="$(jq -r '.object.sha' <<<"$tag_object")" | |
| done | |
| test "$object_type" = "commit" | |
| runs="$(gh run list \ | |
| --repo "$GH_REPO" \ | |
| --workflow release.yml \ | |
| --branch "$RELEASE_TAG" \ | |
| --event push \ | |
| --limit 20 \ | |
| --json databaseId,headBranch,headSha,event,createdAt)" | |
| run_id="$(jq -r \ | |
| --arg tag "$RELEASE_TAG" \ | |
| --arg sha "$tag_sha" \ | |
| 'sort_by(.createdAt) | map(select(.headBranch == $tag and | |
| .headSha == $sha and | |
| .event == "push"))[0].databaseId // empty' \ | |
| <<<"$runs")" | |
| test -n "$run_id" | |
| jobs="$(gh run view "$run_id" --repo "$GH_REPO" --json jobs)" | |
| test "$(jq '[.jobs[] | select(.name == "Build distributions" and | |
| .conclusion == "success")] | length' \ | |
| <<<"$jobs")" -eq 1 | |
| test "$(jq '[.jobs[] | select(.name == "Publish to PyPI" and | |
| (.conclusion == "success" or | |
| .conclusion == "failure"))] | length' \ | |
| <<<"$jobs")" -eq 1 | |
| test "$(jq '[.jobs[] | select(.name == "Generate public release evidence" and | |
| .conclusion == "success")] | length' \ | |
| <<<"$jobs")" -eq 1 | |
| gh run download "$run_id" \ | |
| --repo "$GH_REPO" \ | |
| --name python-package-distributions \ | |
| --dir dist | |
| gh run download "$run_id" \ | |
| --repo "$GH_REPO" \ | |
| --name public-release-evidence \ | |
| --dir release-evidence | |
| python - "$RELEASE_TAG" "$tag_sha" <<'PY' | |
| import hashlib | |
| import json | |
| import sys | |
| from pathlib import Path | |
| tag, commit = sys.argv[1:] | |
| with open("release-evidence/release-evidence.json", encoding="utf-8") as handle: | |
| evidence = json.load(handle) | |
| assert evidence.get("format") == "engraphis-release-evidence/2" | |
| assert evidence.get("package", {}).get("version") == tag.removeprefix("v") | |
| assert evidence.get("tag") == tag | |
| assert evidence.get("commit") == commit | |
| assert evidence.get("provenance", {}).get("source") == {"tag": tag, "commit": commit} | |
| expected = { | |
| item["filename"]: item["sha256"] | |
| for item in evidence.get("artifacts", []) | |
| } | |
| actual = { | |
| path.name: hashlib.sha256(path.read_bytes()).hexdigest() | |
| for path in Path("dist").iterdir() | |
| if path.is_file() and (path.name.endswith(".whl") or path.name.endswith(".tar.gz")) | |
| } | |
| assert expected == actual | |
| PY | |
| - name: Verify any previously published subset | |
| env: | |
| RELEASE_TAG: ${{ inputs.release_tag }} | |
| shell: bash | |
| run: >- | |
| python scripts/verify_release_artifacts.py --dist dist | |
| --version "${RELEASE_TAG#v}" --allow-subset | |
| # gh-action-pypi-publish can leave its receipt in dist. Keep the approved | |
| # artifact set separate for the exact immutable-PyPI verification below. | |
| - name: Freeze verified distribution set | |
| shell: bash | |
| run: | | |
| mkdir verified-dist | |
| cp dist/*.whl dist/*.tar.gz verified-dist/ | |
| - name: Publish only missing verified distributions | |
| uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # v1.14.1 | |
| with: | |
| skip-existing: true | |
| - name: Require the exact complete PyPI file set | |
| env: | |
| RELEASE_TAG: ${{ inputs.release_tag }} | |
| shell: bash | |
| run: >- | |
| python scripts/verify_release_artifacts.py --dist verified-dist | |
| --version "${RELEASE_TAG#v}" --retries 18 --delay 10 | |
| - name: Repair GitHub Release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| RELEASE_TAG: ${{ inputs.release_tag }} | |
| shell: bash | |
| run: | | |
| if gh release view "$RELEASE_TAG" --repo "$GH_REPO" >/dev/null 2>&1; then | |
| gh release upload "$RELEASE_TAG" verified-dist/* release-evidence/release-evidence.json release-evidence/*.cdx.json \ | |
| --repo "$GH_REPO" \ | |
| --clobber | |
| else | |
| gh release create "$RELEASE_TAG" verified-dist/* release-evidence/release-evidence.json release-evidence/*.cdx.json \ | |
| --repo "$GH_REPO" \ | |
| --verify-tag \ | |
| --generate-notes \ | |
| --title "Engraphis ${RELEASE_TAG#v}" \ | |
| --latest | |
| fi |