Skip to content

build: ship the explorer's web UI in the agentenv-framework wheel - #52

Open
earakely-scale wants to merge 3 commits into
mainfrom
build/ship-explorer-ui
Open

earakely-scale wants to merge 3 commits into
mainfrom
build/ship-explorer-ui

Conversation

@earakely-scale

@earakely-scale earakely-scale commented Oct 2, 2026 •

Copy link
Copy Markdown
Collaborator

What and why

The published wheel carries only the explorer's API. The server serves a built UI from agent_env/explorer/static/ (packaged_ui_dir()), but no release step builds one. The ui job builds the static export on every PR as a check and throws it away. So with pip install 'agentenv-framework[explorer]', agent-env up serves JSON at / and prints a …/docs (UI) link that returns 404. This PR ships the UI in every release. It stays in the framework under the [explorer] extra rather than becoming a separate package, as agreed with @polakamtejas.

  • A new explorer-ui job in publish-pypi.yml runs npm ci && npm run build:static and uploads the export as a workflow artifact.
    • It's a separate job because the PyPI jobs can mint tokens. Running npm packages inside them would let a compromised dependency publish to PyPI. This job has contents: read only, and uses no cache, since caches are a known release-poisoning vector.
    • framework downloads the export into explorer/static/ before uv build. A new step runs .github/scripts/check_explorer_ui.py, which fails the publish unless the wheel has agent_env/explorer/static/index.html and every /_next/ script, stylesheet and font that page references. Unit tests are in tst/unit/test_check_explorer_ui.py.
    • protocol now waits for explorer-ui too, so a failed UI build publishes nothing, instead of shipping the protocol and stopping before the framework.
  • Packaging:
    • explorer/static/ is git-ignored so the build output can't be committed, and it's listed as a hatch artifact so it still ships.
    • The sdist leaves out src/agent_env/explorer/ui/ entirely, as the wheel does, and ships the UI as its built export. Hatch reads only the root .gitignore, so packing ui/ took whatever a working tree held there. Building the UI in place produced a 159 MB sdist, over PyPI's 100 MB limit, and a local .env.local would have shipped too (Greptile's catch). The sdist's copy of the UI source was already incomplete anyway: the root lib/ pattern dropped all 42 files of ui/src/lib/.
  • PR-time check: the existing ui job now packages the export the way the release does. It runs the same script, and fails if the sdist contains anything under explorer/ui/, so a packaging regression shows up on a PR rather than mid-release.
  • AGENTS.md: the explorer row says where the UI's source lives, how it ships, and that a source checkout serves only the API.

Licensing was already covered: THIRD_PARTY_NOTICES.md has an "Explorer UI dependencies" section that anticipates a distribution including the built UI, and that file ships in the wheel.

How it was tested

  • PR job path, in a worktree of this commit with the UI built in place and node_modules present:
    • The wheel has all 43 UI files including index.html, no UI source, and is 2.5 MB (1.1 MB today).
    • The sdist is 3.3 MB, has nothing under explorer/ui/, and carries the built UI. With a planted ui/.env.local holding a sentinel, the sdist has 0 .env.local entries and no file containing the sentinel.
    • git status stays clean.
  • Release path, in a clean worktree with the prebuilt export, the job's protocol-pin rewrite and uv build --no-sources:
    • The UI check passes and twine check --strict passes.
    • The check fails when the wheel is broken: the same wheel with _next/ stripped (all 14 missing assets named), and the current UI-less 0.9.1265 wheel (missing index.html).
  • Runtime: I installed the release-built wheel with [explorer] and ran agent-env up:
    • /, /docs and /environments serve the UI, and /api/v1/envs still serves the API.
    • In an earlier prototype, a headless browser rendered the Environments hub, and all five of its same-origin API calls returned 200.
  • check-jsonschema (vendor.github-workflows) and actionlint with shellcheck pass on both workflows.
  • ci: create a GitHub release for each tag, with notes from its pull requests #48 compatibility: this branch merges with ci: create a GitHub release for each tag, with notes from its pull requests #48 without conflicts. The combined publish-pypi.yml (explorer-ui → protocol → framework → release) also passes actionlint.

bump-version is set, so the release this merge triggers is the first to ship the UI. I'll install it from PyPI afterwards and confirm agent-env up serves the explorer.

🤖 Generated with Claude Code

RetriggerConfidence Score: 5/5

The PR appears safe to merge, though two earlier, non-blocking packaging checks remain incomplete.

Fix All in CursorFindings

  1. P2 Missing page assets pass checks ▶
  2. P2 Source files escape archive check ▶
Fix with agent prompt
### Issue 1
.github/workflows/local-backends.yml:undefined-86
This check and the release check only look for `index.html`. A wheel can pass both while missing the `_next` files the explorer serves, leaving users with a page whose scripts or styles do not load. Check for a built asset in the wheel too.

### Issue 2
.github/workflows/local-backends.yml:undefined-87
If the sdist contains `explorer/ui/` and `tar` still has paths to list, `grep -q` stops at the first match and closes the pipe. With `pipefail`, `tar` can then fail from a broken pipe, making the `if` condition false. The job passes instead of flagging the files. Let `grep` read the full listing.

