Commit Graph
286 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 5.5 75460b95cd feat(update): leave an image pin to the version source it declares
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>
2026-10-08 10:30:48 +02:00
kevinveenbirkenbach e78056db08 Merge pull request #816 from infinito-nexus/update/version-sources-web-app-wordpress-20261005-950916f
update(versions): update pins with a declared source (web-app-wordpress)
2026-10-07 13:35:57 +02:00
kevinveenbirkenbach 9fc68a5477 Merge remote-tracking branch 'origin/main' into update/version-sources-web-app-moodle-20261005-4f455e2 2026-10-07 13:21:56 +02:00
kevinveenbirkenbachandClaude Opus 5.5 0c748f74ed fix(web-app-moodle): swap the code tree only after a complete copy
The entrypoint removed the old tree before it copied the new one. A
copy that failed left the volume without a working Moodle, and every
restart removed again instead of recovering. It now copies into a
staging directory inside the volume and swaps the trees once the copy
is complete, so a failed copy leaves the old tree in place.

An image that ships an older release than the volume records no longer
replaces it and exits with a message instead. Two instances on
different releases would otherwise overwrite each other's tree.

upgrade.php reports "No upgrade needed" on stderr, so the task counted
every run on a current site as a change. It reads stderr now.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 10:45:29 +02:00
kevinveenbirkenbach 056bb99a14 Merge remote-tracking branch 'origin/main' into update/version-sources-web-app-wordpress-20261005-950916f 2026-10-07 02:58:03 +02:00
kevinveenbirkenbachandClaude Opus 5.5 d0ec38017a fix(test-e2e-cli): name the negotiated MCP protocol version on requests
MCP Adapter 0.7.0 for WordPress refuses a request that follows
initialize without the MCP-Protocol-Version header: 'Invalid Request:
MCP-Protocol-Version header is required for a 2025-06-18 session'. The
contract probe never sent it, so the CLI test of web-app-wordpress
failed at tools/list.

The probe now keeps the version the server answers initialize with and
sends it on every later request of the streamable HTTP transport, which
the specification asks of a client since 2025-06-18.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 02:58:03 +02:00
kevinveenbirkenbach 402aa2fe9d Merge remote-tracking branch 'origin/main' into update/version-sources-web-app-moodle-20261005-4f455e2 2026-10-07 02:10:13 +02:00
kevinveenbirkenbachandClaude Opus 5.5 d1eee3e26f fix(web-app-moodle): run Moodle 5.3 and upgrade existing sites to it
The pin moved from 4.5.11 to 5.3.0, and the role fitted neither a new
install nor an existing one.

Moodle 5 serves from public/ and routes every other path through
r.php. nginx now uses public/ as its root, hands unknown paths to the
router and passes PATH_INFO for scripts with slash arguments, and
config.php declares the router as configured. The paths the role checks
and writes (version.php, auth, webservice, the OIDC plugin) moved below
public/, the URL of the OIDC plugin stays auth/oidc. The source archive
no longer contains vendor/, so the image installs the Composer
dependencies it declares. The OIDC plugin is pinned to v5.2.1, the
newest release.

An existing site kept its old code: the entrypoint filled the code
volume once and never again. It now records the release it copied and
replaces the tree when the image carries another one, and a new task
runs admin/cli/upgrade.php. Plugins that were installed through the web
interface are not carried over, the README says how to ship them.

A site installed with 5.3 forces login by default, which 4.5 did not,
so its start page answers 303 to the login page and the webserver
health check refused it. The role declares 200 and 303 as the codes
that check accepts: 200 is what an upgraded site keeps answering.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 02:10:13 +02:00
kevinveenbirkenbach d287e55964 Merge remote-tracking branch 'origin/main' into fix/code-scanning-alerts/807 2026-10-07 01:51:19 +02:00
kevinveenbirkenbachandClaude Opus 5.5 45d956e5ea fix(sidecars): cap an engine's sidecar at its provider's size
A Redis sidecar whose consumer declares no services.redis.mem_limit
runs in a container that inherits the consumer's primary entity limit.
Since the lookup replaced the old filter, maxmemory followed that limit
and rose from 204 MiB to between 819 and 3276 MiB in 17 roles.

lookup('service_memory_mb', ..., provider=<role>) caps the result at
the same share of the provider's own limit when the application does
not declare one for that service. A declared limit is not capped, and a
container smaller than the provider keeps its own share. The Redis
sidecar passes provider='svc-db-redis': those 17 roles get 409 MiB, the
seven with a 256 MiB container stay at 204.

