feat: process injection via LiveServerlessMixin #1627
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: CI | |
| on: | |
| pull_request: | |
| branches: [main] | |
| push: | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| issues: write | |
| actions: read | |
| # Cancel in-flight PR runs when a new commit is pushed; never cancel main runs. | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| env: | |
| PYTHON_VERSION: '3.11' | |
| jobs: | |
| # Single source of truth for "what mode should this run be in?". Inspects the | |
| # PR diff and outputs: | |
| # mode = release-please-bypass | full | |
| # should_build = true | false | |
| # The classification is content-based, not just branch-name-based -- that | |
| # closes the CI-bypass hole where a contributor opens a PR from a branch | |
| # named release-please-- and inherits sentinel passes for free. | |
| guard: | |
| name: Guard | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 2 | |
| outputs: | |
| mode: ${{ steps.classify.outputs.mode }} | |
| should_build: ${{ steps.classify.outputs.should_build }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - id: classify | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| BASE_REF: ${{ github.base_ref }} | |
| HEAD_REF: ${{ github.head_ref }} | |
| PR_AUTHOR: ${{ github.event.pull_request.user.login }} | |
| run: | | |
| set -euo pipefail | |
| # Derive the list of release-please-managed files from | |
| # release-please-config.json so this stays in sync automatically. | |
| # Implicit files that release-please always touches: | |
| # - CHANGELOG.md | |
| # - .release-please-manifest.json | |
| # - pyproject.toml (release-type=python bumps the version field) | |
| # Anything declared in `.packages."."."extra-files"` is appended. | |
| # If release-type changes (currently "python"), audit this list. | |
| allowed_files() { | |
| printf '%s\n' \ | |
| 'CHANGELOG.md' \ | |
| '.release-please-manifest.json' \ | |
| 'pyproject.toml' | |
| jq -r '.packages["."]["extra-files"][]?' release-please-config.json | |
| } | |
| # Use fixed-string exact-line matching (`grep -Fxvf`) instead of a | |
| # hand-built regex -- removes the need to escape filenames at all. | |
| is_unexpected() { | |
| # $1 = newline-separated changed files | |
| echo "$1" | grep -Fxvf <(allowed_files) || true | |
| } | |
| # Content-shape check: a metadata-only file list isn't enough on | |
| # push:main, where actor isn't a reliable bypass signal. Verify the | |
| # diffs themselves are version-bump-shaped: pyproject.toml only | |
| # changes its `version = "..."` line, src/runpod_flash/__init__.py | |
| # only changes its `__version__ = "..."` line. Defeats the | |
| # exploitation path where a direct push touches only the metadata | |
| # filenames but smuggles non-version edits (deps, ruff config, | |
| # scripts) into pyproject.toml. | |
| # $1 = git diff base (HEAD~1 for push, origin/BASE_REF for PR). | |
| verify_version_shape() { | |
| local base="$1" | |
| local bad | |
| if git diff --name-only "${base}...HEAD" -- pyproject.toml | grep -q .; then | |
| bad="$(git diff "${base}...HEAD" -- pyproject.toml \ | |
| | grep -E '^[-+]' \ | |
| | grep -vE '^(---|\+\+\+)' \ | |
| | grep -vE '^[-+]version[[:space:]]*=[[:space:]]*"[^"]+"[[:space:]]*$' || true)" | |
| if [ -n "$bad" ]; then | |
| echo "Bypass rejected: pyproject.toml diff touches non-version lines:" | |
| echo "$bad" | |
| return 1 | |
| fi | |
| fi | |
| if git diff --name-only "${base}...HEAD" -- src/runpod_flash/__init__.py | grep -q .; then | |
| bad="$(git diff "${base}...HEAD" -- src/runpod_flash/__init__.py \ | |
| | grep -E '^[-+]' \ | |
| | grep -vE '^(---|\+\+\+)' \ | |
| | grep -vE '^[-+]__version__[[:space:]]*=[[:space:]]*"[^"]+"[[:space:]]*$' || true)" | |
| if [ -n "$bad" ]; then | |
| echo "Bypass rejected: src/runpod_flash/__init__.py diff touches non-__version__ lines:" | |
| echo "$bad" | |
| return 1 | |
| fi | |
| fi | |
| return 0 | |
| } | |
| # Build-trigger heuristic shared between push:main and PR full-mode. | |
| # Build runs when packaging-relevant files change OR a non-Python | |
| # file under src/ is added or modified (data files need explicit | |
| # package-data inclusion; content edits should be smoke-tested by | |
| # validate-wheel.sh). | |
| # $1 = git diff base. | |
| compute_should_build() { | |
| local base="$1" | |
| local changed | |
| changed="$(git diff --name-only "${base}...HEAD")" | |
| if echo "$changed" | grep -qE '^(pyproject\.toml|Makefile|MANIFEST\.in|scripts/validate-wheel\.sh)$'; then | |
| echo "true" | |
| return | |
| fi | |
| if git diff --name-only --diff-filter=AM "${base}...HEAD" | grep -E '^src/.+' | grep -qv '\.py$'; then | |
| echo "true" | |
| return | |
| fi | |
| echo "false" | |
| } | |
| # Identity check: real release-please bot PRs are authored by the | |
| # GitHub App. Used as defense-in-depth alongside the branch prefix + | |
| # diff content check. | |
| BOT_AUTHOR='runpod-release-please-bot[bot]' | |
| # Push to main: classify by diff against the previous commit. When | |
| # release-please's release PR is squash-merged, the resulting commit | |
| # on main only touches the allowed-files set with version-shaped | |
| # diffs -- same code, just version-bumped. Re-running the full | |
| # matrix would be pure waste, and release-please.yml's pypi-publish | |
| # builds its own wheel before publishing. Anything else (feature | |
| # merges, direct admin pushes) gets the full matrix. | |
| # On push events the actor isn't a reliable signal (admins can | |
| # merge the bot's PR by hand) -- diff content is the only check. | |
| if [ "$EVENT_NAME" = "push" ]; then | |
| CHANGED="$(git diff --name-only HEAD~1...HEAD)" | |
| echo "Changed files in this push:" | |
| echo "$CHANGED" | |
| UNEXPECTED="$(is_unexpected "$CHANGED")" | |
| if [ -n "$CHANGED" ] && [ -z "$UNEXPECTED" ] && verify_version_shape HEAD~1; then | |
| echo "Classification: release-please release commit on main (version-bump shape)." | |
| echo "mode=release-please-bypass" >> "$GITHUB_OUTPUT" | |
| echo "should_build=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "Classification: regular push to main." | |
| echo "mode=full" >> "$GITHUB_OUTPUT" | |
| echo "should_build=$(compute_should_build HEAD~1)" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| CHANGED="$(git diff --name-only "origin/${BASE_REF}...HEAD")" | |
| echo "Changed files in this PR:" | |
| echo "$CHANGED" | |
| # release-please bypass on PR: requires ALL FOUR: | |
| # 1. Branch prefix matches `release-please--` | |
| # 2. PR author is the release-please GitHub App | |
| # 3. Diff contains only release-please-managed files | |
| # 4. pyproject.toml/__init__.py diffs are version-bump-shaped | |
| # Any one missing -> fall through to full CI. | |
| if [[ "$HEAD_REF" == release-please--* ]] && [ "$PR_AUTHOR" = "$BOT_AUTHOR" ]; then | |
| UNEXPECTED="$(is_unexpected "$CHANGED")" | |
| if [ -z "$UNEXPECTED" ] && verify_version_shape "origin/${BASE_REF}"; then | |
| echo "Classification: release-please bot PR (version-bump shape)." | |
| echo "mode=release-please-bypass" >> "$GITHUB_OUTPUT" | |
| echo "should_build=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if [ -n "$UNEXPECTED" ]; then | |
| echo "release-please-- branch from bot contains unexpected files; falling through to full CI." | |
| echo "Unexpected files:" | |
| echo "$UNEXPECTED" | |
| fi | |
| elif [[ "$HEAD_REF" == release-please--* ]]; then | |
| echo "release-please-- branch but author is '$PR_AUTHOR' (not '$BOT_AUTHOR'); running full CI." | |
| fi | |
| # Default: full CI; build conditional on content. | |
| echo "mode=full" >> "$GITHUB_OUTPUT" | |
| echo "should_build=$(compute_should_build "origin/${BASE_REF}")" >> "$GITHUB_OUTPUT" | |
| # Fast-fail formatting/lint check. Installs only the dev dependency group | |
| # from the lockfile (warm-cache install is ~1s), then runs ruff via | |
| # `uv run` so we get the exact pinned ruff version that `make | |
| # ci-quality-github` uses. Avoids `uvx ruff` -- that pulls the latest | |
| # ruff, which can drift from the lock and produce CI verdicts that don't | |
| # match local `make quality-check`. | |
| pre-check: | |
| name: Pre-check (format + lint) | |
| runs-on: ubuntu-latest | |
| needs: [guard] | |
| if: ${{ needs.guard.outputs.mode == 'full' }} | |
| timeout-minutes: 3 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: astral-sh/setup-uv@v5 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: uv.lock | |
| - name: Install dev group (frozen, no project) | |
| run: uv sync --only-group dev --frozen | |
| # `uv run` defaults to syncing the environment before each invocation, | |
| # which would undo the dev-only install above and pull in the full | |
| # project. `--no-sync` runs ruff from the existing .venv directly. | |
| - name: Ruff format | |
| run: uv run --no-sync ruff format --check . | |
| - name: Ruff lint | |
| run: uv run --no-sync ruff check . --output-format=github | |
| quality-gates: | |
| name: Quality Gates | |
| runs-on: ubuntu-latest | |
| needs: [guard, pre-check] | |
| if: ${{ needs.guard.outputs.mode == 'full' }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ['3.10', '3.11', '3.12', '3.13'] | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| - name: Set up Python ${{ matrix.python-version }} | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v5 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: uv.lock | |
| - name: Install dependencies | |
| run: make dev | |
| - name: Quality checks | |
| run: make ci-quality-github | |
| - name: Upload test results | |
| uses: actions/upload-artifact@v4 | |
| if: always() | |
| with: | |
| name: test-results-${{ matrix.python-version }} | |
| path: pytest-results-*.xml | |
| build: | |
| name: Build Package | |
| runs-on: ubuntu-latest | |
| needs: [guard, quality-gates] | |
| if: ${{ needs.guard.outputs.mode == 'full' && needs.guard.outputs.should_build == 'true' }} | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: ${{ env.PYTHON_VERSION }} | |
| - uses: astral-sh/setup-uv@v5 | |
| with: | |
| enable-cache: true | |
| cache-dependency-glob: uv.lock | |
| - name: Build package | |
| run: make build | |
| - name: Verify package metadata | |
| run: uv run twine check dist/* | |
| - name: Validate wheel packaging | |
| run: ./scripts/validate-wheel.sh | |
| # Single aggregator. Branch protection on `main` should require ONLY this | |
| # check ("CI / Validation"). This job runs unconditionally (`if: always()`) | |
| # and treats "skipped" as success -- so release-please-bypass runs (where | |
| # pre-check / quality-gates / build are all deliberately skipped by the | |
| # guard) pass cleanly. Anything that genuinely failed or was cancelled | |
| # upstream flips this to red. | |
| # | |
| # Adding or removing upstream jobs (new Python version, new security scan, | |
| # etc.) no longer requires a branch-protection update: just include the new | |
| # job in `needs:` and the results array. | |
| validation: | |
| name: Validation | |
| runs-on: ubuntu-latest | |
| needs: [guard, pre-check, quality-gates, build] | |
| if: always() | |
| timeout-minutes: 1 | |
| steps: | |
| - name: Aggregate upstream results | |
| env: | |
| GUARD: ${{ needs.guard.result }} | |
| PRE_CHECK: ${{ needs.pre-check.result }} | |
| QUALITY_GATES: ${{ needs.quality-gates.result }} | |
| BUILD: ${{ needs.build.result }} | |
| run: | | |
| set -euo pipefail | |
| echo "guard=$GUARD" | |
| echo "pre-check=$PRE_CHECK" | |
| echo "quality-gates=$QUALITY_GATES" | |
| echo "build=$BUILD" | |
| for r in "$GUARD" "$PRE_CHECK" "$QUALITY_GATES" "$BUILD"; do | |
| if [ "$r" != "success" ] && [ "$r" != "skipped" ]; then | |
| echo "::error::Upstream job failed or was cancelled (got: $r)" | |
| exit 1 | |
| fi | |
| done | |
| echo "All upstream jobs succeeded or were intentionally skipped." |