Compare commits

...
3 Commits
Author SHA1 Message Date
devx-ci-bot 663572768b release: v0.19.2
Post-merge / detect-type (push) Successful in 41s
Post-merge / validate-commit-msg (push) Has been skipped
Post-merge / release (push) Has been skipped
Post-merge / sync-wiki (push) Has been skipped
Post-merge / vikunja (push) Has been skipped
Post-merge / configure-repo (push) Has been skipped
Post-merge / publish (push) Has been skipped
Post-merge / badges (push) Successful in 52s
2026-06-27 01:38:41 +02:00
emil 1f2533872d DEVX-66: fix: calibrate molecule weights from actual CI execution times
Post-merge / detect-type (push) Successful in 31s
Post-merge / sync-wiki (push) Successful in 48s
Post-merge / vikunja (push) Successful in 49s
Post-merge / validate-commit-msg (push) Successful in 45s
Post-merge / configure-repo (push) Successful in 52s
Post-merge / release (push) Successful in 1m9s
Post-merge / badges (push) Successful in 1m24s
Post-merge / publish (push) Successful in 52s
2026-06-26 23:36:58 +00:00
gitea-actions-bot cb7e9dbc7e chore: update badge URLs to commit 0ab8d43a [skip ci] 2026-06-26 21:39:38 +02:00
6 changed files with 101 additions and 26 deletions
+6
View File
@@ -2,6 +2,12 @@
All notable changes to this project will be documented in this file.
## [0.19.2] - 2026-06-26
### Bug Fixes
- Calibrate molecule weights from actual CI execution times
## [0.19.1] - 2026-06-26
### Refactor
+6 -6
View File
@@ -16,12 +16,12 @@ quality badges.
[![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/816fbbbb68d1337cf9104b87c643ab2aa2fea62c/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/816fbbbb68d1337cf9104b87c643ab2aa2fea62c/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/816fbbbb68d1337cf9104b87c643ab2aa2fea62c/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/816fbbbb68d1337cf9104b87c643ab2aa2fea62c/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/816fbbbb68d1337cf9104b87c643ab2aa2fea62c/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/816fbbbb68d1337cf9104b87c643ab2aa2fea62c/python.svg)](https://www.python.org/downloads/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/0ab8d43a57cbaf69c93d28df9e2ac5630da4148a/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/0ab8d43a57cbaf69c93d28df9e2ac5630da4148a/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/0ab8d43a57cbaf69c93d28df9e2ac5630da4148a/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/0ab8d43a57cbaf69c93d28df9e2ac5630da4148a/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/0ab8d43a57cbaf69c93d28df9e2ac5630da4148a/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/0ab8d43a57cbaf69c93d28df9e2ac5630da4148a/python.svg)](https://www.python.org/downloads/)
## Why devx?
+6 -6
View File
@@ -12,12 +12,12 @@ project to be reusable across all oblachno-oss repositories.
[![CI](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions/workflows/ci.yml/badge.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![License: GPL-3.0](https://img.shields.io/badge/license-GPL--3.0-blue)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/816fbbbb68d1337cf9104b87c643ab2aa2fea62c/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/816fbbbb68d1337cf9104b87c643ab2aa2fea62c/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/816fbbbb68d1337cf9104b87c643ab2aa2fea62c/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/816fbbbb68d1337cf9104b87c643ab2aa2fea62c/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/816fbbbb68d1337cf9104b87c643ab2aa2fea62c/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/816fbbbb68d1337cf9104b87c643ab2aa2fea62c/python.svg)](https://www.python.org/downloads/)
[![Coverage](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/0ab8d43a57cbaf69c93d28df9e2ac5630da4148a/coverage.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Tests](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/0ab8d43a57cbaf69c93d28df9e2ac5630da4148a/tests.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Docs](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/0ab8d43a57cbaf69c93d28df9e2ac5630da4148a/docs.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/wiki)
[![Code Quality](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/0ab8d43a57cbaf69c93d28df9e2ac5630da4148a/quality.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
[![Version](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/0ab8d43a57cbaf69c93d28df9e2ac5630da4148a/version.svg)](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/releases)
[![Python](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/raw/commit/0ab8d43a57cbaf69c93d28df9e2ac5630da4148a/python.svg)](https://www.python.org/downloads/)
## Overview
+1 -1
View File
@@ -1,3 +1,3 @@
"""devx — reusable development and CI/CD tools for oblachno-oss projects."""
__version__ = "0.19.1"
__version__ = "0.19.2"
+60 -10
View File
@@ -135,24 +135,74 @@ def build_multi_role_pairs(
# Heuristic weights for known heavy molecule scenarios.
# These are estimated from CI run times — scenarios that pull large Docker
# images or run complex Ansible playbooks take longer.
#
# Weights are calibrated from actual CI execution times (converge→destroy):
# nextcloud: ~7.7m → 15
# restore/default: ~5.7m → 11
# customer-apps: ~5.5m → 11
# zitadel/default: ~5.0m → 10
# docker_base/default: ~3.9m → 8
# app_hardening/default: ~1.9m → 4
# app_container/default: ~1.8m → 3
# postgres-upgrade: ~1.7m → 3
# observability/default: ~1.7m → 3
# storage/default: ~1.5m → 3
# storage/object-storage: ~1.0m → 2
# vaultwarden: ~0.7m → 2
# simple-app: ~0.7m → 2
#
# Role-specific weights take priority over scenario-name weights.
# The (role, scenario) tuple is checked first, then the scenario name
# alone, then the default weight.
_ROLE_SCENARIO_WEIGHTS: dict[tuple[str, str], int] = {
("app_container", "nextcloud"): 15,
("app_container", "customer-apps"): 11,
("app_container", "vaultwarden"): 2,
("app_container", "simple-app"): 2,
("app_container", "postgres-upgrade"): 3,
("app_container", "default"): 3,
("restore", "default"): 11,
("zitadel", "default"): 10,
("docker_base", "default"): 8,
("observability", "default"): 3,
("app_hardening", "default"): 4,
("storage", "default"): 3,
("storage", "object-storage"): 2,
}
# Fallback weights by scenario name only (for single-role projects or
# scenarios not in the role-specific table).
_SCENARIO_WEIGHTS: dict[str, int] = {
"nextcloud": 10,
"nextcloud": 15,
"customer-apps": 11,
"restore": 11,
"zitadel": 10,
"docker-base": 8,
"postgresql": 3,
"postgres-upgrade": 3,
"gitea": 8,
"vaultwarden": 7,
"zitadel": 7,
"postgresql": 6,
"redis": 5,
"backup": 5,
"docker-base": 4,
"vaultwarden": 2,
"simple-app": 2,
"object-storage": 2,
"default": 3,
"binary": 2,
}
_DEFAULT_SCENARIO_WEIGHT = 3
def _scenario_weight(scenario: str) -> int:
"""Estimate a weight for a scenario based on its name."""
def _scenario_weight(scenario: str, role: str | None = None) -> int:
"""Estimate a weight for a scenario based on its name and optionally its role.
Role-specific weights take priority over scenario-name-only weights.
"""
s = scenario.lower()
if role is not None:
r = role.lower()
key = (r, s)
if key in _ROLE_SCENARIO_WEIGHTS:
return _ROLE_SCENARIO_WEIGHTS[key]
for key, weight in _SCENARIO_WEIGHTS.items():
if key in s:
return weight
@@ -181,11 +231,11 @@ def _lpt_distribute[T](items: list[T], weights: list[int], max_runners: int) ->
def distribute_multi_role(pairs: list[MultiRoleTestPair], max_runners: int) -> list[list[MultiRoleTestPair]]:
"""Split *pairs* into *max_runners* balanced groups using LPT scheduling.
Each pair is weighted by scenario name heuristics (e.g. ``nextcloud`` is
heavier than ``binary``). Pairs are sorted by weight descending and
Each pair is weighted by role+scenario heuristics (e.g. ``nextcloud`` is
heavier than ``simple-app``). Pairs are sorted by weight descending and
assigned to the runner with the least total weight.
"""
weights = [_scenario_weight(p.scenario) for p in pairs]
weights = [_scenario_weight(p.scenario, p.role) for p in pairs]
return _lpt_distribute(pairs, weights, max_runners)
+22 -3
View File
@@ -483,7 +483,7 @@ class TestCliMultiRole:
class TestScenarioWeight:
def test_known_heavy_scenario(self) -> None:
assert _scenario_weight("nextcloud") == 10
assert _scenario_weight("nextcloud") == 15
assert _scenario_weight("gitea") == 8
def test_known_light_scenario(self) -> None:
@@ -493,13 +493,32 @@ class TestScenarioWeight:
assert _scenario_weight("unknown-scenario") == 3
def test_case_insensitive(self) -> None:
assert _scenario_weight("NextCloud") == 10
assert _scenario_weight("NextCloud") == 15
assert _scenario_weight("GITEA") == 8
def test_substring_match(self) -> None:
assert _scenario_weight("nextcloud-with-redis") == 10
assert _scenario_weight("nextcloud-with-redis") == 15
assert _scenario_weight("custom-gitea-setup") == 8
def test_role_specific_weight(self) -> None:
"""Role+scenario pairs take priority over scenario-name-only weights."""
assert _scenario_weight("default", "restore") == 11
assert _scenario_weight("default", "zitadel") == 10
assert _scenario_weight("default", "docker_base") == 8
assert _scenario_weight("default", "app_hardening") == 4
assert _scenario_weight("default", "app_container") == 3
assert _scenario_weight("default", "storage") == 3
assert _scenario_weight("default", "observability") == 3
def test_role_specific_overrides_scenario_name(self) -> None:
"""vaultwarden has a scenario-name weight of 2, but role-specific is also 2."""
assert _scenario_weight("vaultwarden", "app_container") == 2
assert _scenario_weight("vaultwarden") == 2
def test_customer_apps_weight(self) -> None:
assert _scenario_weight("customer-apps", "app_container") == 11
assert _scenario_weight("customer-apps") == 11
class TestLptDistribute:
def test_equal_weights_produce_even_split(self) -> None: