The integration_discourse settings page rewrites what the user types, and the
rewrite is unconditional. src/components/PersonalSettings.vue:217-224 upstream:
if (this.state.url !== '' && !this.state.url.startsWith('https://')) {
if (this.state.url.startsWith('http://')) {
this.state.url = this.state.url.replace('http://', 'https://')
An onion service carries no certificate and is served over plain http through
Tor, so on such a node the address is rewritten to an endpoint that does not
exist. Run 31658270172, job 94319317649 shows all three steps: the trace's
action log records the spec typing `http://forum.<onion>/`, the PUT to
/apps/integration_discourse/config carries `{"values":{"url":"https://forum.
<onion>/"}}`, and the connect navigation to `https://forum.<onion>//user-api-
key/new` fails with network status -1, which Playwright reports as
`Received string: "chrome-error://chromewebdata/"`.
The app ships compiled JS, so the fix has to reach the bundle rather than the
source. It is measured against the file the appstore actually installs --
nextcloud-releases/integration_discourse v3.3.0, whose
integration_discourse-personalSettings.mjs is the exact filename the trace
loaded from the container; the nextcloud/integration_discourse releases stop at
v2.0.1 in 2023 and still ship .js, which is why the earlier reading of that repo
was misleading.
Verified rather than argued:
- the anchor `startsWith("https://")&&(` occurs exactly once in that file
- after the patch the guard reads
`…startsWith("https://")&&!this.state.url.includes(".onion")&&(…`
and `node --check` accepts the bundle as valid ESM
- the anchor no longer exists afterwards, so a second application is a no-op:
idempotency is structural, not a marker check
- the task was run through a real ansible-playbook against a real copy of the
bundle with only the exec prefixes stubbed: first run changed=1 and patched,
second run changed=0, so changed_when reports honestly
- busybox sed (alpine:3, "This is not GNU sed version 4.0") produces the same
result as GNU sed, which matters because the image is 34-fpm-alpine
`occ app:getpath` locates the app instead of hardcoding custom_apps, which
appears nowhere in the repo and which the appstore may move. The `grep -q … ||
exit 0` makes the task both idempotent and safe when the file is absent.
Clearnet is untouched: the added condition is false for every non-onion URL, so
the upgrade still happens where TLS is available.
Not verified: what `app:getpath` returns in this installation. If the path is
wrong the task no-ops silently -- the same shape as the WordPress healthcheck,
where msmtp fails and a passing curl hides it. A `test -f` with a loud failure
would close that, and the layer behind the redirect has never been reached by a
green run.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
main.yml now reads as the chronological sequence it describes: it includes
01_fullstack.yml, then the single IS_STACK_HOST-gated 02_manager_ops.yml,
whose substeps live in the matching 02_manager_ops/ directory numbered in
execution order. The plugin pipeline moves under its parent step so its
include resolves against its own directory instead of an inherited search
root.
The flush that deploys the stack moves out of the config step to the top of
02_manager_ops.yml. It previously ran after the swarm resolver, which then
waited 600 times for a service that did not exist yet and killed two CI jobs
with "no such service: nextcloud_nextcloud". The second flush and the
re-resolve behind it are removed: neither included file notifies a handler,
so they could never fire.
A new lint pins what made this refactor risky. A relative include is resolved
at runtime, so a typo or a moved file survives ansible-lint and
--syntax-check and only fails mid-deploy; worse, a bare name that matches
under two search roots silently shadows one file with the other.
Comments that only narrated code are dropped from the addon hooks and the
plugin install step.
Per-addon tasks/addons hooks contain only exception logic beyond the generic loader (DRY/SPOT): config-complete/per-user integrations are documented SPOT comments; mattermost/openai/openproject/suitecrm perform genuine bidirectional OAuth/API provisioning (register client on partner, write client_id/secret back) via files/{mattermost,openproject,suitecrm} scripts, gated on the partner container running. meta/addons keys for gitlab/mastodon/matrix/peertube/zammad fixed from the inert url to the admin oauth_instance_url/instances the apps actually read.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>