```suggestion
          if tar tzf dist/*.tar.gz | grep '/explorer/ui/' >/dev/null; then echo "::error::the sdist includes the UI's source tree"; exit 1; fi
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

Release builds now put the explorer’s static web UI in the framework wheel and source distribution, while leaving the UI source in the repository. PR workflows also build and check these distributions so missing UI files or bundled UI source are caught before release.

  • Builds the UI separately, then passes its export to the framework build.
  • Packages the built UI while excluding its source tree.
  • Checks wheel assets and source-distribution contents in PR CI.

Reviews (3) · Last reviewed commit: "build: check every asset the explorer's ..."

The wheel carried only the explorer's API: the server serves a built UI from
agent_env/explorer/static/, but no release step built it, so `agent-env up` from PyPI served JSON
at / and printed a /docs link that 404s.

The publish workflow now builds the UI's static export in its own job, which can't mint a PyPI
token and uses no cache, so no npm package runs where it could publish. The framework job copies
the export into explorer/static/ before building and fails if the wheel lacks index.html, and the
protocol job waits for the UI too, so a failed UI build publishes nothing.

explorer/static/ is git-ignored and listed as a hatch artifact. The sdist excludes the UI's
node_modules, .next and out, which hatch otherwise packs because it reads only the root
.gitignore: building in place made a 159 MB sdist, over PyPI's limit. The ui job on pull requests
packages the export the same way and checks both, so a regression shows up before a release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@earakely-scale
earakely-scale requested a review from a team as a code owner October 2, 2026 18:01
@earakely-scale earakely-scale added github_actions Pull requests that update GitHub Actions code bump-version labels Oct 2, 2026
Comment thread pyproject.toml Outdated
Comment thread .github/workflows/local-backends.yml Outdated
python -m pip install --quiet --index-url https://pypi.org/simple uv
cp -R src/agent_env/explorer/ui/out src/agent_env/explorer/static
uv build --no-sources --out-dir dist
unzip -l dist/*.whl | grep -q 'agent_env/explorer/static/index.html' || { echo "::error::the wheel has no agent_env/explorer/static/index.html"; exit 1; }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing page assets pass checks

This check and the release check only look for index.html. A wheel can pass both while missing the _next files the explorer serves, leaving users with a page whose scripts or styles do not load. Check for a built asset in the wheel too.

Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/local-backends.yml
Line: 86

Comment:
**Missing page assets pass checks**

This check and the release check only look for `index.html`. A wheel can pass both while missing the `_next` files the explorer serves, leaving users with a page whose scripts or styles do not load. Check for a built asset in the wheel too.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Cursor Fix in Claude Code Fix in Codex

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same point as the later comment on line 87. It's fixed in b968051: both checks run .github/scripts/check_explorer_ui.py, which fails unless every /_next/ asset that index.html references is in the wheel. Details are in the reply there.

Hatch reads only the root .gitignore, so packing src/agent_env/explorer/ui/ took whatever a working
tree held there: node_modules, build output, or an .env.local with credentials. The sdist now ships
the UI the way the wheel does, as its built export, and its source stays in the repository. The
sdist's copy of the UI source was already incomplete: the root .gitignore's lib/ dropped all of
ui/src/lib/, so it could not rebuild the UI anyway.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cp -R src/agent_env/explorer/ui/out src/agent_env/explorer/static
uv build --no-sources --out-dir dist
unzip -l dist/*.whl | grep -q 'agent_env/explorer/static/index.html' || { echo "::error::the wheel has no agent_env/explorer/static/index.html"; exit 1; }
if tar tzf dist/*.tar.gz | grep -q '/explorer/ui/'; then echo "::error::the sdist includes the UI's source tree"; exit 1; fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Source files escape archive check

If the sdist contains explorer/ui/ and tar still has paths to list, grep -q stops at the first match and closes the pipe. With pipefail, tar can then fail from a broken pipe, making the if condition false. The job passes instead of flagging the files. Let grep read the full listing.

Suggested change
if tar tzf dist/*.tar.gz | grep -q '/explorer/ui/'; then echo "::error::the sdist includes the UI's source tree"; exit 1; fi
if tar tzf dist/*.tar.gz | grep '/explorer/ui/' >/dev/null; then echo "::error::the sdist includes the UI's source tree"; exit 1; fi
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/local-backends.yml
Line: 87

Comment:
**Source files escape archive check**

If the sdist contains `explorer/ui/` and `tar` still has paths to list, `grep -q` stops at the first match and closes the pipe. With `pipefail`, `tar` can then fail from a broken pipe, making the `if` condition false. The job passes instead of flagging the files. Let `grep` read the full listing.

```suggestion
          if tar tzf dist/*.tar.gz | grep '/explorer/ui/' >/dev/null; then echo "::error::the sdist includes the UI's source tree"; exit 1; fi
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Cursor Fix in Claude Code Fix in Codex

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Both checks now run .github/scripts/check_explorer_ui.py. It reads index.html from the wheel and fails unless every /_next/ script, stylesheet and font that page references is in the wheel too. Writing it surfaced a subtlety: the catch-all page's chunk is referenced percent-encoded (%5B%5B...slug%5D%5D) but stored as [[...slug]], so references are decoded before the lookup. Against real wheels: the full UI wheel passes, the same wheel with _next/ stripped fails and names all 14 missing assets, and the current UI-less 0.9.1265 wheel fails on the missing index.html. tst/unit/test_check_explorer_ui.py covers those cases plus the encoded reference.

Both UI checks looked only for index.html, so a wheel missing the _next/ scripts, stylesheets and
fonts would pass and serve a page that never loads. .github/scripts/check_explorer_ui.py reads
index.html from the wheel and fails unless every /_next/ asset it references is there; references
are percent-decoded, since the catch-all page's chunk is referenced as %5B%5B...slug%5D%5D but stored
as [[...slug]]. The release and the ui job both run it, and its unit tests cover a complete wheel,
missing assets, a missing index.html, an index with no built assets and the encoded reference.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bump-version github_actions Pull requests that update GitHub Actions code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant