Skip to content

feat: unify the three install paths, replace the frozen agent #1

feat: unify the three install paths, replace the frozen agent

feat: unify the three install paths, replace the frozen agent #1

Workflow file for this run

# 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