The lookup now rejects unknown options and a provider that is no role
or declares no limit for the service, so a misspelled option fails
instead of silently lifting the cap.

The memcached sidecar's literal -m 64 takes the same lookup and keeps
its value.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 01:30:01 +02:00
kevinveenbirkenbachandClaude Opus 5.5 e203d6f7ab feat(plugins): size memory parameters from the container limit with one lookup
lookup('service_memory_mb', application_id, service_name, share) returns
a share of a service's mem_limit in whole MiB. It resolves the limit the
way sys-svc-container resolves the container's own: the named service,
then the role's primary entity, then RESOURCE_MEM_LIMIT.

It replaces the three filters of memory_filters, each with its own unit
parser and its own idea of the limit: Redis maxmemory at 0.8 of the
limit, and the Confluence and Jira heap at 0.7 and 0.35.

The central Redis (409 MiB), Confluence (5734/2867) and Jira (4300/2150)
keep their values. A Redis sidecar without a mem_limit of its own does
not: the filter fell back to a fixed 256m, which gave 204 MiB, while the
lookup takes the limit that container really runs with, the primary
entity's. That moves 25 roles to between 409 and 3276 MiB.

The heap clamps of the old filters are gone: the 1024 MiB floor, limit
minus 1024, the 12288 cap and Xms bounded by mem_reservation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 23:05:26 +02:00
kevinveenbirkenbach 0f403630d7 Merge remote-tracking branch 'origin/main' into fix/code-scanning-alerts/807 2026-10-06 21:10:40 +02:00
kevinveenbirkenbach 09894d5d79 Merge pull request #838 from infinito-nexus/fix/review-follow-ups/802
fix(review): handle the findings left on #825, #826 and #828
2026-10-06 21:00:36 +02:00
kevinveenbirkenbach 2f2cbd900b Merge remote-tracking branch 'origin/main' into fix/code-scanning-alerts/807 2026-10-06 19:51:27 +02:00
kevinveenbirkenbachandClaude Opus 5.5 c73bc0ca85 test(web-app-keycloak): follow every async kcadm result to its reap
The guard required a register on every async kcadm job and nothing more.
A result that is registered but no longer handed to async_reap_jobs,
directly or through the list it is collected into, passed it, although
the reap then neither polls that job nor sees it fail.

The test now follows each registered result to an async_reap_jobs
expression, by variable name with word boundaries, so kc_declared_client
does not pass for kc_declared_client_jobs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 16:40:48 +02:00
kevinveenbirkenbachandClaude Opus 5.5 601b21a8f4 test(web-app-penpot): require both service names in the start guard
The stub answered every absolute name, so a guard that asked for the
backend twice and never for the exporter passed both tests. The stub now
records the names it is asked for, and the tests require the backend and
the exporter.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 16:40:48 +02:00
kevinveenbirkenbachandClaude Opus 5.5 8834cc124a fix(web-svc-collabora): build the WOPI host from Nextcloud's own domain
COLLABORA_WOPI_HOST read the scheme and the port with
lookup('tls', 'web-app-nextcloud', ...) and the host with
lookup('domain', 'web-app-nextcloud'). The tls lookup aligns an app term
to its consumer: for a clearnet Collabora it answers for the clearnet
sibling of an onion-primary Nextcloud, while the domain lookup keeps the
onion name. Such a deployment renders https://<onion>:443, an origin
Nextcloud does not use, so the allowlist does not match its WOPI calls.

Nextcloud pins overwritehost and overwriteprotocol to its primary domain,
so the allowlist now asks the tls lookup for that domain instead of for
the app.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 16:40:48 +02:00
kevinveenbirkenbach 0e9354f1a0 Merge remote-tracking branch 'origin/main' into fix/svc-net-tor/chutney-node-workers/824 2026-10-06 16:40:29 +02:00
kevinveenbirkenbachandClaude Opus 5.5 4f8c1f00f1 fix(svc-net-tor): fail the worker check when no torrc can be read
The check listed the torrcs without the limit through "grep -L ...; true".
With no torrc under /opt/chutney/net/nodes, or with one that cannot be
read, grep printed nothing, the trailing true turned its error into
success, and the test reported that every node runs one worker.

