vikunja/service: install Vikunja natively on the guest #2

Merged
pit merged 6 commits from hermes/service-stack into main 2026-10-09 06:03:40 +00:00
Owner

The service layer is applied and verified against the live guest. Vikunja runs at https://vikunja.thepit.space/ once the Edge exists; over the LAN it is serving on 10.12.0.142:3456 now.

What landed

vikunja/service/ — an Ansible role that installs Vikunja natively (no container): PostgreSQL 17 from the Debian repo, the upstream v2.7.0 binary via a checksum-pinned download, a config rendered from a template, and a sandboxed systemd unit. Plus make service-check / make service and the npm/ folder still to come.

Verified on the guest (vmid 142)

Check Result
get_url checksum archive sha256 matches the pinned value
systemctl status vikunja active (running), enabled, starts at boot
GET / 200, frontend served
GET /api/v1/info 200, frontend_url: https://vikunja.thepit.space/
Database PostgreSQL 17.11, 42 tables migrated
vikunja doctor all checks pass (rc=0)
Login as pit 200, token issued
Create a project 201 — and deleted again
Second make service run changed=0

Decisions worth your eye

  • The CLI reads the account password from a TTY only. Stdin gives inappropriate ioctl for device, so it goes through argv (--password, task is no_log) — same trade-off the forgejo role records.
  • No sidecar checksum is published for the release archive, so the sha256 is pinned in defaults/main.yml after measuring it. Bumping the version means updating both the version and that hash.
  • The mailer is off until vault_vikunja_smtp_password is set. mail.smtp2go.com:2525 is wired in; add the password and the next run turns it on.
  • Secrets are in a committed ansible-vault file; the vault password lives in the root .env and the Makefile derives the gitignored vault-pass from it. No secret value appears in the diff (checked).

Not done yet

  • The Edge stack (npm/) — the proxy host vikunja.thepit.space → 10.12.0.142:3456, websockets on. The public URL will not answer until this exists, and it needs the LE cert you are creating in the NPM UI.
  • Registration is already disabled in the config, so pit is the only account; add more via vault_vikunja_account_passwords.
The service layer is applied and verified against the live guest. Vikunja runs at `https://vikunja.thepit.space/` once the Edge exists; over the LAN it is serving on `10.12.0.142:3456` now. **What landed** `vikunja/service/` — an Ansible role that installs Vikunja natively (no container): PostgreSQL 17 from the Debian repo, the upstream `v2.7.0` binary via a checksum-pinned download, a config rendered from a template, and a sandboxed systemd unit. Plus `make service-check` / `make service` and the `npm/` folder still to come. **Verified on the guest (vmid 142)** | Check | Result | | --- | --- | | `get_url` checksum | archive sha256 matches the pinned value | | `systemctl status vikunja` | `active (running)`, enabled, starts at boot | | `GET /` | `200`, frontend served | | `GET /api/v1/info` | `200`, `frontend_url: https://vikunja.thepit.space/` | | Database | PostgreSQL 17.11, 42 tables migrated | | `vikunja doctor` | all checks pass (`rc=0`) | | Login as `pit` | `200`, token issued | | Create a project | `201` — and deleted again | | Second `make service` run | `changed=0` | **Decisions worth your eye** - **The CLI reads the account password from a TTY only.** Stdin gives `inappropriate ioctl for device`, so it goes through argv (`--password`, task is `no_log`) — same trade-off the forgejo role records. - **No sidecar checksum is published** for the release archive, so the sha256 is pinned in `defaults/main.yml` after measuring it. Bumping the version means updating both the version and that hash. - **The mailer is off** until `vault_vikunja_smtp_password` is set. `mail.smtp2go.com:2525` is wired in; add the password and the next run turns it on. - **Secrets** are in a committed `ansible-vault` file; the vault password lives in the root `.env` and the Makefile derives the gitignored `vault-pass` from it. No secret value appears in the diff (checked). **Not done yet** - The Edge stack (`npm/`) — the proxy host `vikunja.thepit.space` → `10.12.0.142:3456`, websockets on. The public URL will not answer until this exists, and it needs the LE cert you are creating in the NPM UI. - Registration is already disabled in the config, so `pit` is the only account; add more via `vault_vikunja_account_passwords`.
Ansible role that turns the fresh LXC into a running task tracker: PostgreSQL
17 from the Debian repo, the upstream v2.7.0 binary, a config rendered from a
template, and a sandboxed systemd unit. Applied and verified against the live
guest (vmid 142): the service answers 200, the API reports its public URL, and
the role is idempotent (a second run changes nothing).

Notes on what shaped it:

- One binary serves the API and the frontend, so there is no separate frontend
  artifact to deploy.
- The release archive is fetched checksum-first; dl.vikunja.io publishes no
  sidecar checksum for it, so the sha256 is pinned in the role after measuring
  it.
- `user create` is not idempotent (exits 1 on an existing account), so the role
  guards it with `user list --email`, which exits 0 when the account exists and
  1 when it does not.
- The account password goes through argv because the CLI reads it from a TTY
  only (verified: stdin gives "inappropriate ioctl for device"); the task is
  no_log, the same trade-off the forgejo role records.
- Secrets live in a committed ansible-vault file; the vault password comes from
  the root .env and the Makefile derives the gitignored vault-pass from it, so
  the repo keeps one secrets file.

The mailer is wired but disabled until an SMTP password is vaulted: a mailer
without one cannot send resets anyway.
`make service` ran the real playbook straight after the check run, so the
reviewed output and the mutating run were one keystroke apart. It now renders
the check run, asks for the environment word, and only then runs for real --
the same shape `guest` uses, minus the artifact there is nothing to apply.

The check run stays the gate: it is what the word confirms against.
With a password set, `mailer.enabled` resolves true and the next service run
renders a live mailer (mail.smtp2go.com:2525). Until now it was off because a
mailer without a password cannot send password resets anyway.

Rotated in place: same vault password, so the ciphertext re-encrypts and every
other value in the file is unchanged.
Author
Owner

Code review — two axes

Fixed point: main (cf685d9). Diff: git diff main...HEAD (13 files, 469 insertions, 3 commits: 9f4fca0, 60c8f06, 510e0a4).

Standards sources found: README.md → ## Conventions only (no CONTRIBUTING/CODING_STANDARDS/AGENTS.md/.ansible-lint/.yamllint in the repo), so the Fowler smell baseline carries the rest of that axis. No issue-tracker file (docs/agents/issue-tracker.md absent), so the Spec axis reviews against this PR's body + the three commit messages.


Standards

Blocking

