Repository navigation
Expand file tree
/
Copy pathsetup.py
More file actions
187 lines (163 loc) · 8.4 KB
/
Copy pathsetup.py
File metadata and controls
187 lines (163 loc) · 8.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
"""KiroCrew packaging — plain setuptools build.
Package metadata, dependencies, and entry points live in ``setup.cfg`` and
``pyproject.toml``; this file only adds a custom ``build_py`` step that copies
the pre-built frontend assets from ``src/kiro_crew/static/dist`` into the
package.
The frontend is built separately with npm/Vite in the ``website/`` directory
and the resulting ``dist/`` is staged as ``src/kiro_crew/static/dist`` before
packaging. Vite emits content-hashed filenames that change on every build, so
``static/dist/`` is intentionally excluded from the ``package_data`` globs in
``setup.cfg``; we copy the directory tree directly here instead.
"""
from __future__ import annotations
import importlib.util
import os
import shutil
import subprocess
import sys
from types import ModuleType
from setuptools import Command, setup
from setuptools.command.build_py import build_py
def _load_edition_stamp() -> ModuleType:
"""Load ``kiro_crew/_edition_stamp.py`` by path; importing the package here would pull its deps."""
path = os.path.join(
os.path.dirname(os.path.abspath(__file__)), "src", "kiro_crew", "_edition_stamp.py"
)
spec = importlib.util.spec_from_file_location("_kiro_crew_edition_stamp", path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
class E2eTestCommand(Command):
"""Run the gated E2E smoke suite (``test/test_e2e_smoke.py``).
Invoked as ``python setup.py test_e2e``. Distinct from the regular test run
(the Makefile ``test`` target / plain ``pytest``) on purpose:
* sets ``KIROCREW_E2E=1`` to lift the ``skipif`` gate in
test_e2e_smoke.py (those tests spawn a real gateway subprocess);
* runs only that one file, serially, clearing the default
``[tool:pytest]`` addopts (``-n auto`` + ``--cov`` + ``--timeout=120``)
-- xdist would spawn one gateway per worker and coverage of a
subprocess gateway is meaningless.
"""
description = "Run the E2E suite (smoke + Playwright dashboard specs)"
user_options: list = []
def initialize_options(self) -> None:
pass
def finalize_options(self) -> None:
pass
def run(self) -> None:
base = os.path.dirname(os.path.abspath(__file__))
env = dict(os.environ)
env["KIROCREW_E2E"] = "1"
# Turn the on-loop persistence discipline into a CI-ENFORCED invariant
# (not just a dev-only convention). The e2e harness spawns a REAL gateway
# subprocess and drives real chat turns through it; ``spawn_feature_gateway``
# inherits this ``env``. With strict mode on, ANY session-JSONL mutator that
# enters ``ConversationLog._locked`` on the gateway event loop (i.e. a raw
# on-loop call that skipped the ``*_off_loop`` helpers) raises
# ``OnLoopPersistError`` immediately, failing the e2e gate at PR time —
# instead of silently losing transcript data under real production
# contention. Bare unit pytest deliberately stays non-strict (its async
# harness legitimately drives mutators on the loop as a convenience); the
# allowlist guard in test/test_history_locking_remediation.py adds a
# deterministic static check that no NEW un-offloaded production call-site
# appears regardless of e2e coverage.
env["KIROCREW_STRICT_ON_LOOP_PERSIST"] = "1"
cmd = [
sys.executable, "-m", "pytest",
os.path.join("test", "test_e2e_smoke.py"),
# Folded in: the dashboard Playwright suite boots the same harness
# gateway (wired to the packaged fake ACP backend via
# KIROCREW_KIRO_BIN) and shells `playwright test` against it, so
# `test_e2e` is the single offline pre-release E2E gate.
os.path.join("test", "test_playwright_e2e.py"),
# Drop the heavy unit-test addopts (-n auto, --cov, --timeout=120);
# the e2e suite runs serially with a longer per-test timeout. 1800s
# gives the Playwright fold headroom: the browser suite runs ~4-5
# min per interpreter leg and retries:2 under box contention can
# push a retry-heavy run past a 600s cap, which would kill it as a
# generic pytest timeout and hide the actual failing specs. Smoke
# tests finish in seconds, so the larger cap costs them nothing.
"-o", "addopts=",
"-p", "no:cacheprovider",
"-v",
"--timeout=1800",
]
print("[test_e2e] KIROCREW_E2E=1 " + " ".join(cmd))
rc = subprocess.call(cmd, cwd=base, env=env)
if rc != 0:
raise SystemExit(rc)
class BuildWithFrontend(build_py):
"""Custom build_py that copies the pre-built frontend dist/ into the package.
Expects ``src/kiro_crew/static/dist`` to already exist in-tree (built by
``npm run build`` in the ``website/`` directory and staged by
``python -m kiro_crew.frontend stage``). If it is missing we print a warning
telling the user to build the frontend, but do not fail — the backend is
still usable without the bundled web UI assets.
"""
def run(self) -> None:
super().run()
self._stamp_edition()
base = os.path.dirname(os.path.abspath(__file__))
src_dist = os.path.join(base, "src", "kiro_crew", "static", "dist")
if os.path.isdir(src_dist):
build_dist = os.path.join(
self.build_lib, "kiro_crew", "static", "dist"
)
if os.path.isdir(build_dist):
shutil.rmtree(build_dist)
shutil.copytree(src_dist, build_dist)
else:
print(
"WARNING: frontend assets not found at "
f"{src_dist}\n"
" The bundled web UI will be missing from this build.\n"
" Build the frontend first:\n"
" cd website && npm install && npm run build\n"
" then stage it: make build, or\n"
" PYTHONPATH=src python -m kiro_crew.frontend stage ."
)
self._copy_changelog(base)
def _stamp_edition(self) -> None:
"""Write ``$KIROCREW_BUILD_EDITION`` (default ``oss``) into the BUILT edition.py.
Only the copy under ``build_lib`` changes, so the source tree stays
``oss``. It is rewritten on every build, ``oss`` included: ``build_py``
skips copying a source file older than its built copy, so a stale
LTS stamp in a reused ``build/`` would otherwise survive into the
next default build.
"""
stamp = _load_edition_stamp()
edition = stamp.requested_edition(os.environ)
path = os.path.join(self.build_lib, "kiro_crew", "edition.py")
try:
if getattr(self, "editable_mode", False):
# An editable install runs the source tree, which stays oss. The
# packaging targets run one as a prerequisite with the variable
# still set, so check the value but stamp nothing.
base = os.path.dirname(os.path.abspath(__file__))
stamp.check_edition(os.path.join(base, "src", "kiro_crew", "edition.py"), edition)
print(f"Editable install: {stamp.ENV}={edition} not stamped; source is oss")
return
stamp.stamp_file(path, edition)
except ValueError as exc:
raise SystemExit(f"ERROR: {exc}") from exc
print(f"Stamped edition={edition} -> {path}")
def _copy_changelog(self, base: str) -> None:
"""Bundle the repo-root CHANGELOG.md into the package.
Pip-wheel installs ship no source tree, so the dashboard's
``/api/changelog`` endpoint (handlers/updates_channel.py:_changelog_path) falls
back to a bundled ``kiro_crew/CHANGELOG.md``. Copy it in here rather than
via package_data because it lives at the repo root, outside the
``src/kiro_crew`` package tree that setuptools globs.
"""
src_changelog = os.path.join(base, "CHANGELOG.md")
if os.path.isfile(src_changelog):
pkg_dir = os.path.join(self.build_lib, "kiro_crew")
if os.path.isdir(pkg_dir):
shutil.copy2(src_changelog, os.path.join(pkg_dir, "CHANGELOG.md"))
setup(
cmdclass={
"build_py": BuildWithFrontend,
"test_e2e": E2eTestCommand,
},
)