The check now counts the limit per file with "grep -cH" and reads the exit
status. A status above 1 or an empty answer fails the test, and a file
with a count of 0 is named as before.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 16:40:28 +02:00
kevinveenbirkenbach 5d854cc005 Merge remote-tracking branch 'origin/main' into fix/code-scanning-alerts/807 2026-10-06 13:43:32 +02:00
kevinveenbirkenbach 40c033fc22 Merge pull request #826 from infinito-nexus/fix/web-app-keycloak/kcadm-config-lock/805
fix(web-app-keycloak): keep concurrent kcadm jobs from losing the config lock
2026-10-06 13:07:26 +02:00
kevinveenbirkenbach c4bcb9a31c Merge pull request #818 from infinito-nexus/update/version-sources-web-app-xwiki-20261005-dc2425a
update(versions): update pins with a declared source (web-app-xwiki)
2026-10-06 13:05:11 +02:00
kevinveenbirkenbach d0ef1f81a1 Merge pull request #825 from infinito-nexus/fix/web-app-penpot/frontend-start-guard/806
fix(web-app-penpot): make the frontend wait for its backend by absolute name
2026-10-06 12:22:36 +02:00
kevinveenbirkenbachandClaude Opus 5.5 7968b47cf0 fix(svc-net-tor): give every chutney node one worker thread
Tor starts its worker threads by the number of CPU cores it detects, and
the cpus limit of the container does not change that number. On a host
with 20 cores every node of the private chutney network ran 21 threads and
grew to about 0.6 GB. The seven relays and authorities filled the 2g
limit and its swap within ten minutes, the kernel killed them one by one,
and no circuit could be built any more.

The entrypoint now writes NumCPUs 1 into the torrc of every chutney node
before it starts the network. Measured on that host over ten minutes: the
nodes run 3 threads and stay at 65 to 86 MB, the container at 0.47 GB
without swap, where it had filled 2 GB plus 2 GB of swap before.

The CLI test of the role fails when a chutney node runs without the limit.

Closes #824

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 11:37:54 +02:00
kevinveenbirkenbachandClaude Opus 5.5 c35f2409a4 fix(web-app-odoo): make the OAuth addon work on Odoo 20
Nobody could sign in to Odoo through Keycloak. Odoo sent an http redirect
URI, and Keycloak refused it as "Invalid parameter: redirect_uri".

The addon auth_oauth_https builds that URI from web.base.url. Two things
kept it from doing so after the image moved to Odoo 20.

Its manifest declared version 19.0.1.8.0, and Odoo marks a module whose
version names another series as not installable. It only warns about that,
so the install task stayed green and the addon was missing. The manifest
now declares 1.8.0, which Odoo prefixes with its own series.

Once installed, the addon read the parameter with get_param, which Odoo 20
replaced by typed getters. The call raised AttributeError, a broad except
logged it and fell back to the URL of the request, which is http behind
the proxy. The addon reads the parameter with get_str now and no longer
catches the error.

A deploy now fails when the addon is not installed afterwards. The check
reads ir_module_module, because the output of "odoo -i" does not tell an
installed module from a skipped one.

The spec logged out by navigating to /web/session/logout. Odoo 20 accepts
POST only on that route and answered 405, so both SSO tests failed after
a successful login. The spec logs out through the user menu now.

A host that runs the suite with several workers still failed it. Odoo
computes the session token of an account from its OAuth access token, so
every new sign-in ends the other sessions of that account, and the spec
files share the administrator. The role caps its suite at one worker
with PLAYWRIGHT_MAX_WORKERS, which the central config now honours, and
the spec asserts the cap.

Closes #822

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 11:37:41 +02:00
kevinveenbirkenbach ab3b365fb9 Merge remote-tracking branch 'origin/main' into fix/code-scanning-alerts/807 2026-10-06 09:18:00 +02:00
kevinveenbirkenbach 0622cce916 Merge remote-tracking branch 'origin/main' into fix/code-scanning-alerts/807 2026-10-06 09:18:00 +02:00
kevinveenbirkenbachandClaude Opus 5.5 3610e7b9ef fix(code-scanning): resolve the alerts that describe real code
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>
2026-10-06 09:18:00 +02:00
kevinveenbirkenbach 682402e5b3 Merge remote-tracking branch 'origin/main' into fix/web-app-keycloak/kcadm-config-lock/805 2026-10-06 09:17:56 +02:00
kevinveenbirkenbachandClaude Opus 5.5 30084f986c fix(web-app-keycloak): keep concurrent kcadm jobs from losing the config lock
In the async pass the role runs kcadm jobs side by side. Every kcadm call in
the Keycloak container shares one config file. kcadm rewrites that file when
it refreshes the access token, about every 55 seconds, takes the file lock
with tryLock() and gives up at once when another kcadm process holds it. One
of two overlapping declared-client jobs then failed with "Failed to get lock
on /opt/keycloak/.keycloak/kcadm.config", and the deploy with it.

