fix: bound foreground run retention recovery (#332) #370
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: Package | |
| on: | |
| pull_request: | |
| paths: | |
| - "lib/python/base_cli/**" | |
| - "pyproject.toml" | |
| - "VERSION" | |
| - "README.md" | |
| - "LICENSE" | |
| - "MANIFEST.in" | |
| - "scripts/validate_package_artifact.py" | |
| - "scripts/validate_installed_package.py" | |
| - "scripts/validate_examples.py" | |
| - "scripts/generate_release_metadata.py" | |
| - "scripts/validate_release_metadata.py" | |
| - "scripts/validate_changelog.py" | |
| - "scripts/validate_release_ref.py" | |
| - "CHANGELOG.md" | |
| - "examples/**" | |
| - "compatibility/**" | |
| - "docs/releasing.md" | |
| - "requirements/release.in" | |
| - "requirements/release.txt" | |
| - ".github/workflows/package.yml" | |
| push: | |
| branches: | |
| - main | |
| tags: | |
| - "v*" | |
| workflow_dispatch: | |
| inputs: | |
| publish_target: | |
| description: "Protected publication target" | |
| required: true | |
| type: choice | |
| options: | |
| - testpypi | |
| - pypi | |
| default: testpypi | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| PACKAGE_NAME: base-cli | |
| jobs: | |
| build: | |
| name: Build and validate distributions | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| outputs: | |
| version: ${{ steps.metadata.outputs.version }} | |
| steps: | |
| - name: Check out source | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Python | |
| uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: "3.13" | |
| - name: Read package version | |
| id: metadata | |
| run: echo "version=$(tr -d '\\r\\n' < VERSION)" >> "$GITHUB_OUTPUT" | |
| - name: Validate release ref | |
| env: | |
| PUBLISH_TARGET: ${{ inputs.publish_target || '' }} | |
| run: | | |
| if [[ "$GITHUB_REF_TYPE" == "tag" && "$GITHUB_REF_NAME" != "v${{ steps.metadata.outputs.version }}" ]]; then | |
| echo "Release tag must be v${{ steps.metadata.outputs.version }}; got $GITHUB_REF_NAME" >&2 | |
| exit 1 | |
| fi | |
| if [[ "$PUBLISH_TARGET" == "pypi" && "$GITHUB_REF_TYPE" != "tag" ]]; then | |
| echo "PyPI publication requires dispatching this workflow from the matching version tag." >&2 | |
| exit 1 | |
| fi | |
| - name: Install validation dependencies | |
| run: python -m pip install ".[dev,typer,quality]" | |
| - name: Run authoritative validation gate | |
| run: ./tests/full_validate.sh | |
| - name: Validate changelog | |
| run: python scripts/validate_changelog.py | |
| - name: Validate tagged release notes | |
| if: ${{ github.ref_type == 'tag' }} | |
| env: | |
| RELEASE_TAG: ${{ github.ref_name }} | |
| run: python scripts/validate_release_ref.py | |
| - name: Prepare clean artifact destination | |
| run: | | |
| git clean -ffdx | |
| mkdir -p dist | |
| - name: Install build and validation tools | |
| run: python -m pip install --require-hashes --no-deps -r requirements/release.txt | |
| - name: Build sdist and wheel | |
| env: | |
| # Keep the reproducibility input numeric; push-event timestamps are | |
| # ISO-8601 strings and are not accepted by wheel/build backends. | |
| SOURCE_DATE_EPOCH: '0' | |
| run: | | |
| rm -rf dist-a dist-b "$RUNNER_TEMP/base-cli-build-a" "$RUNNER_TEMP/base-cli-build-b" | |
| mkdir -p dist-a dist-b | |
| mkdir -p "$RUNNER_TEMP/base-cli-build-a" "$RUNNER_TEMP/base-cli-build-b" | |
| git archive "$GITHUB_SHA" | tar -x -C "$RUNNER_TEMP/base-cli-build-a" | |
| git archive "$GITHUB_SHA" | tar -x -C "$RUNNER_TEMP/base-cli-build-b" | |
| (cd "$RUNNER_TEMP/base-cli-build-a" && python -m build --sdist --wheel --outdir "$GITHUB_WORKSPACE/dist-a") | |
| (cd "$RUNNER_TEMP/base-cli-build-b" && python -m build --sdist --wheel --outdir "$GITHUB_WORKSPACE/dist-b") | |
| python - <<'PY' | |
| import gzip | |
| import os | |
| import tarfile | |
| from pathlib import Path | |
| epoch = int(os.environ["SOURCE_DATE_EPOCH"]) | |
| for directory in (Path("dist-a"), Path("dist-b")): | |
| for path in directory.glob("*.tar.gz"): | |
| with tarfile.open(path, "r:gz") as source: | |
| members = [(member, source.extractfile(member).read() if member.isfile() else None) for member in source.getmembers()] | |
| temporary = path.with_suffix(path.suffix + ".tmp") | |
| with temporary.open("wb") as raw: | |
| with gzip.GzipFile(filename=path.name, mode="wb", fileobj=raw, mtime=epoch) as compressed: | |
| with tarfile.open(fileobj=compressed, mode="w|") as target: | |
| for member, payload in sorted(members, key=lambda item: item[0].name): | |
| member.mtime = epoch | |
| member.uid = member.gid = 0 | |
| member.uname = member.gname = "" | |
| member.pax_headers = {} | |
| target.addfile(member, None if payload is None else __import__("io").BytesIO(payload)) | |
| temporary.replace(path) | |
| PY | |
| (cd dist-a && sha256sum * | sort) > dist-a.SHA256SUMS | |
| (cd dist-b && sha256sum * | sort) > dist-b.SHA256SUMS | |
| diff -u dist-a.SHA256SUMS dist-b.SHA256SUMS | |
| cp dist-a/* dist/ | |
| - name: Validate artifact contents and metadata | |
| run: python scripts/validate_package_artifact.py dist | |
| - name: Validate package indexes | |
| run: python -m twine check dist/* | |
| - name: Generate release checksums and SPDX SBOM | |
| env: | |
| SOURCE_REVISION: ${{ github.sha }} | |
| SOURCE_DATE_EPOCH: '0' | |
| run: python scripts/generate_release_metadata.py dist | |
| - name: Validate release checksums and SPDX SBOM | |
| env: | |
| SOURCE_REVISION: ${{ github.sha }} | |
| run: python scripts/validate_release_metadata.py dist | |
| - name: Upload reviewed distributions | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: base-cli-dist-${{ github.run_id }} | |
| path: | | |
| dist/*.whl | |
| dist/*.tar.gz | |
| if-no-files-found: error | |
| retention-days: 14 | |
| - name: Upload release metadata | |
| uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| with: | |
| name: base-cli-release-metadata-${{ github.run_id }} | |
| path: | | |
| dist/SBOM.spdx.json | |
| dist/SHA256SUMS | |
| if-no-files-found: error | |
| retention-days: 90 | |
| smoke: | |
| name: Install smoke test (Python ${{ matrix.python-version }}) | |
| needs: build | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] | |
| steps: | |
| - name: Check out source | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| - name: Set up Python | |
| uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Download reviewed distributions | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: base-cli-dist-${{ github.run_id }} | |
| path: dist | |
| - name: Install wheel and runtime dependencies | |
| run: python -m pip install dist/base_cli-*.whl | |
| - name: Verify installed package | |
| env: | |
| EXPECTED_VERSION: ${{ needs.build.outputs.version }} | |
| run: | | |
| python - <<'PY' | |
| import base_cli | |
| import importlib.metadata | |
| expected = __import__("os").environ["EXPECTED_VERSION"] | |
| assert base_cli.__version__ == expected, (base_cli.__version__, expected) | |
| assert importlib.metadata.version("base-cli") == expected | |
| assert hasattr(base_cli, "App") | |
| print(f"base-cli {base_cli.__version__} installed successfully") | |
| PY | |
| - name: Exercise installed wheel API and lifecycle | |
| env: | |
| EXPECTED_VERSION: ${{ needs.build.outputs.version }} | |
| run: python -I scripts/validate_installed_package.py | |
| - name: Check installed dependency consistency | |
| run: python -m pip check | |
| publish: | |
| name: Publish reviewed distribution | |
| needs: [build, smoke] | |
| if: ${{ (github.event_name == 'push' && github.ref_type == 'tag') || github.event_name == 'workflow_dispatch' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| environment: | |
| name: ${{ github.event_name == 'push' && 'pypi' || inputs.publish_target }} | |
| url: ${{ github.event_name == 'push' && 'https://pypi.org/p/base-cli' || 'https://test.pypi.org/p/base-cli' }} | |
| permissions: | |
| contents: read | |
| id-token: write | |
| steps: | |
| - name: Download reviewed distributions | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: base-cli-dist-${{ github.run_id }} | |
| path: dist | |
| - name: Publish to TestPyPI | |
| if: ${{ github.event_name == 'workflow_dispatch' && inputs.publish_target == 'testpypi' }} | |
| uses: pypa/gh-action-pypi-publish@4bb033805d9e19112d8c697528791ff53f6c2f74 | |
| with: | |
| packages-dir: dist | |
| repository-url: https://test.pypi.org/legacy/ | |
| - name: Publish to PyPI | |
| if: ${{ (github.event_name == 'push' && github.ref_type == 'tag') || (github.event_name == 'workflow_dispatch' && inputs.publish_target == 'pypi') }} | |
| uses: pypa/gh-action-pypi-publish@4bb033805d9e19112d8c697528791ff53f6c2f74 | |
| with: | |
| packages-dir: dist | |
| attest: | |
| name: Attest reviewed release | |
| needs: [build, smoke] | |
| if: ${{ (github.event_name == 'push' && github.ref_type == 'tag') || github.event_name == 'workflow_dispatch' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: read | |
| id-token: write | |
| attestations: write | |
| steps: | |
| - name: Download reviewed distributions | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: base-cli-dist-${{ github.run_id }} | |
| path: dist | |
| - name: Download release metadata | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: base-cli-release-metadata-${{ github.run_id }} | |
| path: dist | |
| - name: Attest artifact provenance | |
| uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4 | |
| with: | |
| subject-checksums: dist/SHA256SUMS | |
| - name: Attest SPDX SBOM | |
| uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4 | |
| with: | |
| subject-checksums: dist/SHA256SUMS | |
| sbom-path: dist/SBOM.spdx.json | |
| release: | |
| name: Create GitHub Release | |
| needs: [build, smoke, publish, attest] | |
| if: ${{ github.event_name == 'push' && github.ref_type == 'tag' }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Download reviewed distributions | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: base-cli-dist-${{ github.run_id }} | |
| path: dist | |
| - name: Download release metadata | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: base-cli-release-metadata-${{ github.run_id }} | |
| path: dist | |
| - name: Create or update GitHub Release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| tag="$GITHUB_REF_NAME" | |
| assets=(dist/*.whl dist/*.tar.gz dist/SHA256SUMS dist/SBOM.spdx.json) | |
| if gh release view "$tag" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then | |
| gh release upload "$tag" "${assets[@]}" --clobber --repo "$GITHUB_REPOSITORY" | |
| else | |
| gh release create "$tag" "${assets[@]}" \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --title "$tag" \ | |
| --generate-notes \ | |
| --notes "Published distributions and release metadata for $tag. See CHANGELOG.md for the reviewed release notes." | |
| fi |