feat(sdk): publish canonical challenge artifact #717
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: CI | |
| on: | |
| push: | |
| branches: | |
| - "**" | |
| tags: | |
| - "v*.*.*" | |
| pull_request: | |
| workflow_dispatch: | |
| inputs: | |
| confirm_publish: | |
| description: "Type true to publish Docker images to GHCR" | |
| required: true | |
| default: "false" | |
| permissions: | |
| contents: read | |
| jobs: | |
| ruff: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - uses: astral-sh/setup-uv@v5 | |
| - name: Install dependencies | |
| run: uv sync --extra dev --extra master | |
| - name: Ruff | |
| run: uv run ruff check . | |
| format: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - uses: astral-sh/setup-uv@v5 | |
| - name: Install dependencies | |
| run: uv sync --extra dev --extra master | |
| - name: Format | |
| run: uv run ruff format --check . | |
| mypy: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - uses: astral-sh/setup-uv@v5 | |
| - name: Install dependencies | |
| run: uv sync --extra dev --extra master | |
| - name: Mypy | |
| run: uv run mypy src tests | |
| coverage: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - uses: astral-sh/setup-uv@v5 | |
| - name: Install dependencies | |
| run: uv sync --extra dev --extra master | |
| - name: Coverage | |
| run: uv run pytest --cov=base --cov-report=term-missing --cov-fail-under=80 | |
| production-policy: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - uses: astral-sh/setup-uv@v5 | |
| - name: Install dependencies | |
| run: uv sync --extra dev --extra master | |
| - name: Runtime production policy tests | |
| run: >- | |
| uv run pytest | |
| tests/unit/test_config_template_security.py | |
| tests/unit/test_db_and_models.py | |
| tests/unit/test_registry_extended.py | |
| tests/unit/test_admin_registry_proxy.py | |
| -q | |
| postgres-orm: | |
| runs-on: ubuntu-latest | |
| services: | |
| postgres: | |
| image: postgres:16-alpine | |
| env: | |
| POSTGRES_USER: base | |
| POSTGRES_PASSWORD: base | |
| POSTGRES_DB: base_test | |
| ports: | |
| - 5432:5432 | |
| options: >- | |
| --health-cmd "pg_isready -U base -d base_test" | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| env: | |
| BASE_TEST_DATABASE_URL: postgresql+asyncpg://base:base@localhost:5432/base_test | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.12" | |
| - uses: astral-sh/setup-uv@v5 | |
| - name: Install dependencies | |
| run: uv sync --extra dev --extra master | |
| - name: PostgreSQL ORM integration tests | |
| run: uv run pytest tests/integration -m postgres -q | |
| docker-build: | |
| needs: | |
| - ruff | |
| - format | |
| - mypy | |
| - coverage | |
| - production-policy | |
| - postgres-orm | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| include: | |
| - image: base | |
| dockerfile: docker/Dockerfile.validator | |
| build_args: "" | |
| - image: base-master | |
| dockerfile: docker/Dockerfile.master | |
| build_args: "" | |
| - image: base-validator-runtime | |
| dockerfile: docker/Dockerfile.validator-runtime | |
| build_args: | | |
| AGENT_CHALLENGE_REF=7553044666b56d51b1c1ab57f8a8e3c75a33883d | |
| PRISM_REF=9edf6931428359408ba44e63b889e546ae071f46 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: docker/setup-buildx-action@v3 | |
| - name: Build ${{ matrix.image }} image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: ${{ matrix.dockerfile }} | |
| build-args: ${{ matrix.build_args }} | |
| push: false | |
| tags: ghcr.io/baseintelligence/${{ matrix.image }}:ci-${{ github.sha }} | |
| docker-publish: | |
| if: >- | |
| github.event_name != 'pull_request' && | |
| (github.ref == 'refs/heads/main' || | |
| startsWith(github.ref, 'refs/tags/v') || | |
| (github.event_name == 'workflow_dispatch' && inputs.confirm_publish == 'true')) | |
| needs: | |
| - docker-build | |
| - postgres-orm | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| strategy: | |
| matrix: | |
| include: | |
| - image: base | |
| dockerfile: docker/Dockerfile.validator | |
| build_args: "" | |
| - image: base-master | |
| dockerfile: docker/Dockerfile.master | |
| build_args: "" | |
| - image: base-validator-runtime | |
| dockerfile: docker/Dockerfile.validator-runtime | |
| build_args: | | |
| AGENT_CHALLENGE_REF=7553044666b56d51b1c1ab57f8a8e3c75a33883d | |
| PRISM_REF=9edf6931428359408ba44e63b889e546ae071f46 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Verify release tag belongs to main | |
| if: startsWith(github.ref, 'refs/tags/v') | |
| run: | | |
| git fetch --no-tags origin main | |
| git merge-base --is-ancestor "${GITHUB_SHA}" origin/main | |
| - uses: docker/setup-buildx-action@v3 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Generate Docker metadata | |
| id: meta | |
| uses: docker/metadata-action@v5 | |
| with: | |
| images: ghcr.io/baseintelligence/${{ matrix.image }} | |
| tags: | | |
| type=ref,event=branch | |
| type=semver,pattern={{version}} | |
| type=semver,pattern={{raw}} | |
| type=sha,prefix=sha- | |
| type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} | |
| - name: Build and publish ${{ matrix.image }} image | |
| uses: docker/build-push-action@v6 | |
| with: | |
| context: . | |
| file: ${{ matrix.dockerfile }} | |
| build-args: ${{ matrix.build_args }} | |
| push: true | |
| tags: ${{ steps.meta.outputs.tags }} | |
| labels: ${{ steps.meta.outputs.labels }} | |
| publish-self-update-manifest: | |
| # On every merge to main, AFTER the images publish, overwrite the base | |
| # self-update manifest at a STABLE url (raw file on the dedicated `release` | |
| # branch). The base-supervisor http_manifest_detector polls this JSON and | |
| # advances the on-disk `base` release whenever `version` changes (architecture | |
| # sec 9 / research G-A2). `version` is MONOTONIC + filesystem-safe and | |
| # `source_url` is the codeload tarball for this exact ref. | |
| if: github.event_name == 'push' && github.ref == 'refs/heads/main' | |
| needs: | |
| - docker-publish | |
| - postgres-orm | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Publish self-update manifest to the release branch | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| # Retry wrapper: the github-actions[bot] force-push to `release` | |
| # intermittently 403s on the first attempt (a transient GitHub | |
| # permission hiccup a re-run clears), so retry with backoff to keep the | |
| # self-update manifest publish durable instead of needing a manual re-run. | |
| retry() { | |
| local n=0 | |
| until "$@"; do | |
| n=$((n + 1)) | |
| if [ "$n" -ge 5 ]; then | |
| echo "command failed after ${n} attempts: $*" >&2 | |
| return 1 | |
| fi | |
| echo "attempt ${n} failed (transient); retrying in $((n * 5))s..." >&2 | |
| sleep "$((n * 5))" | |
| done | |
| } | |
| # MONOTONIC, filesystem-safe version: the strictly-increasing GitHub | |
| # run number (advances on every merge to main) plus the merged commit | |
| # sha for traceability. No "/", never "." / ".." -> accepted by | |
| # AvailableRelease.__post_init__ (src/base/supervisor/self_update.py). | |
| version="r${{ github.run_number }}-sha-${{ github.sha }}" | |
| # Codeload tarball for THIS ref; tarball_stager strips the single | |
| # top-level dir then runs `uv sync` on the staged release. | |
| source_url="https://codeload.github.com/${{ github.repository }}/tar.gz/${{ github.sha }}" | |
| work="$(mktemp -d)" | |
| printf '{\n "version": "%s",\n "source_url": "%s"\n}\n' \ | |
| "${version}" "${source_url}" > "${work}/self-update-manifest.json" | |
| echo "publishing self-update manifest version=${version}" | |
| cat "${work}/self-update-manifest.json" | |
| cd "${work}" | |
| git init -q | |
| git checkout -q -b release | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add self-update-manifest.json | |
| git commit -q -m "self-update manifest ${version}" | |
| retry git push -q --force \ | |
| "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" \ | |
| release | |
| github-release: | |
| if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') | |
| needs: | |
| - docker-publish | |
| - postgres-orm | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 | |
| with: | |
| fetch-depth: 0 | |
| - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 | |
| with: | |
| python-version: "3.12" | |
| - uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 | |
| - name: Verify tag belongs to main | |
| run: | | |
| git fetch --no-tags origin main | |
| git merge-base --is-ancestor "${GITHUB_SHA}" origin/main | |
| - name: Prepare release metadata | |
| id: release | |
| run: | | |
| set -euo pipefail | |
| version="${GITHUB_REF_NAME#v}" | |
| python - "${version}" <<'PY' | |
| import json | |
| import pathlib | |
| import sys | |
| import tomllib | |
| expected = sys.argv[1] | |
| project = tomllib.loads(pathlib.Path("pyproject.toml").read_text()) | |
| manifest = json.loads( | |
| pathlib.Path( | |
| "src/base/challenge_sdk/release_manifest.json" | |
| ).read_text() | |
| ) | |
| assert project["project"]["version"] == expected | |
| assert manifest["artifact_version"] == expected | |
| assert manifest["release_id"] == f"v{expected}" | |
| PY | |
| echo "version=${version}" >> "$GITHUB_OUTPUT" | |
| echo "SOURCE_DATE_EPOCH=1704067200" >> "$GITHUB_ENV" | |
| - name: Build immutable Python release artifacts | |
| run: | | |
| uv sync --locked --extra dev | |
| uv build --no-build-isolation --wheel --sdist --out-dir dist | |
| sha256sum dist/* | tee dist/SHA256SUMS | |
| - name: Create GitHub release | |
| uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 | |
| with: | |
| tag_name: ${{ github.ref_name }} | |
| name: BASE ${{ steps.release.outputs.version }} | |
| generate_release_notes: true | |
| append_body: true | |
| draft: false | |
| prerelease: ${{ contains(github.ref_name, '-') }} | |
| make_latest: ${{ !contains(github.ref_name, '-') }} | |
| files: | | |
| dist/base-${{ steps.release.outputs.version }}-py3-none-any.whl | |
| dist/base-${{ steps.release.outputs.version }}.tar.gz | |
| dist/SHA256SUMS | |
| body: | | |
| ## Container Images | |
| - `ghcr.io/baseintelligence/base:${{ steps.release.outputs.version }}` | |
| - `ghcr.io/baseintelligence/base:${{ github.ref_name }}` | |
| - `ghcr.io/baseintelligence/base:sha-${{ github.sha }}` | |
| - `ghcr.io/baseintelligence/base-master:${{ steps.release.outputs.version }}` | |
| - `ghcr.io/baseintelligence/base-master:${{ github.ref_name }}` | |
| - `ghcr.io/baseintelligence/base-master:sha-${{ github.sha }}` | |
| ## Deployment Notes | |
| Production deployments should pin the SemVer image tag plus the immutable digest. The `latest` tag is published only from `main`, not from release tags. | |
| ## Documentation | |
| See `docs/versioning.md` for the SemVer, Git tag, GHCR tag, GitHub Release, and production image policy. |