Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab38656f05 | ||
|
|
b558a7e3d6 | ||
|
|
ef5080642c | ||
|
|
ef2d65716c |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"required_status_checks": {
|
"required_status_checks": {
|
||||||
"strict": true,
|
"strict": false,
|
||||||
"contexts": [
|
"contexts": [
|
||||||
"py3.12 on ubuntu-latest",
|
"py3.12 on ubuntu-latest",
|
||||||
"py3.13 on ubuntu-latest",
|
"py3.13 on ubuntu-latest",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
merge_group:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
pull_request:
|
pull_request:
|
||||||
|
|||||||
+38
-14
@@ -4,6 +4,7 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
merge_group:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
push:
|
push:
|
||||||
@@ -63,16 +64,18 @@ jobs:
|
|||||||
comfyui:
|
comfyui:
|
||||||
name: real ComfyUI compatibility
|
name: real ComfyUI compatibility
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
# Chromium and its system libraries come from the pinned Playwright image (tag = @playwright/test in
|
||||||
|
# tools/vendor/package-lock.json; tests/vendor/browser-image.test.mjs checks it). The runner install
|
||||||
|
# pulled them from Ubuntu's apt mirror on every run, and three runs (#349, #378, #382) crawled at
|
||||||
|
# ~26 kB/s for 11-12 minutes until the job hit its limit after every test had passed.
|
||||||
|
container:
|
||||||
|
image: mcr.microsoft.com/playwright:v1.64.1-noble
|
||||||
|
options: --user 1001
|
||||||
timeout-minutes: 24
|
timeout-minutes: 24
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v7
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-node@v7
|
|
||||||
with:
|
|
||||||
node-version: "24"
|
|
||||||
cache: npm
|
|
||||||
cache-dependency-path: tools/vendor/package-lock.json
|
|
||||||
- name: Read the reviewed runtime pins
|
- name: Read the reviewed runtime pins
|
||||||
id: runtime
|
id: runtime
|
||||||
run: |
|
run: |
|
||||||
@@ -97,21 +100,42 @@ jobs:
|
|||||||
ref: ${{ steps.runtime.outputs.kikoRevision }}
|
ref: ${{ steps.runtime.outputs.kikoRevision }}
|
||||||
path: .ci-kikotools
|
path: .ci-kikotools
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- uses: actions/setup-python@v7
|
# The image's own Python and Node 24: setup-python/setup-node install the runner's toolcache builds,
|
||||||
|
# whose entry points name host paths that do not exist in the container. The image has no ensurepip
|
||||||
|
# and the job has no root, so a checksum-pinned pip wheel bootstraps a user-space venv. The image
|
||||||
|
# Python must be the reviewed host Python, so a pin or image bump that disagrees fails here.
|
||||||
|
- name: Python environment on the reviewed host Python
|
||||||
|
env:
|
||||||
|
PIP_WHEEL: https://files.pythonhosted.org/packages/f3/6e/1736e5b4ae2b778ef2f81c47d797de9f891d4d8acb047a24ca37a60294dd/pip-26.2.1-py3-none-any.whl
|
||||||
|
PIP_SHA256: 71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e
|
||||||
|
PINNED_PYTHON: ${{ steps.runtime.outputs.pythonVersion }}
|
||||||
|
run: |
|
||||||
|
image="$(python3 -c 'import sys; print("%d.%d" % sys.version_info[:2])')"
|
||||||
|
[ "$image" = "$PINNED_PYTHON" ] || { echo "::error::image Python $image is not the pinned $PINNED_PYTHON"; exit 1; }
|
||||||
|
venv="$RUNNER_TEMP/venv" wheel="$RUNNER_TEMP/pip-26.2.1-py3-none-any.whl"
|
||||||
|
python3 -m venv --without-pip "$venv"
|
||||||
|
curl -fsSL --retry 3 -o "$wheel" "$PIP_WHEEL"
|
||||||
|
echo "$PIP_SHA256 $wheel" | sha256sum -c -
|
||||||
|
"$venv/bin/python" "$wheel/pip" install --no-index --no-cache-dir "$wheel"
|
||||||
|
echo "$venv/bin" >> "$GITHUB_PATH"
|
||||||
|
- run: python --version && node --version && node --version | grep -q '^v24\.'
|
||||||
|
# pip's download cache (torch alone is ~200 MB); inside the container ~ is the job's HOME (/github/home).
|
||||||
|
- name: Cache pip downloads
|
||||||
|
uses: actions/cache@v6
|
||||||
with:
|
with:
|
||||||
python-version: ${{ steps.runtime.outputs.pythonVersion }}
|
path: ~/.cache/pip
|
||||||
cache: pip
|
key: pip-${{ runner.os }}-playwright-v1.64.1-${{ hashFiles('requirements-dev.txt', '.ci-comfyui/requirements.txt', 'tools/vendor/comfyui-runtime.json') }}
|
||||||
cache-dependency-path: |
|
|
||||||
requirements-dev.txt
|
|
||||||
.ci-comfyui/requirements.txt
|
|
||||||
tools/vendor/comfyui-runtime.json
|
|
||||||
- name: Install CPU runtime and test dependencies
|
- name: Install CPU runtime and test dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
|
||||||
pip install "torch==${{ steps.runtime.outputs.torchVersion }}" "torchvision==${{ steps.runtime.outputs.torchvisionVersion }}" --index-url https://download.pytorch.org/whl/cpu
|
pip install "torch==${{ steps.runtime.outputs.torchVersion }}" "torchvision==${{ steps.runtime.outputs.torchvisionVersion }}" --index-url https://download.pytorch.org/whl/cpu
|
||||||
pip install -r .ci-comfyui/requirements.txt -r requirements-dev.txt "torch==${{ steps.runtime.outputs.torchVersion }}" "torchvision==${{ steps.runtime.outputs.torchvisionVersion }}"
|
pip install -r .ci-comfyui/requirements.txt -r requirements-dev.txt "torch==${{ steps.runtime.outputs.torchVersion }}" "torchvision==${{ steps.runtime.outputs.torchvisionVersion }}"
|
||||||
|
# npm's download cache, keyed on the lockfile. node_modules itself is still built fresh by npm ci.
|
||||||
|
- name: Cache npm downloads
|
||||||
|
uses: actions/cache@v6
|
||||||
|
with:
|
||||||
|
path: ~/.npm
|
||||||
|
key: npm-${{ runner.os }}-playwright-v1.64.1-${{ hashFiles('tools/vendor/package-lock.json') }}
|
||||||
- run: npm ci --prefix tools/vendor --ignore-scripts
|
- run: npm ci --prefix tools/vendor --ignore-scripts
|
||||||
- run: npx --prefix tools/vendor playwright install --with-deps chromium
|
|
||||||
- name: Classic and Nodes 2.0 startup and compatibility
|
- name: Classic and Nodes 2.0 startup and compatibility
|
||||||
env:
|
env:
|
||||||
PM_COMFY_ROOT: ${{ github.workspace }}/.ci-comfyui
|
PM_COMFY_ROOT: ${{ github.workspace }}/.ci-comfyui
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
name: Viewer.js vendor and compatibility
|
name: Viewer.js vendor and compatibility
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
merge_group:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
push:
|
push:
|
||||||
|
|||||||
@@ -356,6 +356,8 @@ class HistoryHooksTests(unittest.TestCase):
|
|||||||
self.assertGreaterEqual(time.monotonic() - began, 1.8)
|
self.assertGreaterEqual(time.monotonic() - began, 1.8)
|
||||||
self.assertLess(time.monotonic() - began, CEILING_SECONDS)
|
self.assertLess(time.monotonic() - began, CEILING_SECONDS)
|
||||||
release.set()
|
release.set()
|
||||||
|
# Drain first: the 2 s bound is not under test here (#370).
|
||||||
|
self.drain(registration)
|
||||||
self.assertTrue(registration.dispose(2))
|
self.assertTrue(registration.dispose(2))
|
||||||
|
|
||||||
def test_restore_disables_capture_and_clears_handles_before_fresh_epoch(self):
|
def test_restore_disables_capture_and_clears_handles_before_fresh_epoch(self):
|
||||||
|
|||||||
@@ -399,6 +399,10 @@ class OutputHooksTests(fixtures.unittest.TestCase):
|
|||||||
self.queue.task_done(1, history("rejected.png"), fixtures.status())
|
self.queue.task_done(1, history("rejected.png"), fixtures.status())
|
||||||
registration.writer.invalidate()
|
registration.writer.invalidate()
|
||||||
release.set()
|
release.set()
|
||||||
|
# Let the writer work through the 1,024 queued commands first
|
||||||
|
# (#370's pattern): on a busy Windows runner that took longer
|
||||||
|
# than dispose's 2 s bound (merge queue, #382).
|
||||||
|
self.drain(registration)
|
||||||
self.assertTrue(registration.dispose(2))
|
self.assertTrue(registration.dispose(2))
|
||||||
self.assertEqual(self.outputs(0)["state"], "not_captured")
|
self.assertEqual(self.outputs(0)["state"], "not_captured")
|
||||||
self.assertEqual(len(self.rows()), 1)
|
self.assertEqual(len(self.rows()), 1)
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ except ImportError:
|
|||||||
# Upper bound a blocked fake write waits for its release. Tests prove ordering
|
# Upper bound a blocked fake write waits for its release. Tests prove ordering
|
||||||
# with events, so this only caps a failing run and never decides a pass.
|
# with events, so this only caps a failing run and never decides a pass.
|
||||||
BLOCK_SECONDS = 10
|
BLOCK_SECONDS = 10
|
||||||
|
# A restore is a full verify/migrate/backup/replace cycle and is bound by the
|
||||||
|
# disk: a busy Windows runner took over 10 s (merge queue, #382). Waits that
|
||||||
|
# span a restore use this ceiling, which stays under the runner's 120 s hang
|
||||||
|
# watchdog, so a real hang still fails with the thread dump.
|
||||||
|
RESTORE_SECONDS = 90
|
||||||
|
|
||||||
|
|
||||||
class OutputWriterTests(fixtures.unittest.TestCase):
|
class OutputWriterTests(fixtures.unittest.TestCase):
|
||||||
@@ -317,8 +322,8 @@ class OutputWriterTests(fixtures.unittest.TestCase):
|
|||||||
|
|
||||||
Returns the patch and an event set once that rotation has returned on
|
Returns the patch and an event set once that rotation has returned on
|
||||||
the worker. A restore is a full verify/migrate/backup/replace cycle, so
|
the worker. A restore is a full verify/migrate/backup/replace cycle, so
|
||||||
the handoff waits on this event with the generous ``BLOCK_SECONDS``
|
the handoff waits on this event with the ``RESTORE_SECONDS`` ceiling
|
||||||
ceiling instead of the short worker-behavior deadline (TESTING.md,
|
instead of the short worker-behavior deadline (TESTING.md,
|
||||||
Windows portability preflight).
|
Windows portability preflight).
|
||||||
"""
|
"""
|
||||||
backup = os.path.join(self.directory.name, f"restore-{start.run_id}.db")
|
backup = os.path.join(self.directory.name, f"restore-{start.run_id}.db")
|
||||||
@@ -361,7 +366,7 @@ class OutputWriterTests(fixtures.unittest.TestCase):
|
|||||||
)
|
)
|
||||||
with rotation:
|
with rotation:
|
||||||
self.finalization(writer, start)
|
self.finalization(writer, start)
|
||||||
self.assertTrue(rotated.wait(BLOCK_SECONDS))
|
self.assertTrue(rotated.wait(RESTORE_SECONDS))
|
||||||
self.wait_for(lambda: writer.status().pending == 0)
|
self.wait_for(lambda: writer.status().pending == 0)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.db.get_execution_outputs(start.run_id)["state"],
|
self.db.get_execution_outputs(start.run_id)["state"],
|
||||||
@@ -403,7 +408,7 @@ class OutputWriterTests(fixtures.unittest.TestCase):
|
|||||||
self.assertEqual(writer.status().pending, 1)
|
self.assertEqual(writer.status().pending, 1)
|
||||||
self.assertEqual(writer.context_budget.charged_bytes, charge)
|
self.assertEqual(writer.context_budget.charged_bytes, charge)
|
||||||
release.set()
|
release.set()
|
||||||
self.assertTrue(rotated.wait(BLOCK_SECONDS))
|
self.assertTrue(rotated.wait(RESTORE_SECONDS))
|
||||||
self.wait_for(lambda: writer.status().pending == 0)
|
self.wait_for(lambda: writer.status().pending == 0)
|
||||||
self.assertEqual(writer.context_budget.charged_bytes, 0)
|
self.assertEqual(writer.context_budget.charged_bytes, 0)
|
||||||
self.assertFalse(writer.status().accepting)
|
self.assertFalse(writer.status().accepting)
|
||||||
@@ -567,6 +572,8 @@ class OutputWriterTests(fixtures.unittest.TestCase):
|
|||||||
self.assertIs(replacement.context_budget, writer.context_budget)
|
self.assertIs(replacement.context_budget, writer.context_budget)
|
||||||
self.assertIsNone(replacement.context_budget.reserve(8388608))
|
self.assertIsNone(replacement.context_budget.reserve(8388608))
|
||||||
release.set()
|
release.set()
|
||||||
|
# Drain first: the 2 s bound is not under test here (#370).
|
||||||
|
drain_writer(writer)
|
||||||
self.assertTrue(writer.close(2))
|
self.assertTrue(writer.close(2))
|
||||||
finally:
|
finally:
|
||||||
release.set()
|
release.set()
|
||||||
@@ -608,6 +615,8 @@ class OutputWriterTests(fixtures.unittest.TestCase):
|
|||||||
writer.invalidate()
|
writer.invalidate()
|
||||||
self.assertEqual(writer.context_budget.charged_bytes, 0)
|
self.assertEqual(writer.context_budget.charged_bytes, 0)
|
||||||
release.set()
|
release.set()
|
||||||
|
# Drain first: the 2 s bound is not under test here (#370).
|
||||||
|
drain_writer(writer)
|
||||||
self.assertTrue(writer.close(2))
|
self.assertTrue(writer.close(2))
|
||||||
|
|
||||||
def test_malformed_context_omission_preserves_independent_valid_output(self):
|
def test_malformed_context_omission_preserves_independent_valid_output(self):
|
||||||
@@ -698,6 +707,8 @@ class OutputWriterTests(fixtures.unittest.TestCase):
|
|||||||
release.set()
|
release.set()
|
||||||
invalidator.join(BLOCK_SECONDS)
|
invalidator.join(BLOCK_SECONDS)
|
||||||
self.assertFalse(invalidator.is_alive())
|
self.assertFalse(invalidator.is_alive())
|
||||||
|
# Drain first: the 2 s bound is not under test here (#370).
|
||||||
|
drain_writer(writer)
|
||||||
self.assertTrue(writer.close(2))
|
self.assertTrue(writer.close(2))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.db.get_execution_outputs(start.run_id)["state"], "complete"
|
self.db.get_execution_outputs(start.run_id)["state"], "complete"
|
||||||
|
|||||||
Vendored
+21
@@ -28,3 +28,24 @@ for (const name of ['frontend-coverage', 'browser-workflows']) {
|
|||||||
assert.match(body, /node --version \| grep -q '\^v24\\\.'/, 'the image Node is the Node 24 the project requires');
|
assert.match(body, /node --version \| grep -q '\^v24\\\.'/, 'the image Node is the Node 24 the project requires');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The real-host job drives Chromium against a managed ComfyUI. `playwright install --with-deps` pulled the browser's
|
||||||
|
// system packages from Ubuntu's apt mirror on every run, and three runs (#349, #378, #382) crawled at ~26 kB/s until
|
||||||
|
// the job hit its limit after every test had passed. The pinned image ships Chromium and its libraries, so the job runs
|
||||||
|
// there; the image Python must be the reviewed host Python from tools/vendor/comfyui-runtime.json.
|
||||||
|
test('real ComfyUI compatibility runs in the pinned Playwright image on the reviewed host Python', () => {
|
||||||
|
const testWorkflow = readFileSync(new URL('../../.github/workflows/test.yml', import.meta.url), 'utf8');
|
||||||
|
const runtime = JSON.parse(readFileSync(new URL('../../tools/vendor/comfyui-runtime.json', import.meta.url), 'utf8'));
|
||||||
|
const match = testWorkflow.match(/^ {2}comfyui:\n([\s\S]*?)(?=^ {2}[a-z][a-z-]*:\n|(?![\s\S]))/m);
|
||||||
|
assert.ok(match, 'comfyui job exists');
|
||||||
|
const body = match[1];
|
||||||
|
assert.match(body, new RegExp(`^ container:\\n image: mcr\\.microsoft\\.com/playwright:v${version.replaceAll('.', '\\.')}-noble$`, 'm'));
|
||||||
|
assert.doesNotMatch(body, /playwright install/, 'Chromium and its system libraries come from the image, not apt');
|
||||||
|
assert.doesNotMatch(body, /apt-get|actions\/setup-(python|node)@/);
|
||||||
|
assert.match(body, /python3 -m venv --without-pip /, 'a user-space venv on the image Python');
|
||||||
|
assert.match(body, /sha256sum -c/, 'the pip wheel that bootstraps the venv is checksum-pinned');
|
||||||
|
assert.match(body, /node --version \| grep -q '\^v24\\\.'/, 'the image Node is Node 24');
|
||||||
|
// The image Python is checked against the pin at run time, so a pin or image bump that disagrees fails loudly.
|
||||||
|
assert.match(body, /steps\.runtime\.outputs\.pythonVersion/, 'the job compares the image Python with the reviewed pin');
|
||||||
|
assert.match(runtime.pythonVersion, /^3\.12$/, 'the reviewed host Python is the image Python (3.12 on noble)');
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user