feat: unify the three install paths, replace the frozen agent #1
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
| # Three guarantees. This workflow turns "the install paths have drifted" from | |
| # something discovered by reading code into a number that moves. | |
| # | |
| # 1. locks — the hash-pinned lock matches requirements.txt everywhere | |
| # 2. parity — pip and conda installs produce the same environment | |
| # 3. installer-e2e — an install produces the same thing as a checkout | |
| # | |
| # A lock can only be generated on the platform it describes, so they are | |
| # generated by hand and committed. CI only verifies they are current. | |
| # | |
| # There is no frozen-agent job: the agent is no longer a PyInstaller bundle, | |
| # which is what removed the whole class of divergence these jobs guard. | |
| name: Install parity | |
| on: | |
| # No push trigger: nothing here writes to the repo, so there is nothing a | |
| # push would accomplish that a PR does not. | |
| pull_request: | |
| paths: | |
| - "requirements.txt" | |
| - "environment.yml" | |
| - "requirements/**" | |
| - "scripts/generate_lock.py" | |
| - "scripts/parity_check.py" | |
| - "scripts/package_source.py" | |
| - "scripts/test_install_e2e.py" | |
| - "app/provision/**" | |
| - "app/paths.py" | |
| - ".github/workflows/parity.yml" | |
| workflow_dispatch: | |
| jobs: | |
| # ────────────────────────────────────────────── | |
| # Every committed lock came from the current | |
| # requirements.txt. | |
| # ────────────────────────────────────────────── | |
| locks: | |
| name: Locks match requirements.txt | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.10" | |
| # Checks all platforms' locks from one runner, and needs no pip resolve: | |
| # it compares the source digest recorded in each lock's header. See | |
| # scripts/generate_lock.py for why re-resolving here would be wrong. | |
| # | |
| # This only CHECKS. Locks are generated by hand — `python | |
| # scripts/generate_lock.py` — on the platform each one describes, and | |
| # committed. release.yml refuses to build a payload if one is missing. | |
| - name: Check locks | |
| run: python scripts/generate_lock.py --check | |
| # ────────────────────────────────────────────── | |
| # pip vs conda must produce the same environment. | |
| # environment.yml used to carry its own package | |
| # list, which drifted by 15 packages. It now | |
| # provides only the runtime, and BOTH paths | |
| # install the same lock — so this job checks | |
| # that the two runtimes agree, not two lists. | |
| # ────────────────────────────────────────────── | |
| parity: | |
| name: pip vs conda (${{ matrix.os_label }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| os_label: linux | |
| - os: windows-latest | |
| os_label: windows | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.10" | |
| - name: Install via pip (from the lock) | |
| shell: bash | |
| run: | | |
| python -m venv .venv-pip | |
| if [ -f .venv-pip/bin/python ]; then PY=.venv-pip/bin/python; | |
| else PY=.venv-pip/Scripts/python.exe; fi | |
| echo "PIP_PY=$PY" >> "$GITHUB_ENV" | |
| "$PY" -m pip install --upgrade pip | |
| # `ls | head -1` would pick another platform's lock once Linux and | |
| # macOS locks exist. app.provision.deps.find_lock resolves the exact | |
| # (platform, python) tag and refuses to fall back — a Linux lock | |
| # pins CUDA-flavoured torch wheels that do not exist on Windows. | |
| LOCK=$("$PY" -c "from app.provision.deps import find_lock; print(find_lock('.') or '')") | |
| if [ -z "$LOCK" ]; then | |
| echo "::error::no lock for this platform — run scripts/generate_lock.py" | |
| exit 1 | |
| fi | |
| echo "using $LOCK" | |
| "$PY" -m pip install --require-hashes -r "$LOCK" | |
| - name: Fingerprint pip install | |
| shell: bash | |
| run: | | |
| "$PIP_PY" scripts/parity_check.py --label pip > fp-pip.json | |
| - uses: conda-incubator/setup-miniconda@v3 | |
| with: | |
| activate-environment: craftbot | |
| environment-file: environment.yml | |
| auto-activate-base: false | |
| # environment.yml deliberately lists no Python packages — they come from | |
| # the shared lock. Without this step the conda env holds only the | |
| # interpreter and system binaries, and the comparison below would report | |
| # a 200-package "divergence" that is really just a missing install. | |
| - name: Install the lock into the conda env | |
| shell: bash -el {0} | |
| run: | | |
| LOCK=$(python -c "from app.provision.deps import find_lock; print(find_lock('.') or '')") | |
| if [ -z "$LOCK" ]; then | |
| echo "::error::no lock for this platform — run scripts/generate_lock.py" | |
| exit 1 | |
| fi | |
| echo "using $LOCK" | |
| python -m pip install --require-hashes -r "$LOCK" | |
| - name: Fingerprint conda install | |
| shell: bash -el {0} | |
| run: python scripts/parity_check.py --label conda > fp-conda.json | |
| - name: Compare | |
| shell: bash | |
| run: python scripts/parity_check.py --compare fp-pip.json fp-conda.json | |
| - name: Upload fingerprints | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: fingerprints-${{ matrix.os_label }} | |
| path: fp-*.json | |
| # ────────────────────────────────────────────── | |
| # The installer path must produce the same thing | |
| # as a source checkout. This is the acceptance | |
| # test for the whole architecture, so it runs on | |
| # every platform we ship. | |
| # ────────────────────────────────────────────── | |
| installer-e2e: | |
| name: Installer E2E (${{ matrix.os_label }}) | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| os_label: linux | |
| - os: windows-latest | |
| os_label: windows | |
| - os: macos-latest | |
| os_label: macos | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.10" | |
| - uses: actions/setup-node@v4 | |
| with: | |
| node-version: "20" | |
| - name: Build frontend | |
| shell: bash | |
| env: | |
| VITE_BACKEND_PORT: "7926" | |
| run: | | |
| cd app/ui_layer/browser/frontend | |
| npm install | |
| npx vite build | |
| - name: Build the source payload | |
| shell: bash | |
| run: | | |
| python scripts/package_source.py | |
| # Structural only: verifies path resolution, the managed-install marker | |
| # and payload completeness. The full run installs 239 packages and is | |
| # too slow for every PR — the `parity` job covers dependency equality. | |
| - name: Install E2E (structural) | |
| shell: bash | |
| run: | | |
| python scripts/test_install_e2e.py --skip-deps | |
| - name: Upload payload | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: payload-${{ matrix.os_label }} | |
| path: dist/CraftBot-src.zip |