Commit Graph
14682 Commits
Author SHA1 Message Date
kevinveenbirkenbachandClaude Opus 4.7 dd7e84257f fix(update/open_pr): skip non-existent credentials paths via truncate -c
actions/checkout records two includeif targets per repo: one under
the host path (/home/runner/work/_temp/...) and one under the
container-internal path (/github/runner_temp/...). Only the host path
exists on the runner; the container path is a placeholder for
docker-based actions.

`truncate` (without -c) aborts on the missing container path with
exit 123. Add `-c` (--no-create) so missing paths are skipped
silently and the existing credentials file still gets blanked.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 13:42:59 +02:00
kevinveenbirkenbachandClaude Opus 4.7 6dc402e2b7 fix(update/open_pr): blank checkout's external credentials file
actions/checkout v6 doesn't put `http.https://github.com/.extraheader`
inside .git/config; it writes that header to an external file at
/home/runner/work/_temp/git-credentials-*.config and pulls it in via
local `includeif.gitdir:<repo>/.git.path = <credfile>` directives.
That's why every earlier `git config --local --unset-all` /
`--remove-section` round was a silent no-op (verified by the previous
debug commit's dump: the extraheader survives because the source-of-
truth lives outside .git/config).

Read every includeif `.path` value from local config, dedupe, and
`truncate -s 0` the targets so the included header chunk becomes
empty. The App installation token then enters the wire via the
URL-embedded `x-access-token:${GH_TOKEN}` credentials with no
competing Authorization header.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 13:37:08 +02:00
kevinveenbirkenbachandClaude Opus 4.7 86a998159b debug(update/open_pr): dump every config scope + probe token identity
The previous strip targeted only `--local` config and the resulting
CI log showed local was already empty, yet the push still authed
as github-actions[bot]. The auth has to be coming from --global,
--system, --worktree, or GH_TOKEN itself is GITHUB_TOKEN.

Dump every scope's http/credential/url entries before and after the
strip, and probe /installation/repositories with GH_TOKEN so the
HTTP status alone (no secret in logs) tells us whether it's an App
installation token or the workflow GITHUB_TOKEN.

Will replace this with the real fix once the next CI run surfaces
where the rogue Authorization actually originates.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 13:30:02 +02:00
kevinveenbirkenbachandClaude Opus 4.7 3dffbb8493 fix(update/open_pr): strip every extraheader via regex + log what was hit
Previous attempts targeted a specific config key path
(`http.https://github.com/.extraheader`). If actions/checkout v6
stores it under a different subsection naming, our unset/remove-section
no-ops silently and the GITHUB_TOKEN-bearing header keeps overriding
the App-token URL credentials.

Walk every `http.*.extraheader` entry the local config holds, pipe
the matched key names through xargs to unset-all each one, and dump
the remaining `http.*` config before pushing so the next CI run
proves what was stripped. The push itself still uses an
x-access-token URL so the App's installation token is the only
Authorization that reaches the wire.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 13:23:27 +02:00
kevinveenbirkenbachandClaude Opus 4.7 d950770385 fix(update/open_pr): nuke checkout's http section and push via tokenised URL
`--replace-all` on http.https://github.com/.extraheader still left the
wire with two Authorization headers ("remote: Duplicate header" → 400).
Drop the existing extraheader **and** the whole `http.https://github.com/`
subsection from local config, then push to a URL that embeds
`x-access-token:${GH_TOKEN}` directly so only one Authorization header
(the App's installation token) reaches the server.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 13:16:10 +02:00
kevinveenbirkenbachandClaude Opus 4.7 34f0c1d6c8 fix(update/open_pr): replace checkout extraheader instead of unsetting
Last attempt (unset + URL-embedded token) still pushed as
github-actions[bot] because actions/checkout's
http.https://github.com/.extraheader Authorization survived the
unset (likely silenced by `|| true`) and outranked URL-embedded
credentials on the wire.

Force-replace the header value with the App installation token's
HTTP Basic equivalent (`x-access-token:<token>` base64'd, same shape
checkout itself uses for GITHUB_TOKEN). Drops the URL rewrite, drops
the unset-then-rewrite dance, and guarantees a single Authorization
header that GitHub honours as the App.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 13:08:35 +02:00
kevinveenbirkenbach 6b63c12573 Merge branch 'main' into feature/req-21-sso-flavor-migration 2026-05-28 13:01:55 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b2a3d003af fix(update/open_pr): suppress url-reachability lint on extraheader unset
`git config --unset-all "http.https://github.com/.extraheader"` is a
git config key path, not a fetchable URL, but
tests/external/repository/test_urls_reachable.py extracts the URL-like
substring and probes it, which returns 404 and fails the suite.

Tag the line with `# nocheck: url` so the URL test skips it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 13:01:55 +02:00
kevinveenbirkenbachandClaude Opus 4.7 55e2a5a4d5 fix(update): push update branches with the App installation token
`actions/checkout` configures git's http.extraheader to authenticate
as GITHUB_TOKEN, which update.yml scopes to `contents: read`. The
subsequent `git push --force` in open_pr.sh therefore tripped 403
("Permission to infinito-nexus/core.git denied to github-actions[bot]")
even though the token-minting step had already produced an App
installation token with `contents: write` in GH_TOKEN.

Drop checkout's extraheader and rewrite origin's URL to embed
GH_TOKEN as x-access-token before pushing, so the App's permissions
actually reach the wire.

Also pin two actionlint `-ignore` patterns in scripts/lint/action.sh
to silence false positives for the `client-id` input on
actions/create-github-app-token@v3 (the action's action.yml carries
the input and marks `app-id` deprecated; actionlint 1.7.12's bundled
schema lags behind the upstream release).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 13:01:55 +02:00
kevinveenbirkenbachandClaude Opus 4.7 f473b12888 chore(workflows): migrate create-github-app-token from app-id to client-id
actions/create-github-app-token@v3 emits "Input 'app-id' has been
deprecated with message: Use 'client-id' instead." on every run.
Swap the input + rename the corresponding repository secret from
BOT_APP_ID to BOT_APP_CLIENT_ID so the value reflects what the
action actually expects: the OAuth-style Client ID (`Iv…` format
shown on the App's General page), not the numeric App ID.

Requires the operator to create a fresh `BOT_APP_CLIENT_ID`
repository secret on infinito-nexus/core before merging; the old
`BOT_APP_ID` secret becomes orphaned and SHOULD be deleted in the
same settings pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 13:01:55 +02:00
kevinveenbirkenbachandClaude Opus 4.7 4085dc1ab1 fix(update): tree-hash dedup + shellcheck SC1091 suppression
open_pr.sh's PR-dedup loop relied on `gh pr diff <num>` which the
GitHub API refuses with HTTP 406 once a PR exceeds 300 changed files.
Under `set -euo pipefail` that killed the workflow whenever any
unrelated large PR was open against the same base branch.

Replace the diff-hash check with a tree-SHA comparison: fetch each
PR's head commit shallowly, compare `git log -1 --format=%T` against
HEAD's tree. Same-tree means same end-state regardless of base SHA,
whitespace, file order, or patch context. Unfetchable heads (deleted
branches, lost permissions) fall through as non-duplicates instead of
aborting the run.

image_versions.sh / repository_refs.sh also pick up an explicit
`# shellcheck disable=SC1091` above their `. .env` source since the
dotenv file is generated at runtime and never present during
shellcheck's static parse.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 13:01:54 +02:00
kevinveenbirkenbach 40b2dfc5f4 Release version 8.0.3 2026-05-28 13:01:54 +02:00
kevinveenbirkenbachandClaude Opus 4.7 ee9800200e fix(update/open_pr): suppress url-reachability lint on extraheader unset
`git config --unset-all "http.https://github.com/.extraheader"` is a
git config key path, not a fetchable URL, but
tests/external/repository/test_urls_reachable.py extracts the URL-like
substring and probes it, which returns 404 and fails the suite.

Tag the line with `# nocheck: url` so the URL test skips it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 13:01:36 +02:00
kevinveenbirkenbachandClaude Opus 4.7 ce30948ed4 fix(update): push update branches with the App installation token
`actions/checkout` configures git's http.extraheader to authenticate
as GITHUB_TOKEN, which update.yml scopes to `contents: read`. The
subsequent `git push --force` in open_pr.sh therefore tripped 403
("Permission to infinito-nexus/core.git denied to github-actions[bot]")
even though the token-minting step had already produced an App
installation token with `contents: write` in GH_TOKEN.

Drop checkout's extraheader and rewrite origin's URL to embed
GH_TOKEN as x-access-token before pushing, so the App's permissions
actually reach the wire.

Also pin two actionlint `-ignore` patterns in scripts/lint/action.sh
to silence false positives for the `client-id` input on
actions/create-github-app-token@v3 (the action's action.yml carries
the input and marks `app-id` deprecated; actionlint 1.7.12's bundled
schema lags behind the upstream release).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:47:59 +02:00
kevinveenbirkenbachandClaude Opus 4.7 29a01ff538 chore(workflows): migrate create-github-app-token from app-id to client-id
actions/create-github-app-token@v3 emits "Input 'app-id' has been
deprecated with message: Use 'client-id' instead." on every run.
Swap the input + rename the corresponding repository secret from
BOT_APP_ID to BOT_APP_CLIENT_ID so the value reflects what the
action actually expects: the OAuth-style Client ID (`Iv…` format
shown on the App's General page), not the numeric App ID.

Requires the operator to create a fresh `BOT_APP_CLIENT_ID`
repository secret on infinito-nexus/core before merging; the old
`BOT_APP_ID` secret becomes orphaned and SHOULD be deleted in the
same settings pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:47:59 +02:00
kevinveenbirkenbachandClaude Opus 4.7 a988c12606 fix(update): tree-hash dedup + shellcheck SC1091 suppression
open_pr.sh's PR-dedup loop relied on `gh pr diff <num>` which the
GitHub API refuses with HTTP 406 once a PR exceeds 300 changed files.
Under `set -euo pipefail` that killed the workflow whenever any
unrelated large PR was open against the same base branch.

Replace the diff-hash check with a tree-SHA comparison: fetch each
PR's head commit shallowly, compare `git log -1 --format=%T` against
HEAD's tree. Same-tree means same end-state regardless of base SHA,
whitespace, file order, or patch context. Unfetchable heads (deleted
branches, lost permissions) fall through as non-duplicates instead of
aborting the run.

image_versions.sh / repository_refs.sh also pick up an explicit
`# shellcheck disable=SC1091` above their `. .env` source since the
dotenv file is generated at runtime and never present during
shellcheck's static parse.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 12:23:15 +02:00
kevinveenbirkenbach b12397e988 Release version 8.0.3 v8.0.3 2026-05-28 11:51:38 +02:00
kevinveenbirkenbach 1e0f86d0d0 Merge branch 'main' into feature/req-21-sso-flavor-migration 2026-05-28 11:50:53 +02:00
kevinveenbirkenbachandClaude Opus 4.7 3537d4d0a6 fix(update): load .env before invoking python updaters
`update.yml` jobs (Docker image versions, repository refs) shelled
straight into `python3 -m cli.contributing.update.{docker,repository}`
without sourcing the project dotenv. After eb4bcd23a removed the
`.get(..., default)` fallback in resolve_max_fetch_workers(),
KeyError: 'INFINITO_WORKER_FETCH' surfaced on every run.

Both wrapper scripts now `make dotenv` (idempotent) and `set -a; . .env;
set +a` before exec'ing the updater, restoring the SPOT env chain on
CI runners that don't go through compose.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 11:26:23 +02:00
kevinveenbirkenbach 211fbcecf8 Release version 8.0.2 v8.0.2 2026-05-28 11:12:31 +02:00
kevinveenbirkenbachandClaude Opus 4.7 0f0e1dd98b chore(pkgmgr): bump pin to v1.15.2
v1.15.2 ships the `alias: infinito` entry in pkgmgr's
src/pkgmgr/config/defaults.yaml so `pkgmgr install infinito`
(and `pkgmgr path/version infinito`) resolves again. Without
this bump, sys-cli, web-app-navigator and the test-install-pkgmgr
CI job stay broken on the upstream identifier-rename fallout.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 11:11:21 +02:00
kevinveenbirkenbachandClaude Opus 4.7 848fd08308 fix(docker): drop pkgmgr from dev container, install in-tree via make
`pkgmgr install infinito` started failing in CI after the floating
`pkgmgr-<distro>:stable` base image was rebuilt with kpmx 1.15.x, which
no longer registers the `infinito` identifier in its default config:

    Identifier 'infinito' did not match any repository in config.

The error string then flowed through `pkgmgr path infinito` in
entry.sh and was treated as the install directory, leading to
`make install` in a bogus cwd. This broke test-development,
test-install-pkgmgr, and build-ci-images for every distro.

The pkgmgr step in the dev Dockerfile is redundant — `make install`
runs `pip install -e .` which already produces the `infinito` CLI
shim via pyproject.toml's [project.scripts] entry. Drop the pkgmgr
calls, the rsync-into-INFINITO_PATH dance, and the `INFINITO_PATH`
variable; everything operates on `INFINITO_SRC_DIR` directly.

Production `pkgmgr install` paths (roles/sys-cli, roles/web-app-navigator,
.github/workflows/test-install-pkgmgr.yml) are intentionally untouched —
the test-install-pkgmgr job is what surfaces the upstream kpmx breakage.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 10:50:35 +02:00
kevinveenbirkenbach 4187237ed0 docs(req-21): rename stale oidc/oauth2 service-gate refs to sso
Three docs still referenced the pre-collapse service keys:

* docs/contributing/artefact/files/role/playwright.specs.js.md — biber
  and administrator auth-chain diagrams listed
  `skipUnlessServiceEnabled('oidc' | 'oauth2' | 'ldap')`; the unified gate
  is `'sso' | 'ldap'`.
* docs/contributing/design/role/services/base.md — the `lookup('service',
  ...)` example block carried the now-invalid `'oidc'` key; replaced
  with `'sso'`.
* docs/requirements/019-playwright-meta-services-parity.md — the
  prometheus, matomo, and mailu rows still spelled
  `safeSkipUnlessEnabled("oauth2"|"oidc")` and
  `isServiceEnabled("oauth2")`; all rewritten to `"sso"`.
2026-05-28 09:43:52 +02:00
kevinveenbirkenbach adf1d9cd75 fix(web-app-wordpress/playwright): resolve SSO_SERVICE_ENABLED via sso (req 21)
`lookup('service', 'oidc')` blew up during render after the oauth2+oidc →
sso collapse — the `oidc` key no longer exists in the service registry.
Rename the lookup to `sso` so the WordPress Playwright env renders again.
2026-05-28 09:40:15 +02:00
kevinveenbirkenbach b512ecd318 fix(web-app-openwebui/playwright): gate OIDC login tests on sso (req 21)
`skipUnlessServiceEnabled("oidc")` tripped service-gating's unknown-service
guard after the oauth2+oidc → sso collapse. Rename the gate to `sso` so
both admin and biber persona tests skip/run via the rendered
`SSO_SERVICE_ENABLED` flag.
2026-05-28 09:39:31 +02:00
kevinveenbirkenbach 5f0ad01e42 fix(web-app-taiga/playwright): flavor-gate taiga{Oauth2,Oidc}Enabled via SSO_FLAVOR
Post-req-21 both flags were bound on `isServiceEnabled("sso")`, so any
sso-enabled variant entered the oauth2-proxy redirect branch — which
timed out because Taiga is `flavor: oidc` (in-app `taiga-contrib-oidc-auth`,
no oauth2-proxy sidecar). Render `SSO_FLAVOR` from the unified `sso`
resolver and split the predicates so taiga{Oauth2,Oidc}Enabled track the
actual flavor.
2026-05-28 09:38:40 +02:00
kevinveenbirkenbach b1f8bf76dc Merge branch 'main' into feature/req-21-sso-flavor-migration 2026-05-28 09:25:28 +02:00
kevinveenbirkenbach fae8fa468a Release version 8.0.1 v8.0.1 2026-05-28 09:14:46 +02:00
kevinveenbirkenbachandClaude Opus 4.7 25d709785c feat(make): add make requirements-archive target
Installs kpmx and runs `pkgmgr archive docs/requirements` in one
shot. The requirements-completeness lint failure message now points
contributors at this target instead of asking them to invoke pkgmgr
directly.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 09:13:02 +02:00
kevinveenbirkenbachandClaude Opus 4.7 6f9fb4c55b refactor(test/requirements-completeness): inline discovery and unchecked counter
The lint test only used two tiny helpers from kpmx
(iter_archivable_files, count_unchecked_items). Inlining them is ~15
lines and lets us drop kpmx from dev dependencies entirely, so neither
host nor container has to install the package just for a lint that
walks docs/requirements/ and counts task-list markers. The
pkgmgr archive CLI remains the canonical archival tool; the failure
message still points there.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 09:07:23 +02:00
kevinveenbirkenbachandClaude Opus 4.7 55492b1d9e chore(deps-dev): add kpmx>=1.15.0 for the requirements archive lint
tests/lint/repository/documentation/test_requirements_completeness.py
imports pkgmgr.actions.archive after the v8.0.x archive-CLI move out
of cli/contributing/requirements/. The test container installs dev
dependencies via `pip install -e .[dev]`, so kpmx must be listed
there or the lint test fails with `ModuleNotFoundError: pkgmgr`
during collection.

Pinned to >=1.15.0 because the archive subcommand and the
pkgmgr.actions.archive package only ship in that release.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 08:26:36 +02:00
kevinveenbirkenbachandClaude Opus 4.7 519010c439 refactor(requirements): move archive CLI from cli/ to kpmx (pkgmgr)
The fully-checked-Markdown archive helper used to live under
cli/contributing/requirements/archive/ in this repo. It is a generic
spec-folder-management primitive (numbered NNN-topic.md files,
task-list completeness check, README ## Archive section append) and
now ships in pkgmgr.actions.archive so every kpmx-managed repository
can rely on the same convention.

Changes here:

* tests/lint/repository/documentation/test_requirements_completeness.py
  imports the archive primitives from pkgmgr.actions.archive instead
  of the vendored cli.contributing.requirements.archive, and points
  contributors at `pkgmgr archive docs/requirements` in the failure
  message.
* cli/contributing/requirements/README.md becomes a stub that
  redirects readers to the kpmx archive subcommand.
* The vendored cli/contributing/requirements/archive/ package and its
  unit tests under tests/unit/ are deleted.
* The pkgmgr role pin bumps to v1.15.0 to pick up the new archive
  subcommand on deployed hosts.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 08:06:16 +02:00
kevinveenbirkenbachandClaude Opus 4.7 dc64955a70 fix(playwright): satisfy eslint preserve-caught-error + no-useless-assignment
Five eslint errors blocking make lint-javascript:

* personas/guest.js, web-app-opencloud/playwright.spec.js,
  web-app-opentalk/playwright.spec.js — re-thrown wrapped errors now
  carry { cause: err } so the symptom keeps a link to the root cause.
* personas/utils/csp.js — drop let blockedHost = "" initial value;
  the catch arm returns early, so the empty string was never read.
* web-app-wordpress/_shared.js — drop let match = null initial value;
  both branches assign before the readers fire.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 07:30:53 +02:00
kevinveenbirkenbachandClaude Opus 4.7 e82d5b94bb chore(pkgmgr): pin to v1.14.0 (was floating stable)
v1.14.0 ships:
* `pkgmgr release --retry` re-deploys an existing HEAD release without
  re-tagging or modifying any files (re-push tag + re-align latest +
  re-run publish), so a release whose post-tag push or PyPI upload
  failed mid-flight can be recovered without code churn.
* package_name now resolves from packaging/debian/control,
  packaging/arch/PKGBUILD, or RPM .spec instead of the repo folder
  basename, so renaming a repo no longer flips the distro-package
  identity. This is what unblocks the upcoming infinito-nexus-core
  Debian builds whose changelog/control mismatch caused
  `dpkg-source --before-build` to exit 2.

Explicit pin (vs the prior floating `stable`) gives reproducible
deploys across hosts and locks the new package_name resolver behaviour
to a known version.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 07:30:53 +02:00
kevinveenbirkenbach 42d326e5c4 Merge branch 'main' into feature/web-app-zammad 2026-05-28 00:09:56 +02:00
kevinveenbirkenbachandClaude Opus 4.7 3d50baf359 docs(agents/iteration/role): sharpen in-container verify rule + full-matrix flow
Three iteration-rule sharpenings, all surfaced by repeated foot-guns in
the web-app-zammad role-build loop:

- Promote "validate the fix in-container before redeploying" from SHOULD
  to MUST. Adds an explicit reproducible-there carve-out (container
  crashes at startup, misconfigured env var, image-side bug, in-container
  script failure) so the rule is unambiguous on when it applies; the
  only exception is failures that genuinely depend on multi-container or
  post-deploy Ansible state.
- Playwright-specific addendum: when the failing unit is a Playwright
  spec, "in-container validation" means running
  `make compose-playwright role=<role>` against the live stack — not
  just `curl` against the underlying API. The spec runs in a browser
  context with env loading, locator strategies, and cookie state that
  `curl` does not exercise.
- New "Full-matrix iteration flow" subsection under "Matrix variants"
  documenting the variant-pinned `mode=update` → per-remaining-variant
  `mode=reinstall full_cycle=true` → final unpinned full-matrix
  `mode=reinstall full_cycle=true` recipe. Closes the gap where the
  existing "Matrix variants" rules only described focused single-variant
  debug and full-matrix initiation, but not the cycle that keeps full
  cross-variant coverage as the iteration target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 00:09:27 +02:00
kevinveenbirkenbachandClaude Opus 4.7 50f8f1251a merge: keep HEAD refactor; preserve nginx-resolv WHY comment from remote
fork/feature/web-app-zammad held an older variant of the same 5
Zammad-role commits (different SHAs after a local rebase). HEAD is a
content superset (adds the docs-iteration sharpen + the
basic_auth_password→api_bot_password rename + SPOT migrations), so all
15 conflicted paths resolved with --ours. Cherry-picked the
nginx-resolv comment from the incoming side into compose.yml.j2 — it
documents WHY the entrypoint sed-patch exists and is worth keeping.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 00:08:48 +02:00
kevinveenbirkenbachandClaude Opus 4.7 177f039b11 refactor(web-app-zammad): rename basic_auth_password → api_bot_password + SPOTs
- Rename credential basic_auth_password → api_bot_password and var
  ZAMMAD_ADMINISTRATOR_PASSWORD → ZAMMAD_API_BOT_PASSWORD to reflect
  that the value is the role-internal bot's local password, not the
  administrator's (reused for the wizard-bypass admin only because both
  need the same shell-safe alphanumeric form).
- Introduce ORGANIZATION constant (defaults to SOFTWARE_NAME) and migrate
  organization-string consumers (mattermost team, shopware shop title,
  dashboard login/logout descriptions, simpleicons "provided by",
  user-administrator description/lastname).
- Add HOST_LL_CC_TAG (BCP-47 form, e.g. en-gb) and use in auto_wizard.
- Add NETWORK_TRUSTED_PROXY_CIDRS SPOT and consume from suitecrm,
  shopware and zammad reverse-proxy configs.
- Extract ZAMMAD_WEB_PROTOCOL via lookup('tls', …, 'protocols.web')
  instead of inline 'https' if … else 'http'.
- Use database_query for CREATEDB grant so it works on shared+non-shared
  databases; back the SQL with files/sql/grant_createdb.sql SPOT.
- Read administrator firstname/lastname from users lookup in
  auto_wizard.json.j2 instead of hardcoding.
- Drop redundant elasticsearch: block from web-app-zammad services.yml
  (no consumer reads it).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 00:03:15 +02:00
kevinveenbirkenbachandClaude Opus 4.7 1042523a45 docs(agents/iteration/role): sharpen in-container verify rule + full-matrix flow
Three iteration-rule sharpenings, all surfaced by repeated foot-guns in
the web-app-zammad role-build loop:

- Promote "validate the fix in-container before redeploying" from SHOULD
  to MUST. Adds an explicit reproducible-there carve-out (container
  crashes at startup, misconfigured env var, image-side bug, in-container
  script failure) so the rule is unambiguous on when it applies; the
  only exception is failures that genuinely depend on multi-container or
  post-deploy Ansible state.
- Playwright-specific addendum: when the failing unit is a Playwright
  spec, "in-container validation" means running
  `make compose-playwright role=<role>` against the live stack — not
  just `curl` against the underlying API. The spec runs in a browser
  context with env loading, locator strategies, and cookie state that
  `curl` does not exercise.
- New "Full-matrix iteration flow" subsection under "Matrix variants"
  documenting the variant-pinned `mode=update` → per-remaining-variant
  `mode=reinstall full_cycle=true` → final unpinned full-matrix
  `mode=reinstall full_cycle=true` recipe. Closes the gap where the
  existing "Matrix variants" rules only described focused single-variant
  debug and full-matrix initiation, but not the cycle that keeps full
  cross-variant coverage as the iteration target.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 00:03:15 +02:00
kevinveenbirkenbachandClaude Opus 4.7 2f52a80ece feat(web-app-zammad): expand Playwright coverage + extract bootstrap Ruby
Adds 7 new Playwright scenarios covering helpdesk-core workflows and
infrastructure regressions:

  - test-csp-headers          — CSP response + meta-tag parity
  - test-alias-domain         — server-name alias serves 2xx, not 30x
  - test-rest-api-ticket-create — POST /api/v1/tickets via Basic auth
  - test-ticket-create-as-customer — biber SPA → fill form → submit
  - test-ticket-reply-as-agent — API-seeded ticket; admin posts a reply
  - test-websocket-realtime   — open ticket; API appends article;
                                expect realtime push into the open SPA
  - test-mail-to-ticket       — raw-socket SMTP to Mailu helpdesk
                                mailbox; poll Zammad until ticket
                                appears

Auto-configures Zammad's Email channel (inbound IMAP + outbound SMTP)
against Mailu via a new post-bootstrap rails-runner step. The Ruby for
both bootstrap steps lives in standalone files and is piped through
`container exec -i ... bundle exec rails r -`; the previous wrapper
shell scripts (`apply_*.sh`) are removed in favour of inline `shell:`
calls in tasks/01_core.yml. Env vars are forwarded into the container
via `container exec -e VAR ...` so the Ruby uses `ENV.fetch` and stays
value-free / portable.

vars/main.yml grows three constants:

  - ZAMMAD_OMNIAUTH_INIT_CONTAINER_PATH — bind-mount target inside the
    container (was hard-coded four times in compose.yml.j2)
  - ZAMMAD_HELPDESK_USER / _EMAIL / _PASS — derived from the `helpdesk`
    mail-bot user provisioned via meta/users.yml
  - ZAMMAD_MAIL / ZAMMAD_EMAIL_ENABLED — derived from the central email
    lookup + the services.email gate

playwright.env.j2 grows MAIL_SMTP_HOST/PORT/USER/PASS + HELPDESK_EMAIL
for the SMTP injection in the mail-to-ticket scenario, sorted ASC per
the env-key lint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 00:03:15 +02:00
kevinveenbirkenbachandClaude Opus 4.7 0656671778 refactor(web-app-zammad): split Playwright spec, dedupe lookups into vars
Playwright spec split into per-test partials, mirroring the web-app-kix
layout (lint-conform: top-level `playwright.spec.js` requires the
partials and shared module):

- `files/playwright/_shared.js`        — env vars, `signInViaZammadOidc`,
                                         `zammadLogout`, `beforeEach`
- `files/playwright/test-landing.js`
- `files/playwright/test-login-administrator.js`
- `files/playwright/test-login-biber.js`
- `files/playwright/test-guest-persona.js`

Variable dedup in `vars/main.yml`:

- `ZAMMAD_VOLUMES_DIR`               — single base path
- `ZAMMAD_AUTO_WIZARD_HOST_PATH`     — derived
- `ZAMMAD_OMNIAUTH_INIT_HOST_PATH`   — derived (was repeated 4x in compose)
- `ZAMMAD_OIDC_ENABLED`              — single source for the OIDC gate

All consumers in `tasks/01_core.yml`, `templates/compose.yml.j2`,
`templates/auto_wizard.json.j2`, `templates/playwright.env.j2` now read
the constants instead of inlining the `lookup(...)` expressions.

Drive-by: `meta/main.yml` description narrowed to describe Zammad itself
(role-specific text moved out of the upstream-facing galaxy_info field).

`make autoformat` clean, `make test` green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 00:03:15 +02:00
kevinveenbirkenbachandClaude Opus 4.7 c9f92d30bf style(web-app-zammad): autoformat schema.yml api_token description
`make autoformat` line-wraps the api_token credential description in
meta/schema.yml at the standard yamllint column. No semantic change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 00:03:15 +02:00
kevinveenbirkenbachandClaude Opus 4.7 0f6a565dc5 feat(web-app-zammad): add Zammad helpdesk role with OIDC SSO
Adds roles/web-app-zammad/ — a brand-new web-app role deploying Zammad as
the Infinito.Nexus helpdesk with direct OpenID Connect SSO against the
shared Keycloak client. Tracks requirement 022.

Stack: ghcr.io/zammad/zammad 6.5.0 (init / railsserver / websocket /
scheduler / nginx, all driven by upstream command modes), bundled
elasticsearch 8.13.4 + bundled memcached (no central svc role exists),
central svc-db-postgres reused via lookup('database', application_id).
Variant matrix mirrors web-app-kix: V1 oidc+ldap, V2 all-false, V3 ldap-only.

Bootstrap flow (tasks/01_core.yml):
- Stage auto_wizard.json into the role's volumes dir AFTER sys-stk-full
  creates the directory; bind-mount it into zammad-init so the upstream
  rake zammad:setup:auto_wizard bypasses the setup UI on first boot.
- Stage zz_omniauth_allow_get.rb initializer + bind-mount into rails /
  websocket / scheduler / nginx, since the Zammad SPA's SSO link is a
  plain GET and omniauth 2.x forbids GET by default.
- ALTER ROLE <zammad-db-user> WITH CREATEDB so Zammad's unconditional
  `rake db:create` doesn't trip on insufficient privilege when the
  database is pre-created by svc-db-postgres.
- Apply post-bootstrap settings via apply_oidc_settings.sh
  (rails-runner inside zammad-railsserver): fqdn, http_type so Zammad
  emits the correct redirect_uri to Keycloak; auth_openid_connect +
  auth_openid_connect_credentials so the SSO button works.

Workarounds embedded in compose.yml.j2:
- zammad-nginx command override that copies /docker-entrypoint.sh to
  /tmp and patches `grep "nameserver"` to `grep -E "^nameserver"`,
  because Docker writes comment lines like `# Overrides: …` into
  /etc/resolv.conf that the upstream grep matches as substrings,
  yielding an invalid `resolver … Overrides: …` directive nginx rejects.
- DATABASE_URL pre-populated in env.j2 so post-bootstrap
  `container exec rails r` calls (which bypass the entrypoint) can
  load Rails without database.yml.

Playwright (files/playwright/playwright.spec.js, single-file lint-conform
layout): landing-reachable + administrator OIDC + biber OIDC + guest
public-landing scenarios. Personas skip-correctly via
skipUnlessServiceEnabled('oidc') under V2 / V3.

V1 end-to-end green: admin + biber Playwright pass through the full
Keycloak SSO round-trip. V2 (all-false) and V3 (ldap-only) green on
baseline reinstall with full_cycle=true.

Follow-ups indexed in roles/web-app-zammad/TODO.md: Zammad LdapSource
auto-create for V3, mail channel auto-config + end-to-end mail-to-ticket
test, Keycloak-group-to-Zammad-role mapping (Zammad doesn't ship a
built-in claim-to-role translator — currently all OIDC logins default to
Customer per Decision #5 fallback), Playwright assertion for the
`zammad.helpdesk.*` server-name alias.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 00:03:15 +02:00
kevinveenbirkenbachandClaude Opus 4.7 b4652a2d30 docs(requirements): add 022 web-app-zammad with OIDC SSO
New requirement for a `web-app-zammad` role that integrates Zammad as
the Infinito.Nexus helpdesk with OIDC-direct SSO against Keycloak.

Captures 11 operator-confirmed design decisions before implementation:

- Canonical `helpdesk.infinito.example` + true server-name alias
  `zammad.helpdesk.infinito.example` (no 301 redirect).
- SSO flavor: OIDC-direct via Zammad's native OmniAuth strategy
  (no oauth2-proxy sidecar). Uses the legacy `services.oidc.*` shape
  (pre-021); does not block on 021.
- Keycloak OIDC client auto-provisioned via `web-app-keycloak`.
- Keycloak group -> Zammad role mapping (Admin/Agent/Customer) if
  Zammad's OIDC strategy supports group claims natively; otherwise
  documented limitation.
- Upstream `ghcr.io/zammad/zammad` image pinned to latest stable
  semver. Self-built image (a la 015 Moodle) explicitly out of scope.
- Central-service reuse: `svc-db-postgres`, `svc-db-redis`,
  `svc-db-memcached`. Elasticsearch bundled in-role (no central
  `svc-db-elasticsearch` exists today).
- Email in scope: SMTP outbound via `sys-svc-mail-smtp` /
  `web-app-mailu`, IMAP inbound (mail-to-ticket) auto-wired.
- Setup wizard bypassed on first deploy via env-vars + post-start
  API call so a fresh deploy lands a ready-to-use instance.
- Playwright biber + administrator personas in scope (per 019).
- `meta/variants.yml` defines three variants mirroring web-app-kix:
  V1 oidc+ldap, V2 all-false, V3 ldap-only.

Acceptance criteria are grouped per concern (routing/TLS, role
layout & image, central-service reuse, SSO/OIDC, LDAP, email,
first-admin bootstrap, variants, playwright, health & quality,
documentation) so each item is independently verifiable.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 00:03:15 +02:00
kevinveenbirkenbachandClaude Opus 4.7 954cb3ff07 fix(debian/changelog): keep source name as infinito-nexus, not infinito-nexus-core
Release 8.0.0 (12e53905f) bumped the changelog's top entry to
`infinito-nexus-core` while every other packaging artefact still
addresses the distro package as `infinito-nexus`:

  * `packaging/arch/PKGBUILD`             pkgname=infinito-nexus
  * `packaging/fedora/infinito-nexus.spec` Name: infinito-nexus
  * `packaging/debian/control`             Source/Package: infinito-nexus
  * `scripts/install/package.sh`           globs for infinito-nexus_*.deb,
                                            infinito-nexus-*.pkg.tar.*,
                                            infinito-nexus-*.rpm

dpkg-source --before-build refused to build because the changelog said
`infinito-nexus-core` while control said `infinito-nexus`; after the
preceding (now reverted) "align control with changelog" attempt, the
dpkg output filename became `infinito-nexus-core_8.0.0-1_all.deb`,
which fails the installer's `infinito-nexus_*.deb` glob check.

The repository name (`infinito-nexus-core`) is unrelated to the
distro-package name. The packaging convention is the short form
`infinito-nexus`. Align the changelog with the rest of the packaging
fleet instead of dragging every other file along.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 19:52:58 +02:00
kevinveenbirkenbachandClaude Opus 4.7 0eca7d7b16 fix(debian/changelog): keep source name as infinito-nexus, not infinito-nexus-core
Release 8.0.0 (12e53905f) bumped the changelog's top entry to
`infinito-nexus-core` while every other packaging artefact still
addresses the distro package as `infinito-nexus`:

  * `packaging/arch/PKGBUILD`             pkgname=infinito-nexus
  * `packaging/fedora/infinito-nexus.spec` Name: infinito-nexus
  * `packaging/debian/control`             Source/Package: infinito-nexus
  * `scripts/install/package.sh`           globs for infinito-nexus_*.deb,
                                            infinito-nexus-*.pkg.tar.*,
                                            infinito-nexus-*.rpm

dpkg-source --before-build refused to build because the changelog said
`infinito-nexus-core` while control said `infinito-nexus`; after the
preceding (now reverted) "align control with changelog" attempt, the
dpkg output filename became `infinito-nexus-core_8.0.0-1_all.deb`,
which fails the installer's `infinito-nexus_*.deb` glob check.

The repository name (`infinito-nexus-core`) is unrelated to the
distro-package name. The packaging convention is the short form
`infinito-nexus`. Align the changelog with the rest of the packaging
fleet instead of dragging every other file along.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 19:51:50 +02:00
kevinveenbirkenbach 80010a295c Revert "fix(debian): align source/package name with changelog (infinito-nexus-core)"
This reverts commit 7c61dbfb3fbafad24f342adb66a2f1090ecfeea5.
2026-05-27 19:51:49 +02:00
kevinveenbirkenbachandClaude Opus 4.7 ea6456a75d fix(debian): align source/package name with changelog (infinito-nexus-core)
`dpkg-source --before-build` exited with status 2 across every distro in
the CI test-development matrix + both build-ci-images jobs because the
control file still declared `Source: infinito-nexus` / `Package:
infinito-nexus` while the changelog top entry (Release 8.0.0,
12e53905f) already advertises the new project name
`infinito-nexus-core`. dpkg-source compares the two and refuses to
build when they disagree (Policy §5.2).

Rename `Source` + `Package` in control to match. Also pull the
substvars filename and build-dir paths in `.gitignore` along, and
update the cosmetic postinst echo so the user-visible install message
matches the actual package name.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 19:24:38 +02:00
kevinveenbirkenbachandClaude Opus 4.7 f19a0b246b fix(playwright): satisfy eslint preserve-caught-error + no-useless-assignment
Five eslint errors blocking `make lint-javascript`:

* `personas/guest.js`, `web-app-opencloud/playwright.spec.js`,
  `web-app-opentalk/playwright.spec.js` — re-thrown wrapped errors now
  carry `{ cause: err }` so the symptom keeps a link to the root cause.
* `personas/utils/csp.js` — drop `let blockedHost = ""` initial value;
  the catch arm returns early, so the empty string was never read.
* `web-app-wordpress/_shared.js` — drop `let match = null` initial value;
  both branches assign before the readers fire.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 18:54:44 +02:00
kevinveenbirkenbachandClaude Opus 4.7 d6c9ba38fb docs(web-app-fusiondirectory/README): align wording with services.sso refactor
Replace "oauth2-proxy" descriptor with "SSO-proxy sidecar (provided by
web-app-keycloak)" so the Overview and Features sections both speak the
post-021 vocabulary. Upstream image name stays as-is.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 18:31:08 +02:00