keycloak_kcadm_update now repeats a call that lost the lock, up to ten times
one second apart. The call fails while it refreshes its token, before it
sends its own request, so repeating it is safe. Every other error still ends
the call at once.

That covers the module only, so the async jobs are reduced to the ones that
go through it:

- The default-scope assignment was an async shell task. It ran beside the
  shell tasks that follow it, and neither side repeats a lost lock. It runs
  in line now.
- The two mail-settings jobs were async without a register, so nothing
  reaped them and a failure was never seen. They run in line now.

The client job and the declared-client jobs stay async. A test fails when an
async kcadm task does not use the module or is not registered for the reap.

A config file of its own per call was the other option. It does not work by
itself, because the admin session lives only in the shared file.

Closes #805

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 09:17:56 +02:00
kevinveenbirkenbach 123f85f1c0 Merge remote-tracking branch 'origin/main' into fix/web-app-penpot/frontend-start-guard/806 2026-10-06 09:17:52 +02:00
kevinveenbirkenbach 207553eeb2 Merge remote-tracking branch 'origin/main' into fix/web-app-penpot/frontend-start-guard/806 2026-10-06 09:17:51 +02:00
kevinveenbirkenbachandClaude Opus 5.5 5494fbad48 fix(web-app-penpot): make the frontend wait for its backend by absolute name
nginx resolves its upstreams once, at start, so the frontend starts through
a guard that waits until `backend` and `exporter` resolve. A container
resolves with a search domain, and the zone of a deployment answers every
name with the address of the ingress. A service that Docker's DNS did not
know yet therefore resolved anyway, as `backend.<domain>`. The guard let
nginx start at once and nginx pinned the ingress as its backend. Every API
call then failed with 502 and the SPA showed "Bad Gateway" instead of the
login form, until the container was restarted.

The guard now asks for the absolute names, `backend.` and `exporter.`. The
search domain is not applied to those, so the lookup fails until the
services themselves resolve.

Closes #806

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 09:17:51 +02:00
kevinveenbirkenbachandClaude Opus 5.5 e142d07f97 fix(web-app-xwiki): upgrade an add-on that is not on its pinned version
The extension installer returned as soon as an add-on was installed, in
whatever version. A new pin in meta/addons/ therefore reached a fresh wiki
only, and an existing wiki kept the version it was first deployed with.

The installer now skips an add-on only when the installed version equals
the pin. Otherwise it calls install(id, version, namespace), which the
Extension Manager resolves as an upgrade, and it reports an error when the
add-on is not on the pin afterwards.

The check that follows the installer compared presence only. It takes the
pin as well, so a deploy fails when an add-on is installed in another
version.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-06 09:17:15 +02:00
kevinveenbirkenbach be800e8741 Merge pull request #803 from infinito-nexus/fix/sys-ctl-hlth-msmtp/empty-reply-is-transient/790
fix(mail): retry a connection the mail server drops before its greeting
2026-10-06 02:28:06 +02:00
kevinveenbirkenbach 88418016ba Merge pull request #788 from kevinveenbirkenbach/fix/ci-fork-image-wait/784
fix(ci): keep the fork image wait alive and let it find its privileged run
2026-10-06 01:36:54 +02:00
kevinveenbirkenbachandClaude Opus 5.5 89b95d751c fix(update): stop bumping pins of eol roles
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>
2026-10-06 01:09:10 +02:00
kevinveenbirkenbachandClaude Opus 5.5 517d40484e test(mail): select autoescape for the rendered alarm script
The unit test of the alarm script built its Jinja environment with
autoescape switched off, which code scanning reports as a possible XSS
sink and fails the PR on. The template is a shell script, so
select_autoescape() leaves it unescaped exactly as before, and the finding
no longer applies.

Refs #790

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 17:19:56 +02:00
kevinveenbirkenbachandClaude Opus 5.5 4c955d080f feat(update): fail a declared update source whose pin is not a semver
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>
2026-10-05 12:48:22 +02:00
kevinveenbirkenbachandClaude Opus 5.5 7c7b895ae9 fix(ci): match the privileged run of a fork PR by its number as well
The wait job picks the newest privileged run on the head commit of the
PR. Two fork PRs that share a head repository, branch and commit but
target different bases each start such a run, and the commit alone cannot
tell them apart. A failed run of the other PR would then end the wait of
this one while its images are still being built.

The run name of the workflow carries the PR number, so the match now
requires it next to the head commit. A run without that name matches
nothing, and the wait does what it does whenever no run is found: it
keeps polling for the images until they exist or the wait times out.

Raised by the CodeRabbit review of this PR.

Refs #784

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 12:07:14 +02:00
kevinveenbirkenbach 1d03c001ea Merge remote-tracking branch 'origin/main' into fix/sys-ctl-hlth-msmtp/empty-reply-is-transient/790 2026-10-05 11:45:00 +02:00
kevinveenbirkenbachandClaude Opus 5.5 b37cf2e9fe fix(mail): retry a connection the mail server drops before its greeting
Stalwart closes a connection that exceeds its per-IP connection budget
without a greeting. msmtp reports that as "the server sent an empty reply"
with exit 76, and the mail health check treated exit 76 as permanent. One
such drop, in a second in which Alertmanager flushed its notifications,
failed a deploy although the same path accepted a mail one second later.

The health check now retries exit 76 when msmtp names the empty reply,
within the ten attempts it already gives to exit 69, 75 and 124. Every
other protocol error still ends the check at once.

The alarm mail was sent once, so the alarm about a failed unit was lost to
the same drop. It now makes three attempts ten seconds apart for the
transient cases of the health check and still reports the last error.

The limit on the server is left alone. Exempting the node's own senders by
source address would also exempt Tor clients and, in swarm, every external
client. That question is tracked in #793.

Closes #790

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 11:44:58 +02:00
kevinveenbirkenbach 8e9fa797fe Merge remote-tracking branch 'origin/main' into fix/ci-fork-image-wait/784 2026-10-05 03:13:43 +02:00
kevinveenbirkenbach a306b225c3 Merge remote-tracking branch 'origin/main' into update/onlyoffice-nextcloud-connector 2026-10-05 03:13:35 +02:00
kevinveenbirkenbachandClaude Opus 5.5 69fceafb5d fix(ci): limit the fingerprint shortcut to branches of this repository
The duplicate check accepted any open PR whose head branch started with
the dedupe prefix and ended in the fingerprint of the fresh proposal. The
listing also returns PRs from forks, and the fingerprint can be derived
from public data, so a fork PR with such a branch name would have made the
bot skip its own update PR without carrying the change.

The shortcut now only applies to PRs whose head branch lives in this
repository, which only accounts with write access can create. A fork PR
is still compared by the content of its head, as before.

Raised by the CodeRabbit review of PR #781.

Refs #780

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 03:06:57 +02:00
kevinveenbirkenbachandClaude Opus 5.5 739c78bad1 feat(update): bump addon pins through their declared source
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>
2026-10-05 02:50:48 +02:00
kevinveenbirkenbachandClaude Opus 5 81ab6e4ffc feat(update): let a version pin declare its own upstream source
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>
2026-10-05 02:50:48 +02:00
kevinveenbirkenbach f6228580be Merge remote-tracking branch 'origin/main' into fix/ci-fork-image-wait/784 2026-10-04 22:18:20 +02:00
kevinveenbirkenbachandClaude Opus 5.5 3ab0153856 fix(ci): find the privileged run of a fork PR by its head commit
The wait for fork CI images never found the privileged workflow run it
watches. It selected a run only when the run's pull_requests array held
the PR number, and for a run whose head repository is a fork the API
returns that array empty. The job only runs for fork PRs, so the lookup
matched in no case it exists for.

The early exit for a failed privileged run was therefore unreachable.
When that run failed, the job kept polling until its timeout of 1980
attempts and held a runner for about five and a half hours.

The lookup now asks the API for the runs of the PR head commit and
selects by head_sha. That also replaces a paginated listing of every run
of the workflow with one small request per attempt. Without a known head
commit the match by PR number stays.

Refs #784

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 20:55:54 +02:00
kevinveenbirkenbachandClaude Opus 5.5 53f71d85c8 fix(ci): keep waiting for fork images when a run lookup fails
The job that waits for the CI images of a fork PR ended on the first
failed GitHub API call. The lookup of the privileged workflow run is
assigned with run_json="$(find_matching_run)" under set -e, so a failing
gh ended the script, although the loop is built for 1980 attempts. PR #782
went red that way: GitHub answered one request with HTTP 504 in attempt
31, while the privileged run was still queued.

A failed lookup now counts as "no information in this attempt" and the
loop goes on waiting for the images, which are its success criterion.
Thirty failed lookups in a row end the job with a message that names the
cause, so a broken token does not hold a runner until the timeout.

The test runs the script against a gh that fails every lookup. The
previous script fails both cases at the first lookup.

Closes #784

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-04 20:55:54 +02:00