Skip to content

🔨🤖 Dev slots: run several checkouts on one docker daemon - #9850

Draft
sanderegg wants to merge 5 commits into
ITISFoundation:masterfrom
sanderegg:maintenance/pytest-per-run-host-ports
Draft

sanderegg wants to merge 5 commits into
ITISFoundation:masterfrom
sanderegg:maintenance/pytest-per-run-host-ports

Conversation

@sanderegg

@sanderegg sanderegg commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Important

Developers, after pulling this:

  1. Run make devenv once, in any worktree. It installs pre-commit as a uv tool in ~/.local/bin, removes it from .venv and installs the git hook from the uv tool. To check, run sed -n 6p .git/hooks/pre-commit. It should print ~/.local/share/uv/tools/pre-commit/bin/python.
  2. Recreate .env from .env-devel. Until you do, make prints the diff and stops. Your main checkout then gets slot 0 and keeps its current stack names, ports and docker volumes, including the postgres and S3 data.
  3. Remove test stacks with the old names. List them with docker stack ls | grep '^pytest-' and remove each with docker stack rm <name>.

To work in a second checkout, run git worktree add ../osparc-simcore-<topic> <branch>, then make devenv and make build (or make build-devel) in it. Its first make takes the next free slot and asks you to run it again once. make info lists the slot's stack names, and make up-* prints its ports.

Details are in docs/parallel-worktrees.md.

What do these changes do?

Several checkouts of the repo, for example git worktrees, can now build, deploy and run integration tests on one docker daemon at the same time.

Each checkout leases a dev slot N from 0 to 9. The slot decides the stack names, host ports and local image tags of the checkout.

slot 0 same as before? slot N > 0 (new)
dev stacks master-simcore, ops, vendors ✅ yes simcore-sN, ops-sN, vendors-sN
dev volumes, core master-simcore_postgres_data, _redis-data, _rabbit_data, _computational_shared_data ✅ yes simcore-sN_postgres_data, _redis-data, _rabbit_data, _computational_shared_data
dev volumes, ops ops_s3_storage_data, ops_portainer_data ✅ yes ops-sN_s3_storage_data, ops-sN_portainer_data
local images local/<svc>:production, :development ✅ yes local/<svc>:production-sN, :development-sN
host ports 9081, 9001, 3001, ... ✅ yes slot-0 port + N*100, e.g. 9381 in slot 3
test stacks pytest-s0-<branch>-core, -ops ⚠️ no, was pytest-<branch>-core-<uid> pytest-sN-<branch>-core, -ops
test volumes pytest-s0-<branch>-core_postgres_data, ... ⚠️ no, follow the test stack name pytest-sN-<branch>-core_postgres_data, ...

Slot 0 keeps the current dev stack names, volume names, ports and image tags, so a single checkout keeps its database and S3 data, and CI behaves as before. The dev volumes depend only on the slot, not on the branch, so the data stays when a worktree switches branch. Test stacks name their volumes after the stack (pytest-sN-<branch>-core_postgres_data, ...) and remove them on teardown.

