ruff 0.17.0 adds `datetime` -> `dt` to the default aliases of
flake8-import-conventions, and the project selects ICN, so lint-python
failed with ICN001 in seven files.
The files now import `datetime as dt`. ruff's unsafe fix also renamed
two local names in utils/github/playwright_summary.py into the alias:
the `_to_iso_z` parameter and the parsed value in
`_parse_iso_timestamp`. Both are now called `moment`; with the bare fix,
nine tests of test_playwright_summary.py failed with UnboundLocalError
and AttributeError.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
install-lint provisioned chrome-headless-shell on every run, and `make lint`
runs install-lint once per lint target in parallel whenever its stamp is not
fresh. On CI the stamp never is (eslint lives in node_modules, not on PATH),
so one lint job provisioned 16 times (run 38455552981). As root, puppeteer's
--install-deps runs `apt-get satisfy` even for an installed browser, and two
of those at once fail on the dpkg lock; a failed provisioning then deleted the
shared browser cache while lint-mermaid could be rendering with it.
The browser is now provisioned only when it is not installed, and --force
drops its cache so `make install-lint-force` still reinstalls it with its
libraries.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
test_no_uncached_file_reads rejects Path.read_text in tests unless the file is
written in the test itself. The mermaid script test reads the call log its
stubs append to during the run, so that read carries `# nocheck: cache-read`.
The mermaid cli test is formatted with ruff.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The scheduled mirror job has failed every day since 2026-09-25 on the same two
images: quay.io/minio/minio and quay.io/minio/mc answer `unauthorized` to
anonymous pulls ("2/197 failed", run 38352569503). Both belong to
web-app-minio, an eol role outside the lifecycle envelope that default.env
tests, so no CI deploy pulls them.
iter_role_images now skips every role that is_maintained() reports as
unmaintained, the check the version updaters already apply to the same list.
On the tree the discovery yields 187 images instead of 192; the five it drops
belong to the four eol roles web-app-confluence, web-app-jira, web-app-minio
and web-app-phpldapadmin. The mirror sync, resolver and wait read this list,
and their descriptions and mirror.md now say so.
The roles directory is resolved first: is_maintained reads a relative path as
a role name and would keep every role.
475ab9b386 on feature/store-i18n takes the same approach with a lifecycle
reader of its own; main already has is_maintained for it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
get_merged_domains gated the onion injection on node_onion being set AND
svc-net-tor being in group_names. When the node was empty it answered the
clearnet map with no error and no log line, and because group_names is
part of the cache key, a clearnet map and an onion map coexist in one
process under separate keys. One render then serves onion while another
freezes a clearnet asset URL into a stylesheet.
That URL is unrecoverable at request time: body_filter.lua returns a CSS
response before onionize(), so the <link href> in the head is rewritten
while the url(...) inside the stylesheet body is not. The symptom is a
single toHaveCSS mismatch and nothing else.
An empty node under a deployed svc-net-tor now raises. A scope carrying no
group_names at all stays silent, because that is indistinguishable from a
legitimate non-tor deploy.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
make lint runs install-lint once and then dispatches its fifteen targets
in parallel. scripts/lint/mermaid.sh provisioned chrome-headless-shell
inside that parallel phase with `npx puppeteer browsers install
--install-deps`, whose `apt-get satisfy` carries no lock timeout. In a
fresh stack container another target held the apt lock, the shared
libraries stayed missing, the script only warned, and all 289 Markdown
files then failed on libnspr4.so (#795).
utils/install/lint/mermaid_cli.py now provisions the browser right after
mmdc, inside the serial install-lint step, as root with --install-deps
and as a user without it, because puppeteer refuses to install libraries
without root. A failed install deletes the chrome-headless-shell cache
folder and retries once under a 900 second timeout per attempt; a cut-off
download otherwise leaves a folder without its executable, and puppeteer
refuses every later install of that build. The install-lint stamp counts
as fresh only while the browser executable exists, so a recreated
container provisions it again.
mermaid.sh no longer calls apt-get or npx. It renders one minimal diagram
first and, when that fails, exits with mmdc's output instead of reporting
every Markdown file.
Verified: tests/unit/python/scripts/lint/test_mermaid.py stubs mmdc,
npx and apt-get. With a browser that cannot load libnspr4.so the script
exits 1 with that line, before any Markdown file renders, and mmdc is the
only tool it calls. Against main's mermaid.sh the same test fails: it
calls apt-get and npx and reports the files one by one. The installer and
stamp cases pass, shellcheck is clean.
Not verified: a full make lint in a recreated stack container. The race
reproduced in one of two runs in the issue.
feature/store-i18n changed the provisioning in mermaid.sh with 2f5f52bd1a
and dc67bce5f0 (timeout, deleting a cut-off browser). Both behaviours
moved into mermaid_cli.py here, so its merge takes this version of
mermaid.sh.
Closes#795
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Twelve files conflicted; most resolutions keep both sides rather than pick
one.
cli env.py and compose.py: both branches extend the same spots. The tools
override is appended before write_override, because that call filters the
mirror override to the compose files it is handed. compose.py keeps this
branch's CACHE_ROLE_SHELL_DIR, since the cache scripts moved into a role and
main's literal scripts/docker/cache path is their old home, and takes main's
mirrored init image alongside it.
utils/docker/image/discovery.py: this branch's version_variants expansion
plus main's whole runner-image block, which runner_mirror imports.
sys-svc-compose network create: main's side wins on substance and this
branch's on structure. main carries eda6249d7b, the fix for a probe that
matched an id prefix and let a sibling shadow the exact name; this branch had
refactored the task into create/overlay.yml and create/bridge.yml but brought
back the docker_network_info probe that fix replaced. The subfiles stay and
now probe through the exact-name lookup. main's half also read
SWARM_CLUSTER_AVAILABLE, which this branch removed in favour of
IS_SWARM_MODE, as the merge base confirms, so the gates follow this branch.
The lookup's import follows this branch's get_entity_name to entity_name
rename.
web-app-pixelfed: the custom entrypoints stay removed, since the new image
brings its own and the variables behind them no longer exist, and main's
host_alias=true comes along.
web-app-suitecrm: main's session.yaml and its config/packages path, because
only that version wires framework.session and only that directory is read by
Symfony.
web-app-odoo: main's onionOptions import is required by the test.use below
it; this branch's test-seaweedfs registration stays.
Four defects surfaced that no conflict marker shows.
The auto-merge of web-app-suitecrm/vars/main.yml duplicated
SUITECRM_SESSION_CONFIG_HOST, which ansible-lint rejects. Removing it exposed
the second: SUITECRM_SESSION_CONFIG_DOCKER still named config/session.yaml
while the mount had moved to config/packages, so the freshness comparison in
tasks/01_manager_ops.yml would have tested a path nothing writes.
svc-db-elasticsearch carried environment: on two include_tasks, which Ansible
accepts on include_role only. ELASTIC_PW now sits on the five tasks that read
it, which is also the tighter scope for a credential.
The openclaw templates render NETWORK_TRUSTED_PROXY_CIDRS while their test
renderer passes only its explicit kwargs, so seven tests raised Undefined.
That predates the merge: the template line is on this branch and not on main,
and both sides share the same test file.
test_runner_mirror neutralises four environment variables under clear=False
and missed three, so the gpu and tools overrides leaked in from the container
and the assertion on the layered list broke. It now neutralises all seven.
make quality-high passes both phases.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A lockup slot on a page or panel drew its title in the light
--design-primary with a --design-on-primary outline in both color schemes.
With an orange base that is about 3:1 on the light surface, and the SVG
never changed in dark mode; the Sphinx, Jenkins and Jellyfin passes
reported it.
sys-front-inj-design now passes --design-link with a --design-surface-1
outline for light and their dark values for dark. render_assets() hands
the dark pair to every slot without frame: true, and render_svg() adds a
style rule under prefers-color-scheme: dark that overrides the fill and
stroke attributes. Slots on brand chrome keep the frame colors, and the PNG
keeps the light pair. The design_assets action requires the two new
arguments; its two callers, assets.yml and Keycloak's theme task, pass
them.
A unit test asserts the dark rule on page slots and its absence on frame
and logo-only slots; it fails when the dark pair is not handed on. In
headless Chromium an <img> of the SVG renders the title #825200 on #fff5ea
in light and #ffa500 on #140e07 in dark.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The GHCR mirror covered role images only. The images the runner pulls on
its own went straight to Docker Hub: coredns from compose.yml, the cache
stack in compose/cache.override.yml and alpine:latest, the init image of
scripts/docker/cache/package-frontend/certs.sh. With the anonymous
githubactions quota spent, the guide step of Postmarks#0 (main run
37954203718) and Shopware#2 (#872) died on "toomanyrequests" while pulling
alpine:latest, after coredns had hit the same limit.
iter_runner_images (utils/docker/image/discovery.py) finds those images,
and the mirror sync and the fork-PR wait now cover them next to the role
images. On a real GitHub runner compose_file_args layers a generated
build/compose/runner-mirror.override.yml that points only the services of
the layered compose files at their mirror, and the certificate generator
gets the mirrored init image. Local runs and act are unchanged, and the
coredns literal stays in compose.yml for Dependabot. test-dns now waits for
images-mirror-missing like the deploy and workspace jobs, since it brings up
the stack too.
Verified with docker compose config on a simulated GitHub runner: the
service set is unchanged and only the runner images point at
ghcr.io/infinito-nexus/core/mirror/docker.io/..., and an existing role mirror
under that scheme pulls anonymously.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ruff format rewraps the read-back line in test_repository.py, and ruff
check reports the nested with in test_fetch.py as SIM117. Both fail
lint-python.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An http_regex source read one document, so a tag whose parts upstream
keeps in two files could only carry the second part as a literal. Every
pin also fetched its document on its own: a failed request left one pin
of a release behind while the others moved.
url now takes a list whose bodies are searched as one text, and every
document is fetched once per run, so all pins that read it resolve against
the same snapshot. The HTTP reads move into utils/update/fetch.py.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The repository updater assigns ref: lines to entities by position: every
entity that pins a value takes the next line carrying that value. A ref
that declares its own source was dropped before it took its line, so a
second entity pinning the same value was given the sourced entity's line,
and an update of it would have rewritten the wrong ref.
A sourced ref now takes its line first and is skipped afterwards, the
same order the nocheck marker already follows.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A pin that names its own upstream in an update: block was still collected
by the image updater or, for a ref, by the repository updater. Those raise
it to the newest tag of its registry or repository, which for a component
of a release is not the release's value. A tag built from several upstream
values, such as <freeswitch>-<bigbluebutton>, could not be resolved at
all: the resolver takes one capture and keeps a pin on its suffix.
The image updater now skips a version and the repository updater a ref
that declares a source for itself. An http_regex source accepts a template
over the pattern's named groups, and a templated pin moves whenever the
assembled value changes, its suffix included.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A pin that names its own upstream in an update: block was still collected
by the image updater, which raises it to the newest tag of its registry.
For a component of a release bundle that tag is not the bundle's, so the
two updaters would propose different versions for the same line and such
a pin had to carry nocheck: docker-version.
The image updater now skips a version that declares a source for itself.
The source resolver alone bumps it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A generated lockup drew its title in the primary color with an
on-primary outline. On a header or sidebar that a role maps onto the
frame token, that put primary-colored text on a primary-toned ground.
A slot now takes `frame: true` in the role's design entry. The asset
action receives a second color pair from the palette, the on-frame
color as fill and the frame color as outline, and the renderer picks
the pair per slot. Slots without the flag render as before.
Keycloak calls the asset action directly and already rendered its
brand slot in the on-frame color, so it passes that color for both
pairs and keeps its output.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflicts:
- roles/web-app-pihole/meta/services.yml: main dropped the unused redis
entry, this branch changed the javascript entry directly below it. The
redis entry is gone and the javascript entry keeps the flag bound to
the SSO flag of the role and its nocheck reasons.
- README.md: the generated role table keeps the renamed web-svc-design
role and lists Pi-hole without Redis and without the CDN.
The role diagram of Pi-hole is regenerated, because both sides removed
an edge and the index of the red link moved.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
1d57e22926 raised web-app-docs' mermaid_version from 11.4.1 to 12.1.0 and left
mermaid_sha256 describing the 11.4.1 asset. The Dockerfile fetches that asset
with ADD --checksum, so every round that deploys docs died in the build with
#7 ERROR: digest mismatch sha256:6484afc3...: sha256:a43bc1af...
which names neither the pin nor the role. Seven of the eleven failures of run
37440146966 are this one line. The pin now carries the digest the CDN actually
serves, measured three ways that agree: a host curl, the new helper, and
buildkit's own computed value in those jobs.
The separation is the real defect, so the updater closes it. A version pin may
now declare checksum_key and checksum_url beside its type, and after writing a
new version apply_updates refetches that URL with {version} substituted, hashes
it, and replaces the value on the checksum_key line of the same entity block.
A pin that declares neither behaves as before and performs no request.
invalid_declarations rejects the two ways the declaration could silently do
nothing: a checksum_key naming a key the entity does not carry, and a
checksum_url without the placeholder.
source.py stood at 496 of the 500-line cap, so the three line locators move to
utils/update/pins.py and checksum.rewrite reuses them instead of repeating the
block-scoped search.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A gate run died two seconds in with PermissionError on
compose/package-cache/pip.conf. The worktree is bind-mounted into the
container, so a container that rendered these files owns them afterwards
while the directory stays the host's. Overwriting needs the file's
permission and unlinking the directory's, which is why the host could no
longer write a file in a folder it owns.
_write now catches the refusal and replaces the inode instead. Only after a
refusal, never by default: compose/swarm/cache.override.yml bind-mounts an
apt list into a container by inode, and swapping one that was writable
anyway would leave that mount reading a file nobody updates any more. The
new test pins that half as well, so the guard cannot be simplified away.
Shown failing first: the previous _write raises PermissionError on the same
input the test feeds the new one. Shown working in place: lint-ansible came
back in 3m40s through a run during which make dotenv-force re-rendered these
very files, where the same sequence had aborted the target after two seconds
before.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Of the 76 open code scanning alerts, 39 point at code that can change.
svc-ai-mcp-adapter:
- A path argument that is empty or made only of dots is refused.
Percent-encoding keeps a separator inside its segment, but "." and ".."
need none, so an upstream that resolves dot segments could answer from a
path the contract does not name.
- parse_duration splits on the units. The pattern it used before rescanned
a long run of digits from every start position.
Tests and the README generators:
- Every Jinja environment built with autoescape=False and a "noqa: S701"
uses select_autoescape now, as six test modules and
utils/templating/jinja.py already did. It escapes templates named
*.html, *.htm and *.xml, which none of these call sites loads.
- Dead code goes: a doubled "rel" block in the email lint, an unread
_SKIP_DIRS, and a module that _resolve returned to no caller.
- Two "except ...: pass" blocks become contextlib.suppress and
assertRaises.
- Nine test modules import unittest.mock one way instead of two, and one
lookup test imports its module once.
Closes#807
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The gateway answered two sources with "<source>\n---\n<rendering>" and the
whole answer was stored, so twenty languages shipped the English text as
their own translation and the release gate stayed green. Every criterion
passed it: echo compares for equality and the answer is longer, the markup
comparison counts only []{}`*() so a rule of hyphens is invisible to it, and
the rendering the server appended keeps the spans, the names and the length.
echoes() reports the same "echo" criterion when the translation contains its
whole source, under the floor the equality case already uses: a short source
may legitimately survive inside a longer rendering, a sentence may not.
reason() is the single source of truth for both ends, so the client now drops
such an answer on arrival and the gate reports one that got in earlier; make
i18n-prune clears those.
Swept every core catalog: exactly the 21 damaged entries are reported, all as
echo, and nothing else in the corpus. The old predicate returns no reason at
all for the same input.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The update bot opened a version PR for web-app-jira (#821) although the role is end of life: none of the updaters looked at the lifecycle. utils.update.base.is_maintained now answers that question once, and the collectors for image versions, declared version sources, pip pins, repository refs and the addon check skip a role whose lifecycle is eol.
The lint that requires every semver pin to be watched shares that scope, otherwise it would report the pins of the eol roles as unwatched. Every other stage, deprecated included, stays in the update scope.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
For a very dark base color the light palette used the base itself as
link color, which is as dark as body text: #001f3f gave a link at
contrast 1.02 against the text, so links in tables read as plain text.
The dark palette did the same for a very light base (#FFFF00: 1.11).
The lightness the link search starts from is clamped to 0.45..0.55 in
light and 0.70..0.80 in dark mode. #001f3f now yields #37577c in light
mode (2.27 against the text, AA on every surface). Bases inside the
band, #FFA500 among them, produce the same palette as before.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Nothing held a role's architectures to reality: test_image_reachable proves a tag exists, test_arm64_images proves four named roles publish arm64, and the closure lint only proves the declarations do not intersect to nothing. An amd64-only bump became a scheduling failure on an arm64 row rather than a red test.
A new external test sweeps every pinned image and judges a role by the intersection over its services, because a role runs only where all of its images run; judging per image would demand arm64 from an amd64-only role's nginx and call a narrowing stale because nginx publishes arm64 while the application does not. A role declaring nothing must cover every architecture the matrix rotates; a role that narrows must narrow to exactly its capability, which is what retires its TODO by itself. An unread image downgrades the stale verdict to a warning, since the image a probe could not read may be the one the narrowing exists for. The probe reads the mirror first and caches on disk, because the sweep is ~160 manifests and Docker Hub counts its allowance per IP, shared.
A new lint requires every architectures declaration to carry a nocheck marker and a Reason. bigbluebutton and opentalk keep their pins with measured reasons; decidim, pretix and taiga lose theirs, because each builds its own image and their upstream Dockerfiles are arch-neutral. Their source builds are not converted yet, so until that lands the matrix may hand them an arm64 row that fails at FROM.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The palette derives the link color and the four status colors against
all five surfaces (the three surface levels plus their hover and active
states) instead of the two content surfaces, and a status color also
reaches AA on its own subtle tint. The subtle and border tints of the
status colors move from a CSS color-mix into the palette, so both modes
carry them as plain values.
New tokens --design-frame, --design-on-frame, --design-frame-hover and
--design-frame-active give large brand surfaces such as a sidebar a deep
tone with AAA text in light and dark mode. The default focus outline
reads --design-focus and falls back to --design-link, so a frame can
keep its focus indicator visible.
The design lookup exposes the generated icon under urls.favicon_ico,
because a role may declare a slot named favicon. Slot names must be
kebab-case; a violation fails the lookup. resolve_branding returns a
bootstrap flag whose default false lives in web-svc-design.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
0a3da88912 closed the EOL contract at the declaration level: the four retired
roles carry `enabled: false`, an end-of-life README banner and three lints.
iter_role_images still enumerated their images, so the mirror job kept trying
to copy tags nobody can consume. MinIO's quay tags answer 401 to anonymous
pulls and exist nowhere else, which failed that job on every run while no
deploy could have reached the result: an eol role sits outside the lifecycle
envelope default.env tests.
role_lifecycle() reads the stage off the one entry that carries a `lifecycle`
field, which is the role's primary service, and iter_role_images skips the
retired ones. Measured against the tree: 202 images, with web-app-confluence,
web-app-jira, web-app-minio and web-app-phpldapadmin all absent.
test_version_pins_are_watched.py gains the same skip. utils/update/docker.py:271
builds its watched set from iter_role_images, so dropping the retired roles
there made their `version:` pins look unwatched and the lint went red on three
of them. The coherent reading is that a role no updater can reach cannot
declare a watcher either, and the lint docstring now says so rather than
claiming every pin.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
666312dfbd settled the catalog client on the dedicated LibreTranslate. One
engine cannot be compared against itself, so the direct path had no way to
improve on a language it translates badly: of the 2854 entries of the `ko`
catalog it produced 1 translated entry and 2704 `structure` rejections.
The client now asks the gateway, which fronts several engines and keeps a
history of which one survives a given signature. Protected spans are masked
as `<x id="N"></x>` before the request and declared per item, so the gateway
can reject an answer that altered them instead of passing it on. `_reask`
then retries each rejected entry with the engine that produced it in
`exclude`, which reaches a different backend rather than the same one. The
same catalog measured 17 translated entries with 2649 rejections once the
gateway checked the markup, and 331 with 796 once the retry could exclude.
The full picture a rejection needs is only available in the client: 2431 of
those 2649 structure rejections involve markup the masking hides from the
gateway, so the engine-switching decision belongs on this side of the call.
`server()` reuses a gateway that already answers and otherwise deploys the
bundle the gateway needs. It does not reintroduce the throwaway container
666312dfbd removed: there is no `docker run` here, the roles deploy through
the normal path and their models still come from the role images built
through the package cache. `redeploy=true` is offered because the reuse check
only asks whether a gateway answers, which cannot see that the gateway's own
code changed since it started. A run refuses outright while a deploy is in
progress, because both touch the same container stack, and leaves the deploy
alone.
`deployed()` additionally requires a non-empty catalogue, so a gateway that
answers while serving no pairs counts as not ready rather than as a backend
that refuses everything. `probe(url)` is split out of `deployed(root)` so one
readiness check serves a single engine and the gateway alike.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The version-source resolver orders upstream versions by semver. A pin that
is not one, such as `latest` or a branch name, has no depth, so no candidate
ever matches it and the declaration never moves anything. Nothing reported
that: the watched-pins lint only looks at values that already are a semver.
test_version_keys now fails on an `update:` block whose pinned key does not
hold a semver. A monitored addon the resolver does not read is held to the
same rule as soon as it pins a `version`, because the catalog check skips a
non-semver pin just as silently. An addon without a `version` keeps tracking
the app default and is left alone.
The Stalwart README no longer asks for `webui_version` to be bumped by hand
next to the image tag. Its `update:` block names the upstream tags, the
server itself follows `releases/latest` of the bundle, and the resolver
plans v1.0.5 to v1.0.11 for it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The expectation in test_a_rejected_translation_never_spills_over_its_comment_line encoded the old mechanism, an escaped newline, which is what introduced the backslash that broke docs/tr and docs/zh. The invariant its name states still holds with a space, so the expectation moves and the fix stays.
A second test asserts the property that actually prevents the defect: a sample containing [test\\_file.py] leaves no backslash in the comment, whatever babel does when it wraps it. Nothing guarded that before, which is why two unparseable catalogs only surfaced under a manual msgfmt run.
make test-unit passes in the container: 7171 passed, 9 skipped, no failures.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The version-source updater only read meta/services.yml. An addon pinned in
meta/addons/<id>.yml stayed where it was: the ONLYOFFICE connector for
Nextcloud sat on v10.1.2, which does not install on Nextcloud 35, until
someone bumped it by hand.
The updater now reads addon files too. An addon declares its source in the
`update:` block it already has, beside monitored, catalog and upstream_id.
A monitored addon of the github-releases catalog needs nothing more: its
upstream_id names the repository whose tags are its versions. The
update-version-sources job opens the PRs, one per role.
Every pinned addon is wired:
- nextcloud onlyoffice carries its own version pin now. Its archive URL
stays a literal that carries the pin, because
test_pinned_addon_archives_available reads it as written
- wordpress mcp-adapter, moodle auth_oidc and jellyfin sso-authentication
follow the tags of their repository, moodle within its v4.5 series
- the three xwiki extensions read their Maven metadata
- pretix-oidc reads the PyPI simple index. The JSON API answers 404 behind
the package cache the test stack routes pypi.org through
- joomla's plugin is built here and jellyfin's ldap-authentication is bound
to the md5 of its archive; both are opted out with that reason
A bump rewrites the pin and the config.archive line together, and an
archive that does not carry the pin fails test_version_keys, so the two
cannot drift apart again. test_version_pins_are_watched covers addon pins
as well: a new one without a source or a reasoned opt-out fails the lint.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Docker updater watches image plus version, the repository updater
repository plus ref. A dozen pins fell outside both - an app_version, a
release, a tag in a registry neither speaks - so nothing ever told anyone
they had gone stale.
Such a pin now names its upstream next to itself in an `update:` block:
git_tags, registry_tags, npm, http_regex or a role-local script, with
`match` selecting a tag flavour and `strip` removing a prefix the pin does
not carry. utils/update/source.py resolves them beside the two existing
updaters, `python -m cli.contributing.update.source` bumps them, the
update-version-sources job opens one PR per role, and
tests/external/update/source warns while a bump is pending.
Two lints hold the rule: every semver pin is watched by one of the three
updaters or opted out with a reason, and a semver only ever sits behind
`version`, `<what>_version` or `ref` - which renames moodle's release to
app_version and simpleicons' local_version to local_tag, the latter being
the tag of a locally built image rather than an upstream version. A
declaration naming a key the entity does not carry, or a type the resolver
does not know, fails the lint instead of resolving nothing in silence.
Picked from feature/store-i18n (94d607fd0a) onto current main. Two things
differ from the original: svc-ai-lmstudio no longer carries an app_version
pin, so its declaration is dropped, and web-app-stalwart's webui_version,
added since, declares the git tags of the webui repository.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
d6d7ff8c9e made iter_role_images yield one ref per version_variants suffix so
the mirror copies the extra tags. Two of its four consumers assumed one ref per
service. The resolver writes services[service] = {image, version}, so the last
ref won and the applications override carried v1.9.6-cuda; the engine role
appends -cuda itself and asked the mirror for v1.9.6-cuda-cuda, a tag nothing
ever pushed. The version updater builds one entry per ref and writes back into
meta/services.yml, so it would have replaced the declared version with a cuda
tag once this branch reaches the bot on main.
ImageRef now carries `derived`. The resolver and the updater skip those refs;
the mirror sync keeps iterating all of them, which is what the variants exist
for, and the mirror wait already keyed on the full ref including the version.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Autoformat output from a quality run, carried along since. The syntax tree is
unchanged.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four libretranslate jobs in run 37173067619 failed with
failed to solve: ghcr.io/…/mirror/docker.io/libretranslate/libretranslate:v1.9.6-cuda: not found
svc-ai-libretranslate-engine and web-svc-libretranslate append '-cuda' to the version
in vars/main.yml when the gpu flag is set. iter_role_images is the only enumeration
the mirror pipeline sees and it reads image and version alone, so the derived tag was
never a candidate for mirroring and resolved against a registry that has no such tag.
Upstream does publish it: Docker Hub answers 200 for v1.9.6-cuda, the mirror 404.
The suffixes are now declared next to the version they extend, and discovery yields a
ref per suffix. A suffix rather than a full tag keeps the version number in one place
when it is bumped.
The gpu flag is static: it is set in meta/services.yml, no inventory overrides it, and
resource_filter reads configuration rather than hardware. Every host therefore takes
the cuda path today, which is why all four jobs failed rather than some.
Rejected: mirror_policy: skip, which has no users in this repository and would route
these two images around the mirror to Docker Hub permanently, reintroducing the rate
limit the mirror exists to avoid. Also rejected: baking '-cuda' into version, which
fixes the mirror but removes the GPU switch.
The test pins both halves; without the discovery change its two variant cases fail
and its two base cases still pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>