B1. make service / make service-check print every vaulted secret to the terminal — the rendered config embeds secrets (config.yml.j2 L5 secret:, L15 password:, L28 password:), and the template task has no no_log (roles/vikunja/tasks/main.yml L111-118). The Makefile runs exactly that task with --check --diff (Makefile L79-81, L101-105); template is check-mode-safe, so it runs and --diff dumps the rendered file. The DB password, JWT secret, and SMTP password leak to the terminal/logs before the operator is asked for the confirm word. postgresql_user and account-create correctly carry no_log: true (L96, L242); this one was missed. Fix: no_log: true on the template task and/or drop --diff from service_rehearsal. (Commit 510e0a4 turned the mailer on, so the SMTP password is live in this dump too.)

Significant

S2. host_key_checking = accept_new silently disables host-key checking — vikunja/service/ansible.cfg:4. The option is a boolean; any non-truthy string coerces to False. ansible-config dump confirms accept_new, accept-new, and bogus all resolve to HOST_KEY_CHECKING = False with no warning, so Ansible passes StrictHostKeyChecking=no — which also accepts changed keys (MITM exposure). For real accept-new semantics: [ssh_connection] ssh_args = -o StrictHostKeyChecking=accept-new.

Nits

  • ansible.cfg L6 vault_password_file / L2 inventory are CWD-relative; ansible-playbook from the repo root won't load them and vault decryption fails.
  • vault_pass_sync (Makefile L54-62) writes then chmod 600 — the file is briefly at umask (644). umask 077 before the write closes it.
  • unarchive ... creates: with the versioned filename leaves the old /opt/vikunja/vikunja-vX-linux-amd64 behind on a version bump.

Claim checks

  • “No secret value appears in the diff (checked)” — holds. Decrypted vault.yml with the repo vault-pass; no vaulted value appears in git diff main...hermes/service-stack.
  • Idempotency — guards look right by inspection (user list --email rc-gate, /usr/bin/psql stat, vikunja_bin.stat.exists); migrate relies on a changed_when stdout heuristic. Not executed.
  • Conventions — clean. Work is on a branch, main untouched; every IP is RFC1918 LAN; the only public name is vikunja.thepit.space (permitted); vault-pass gitignored and never committed.

Spec

(a) Missing / partial

  • “add more via vault_vikunja_account_passwords” is half-wired: tasks/main.yml indexes vault_vikunja_account_passwords[item.item.username] with no default, so adding an account needs edits in two places and a username in only one fails the run.
  • “PostgreSQL 17 from the Debian repo”: the task installs bare apt: name: postgresql — no version pin/assert; fine on Debian 13, silently a different major elsewhere.
  • PR decision “The mailer is off until vault_vikunja_smtp_password is set” is stale vs HEAD: commit 510e0a4 stores that secret, so mailer.enabled now resolves true (the body's verified table predates this).

(b) Not asked for (scope creep)

  • The Edge is explicitly “Not done yet”, yet config.yml.j2/vars.yml already hard-code ipextractionmethod: "xff" and trustedproxies: 10.12.0.113/32 (NPM, which does not exist yet).
  • files.maxsize: "20MB", the .gitignore *.plaintext staging rule, and host_key_checking = accept_new — none appear in the spec.

(c) Implemented but wrong

  • ansible.cfg host_key_checking = accept_new is not a valid boolean → coerced to False (host-key checking disabled outright, not “accept new”).
  • migrate changed_when: "'Ran all migrations successfully' not in vikunja_migrate.stdout" can never be true — Vikunja logs that exact line on every successful run regardless of whether migrations applied. So the change signal the “idempotent (a second run changes nothing)” claim rests on is vacuous for this task.
  • vikunja_log_dir is created and added to ReadWritePaths, but nothing sets log.path; logs actually go to <rootpath>logs (/var/lib/vikunja/logs), leaving /var/log/vikunja unused.

Confirmed true (independently checkable spec claims): pinned sha256 matches the live v2.7.0 archive (569861fd…); user list --email exits 1 when absent / 0 when present; vault.yml is ciphertext-only and vault-pass/.env are gitignored and untracked.


Summary — Standards: 1 blocking + 1 significant + 3 nits; worst = B1 (secrets printed by the rehearsal/apply gate). Spec: 3 missing/partial, 2 scope-creep, 3 implemented-but-wrong; worst = the migrate changed_when making the idempotency signal vacuous (plus the accept_new misconfiguration shared with Standards).

## Code review — two axes Fixed point: `main` (`cf685d9`). Diff: `git diff main...HEAD` (13 files, 469 insertions, 3 commits: `9f4fca0`, `60c8f06`, `510e0a4`). Standards sources found: `README.md` → `## Conventions` only (no CONTRIBUTING/CODING_STANDARDS/AGENTS.md/.ansible-lint/.yamllint in the repo), so the Fowler smell baseline carries the rest of that axis. No issue-tracker file (`docs/agents/issue-tracker.md` absent), so the Spec axis reviews against this PR's body + the three commit messages. --- ## Standards ### Blocking **B1. `make service` / `make service-check` print every vaulted secret to the terminal** — the rendered config embeds secrets (`config.yml.j2` L5 `secret:`, L15 `password:`, L28 `password:`), and the template task has no `no_log` (`roles/vikunja/tasks/main.yml` L111-118). The Makefile runs exactly that task with `--check --diff` (`Makefile` L79-81, L101-105); `template` is check-mode-safe, so it runs and `--diff` dumps the rendered file. The DB password, JWT secret, and SMTP password leak to the terminal/logs *before* the operator is asked for the confirm word. `postgresql_user` and account-create correctly carry `no_log: true` (L96, L242); this one was missed. Fix: `no_log: true` on the template task and/or drop `--diff` from `service_rehearsal`. (Commit `510e0a4` turned the mailer on, so the SMTP password is live in this dump too.) ### Significant **S2. `host_key_checking = accept_new` silently disables host-key checking** — `vikunja/service/ansible.cfg:4`. The option is a boolean; any non-truthy string coerces to `False`. `ansible-config dump` confirms `accept_new`, `accept-new`, and `bogus` all resolve to `HOST_KEY_CHECKING = False` with no warning, so Ansible passes `StrictHostKeyChecking=no` — which also accepts *changed* keys (MITM exposure). For real `accept-new` semantics: `[ssh_connection] ssh_args = -o StrictHostKeyChecking=accept-new`. ### Nits - `ansible.cfg` L6 `vault_password_file` / L2 `inventory` are CWD-relative; `ansible-playbook` from the repo root won't load them and vault decryption fails. - `vault_pass_sync` (`Makefile` L54-62) writes then `chmod 600` — the file is briefly at umask (644). `umask 077` before the write closes it. - `unarchive ... creates:` with the versioned filename leaves the old `/opt/vikunja/vikunja-vX-linux-amd64` behind on a version bump. ### Claim checks - *“No secret value appears in the diff (checked)”* — **holds.** Decrypted `vault.yml` with the repo `vault-pass`; no vaulted value appears in `git diff main...hermes/service-stack`. - *Idempotency* — guards look right by inspection (`user list --email` rc-gate, `/usr/bin/psql` stat, `vikunja_bin.stat.exists`); `migrate` relies on a `changed_when` stdout heuristic. Not executed. - *Conventions* — **clean.** Work is on a branch, `main` untouched; every IP is RFC1918 LAN; the only public name is `vikunja.thepit.space` (permitted); `vault-pass` gitignored and never committed. --- ## Spec **(a) Missing / partial** - “add more via `vault_vikunja_account_passwords`” is half-wired: `tasks/main.yml` indexes `vault_vikunja_account_passwords[item.item.username]` with no `default`, so adding an account needs edits in two places and a username in only one fails the run. - “PostgreSQL 17 from the Debian repo”: the task installs bare `apt: name: postgresql` — no version pin/assert; fine on Debian 13, silently a different major elsewhere. - PR decision “The mailer is off until `vault_vikunja_smtp_password` is set” is **stale vs HEAD**: commit `510e0a4` stores that secret, so `mailer.enabled` now resolves true (the body's verified table predates this). **(b) Not asked for (scope creep)** - The Edge is explicitly “Not done yet”, yet `config.yml.j2`/`vars.yml` already hard-code `ipextractionmethod: "xff"` and `trustedproxies: 10.12.0.113/32` (NPM, which does not exist yet). - `files.maxsize: "20MB"`, the `.gitignore` `*.plaintext` staging rule, and `host_key_checking = accept_new` — none appear in the spec. **(c) Implemented but wrong** - `ansible.cfg` `host_key_checking = accept_new` is not a valid boolean → coerced to `False` (host-key checking disabled outright, not “accept new”). - `migrate` `changed_when: "'Ran all migrations successfully' not in vikunja_migrate.stdout"` can never be true — Vikunja logs that exact line on every successful run regardless of whether migrations applied. So the change signal the “idempotent (a second run changes nothing)” claim rests on is vacuous for this task. - `vikunja_log_dir` is created and added to `ReadWritePaths`, but nothing sets `log.path`; logs actually go to `<rootpath>logs` (`/var/lib/vikunja/logs`), leaving `/var/log/vikunja` unused. *Confirmed true (independently checkable spec claims): pinned sha256 matches the live v2.7.0 archive (`569861fd…`); `user list --email` exits 1 when absent / 0 when present; `vault.yml` is ciphertext-only and `vault-pass`/`.env` are gitignored and untracked.* --- **Summary** — Standards: 1 blocking + 1 significant + 3 nits; worst = B1 (secrets printed by the rehearsal/apply gate). Spec: 3 missing/partial, 2 scope-creep, 3 implemented-but-wrong; worst = the `migrate` `changed_when` making the idempotency signal vacuous (plus the `accept_new` misconfiguration shared with Standards).
Spec:
- The config template now carries `no_log`. Verified: `template --check --diff`
  prints the rendered file verbatim, so with --diff the DB password, JWT secret
  and SMTP password were printed whenever the config changed. A probe confirms
  no_log suppresses that diff while --diff stays on for the rest of the run.
- `host_key_checking = accept_new` is not a boolean; it coerced to False and
  Ansible passed StrictHostKeyChecking=no, which also accepts a *changed* key.
  Replaced with `ssh_args = -o StrictHostKeyChecking=accept-new` for real
  accept-new semantics.
- The migrate task's `changed_when` was vacuous: Vikunja logs "Ran all
  migrations successfully." on every run, applied or not (verified on a fresh
  and a second run). It is now `changed_when: false`, which is what a
  run-every-time command with nothing to report is.
- The binary install is version-aware: it reads the installed version and only
  downloads/unpacks when the pinned version differs. Without this the previous
  `creates:` guard re-extracted every run, and deleting the versioned file (to
  stop version bumps leaving copies behind) made the next run's copy source
  vanish. Both are now handled by one condition.
- `vikunja_log_dir` was created and granted write access but never written to:
  `log.standard` defaults to stdout, so logs go to the journal. The unused dir,
  the `log.path` it needed, and the write grant are gone.
- The PR's mailer claim was stale (the SMTP password landed after the body was
  written); the README now records the mailer as on.

Standards:
- Added asserts for the two silent-failure cases: the host must be amd64 (the
  pinned release is linux-amd64) and every account must have a vaulted password
  (a missing key otherwise failed deep in the loop as an undefined variable).
- `vault_pass_sync` writes under `umask 077` instead of chmod-after-write, so
  the file is never briefly world-readable.
- Dropped the scope-creep bits: `files.maxsize` (the 20MB default is unchanged)
  and the `*.plaintext` ignore rule.

Checked claim: the reviewer's "no secret in the diff (checked)" and the
repo conventions still hold; every value in the vault is untouched.
`ansible_architecture` is a top-level injected fact, deprecated in favour of
`ansible_facts["architecture"]` (removed in ansible-core 2.24). The assert
still passes; the deprecation warning is gone (verified: 0 DEPRECATION lines
in a full run).
Author
Owner

Review fixes are in, verified against the live guest. main is untouched at cf685d9.

Blocking

B1 — secrets printed by the rehearsal. Real, and reproduced: a probe playbook running template --check --diff printed the rendered file verbatim (+secret: "SUPERSECRETVALUE123"). On the live host it showed ok only because the config already matched, so the leak was latent, not absent. Fixed with no_log: true on the template task. I first dropped --diff from service_rehearsal, then reverted it: with no_log the secret-bearing diff is suppressed and the harmless diffs stay visible. Verified: no secret value appears in the rehearsal output.

Significant

S2 — host_key_checking = accept_new. Confirmed a boolean coercion: accept_new, accept-new, and bogus all resolve to False, so Ansible was passing StrictHostKeyChecking=no — which also accepts a changed key. Replaced with ssh_args = -o StrictHostKeyChecking=accept-new.

Implemented but wrong

  • migrate changed_when was vacuous. Verified: Vikunja logs "Ran all migrations successfully." on a fresh DB and on a no-op run alike — the line never varies. Now changed_when: false, which is what a run-every-time command with nothing to report is.
  • vikunja_log_dir unused. Correct, and log.path was indeed missing — but setting it doesn't help: log.standard defaults to stdout, so logs go to the journal either way. Rather than create a directory nothing writes to, I removed the dir, the write grant, and log.path. Confirmed on the guest: logs land in the journal; the directory is gone.

Spec gaps

  • Account passwords half-wired — added an assert that every account has a vaulted password, so a missing key fails with a clear message instead of an undefined-variable error mid-loop.
  • No version assert — added one; the pinned release is linux-amd64, so the host must be x86_64.
  • Stale mailer claim — the README now records the mailer as on, and notes account passwords are create-time only.
  • Scope creep — dropped files.maxsize (the 20MB default is unchanged) and the *.plaintext ignore rule.

Nits

  • vault_pass_sync writes under umask 077 rather than chmod-after-write, so the file is never briefly world-readable.
  • The relative-path nit is a false alarm: DEFAULT_VAULT_PASSWORD_FILE resolves to the absolute service/vault-pass from any working directory (checked), so it's left as is with a comment.

A regression I introduced

The creates: nit made me delete the versioned binary after install — which broke the next run, because the copy still read it as its source (Source /opt/vikunja/vikunja-v2.7.0-linux-amd64 not found). That failed a real run. Rewrote it: read the installed version, install only when the pinned version differs. Both the nit and the regression are gone.

Also

The amd64 assert I added carried its own [DEPRECATION WARNING]: INJECT_FACTS_AS_VARS (top-level fact injection, removed in ansible-core 2.24). Switched to ansible_facts["architecture"].

Verification

Two consecutive real runs on vmid 142: changed=3 then changed=0, failed=0, no fatal. ansible-playbook --syntax-check passes. No secret value in the rehearsal output or anywhere in the worktree; every value in the vault is untouched.

Review fixes are in, verified against the live guest. `main` is untouched at `cf685d9`. ## Blocking **B1 — secrets printed by the rehearsal.** Real, and reproduced: a probe playbook running `template --check --diff` printed the rendered file verbatim (`+secret: "SUPERSECRETVALUE123"`). On the live host it showed `ok` only because the config already matched, so the leak was latent, not absent. Fixed with `no_log: true` on the template task. I first dropped `--diff` from `service_rehearsal`, then reverted it: with `no_log` the secret-bearing diff is suppressed and the harmless diffs stay visible. Verified: no secret value appears in the rehearsal output. ## Significant **S2 — `host_key_checking = accept_new`.** Confirmed a boolean coercion: `accept_new`, `accept-new`, and `bogus` all resolve to `False`, so Ansible was passing `StrictHostKeyChecking=no` — which also accepts a *changed* key. Replaced with `ssh_args = -o StrictHostKeyChecking=accept-new`. ## Implemented but wrong - **`migrate` `changed_when` was vacuous.** Verified: Vikunja logs `"Ran all migrations successfully."` on a fresh DB and on a no-op run alike — the line never varies. Now `changed_when: false`, which is what a run-every-time command with nothing to report is. - **`vikunja_log_dir` unused.** Correct, and `log.path` was indeed missing — but setting it doesn't help: `log.standard` defaults to `stdout`, so logs go to the journal either way. Rather than create a directory nothing writes to, I removed the dir, the write grant, and `log.path`. Confirmed on the guest: logs land in the journal; the directory is gone. ## Spec gaps - **Account passwords half-wired** — added an assert that every account has a vaulted password, so a missing key fails with a clear message instead of an undefined-variable error mid-loop. - **No version assert** — added one; the pinned release is linux-amd64, so the host must be x86_64. - **Stale mailer claim** — the README now records the mailer as on, and notes account passwords are create-time only. - **Scope creep** — dropped `files.maxsize` (the 20MB default is unchanged) and the `*.plaintext` ignore rule. ## Nits - `vault_pass_sync` writes under `umask 077` rather than chmod-after-write, so the file is never briefly world-readable. - The relative-path nit is a false alarm: `DEFAULT_VAULT_PASSWORD_FILE` resolves to the absolute `service/vault-pass` from any working directory (checked), so it's left as is with a comment. ## A regression I introduced The `creates:` nit made me delete the versioned binary after install — which broke the *next* run, because the `copy` still read it as its source (`Source /opt/vikunja/vikunja-v2.7.0-linux-amd64 not found`). That failed a real run. Rewrote it: read the installed version, install only when the pinned version differs. Both the nit and the regression are gone. ## Also The amd64 assert I added carried its own `[DEPRECATION WARNING]: INJECT_FACTS_AS_VARS` (top-level fact injection, removed in ansible-core 2.24). Switched to `ansible_facts["architecture"]`. ## Verification Two consecutive real runs on vmid 142: `changed=3` then `changed=0`, `failed=0`, no `fatal`. `ansible-playbook --syntax-check` passes. No secret value in the rehearsal output or anywhere in the worktree; every value in the vault is untouched.
Author
Owner

Code review — two axes (re-review)

Fixed point: main (cf685d9). Diff: git diff main...HEAD (13 files, 528 insertions, 5 commits: 9f4fca0, 60c8f06, 510e0a4, 2bec6bb, a247388). This re-run also checks the prior review (#issuecomment-647) against the fix commits and the author's reply (#issuecomment-650).

Standards sources: README.md → ## Conventions only (no CONTRIBUTING / CODING_STANDARDS / AGENTS.md / .ansible-lint / .yamllint in the repo), so the Fowler smell baseline carries the rest. No issue-tracker file, so the Spec axis reviews against the PR body + the five commit messages.


Standards

Documented-standard violations: none. Convention (README.md:120-124): branch/PR obeyed; no address committed. Every IP is private (10.12.0.142/24, 10.12.0.1, proxy 10.12.0.113/32); vikunja.thepit.space is a public service domain and mail.smtp2go.com an upstream mirror — both explicitly allowed. Vault stays ciphertext.

Baseline smells (all judgement calls):

  1. Duplicated Code — when: not ansible_check_mode or vikunja_bin.stat.exists | default(false) repeats 6× (tasks/main.yml L204, L212, L227, L250, L273; handlers/main.yml L7). A single set-fact gate could drive them all.
  2. Duplicated Code — the runuser -u {{ vikunja_user }} -- {{ vikunja_binary }} … argv repeats 3× (L228-237, L251-263, L275-291). Extract a shared prefix.
  3. Data Clumps — the versioned path {{ vikunja_install_dir }}/vikunja-v{{ vikunja_version }}-linux-amd64 (plus the /tmp zip name) recurs 5× (L93, L100, L104, L109, L121); one vikunja_release_* var would carry the clump.
  4. Primitive Obsession — the version gate compares substrings: vikunja_version not in (vikunja_bin_version.stdout | default('')) (tasks/main.yml L86-87). "2.7.0" is a substring of e.g. "12.7.0", so an install could be silently skipped. Parse the version and match exactly.
  5. Mysterious Name — vikunja_pg registers a stat of /usr/bin/psql (L128-131); the name doesn't say "postgres installed". inventories/prod/hosts also names both the group and the host vikunja (Ansible warns on the collision).

Prior Standards findings — resolution:

  • B1 — RESOLVED. Template task now carries no_log: true (L164-166). Verified: ansible-playbook --check --diff with no_log prints only changed, no diff and no secret.
  • S2 — RESOLVED. ansible.cfg uses ssh_args = -o StrictHostKeyChecking=accept-new. Cross-checked against plugins/connection/ssh.py: StrictHostKeyChecking=no is added only when host_key_checking is False, and the dump shows it defaulting True.
  • Nit (CWD-relative paths) — RESOLVED. From /tmp, ansible-config dump shows both DEFAULT_HOST_LIST and DEFAULT_VAULT_PASSWORD_FILE resolving to the absolute service/ paths — Ansible resolves cfg paths relative to the cfg file.
  • Nit (vault_pass_sync chmod) — RESOLVED. Now (umask 077; printf … > $(VAULT_PASS)) (Makefile:60).
  • Nit (creates: leftover) — RESOLVED. Version-aware install plus an explicit file: state=absent (L118-122).
  • Regression fix — correct. The set_fact gate (L84-87) coerces to a real bool (bump→True, same→False); download/unarchive/copy/delete share one condition, so there is no stale copy source and no version accumulation. The only caveat is the substring match (smell #4).

Spec

(a) Missing / partial

  • "add more via vault_vikunja_account_passwords" — still half-wired. Accounts live in two files: defaults/main.yml vikunja_accounts and vault.yml vault_vikunja_account_passwords. The new assert (item.username in vault_vikunja_account_passwords, tasks/main.yml L32-42) catches a username missing its password — it converts a deep undefined-var crash into a clear failure, but the two-place edit and the undocumented recipe remain. Partially resolved.
  • "PostgreSQL 17 from the Debian repo" — the task installs bare apt: name: postgresql (tasks/main.yml L13) with no version pin or assert; the README asserts "PostgreSQL 17" (L68) that nothing verifies. The amd64 assert (L22-28) does not cover this. Not resolved.

(b) Not asked for (scope creep)

  • config.yml.j2 still hard-codes ipextractionmethod: "xff" and trustedproxies: "10.12.0.113/32" (L7-8; vars.yml L8) for the NPM Edge that "will not answer until this exists". Not asked for, and dead config while the proxy is absent. Not resolved.
  • files.maxsize and the .gitignore *.plaintext rule — dropped, resolved. host_key_checking accept_new — reframed into the valid ssh_args -o StrictHostKeyChecking=accept-new (ansible.cfg L14). Partially (an SSH-policy line remains, now in correct form).

(c) Implemented but wrong

  • host_key_checking accept_new (invalid boolean, coerced to False) → removed, replaced with ssh_args. Resolved.
  • migrate changed_when (dead branch) → now explicit changed_when: false with rationale (L241). Resolved.
  • vikunja_log_dir created and added to ReadWritePaths with nothing writing it → dir removed; ReadWritePaths={{ vikunja_home }}. Resolved.
  • Mailer claim stale vs HEAD → README now records "The mailer is on" (L92). Resolved.

Remaining findings to raise:

  • (a) PostgreSQL version unpinned despite the "17" claim (README.md:68 vs tasks/main.yml:13).
  • (a) Account-add still edits two files; no README how-to.
  • (b)/(c) xff + trustedproxies provisioned for a proxy that does not exist — unrequested, and ipextractionmethod: xff with no live proxy is the kind of wrong-when-inert config worth deferring to the npm/ PR.

Verdict: 5 of 8 prior Spec findings resolved; account wiring partial, PG pin missing, xff/trustedproxies creep open.


Summary — Standards: 0 documented-standard breaches, 5 judgement-call smells (all prior Standards findings resolved); worst = the substring version gate (#4), which could silently skip an install. Spec: 3 open findings (PG pin, account wiring, xff/trustedproxies creep), 5 of 8 prior items resolved; worst = the unpinned PostgreSQL version contradicting the README's "PostgreSQL 17" claim.

## Code review — two axes (re-review) Fixed point: `main` (`cf685d9`). Diff: `git diff main...HEAD` (13 files, 528 insertions, 5 commits: `9f4fca0`, `60c8f06`, `510e0a4`, `2bec6bb`, `a247388`). This re-run also checks the prior review (#issuecomment-647) against the fix commits and the author's reply (#issuecomment-650). Standards sources: `README.md` → `## Conventions` only (no CONTRIBUTING / CODING_STANDARDS / AGENTS.md / .ansible-lint / .yamllint in the repo), so the Fowler smell baseline carries the rest. No issue-tracker file, so the Spec axis reviews against the PR body + the five commit messages. --- ## Standards **Documented-standard violations: none.** Convention (`README.md:120-124`): branch/PR obeyed; no address committed. Every IP is private (`10.12.0.142/24`, `10.12.0.1`, proxy `10.12.0.113/32`); `vikunja.thepit.space` is a public service domain and `mail.smtp2go.com` an upstream mirror — both explicitly allowed. Vault stays ciphertext. **Baseline smells (all judgement calls):** 1. **Duplicated Code** — `when: not ansible_check_mode or vikunja_bin.stat.exists | default(false)` repeats 6× (`tasks/main.yml` L204, L212, L227, L250, L273; `handlers/main.yml` L7). A single set-fact gate could drive them all. 2. **Duplicated Code** — the `runuser -u {{ vikunja_user }} -- {{ vikunja_binary }} …` argv repeats 3× (L228-237, L251-263, L275-291). Extract a shared prefix. 3. **Data Clumps** — the versioned path `{{ vikunja_install_dir }}/vikunja-v{{ vikunja_version }}-linux-amd64` (plus the `/tmp` zip name) recurs 5× (L93, L100, L104, L109, L121); one `vikunja_release_*` var would carry the clump. 4. **Primitive Obsession** — the version gate compares substrings: `vikunja_version not in (vikunja_bin_version.stdout | default(''))` (`tasks/main.yml` L86-87). `"2.7.0"` is a substring of e.g. `"12.7.0"`, so an install could be silently skipped. Parse the version and match exactly. 5. **Mysterious Name** — `vikunja_pg` registers a `stat` of `/usr/bin/psql` (L128-131); the name doesn't say "postgres installed". `inventories/prod/hosts` also names both the group and the host `vikunja` (Ansible warns on the collision). **Prior Standards findings — resolution:** - **B1 — RESOLVED.** Template task now carries `no_log: true` (L164-166). Verified: `ansible-playbook --check --diff` with `no_log` prints only `changed`, no diff and no secret. - **S2 — RESOLVED.** `ansible.cfg` uses `ssh_args = -o StrictHostKeyChecking=accept-new`. Cross-checked against `plugins/connection/ssh.py`: `StrictHostKeyChecking=no` is added only when `host_key_checking is False`, and the dump shows it defaulting True. - **Nit (CWD-relative paths) — RESOLVED.** From `/tmp`, `ansible-config dump` shows both `DEFAULT_HOST_LIST` and `DEFAULT_VAULT_PASSWORD_FILE` resolving to the absolute `service/` paths — Ansible resolves cfg paths relative to the cfg file. - **Nit (`vault_pass_sync` chmod) — RESOLVED.** Now `(umask 077; printf … > $(VAULT_PASS))` (`Makefile:60`). - **Nit (`creates:` leftover) — RESOLVED.** Version-aware install plus an explicit `file: state=absent` (L118-122). - **Regression fix — correct.** The `set_fact` gate (L84-87) coerces to a real bool (bump→True, same→False); download/unarchive/copy/delete share one condition, so there is no stale copy source and no version accumulation. The only caveat is the substring match (smell #4). --- ## Spec **(a) Missing / partial** - *"add more via `vault_vikunja_account_passwords`"* — still half-wired. Accounts live in **two** files: `defaults/main.yml` `vikunja_accounts` and `vault.yml` `vault_vikunja_account_passwords`. The new assert (`item.username in vault_vikunja_account_passwords`, `tasks/main.yml` L32-42) catches a username missing its password — it converts a deep undefined-var crash into a clear failure, but the two-place edit and the undocumented recipe remain. **Partially resolved.** - *"PostgreSQL 17 from the Debian repo"* — the task installs bare `apt: name: postgresql` (`tasks/main.yml` L13) with **no version pin or assert**; the README asserts "PostgreSQL 17" (L68) that nothing verifies. The amd64 assert (L22-28) does not cover this. **Not resolved.** **(b) Not asked for (scope creep)** - `config.yml.j2` still hard-codes `ipextractionmethod: "xff"` and `trustedproxies: "10.12.0.113/32"` (L7-8; `vars.yml` L8) for the NPM Edge that *"will not answer until this exists"*. Not asked for, and dead config while the proxy is absent. **Not resolved.** - `files.maxsize` and the `.gitignore` `*.plaintext` rule — **dropped, resolved.** `host_key_checking accept_new` — reframed into the valid `ssh_args -o StrictHostKeyChecking=accept-new` (`ansible.cfg` L14). **Partially** (an SSH-policy line remains, now in correct form). **(c) Implemented but wrong** - `host_key_checking accept_new` (invalid boolean, coerced to False) → removed, replaced with `ssh_args`. **Resolved.** - `migrate` `changed_when` (dead branch) → now explicit `changed_when: false` with rationale (L241). **Resolved.** - `vikunja_log_dir` created and added to `ReadWritePaths` with nothing writing it → dir removed; `ReadWritePaths={{ vikunja_home }}`. **Resolved.** - Mailer claim stale vs HEAD → README now records *"The mailer is **on**"* (L92). **Resolved.** **Remaining findings to raise:** - (a) PostgreSQL version unpinned despite the "17" claim (`README.md:68` vs `tasks/main.yml:13`). - (a) Account-add still edits two files; no README how-to. - (b)/(c) `xff` + `trustedproxies` provisioned for a proxy that does not exist — unrequested, and `ipextractionmethod: xff` with no live proxy is the kind of wrong-when-inert config worth deferring to the `npm/` PR. **Verdict:** 5 of 8 prior Spec findings resolved; account wiring partial, PG pin missing, xff/trustedproxies creep open. --- **Summary** — Standards: 0 documented-standard breaches, 5 judgement-call smells (all prior Standards findings resolved); worst = the substring version gate (#4), which could silently skip an install. Spec: 3 open findings (PG pin, account wiring, xff/trustedproxies creep), 5 of 8 prior items resolved; worst = the unpinned PostgreSQL version contradicting the README's "PostgreSQL 17" claim.
Spec:
- The version gate compared substrings: `"2.7.0" in "v12.7.0"` is true, so a
  12.x binary on disk would have skipped an install it should have done. Parse
  the version and compare exactly (verified on the real two-line output and on
  the 12.7.0 and garbage cases).
- PostgreSQL is now `postgresql-17` rather than the unpinned `postgresql`, and
  the role asserts that major is the one installed — the README claimed 17 and
  nothing checked it. The check is a stat of /usr/lib/postgresql/17, not a
  shelled `psql --version`: a command skips in check mode and left the assert
  reading an unregistered variable.
- Dropped the `xff` + `trustedproxies` config: it was provisioned for an Edge
  that does not exist, and `ipextractionmethod: xff` with no proxy behind it is
  wrong while inert. Now `direct`, with a note to switch when npm/ lands.
- README documents that adding an account is a two-file edit.

Standards:
- Collapsed the install guard from six copies of
  `not ansible_check_mode or vikunja_bin.stat.exists | default(false)` to one
  `vikunja_installed` fact (handler included).
- Collapsed the three `runuser -u … -- …` argv blocks to one `vikunja_cli`.
- Collapsed the versioned release path, repeated five times across two files,
  into `vikunja_release_binary` / `vikunja_release_zip`.
- Renamed `vikunja_pg`'s task to say it checks PostgreSQL, not "the database".
- The downloaded archive is now cleaned up unconditionally, so a bump does not
  leave the previous zip in /tmp.

Verified on the live guest: check mode passes, then two real runs, changed=1
and changed=0, no fatal and no deprecation.
Author
Owner

Re-review fixes are in, verified against the live guest. main is untouched at cf685d9; head is 710f668.

The worst finding — real bug, fixed

Version gate (smell #4). Confirmed by check: "2.7.0" in "v12.7.0" is True, so a 12.x binary on disk would have skipped an install it should have done. Now it parses the version and compares exactly. Verified against the real two-line vikunja version output, the 12.7.0 case, and garbage — correct version → no reinstall, 12.7.0 → reinstall, garbage → reinstall.

Spec findings

  • PostgreSQL unpinned. Now postgresql-17 instead of bare postgresql, plus an assert that the 17 tree is present. The README claimed 17 and nothing verified it.
  • xff + trustedproxies creep. Dropped. Provisioned for an Edge that does not exist, and ipextractionmethod: xff with no proxy behind it is wrong while inert. Now direct, with a note to switch to xff + trustedproxies when npm/ lands. vikunja_trusted_proxies removed from group_vars.
  • Account wiring. The two-file edit remains (entry in vikunja_accounts, password in the vault) — that is inherent to keeping the password a secret in the vault — but the README now documents the recipe, and the assert added last round catches a missing password with a clear message.

Smells

  • #1 duplicated guard. The six not ansible_check_mode or vikunja_bin.stat.exists | default(false) copies are now one vikunja_installed fact, used by every guard and the handler.
  • #2 duplicated argv. The three runuser -u … -- … blocks are one vikunja_cli prefix; each call appends its own subcommand.
  • #3 data clump. The versioned path, repeated five times across two files, is now vikunja_release_binary / vikunja_release_zip.
  • #5 mysterious name. vikunja_pg's task renamed to say it checks PostgreSQL, not "the database".

The group/host vikunja collision in the inventory is left deliberately: it is infra-forge's convention, Ansible only warns, and renaming either side risks two hosts sharing an address if the group and host diverge.

A bug I introduced and caught

My first PostgreSQL assert shelled out to psql --version. It passed on a direct run but failed the --check rehearsal — command/shell tasks skip in check mode, so the registered variable never existed and the assert read ''. That is precisely the gate run before an apply. Replaced with a stat of /usr/lib/postgresql/17, which runs in check mode. I also found a stale release zip left in /tmp from that same gap (the cleanup was gated on install_needed) and made it unconditional.

Verification

Check mode passes, then two real runs on vmid 142: changed=1 then changed=0, failed=0, no fatal, no deprecation. /opt/vikunja holds the single stable binary, /tmp has no leftover zip, and the config now carries ipextractionmethod: "direct". Every value in the vault is untouched.

Re-review fixes are in, verified against the live guest. `main` is untouched at `cf685d9`; head is `710f668`. ## The worst finding — real bug, fixed **Version gate (smell #4).** Confirmed by check: `"2.7.0" in "v12.7.0"` is `True`, so a 12.x binary on disk would have skipped an install it should have done. Now it parses the version and compares exactly. Verified against the real two-line `vikunja version` output, the `12.7.0` case, and garbage — correct version → no reinstall, `12.7.0` → reinstall, garbage → reinstall. ## Spec findings - **PostgreSQL unpinned.** Now `postgresql-17` instead of bare `postgresql`, plus an assert that the 17 tree is present. The README claimed 17 and nothing verified it. - **`xff` + `trustedproxies` creep.** Dropped. Provisioned for an Edge that does not exist, and `ipextractionmethod: xff` with no proxy behind it is wrong while inert. Now `direct`, with a note to switch to `xff` + `trustedproxies` when `npm/` lands. `vikunja_trusted_proxies` removed from `group_vars`. - **Account wiring.** The two-file edit remains (entry in `vikunja_accounts`, password in the vault) — that is inherent to keeping the password a secret in the vault — but the README now documents the recipe, and the assert added last round catches a missing password with a clear message. ## Smells - **#1 duplicated guard.** The six `not ansible_check_mode or vikunja_bin.stat.exists | default(false)` copies are now one `vikunja_installed` fact, used by every guard and the handler. - **#2 duplicated argv.** The three `runuser -u … -- …` blocks are one `vikunja_cli` prefix; each call appends its own subcommand. - **#3 data clump.** The versioned path, repeated five times across two files, is now `vikunja_release_binary` / `vikunja_release_zip`. - **#5 mysterious name.** `vikunja_pg`'s task renamed to say it checks PostgreSQL, not "the database". The group/host `vikunja` collision in the inventory is left deliberately: it is infra-forge's convention, Ansible only warns, and renaming either side risks two hosts sharing an address if the group and host diverge. ## A bug I introduced and caught My first PostgreSQL assert shelled out to `psql --version`. It passed on a direct run but **failed the `--check` rehearsal** — `command`/`shell` tasks skip in check mode, so the registered variable never existed and the assert read `''`. That is precisely the gate run before an apply. Replaced with a `stat` of `/usr/lib/postgresql/17`, which runs in check mode. I also found a stale release zip left in `/tmp` from that same gap (the cleanup was gated on `install_needed`) and made it unconditional. ## Verification Check mode passes, then two real runs on vmid 142: `changed=1` then `changed=0`, `failed=0`, no `fatal`, no deprecation. `/opt/vikunja` holds the single stable binary, `/tmp` has no leftover zip, and the config now carries `ipextractionmethod: "direct"`. Every value in the vault is untouched.
Author
Owner

Code review — two axes (re-review, round 3)

Fixed point: main (cf685d9). Diff: git diff main...HEAD (13 files, 554 insertions, 6 commits: 9f4fca0, 60c8f06, 510e0a4, 2bec6bb, a247388, 710f668). This re-run checks the round-2 review (#issuecomment-651) against the fix commit 710f668 and the author's reply (#issuecomment-653).

Standards sources: README.md → ## Conventions only (no CONTRIBUTING / CODING_STANDARDS / AGENTS.md / .ansible-lint / .yamllint). No issue-tracker file, so Spec reviews against the PR body + the six commit messages.


Standards

Documented-standard violations: none. Both conventions hold — work is on a branch/PR (main untouched at cf685d9), and no address is committed (only private 10.12.0.142, already on main; vikunja.thepit.space is an explicitly-allowed public domain).

Baseline smells (all judgement calls):

  1. Duplicated Code (residual). The check-mode guard still carries two idioms. The binary path uses the unified vikunja_installed fact; the postgres path re-inlines the raw stat 3× — tasks/main.yml L154, L165, L184: when: not ansible_check_mode or vikunja_pg.stat.exists | default(false). Round-2 smell #1 unified vikunja_bin but not vikunja_pg.
  2. Data Clumps. become: true / become_user: postgres / become_method: ansible.builtin.su repeats verbatim on both the role and db tasks.
  3. Duplicated Code (Makefile). confirm_word re-implements the read/compare/abort block already inside confirm_and_apply; the latter could call the former.
  4. Mysterious Name. vikunja_pg still names a /usr/bin/psql stat, and vikunja_installed (binary present) vs vikunja_release_installed (version string) read alike but mean different things.
  5. Long Module. tasks/main.yml is 306 lines; its six numbered sections could be include_tasks.
  6. Undeclared interface. config.yml.j2 consumes vikunja_public_url / vikunja_domain, supplied only by group_vars; absent from defaults/main.yml, so the role is not self-contained.
  7. Name collision. Group [vikunja] and host vikunja share a name (Ansible warns). The cited "infra-forge convention" is not a standard documented in this repo.

Round-2 Standards findings — resolution:

  • #1 duplicated vikunja_bin guard — RESOLVED (vikunja_installed fact), but the analogous vikunja_pg guard remains 3×.
  • #2 runuser … argv — RESOLVED (vikunja_cli prefix in defaults/main.yml).
  • #3 versioned-path clump — RESOLVED (vikunja_release_binary / vikunja_release_zip).
  • #4 substring version gate — RESOLVED (regex parse version v(\d+\.\d+\.\d+) + exact != vikunja_version).
  • #5 Mysterious Name — PARTIAL (task renamed, but the vikunja_pg register and the group/host collision remain).
  • Earlier B1 secret leak — RESOLVED (no_log: true on the template task, L205); S2 host-key coercion — RESOLVED ([ssh_connection] ssh_args = -o StrictHostKeyChecking=accept-new).

Spec

(a) Missing / partial: none. Every deliverable in the body is present; npm/ is explicitly deferred ("still to come").

(b) Scope creep: none remaining. The added pieces (.env.example ANSIBLE_VAULT_PASSWORD, vault-pass ignore, amd64 assert, mailer) are each named in the body or commit messages.

(c) Implemented but wrong:

  • The --check "gate" exercises little. Commit 60c8f06: "The check run stays the gate: it is what the word confirms against." But command/shell tasks skip under --check (independently verified: a probe command reports skipped=True), so on the guest make service-check skips migrate (L265) and both account tasks (L280, L291) — the failure-prone half. The operator confirms a rehearsal that never rehearsed migrations or account creation. Minor corollary: since the account-list task also skips, item.rc | default(1) would mark every account "missing" — harmless only because the create task skips too.

Round-2 Spec findings — resolution:

  • PostgreSQL unpinned — RESOLVED (postgresql-{{ vikunja_db_major }} + a check-mode-safe stat /usr/lib/postgresql/17 assert, L177-192); README:68's "PostgreSQL 17" is now enforced.
  • Account wiring — RESOLVED (README "Adding an account" documents the two-file edit; the assert turns a missing vault key into a clear failure). The two-file edit is inherent to a vaulted secret, not a defect.
  • xff + trustedproxies creep — RESOLVED (ipextractionmethod: "direct", no trustedproxies / vikunja_trusted_proxies left in the tree; matches the upstream default).

Summary — Standards: 0 documented-standard breaches, 7 judgement-call smells (4 of 5 prior smells resolved, 1 partial); worst = the residual vikunja_pg guard duplication. Spec: 0 missing, 0 scope creep, 1 implemented-but-wrong (the thin --check gate); all 3 round-2 Spec findings resolved. Both axes clean of hard/blocking issues.

Merge recommendation: yes — merge. No blocking or significant findings remain on either axis; the open items are judgement-call smells and one non-blocking gap where the --check gate can't rehearse the migrate/account steps (worth a follow-up, not a hold).

## Code review — two axes (re-review, round 3) Fixed point: `main` (`cf685d9`). Diff: `git diff main...HEAD` (13 files, 554 insertions, 6 commits: `9f4fca0`, `60c8f06`, `510e0a4`, `2bec6bb`, `a247388`, `710f668`). This re-run checks the round-2 review (#issuecomment-651) against the fix commit `710f668` and the author's reply (#issuecomment-653). Standards sources: `README.md` → `## Conventions` only (no CONTRIBUTING / CODING_STANDARDS / AGENTS.md / .ansible-lint / .yamllint). No issue-tracker file, so Spec reviews against the PR body + the six commit messages. --- ## Standards **Documented-standard violations: none.** Both conventions hold — work is on a branch/PR (`main` untouched at `cf685d9`), and no address is committed (only private `10.12.0.142`, already on `main`; `vikunja.thepit.space` is an explicitly-allowed public domain). **Baseline smells (all judgement calls):** 1. **Duplicated Code (residual).** The check-mode guard still carries two idioms. The binary path uses the unified `vikunja_installed` fact; the postgres path re-inlines the raw stat **3×** — `tasks/main.yml` L154, L165, L184: `when: not ansible_check_mode or vikunja_pg.stat.exists | default(false)`. Round-2 smell #1 unified `vikunja_bin` but not `vikunja_pg`. 2. **Data Clumps.** `become: true` / `become_user: postgres` / `become_method: ansible.builtin.su` repeats verbatim on both the role and db tasks. 3. **Duplicated Code (Makefile).** `confirm_word` re-implements the read/compare/abort block already inside `confirm_and_apply`; the latter could call the former. 4. **Mysterious Name.** `vikunja_pg` still names a `/usr/bin/psql` stat, and `vikunja_installed` (binary present) vs `vikunja_release_installed` (version string) read alike but mean different things. 5. **Long Module.** `tasks/main.yml` is 306 lines; its six numbered sections could be `include_tasks`. 6. **Undeclared interface.** `config.yml.j2` consumes `vikunja_public_url` / `vikunja_domain`, supplied only by `group_vars`; absent from `defaults/main.yml`, so the role is not self-contained. 7. **Name collision.** Group `[vikunja]` and host `vikunja` share a name (Ansible warns). The cited "infra-forge convention" is not a standard documented in this repo. **Round-2 Standards findings — resolution:** - **#1 duplicated `vikunja_bin` guard — RESOLVED** (`vikunja_installed` fact), but the analogous `vikunja_pg` guard remains 3×. - **#2 `runuser …` argv — RESOLVED** (`vikunja_cli` prefix in `defaults/main.yml`). - **#3 versioned-path clump — RESOLVED** (`vikunja_release_binary` / `vikunja_release_zip`). - **#4 substring version gate — RESOLVED** (regex parse `version v(\d+\.\d+\.\d+)` + exact `!= vikunja_version`). - **#5 Mysterious Name — PARTIAL** (task renamed, but the `vikunja_pg` register and the group/host collision remain). - Earlier **B1 secret leak — RESOLVED** (`no_log: true` on the template task, L205); **S2 host-key coercion — RESOLVED** (`[ssh_connection] ssh_args = -o StrictHostKeyChecking=accept-new`). --- ## Spec **(a) Missing / partial:** none. Every deliverable in the body is present; `npm/` is explicitly deferred ("still to come"). **(b) Scope creep:** none remaining. The added pieces (`.env.example` `ANSIBLE_VAULT_PASSWORD`, `vault-pass` ignore, amd64 assert, mailer) are each named in the body or commit messages. **(c) Implemented but wrong:** - **The `--check` "gate" exercises little.** Commit `60c8f06`: *"The check run stays the gate: it is what the word confirms against."* But `command`/`shell` tasks skip under `--check` (independently verified: a probe `command` reports `skipped=True`), so on the guest `make service-check` skips `migrate` (L265) **and** both account tasks (L280, L291) — the failure-prone half. The operator confirms a rehearsal that never rehearsed migrations or account creation. Minor corollary: since the account-list task also skips, `item.rc | default(1)` would mark every account "missing" — harmless only because the create task skips too. **Round-2 Spec findings — resolution:** - **PostgreSQL unpinned — RESOLVED** (`postgresql-{{ vikunja_db_major }}` + a check-mode-safe `stat /usr/lib/postgresql/17` assert, L177-192); README:68's "PostgreSQL 17" is now enforced. - **Account wiring — RESOLVED** (README "Adding an account" documents the two-file edit; the assert turns a missing vault key into a clear failure). The two-file edit is inherent to a vaulted secret, not a defect. - **`xff` + `trustedproxies` creep — RESOLVED** (`ipextractionmethod: "direct"`, no `trustedproxies` / `vikunja_trusted_proxies` left in the tree; matches the upstream default). --- **Summary** — Standards: 0 documented-standard breaches, 7 judgement-call smells (4 of 5 prior smells resolved, 1 partial); worst = the residual `vikunja_pg` guard duplication. Spec: 0 missing, 0 scope creep, 1 implemented-but-wrong (the thin `--check` gate); all 3 round-2 Spec findings resolved. Both axes clean of hard/blocking issues. **Merge recommendation: yes — merge.** No blocking or significant findings remain on either axis; the open items are judgement-call smells and one non-blocking gap where the `--check` gate can't rehearse the migrate/account steps (worth a follow-up, not a hold).
pit merged commit 43a1c85da7 into main 2026-10-09 06:03:40 +00:00
pit deleted branch hermes/service-stack 2026-10-09 06:03:40 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
olympus/infra-tracker!2
No description provided.