Changes:

  • scripts/dev_slot.py leases the slots. It uses only the standard library because make runs it before any venv exists, and pytest loads the same file. A config-only docker network osparc-dev-slot-N, labelled with the checkout path, holds slot N. Docker refuses a second network with the same name, so two checkouts cannot lease the same slot. Each lease deletes the networks of checkouts that no longer exist.
  • Every published port in the compose files is ${X_HOST_PORT:-<slot-0 port>}. The ops volumes take their names from the ops stack.
  • In .env-devel, the URLs with a host port (S3_ENDPOINT, AGENT_VOLUMES_CLEANUP_S3_ENDPOINT, DIRECTOR_PUBLISHED_HOST_NAME, INVITATIONS_OSPARC_URL) use the slot's port variables.
  • In the Makefile, make .env and make up-* lease the slot, and slot=N moves the checkout to slot N. make down removes only this checkout's stacks. make leave stops with an error while other checkouts have stacks, unless you pass force=1. The build and tag targets add the slot's suffix to the image tags.
  • The frontend compiler container, its compose project and the qx serve port depend on the slot.
  • .vscode/Makefile writes launch.json with the slot's debug ports. The clusters-keeper attach config now uses port 3015 instead of 3006, which belongs to api-server.
  • In pytest-simcore:
    • Test stacks are named pytest-sN-<branch>-{core,ops} instead of pytest-<branch>-{core,ops}-<uid> (🔨 Scope test docker stacks to each checkout for concurrent runs #9832). Long branch names are cut to 18 characters.
    • Before deploying, the tests remove their slot's stacks left from another branch.
    • A run stops before deploying if another stack or container already publishes one of the slot's host ports, for example this checkout's dev stack.
    • tests/public-api reads the traefik and api-server ports from the slot.
  • docker_compose.py and docker_swarm.py now use LF line endings instead of CRLF.
  • make devenv installs pre-commit as a uv tool and installs the git hook from it. All worktrees share .git/hooks, and pre-commit install writes the path of its own interpreter into the hook. Before, the hook pointed to the .venv of the worktree that ran make devenv last, and commits failed in every worktree once that .venv was gone. This PR removes pre-commit from requirements/devenv.txt and from all 33 _tools.txt files. I regenerated them without --upgrade, so the diff only deletes pre-commit and the packages that only it needed.

Related issue/s

Follow-up of #9832 and #9834.

How to test

  • Run pytest packages/pytest-simcore/tests/. The tests cover the slot names and ports, port collisions between all slots, the compose defaults, leasing (reuse, moving, concurrent leases, deleted checkouts) and the port check.
  • Run make up-devel in two worktrees. docker stack ls shows master-simcore and simcore-s1, and the endpoint tables list different ports.
  • Start make up-devel in a checkout, then run its integration tests. They stop and ask you to run make down.

Dev-ops

  • Only local development files change: .env-devel, docker-compose.local.yml, docker-compose-ops*.yml, docker-compose-build.yml and the Makefiles. The deployed compose files are unchanged.
  • New variables, with slot-0 defaults: OSPARC_DEV_SLOT, *_HOST_PORT, SWARM_OPS_STACK_NAME, SWARM_VENDORS_STACK_NAME, LOCAL_IMAGE_TAG_SUFFIX.
  • Assisted-by: GitHub Copilot

@github-actions github-actions Bot added a:infra+ops maintenance of infrastructure or operations (discussed in retro) a:doc documentation, reporting, admin tasks a:director-v2 issue related with the director-v2 service a:redis area: redis service labels Oct 19, 2026
@pcrespov
pcrespov requested a balanced review from Copilot October 19, 2026 14:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

Unresolved routing, checkout isolation, and lifecycle defects prevent safe concurrent test runs.

8 open issues
What changed in this PR

Extends checkout-scoped test infrastructure with ephemeral host ports so separate worktrees can share one Docker daemon.

Changes:

  • Rewrites test ports, stages deployment, and updates embedded endpoints.
  • Adds checkout claims, swarm coordination, scoped lookups, and regression tests.
  • Updates director-v2 fixtures and maintenance-skill metadata.

Validation: Two isolated Python checks ran. Project tests and pre-commit were not run in the dependency-free review environment.

File Description
services/​docker-compose.local.yml Documents test/debug port behavior; comment-only change.
services/​director-v2/​tests/​integration/​02/​test_mixed_dynamic_sidecar_and_legacy_project.py Removes fixed stack override.
services/​director-v2/​tests/​integration/​02/​test_dynamic_sidecar_nodeports_integration.py Removes fixed stack override.
services/​director-v2/​tests/​integration/​02/​conftest.py Derives checkout-specific network name.
services/​director-v2/​tests/​conftest.py Uses checkout-specific stack configuration.
packages/​pytest-simcore/​tests/​test_helpers_swarm_coordination.py Tests swarm membership and cleanup.
packages/​pytest-simcore/​tests/​test_helpers_stack_names.py Tests stack names and run directories.
packages/​pytest-simcore/​tests/​test_helpers_host_ports.py Tests port rewriting and collection.
packages/​pytest-simcore/​tests/​test_helpers_docker.py Checks lookup names against compose services.
packages/​pytest-simcore/​tests/​test_helpers_checkout_claim.py Tests checkout claim lifecycle.
packages/​pytest-simcore/​src/​pytest_simcore/​traefik_service.py Scopes Traefik port lookups.
packages/​pytest-simcore/​src/​pytest_simcore/​simcore_webserver_service.py Scopes webserver port lookup.
packages/​pytest-simcore/​src/​pytest_simcore/​simcore_storage_service.py Scopes storage port lookup.
packages/​pytest-simcore/​src/​pytest_simcore/​simcore_services.py Scopes generic service lookups.
packages/​pytest-simcore/​src/​pytest_simcore/​simcore_dask_service.py Scopes scheduler port lookup.
packages/​pytest-simcore/​src/​pytest_simcore/​s3_storage_service.py Uses checkout-specific ops stack.
packages/​pytest-simcore/​src/​pytest_simcore/​redis_service.py Corrects and scopes Redis lookup.
packages/​pytest-simcore/​src/​pytest_simcore/​rabbit_service.py Scopes broker and management lookups.
packages/​pytest-simcore/​src/​pytest_simcore/​postgres_service.py Scopes database lookup and assertion.
packages/​pytest-simcore/​src/​pytest_simcore/​httpbin_service.py Scopes container names by checkout.
packages/​pytest-simcore/​src/​pytest_simcore/​helpers/​xdist.py Identifies the run-owner process.
packages/​pytest-simcore/​src/​pytest_simcore/​helpers/​swarm_coordination.py Coordinates daemon-wide swarm ownership.
packages/​pytest-simcore/​src/​pytest_simcore/​helpers/​stack_names.py Generates stack and run identifiers.
packages/​pytest-simcore/​src/​pytest_simcore/​helpers/​host_ports.py Implements port policy, rebaking, and reporting.
packages/​pytest-simcore/​src/​pytest_simcore/​helpers/​docker.py Adds stack-filtered port discovery.
packages/​pytest-simcore/​src/​pytest_simcore/​helpers/​checkout_claim.py Guards concurrent checkout use.
packages/​pytest-simcore/​src/​pytest_simcore/​docker_swarm.py Stages deployment and coordinates cleanup.
packages/​pytest-simcore/​src/​pytest_simcore/​docker_compose.py Applies scoped names and port policy.
packages/​pytest-simcore/​src/​pytest_simcore/​docker_api_proxy.py Scopes proxy port lookup.
.github/​skills/​update-user-messages/​SKILL.md Updates metadata and example formatting.
.github/​skills/​refactor-pydantic-annotated-fields/​SKILL.md Updates metadata and example formatting.
.github/​skills/​refactor-aiohttp-appkey/​SKILL.md Updates metadata and example formatting.
.github/​skills/​fix-pre-commit-hooks/​SKILL.md Updates skill metadata and lint comments.

Review effort: Balanced

Comment thread packages/pytest-simcore/src/pytest_simcore/docker_compose.py Outdated
Comment thread packages/pytest-simcore/src/pytest_simcore/helpers/docker.py Outdated
Comment on lines +143 to +144
for old, new in replacements.items():
replaced, n = patterns[old].subn(f":{new}", new_value)
Comment on lines +228 to +232
claim_checkout(
get_checkout_token(osparc_simcore_root_dir),
run_id=token,
watched_pid=watched_pid,
)
Comment thread packages/pytest-simcore/src/pytest_simcore/docker_swarm.py Outdated
log.info("published host ports of this run:\n%s", lines)
if run_dir is not None:
report_path = run_dir / "published_ports.json"
report_path.write_text(yaml.dump(table, default_flow_style=False, sort_keys=True))
Comment thread packages/pytest-simcore/src/pytest_simcore/helpers/stack_names.py Outdated

@pcrespov pcrespov left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

really cool!

Comment thread .github/skills/fix-pre-commit-hooks/SKILL.md
Comment thread .github/skills/fix-pre-commit-hooks/SKILL.md
Comment thread .github/skills/refactor-pydantic-annotated-fields/SKILL.md
Comment thread packages/pytest-simcore/src/pytest_simcore/helpers/checkout_claim.py Outdated
Comment thread packages/pytest-simcore/src/pytest_simcore/helpers/checkout_claim.py Outdated
Comment thread packages/pytest-simcore/src/pytest_simcore/helpers/checkout_claim.py Outdated
Comment thread packages/pytest-simcore/src/pytest_simcore/helpers/docker.py Outdated
Comment thread packages/pytest-simcore/src/pytest_simcore/helpers/host_ports.py Outdated
Comment thread packages/pytest-simcore/src/pytest_simcore/helpers/xdist.py
Comment thread packages/pytest-simcore/src/pytest_simcore/helpers/swarm_coordination.py Outdated
@sanderegg
sanderegg marked this pull request as draft October 19, 2026 15:21
@sanderegg sanderegg added this to the KonradZuse milestone Oct 19, 2026

@GitHK GitHK left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ask again when ready

sanderegg and others added 2 commits October 22, 2026 18:15
Stack/network/volume/image names and host ports are daemon-global, so two
checkouts (e.g. git worktrees) collided on `make up-*`, `make build` and the
integration tests. Every checkout now leases a dev slot N (0-9):

- stacks master-simcore/ops/vendors (slot 0, unchanged) or simcore-sN/ops-sN/
  vendors-sN; pytest stacks pytest-sN-{core,ops}; traefik zone per slot
- host ports = slot-0 port + N*100 via ${*_HOST_PORT:-<slot-0 port>} in the
  compose files and in the .env-devel URLs (S3_ENDPOINT, ...)
- local images tagged production-sN/development-sN; frontend compiler
  container/compose project and qx serve port scoped per slot
- the registry is the daemon: a config-only network osparc-dev-slot-N labelled
  with the checkout path (atomic lease, stale markers reclaimed on lease)
- `make down` only removes this checkout's stacks; `make leave` refuses while
  other checkouts' stacks run unless force=1; VS Code launch.json rendered
  with the slot's debug ports
- pytest leases the checkout's slot, fails fast when the slot's ports are
  published by anything else (e.g. this checkout's dev stack), and replaces
  the branch+uid stack names

See docs/parallel-worktrees.md and scripts/dev_slot.py.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
- docker_compose.py and docker_swarm.py are stored with LF line endings
- shorter comments, docstrings and messages in the dev-slot code
- docs/parallel-worktrees.md rewritten concisely

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@sanderegg
sanderegg force-pushed the maintenance/pytest-per-run-host-ports branch from a423fda to 7919702 Compare October 22, 2026 16:15
@github-actions github-actions Bot added a:frontend issue affecting the front-end (area group) a:static-webserver static-webserver service labels Oct 22, 2026
@sanderegg sanderegg changed the title 🔨 Publish test stack host ports ephemerally per pytest run 🔨🤖 Dev slots: run several checkouts on one docker daemon Oct 22, 2026
sanderegg and others added 3 commits October 22, 2026 18:34
- the slot keeps checkouts apart, the branch (cut to 18 chars) tells humans
  what a stack is
- before deploying, test stacks of the same slot left from another branch are
  removed
- dev stacks keep their slot-only names (simcore-sN, ...)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…worktrees

Git worktrees share .git/hooks, and `pre-commit install` writes the
interpreter that ran it into the hook. With `make devenv` that was the
.venv of whichever worktree ran it last, so commits in every worktree broke
once that .venv was removed or recreated.

`make devenv` now installs pre-commit as a uv tool and installs the hook from
there. `make clean-hooks` removes the hook from the common git dir, which also
works inside a worktree.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
With pre-commit in .venv, `pre-commit install` from an activated venv
pointed the shared hook back to that .venv. pre-commit is now only installed
as a uv tool by `make devenv`, which also removes it from existing venvs.

- requirements/devenv.txt: drop pre-commit
- */requirements/_tools.txt: regenerated without --upgrade; only pre-commit
  and the packages it alone pulled in are removed

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Comment thread Makefile
# Creating config for stack with 'local/{service}:production' (except of static-webserver -> static-webserver:development) to $@
@export DOCKER_REGISTRY=local && \
export DOCKER_IMAGE_TAG=production && \
export DOCKER_IMAGE_TAG=production$(LOCAL_IMAGE_TAG_SUFFIX) && \
Comment thread Makefile
.init-swarm:
# Ensures swarm is initialized (careful we use a default pool of 172.20.0.0/14. Ensure you do not use private IPs in that range!)
$(if $(SWARM_HOSTS),,docker swarm init --advertise-addr=$(get_my_ip) --default-addr-pool 172.20.0.0/14)
@$(_lease_dev_slot)
Comment thread scripts/dev_slot.py
if checkout and Path(checkout).is_dir():
alive[slot] = checkout
else:
_remove_marker(slot, run)
Comment thread .vscode/Makefile

all: $(VSCODE_CONFIGS) ## creates all vscode config files from templates

launch.json: launch.template.json ## creates launch.json from its template, with the dev slot's debug ports
Comment thread Makefile
Comment on lines +694 to +696
leased=$$($(DEV_SLOT_SCRIPT) lease --checkout $(CURDIR) --env-file $(CURDIR)/.env $(if $(slot),--slot $(slot))); \
if [ "$$leased" != "$(OSPARC_DEV_SLOT)" ]; then \
echo "ERROR: this checkout now holds dev slot $$leased (this make run used $(OSPARC_DEV_SLOT)): re-run make"; \
Comment thread scripts/dev_slot.py

# slot markers ----------------------------------------------------------------------------

type RunDocker = Callable[[list[str]], subprocess.CompletedProcess[str]]

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Comment thread Makefile
Comment on lines +532 to 533
$(filter $(shell docker stack ls --format={{.Name}} 2>/dev/null),$(SWARM_STACK_NAME) $(SWARM_VENDORS_STACK_NAME) $(SWARM_OPS_STACK_NAME)),\
docker stack rm $(stack);)
Comment thread Makefile
$(if $(wildcard $@), \
@echo "WARNING ##### $< is newer than $@ ####"; diff -uN $@ $<; false;,\
@echo "WARNING ##### $@ does not exist, cloning $< as $@ ############"; cp $< $@)
@$(_lease_dev_slot)
Comment thread scripts/dev_slot.py
Comment on lines +297 to +299
for extra in owned:
if extra != slot:
_remove_marker(extra, run)

static-webserver:
image: local/static-webserver:${BUILD_TARGET:?build_target_required}
image: local/static-webserver:${BUILD_TARGET:?build_target_required}${LOCAL_IMAGE_TAG_SUFFIX:-}
@sanderegg
sanderegg force-pushed the maintenance/pytest-per-run-host-ports branch from 7919702 to cdb7483 Compare October 23, 2026 15:18
@sanderegg
sanderegg force-pushed the maintenance/pytest-per-run-host-ports branch 2 times, most recently from 8c639d4 to dc0750c Compare October 24, 2026 13:37
@github-actions github-actions Bot added a:storage issue related to storage service a:director issue related with the director service a:webserver webserver's codebase. Assigning the area is particularly useful for bugs a:services-library issues on packages/service-libs a:api framework api, data schemas, a:database associated to postgres service and postgres-database package e2e Bugs found by or related to the end-2-end testing a:catalog catalog service a:apiserver api-server service a:dynamic-sidecar dynamic-sidecar service a:dask-service Any of the dask services: dask-scheduler/sidecar or worker a:models-library a:autoscaling autoscaling service in simcore's stack a:ooil integration-library or ooil a:agent agent service a:invitations invitations service a:resource-usage-tracker resource usage tracker service a:clusters-keeper a:payments area: payments service a:dynamic-scheduler a:aws-library a:datcore-adapter a:simcore-sdk a:notifications a:celery-library dependencies Pull requests that update a dependency file labels Oct 24, 2026
@sonarqubecloud

Copy link
Copy Markdown

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

Labels

a:agent agent service a:api framework api, data schemas, a:apiserver api-server service a:autoscaling autoscaling service in simcore's stack a:aws-library a:catalog catalog service a:celery-library a:clusters-keeper a:dask-service Any of the dask services: dask-scheduler/sidecar or worker a:database associated to postgres service and postgres-database package a:datcore-adapter a:director issue related with the director service a:director-v2 issue related with the director-v2 service a:doc documentation, reporting, admin tasks a:dynamic-scheduler a:dynamic-sidecar dynamic-sidecar service a:frontend issue affecting the front-end (area group) a:infra+ops maintenance of infrastructure or operations (discussed in retro) a:invitations invitations service a:models-library a:notifications a:ooil integration-library or ooil a:payments area: payments service a:redis area: redis service a:resource-usage-tracker resource usage tracker service a:services-library issues on packages/service-libs a:simcore-sdk a:static-webserver static-webserver service a:storage issue related to storage service a:webserver webserver's codebase. Assigning the area is particularly useful for bugs dependencies Pull requests that update a dependency file e2e Bugs found by or related to the end-2-end testing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants