mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
fix(web-app-wordpress): run every addon through its declaration
The deploy died in the drill's second pass on `wp plugin install activitypub --activate`: the first pass had installed it, the DR restore put the tree back, and wp-cli refuses to unpack onto an occupied directory. `--force` would have made that green by overwriting whatever the restore produced - on a branch about NFS-backed volumes, the worst possible place to paper over a restore. The probe keeps it red instead: `wp plugin is-installed` answers from `get_plugins()` alone (verified in the pinned wp-cli phar: `Fetchers\Plugin::get()` iterates `get_plugins()` and nothing else), so a tree WordPress cannot parse still reaches the install and still fails on folder_exists. Activation moves into its own step. Chained to `install --activate`, wp-cli sets chained_command and skips the result check, so an activation failure returns 0 today. The same install path now serves both call sites; the addon hook carried its own copy, which is why one defect needed fixing twice. mu-plugins follow the declaration the way discourse already does. The loop reads `mechanism: mu_plugin` from meta/addons/ instead of spelling two filenames, which is how `infinito-http-ca-trust` shipped undeclared while `infinito-oidc-rbac-mapper` carried a `required: true` that nothing evaluated. Both install paths honour that field now - a required addon aborts the play, an optional one warns and is skipped - which is the first place in the repo to read it. Three guards, because the loop replaced two guaranteed literals: an assert that every required addon is claimed by some install path, a lint pairing each vendored addon with its shipped file in both directions, and the spec the addon contract demands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
777d46e276
commit
2f7cd543ad
12 files changed
+231
-61
No files matched your search
@@ -119,7 +119,7 @@ docker run --rm -it \
|
||||
|
||||
The goal of this deployment is to provide a production-ready, scalable WordPress instance with multisite capabilities and enhanced performance. By automating the custom image build and configuration processes via Docker Compose and Ansible, it minimizes manual intervention, reduces errors, and allows you to concentrate on building great content.
|
||||
|
||||
## Multisite (requirement 005)
|
||||
## Multisite
|
||||
|
||||
WordPress Multisite is opt-in. Set `services.wordpress.multisite.enabled: true` in the inventory to convert the deployed instance into a sub-domain Multisite network. Every entry in `domains.canonical` becomes a site in the network; the first entry is the network primary, subsequent entries are child sites.
|
||||
|
||||
@@ -132,9 +132,8 @@ Operator-facing instructions for assigning these groups live in [Administration
|
||||
|
||||
## Addons
|
||||
|
||||
Plugins and the OIDC→RBAC mu-plugin are declared in
|
||||
[`meta/addons/`](./meta/addons/) under the unified addon contract
|
||||
(requirement 026). The OIDC and WP-Discourse runtime config lives in each addon's `config:` block.
|
||||
Every plugin and mu-plugin is declared in [`meta/addons/`](./meta/addons/) under
|
||||
the unified addon contract. The OIDC and WP-Discourse runtime config lives in each addon's `config:` block. Both install paths read those declarations: a `required` addon that fails to install stops the deploy, an optional one warns and is skipped.
|
||||
|
||||
| Addon | Mechanism | Default state | Bridges |
|
||||
|-------|-----------|---------------|---------|
|
||||
@@ -142,17 +141,18 @@ Plugins and the OIDC→RBAC mu-plugin are declared in
|
||||
| `wp-discourse` | `plugin` | enabled with the `discourse` service | `discourse` → `web-app-discourse` |
|
||||
| `activitypub` | `plugin` | always enabled (Fediverse federation) | none |
|
||||
| `infinito-oidc-rbac-mapper` | `mu_plugin` | `required` (always installed, vendored) | `sso` → `web-app-keycloak` |
|
||||
| `infinito-http-ca-trust` | `mu_plugin` | `required` (always installed, vendored) | none |
|
||||
|
||||
The OIDC login + RBAC paths are covered by `test-admin-oidc-login.js` /
|
||||
`test-rbac-roles.js`, the Discourse round-trip by `test-discourse-roundtrip.js`.
|
||||
|
||||
## Playwright service dependencies
|
||||
|
||||
The Playwright suite in [files/playwright/playwright.spec.js](files/playwright/playwright.spec.js) gates its scenarios on the following shared services (requirement 006). Scenarios that depend on a service report as `skipped` when the corresponding `<SERVICE>_SERVICE_ENABLED=false` in the staged `.env`:
|
||||
The Playwright suite in [files/playwright/playwright.spec.js](files/playwright/playwright.spec.js) gates its scenarios on the following shared services. Scenarios that depend on a service report as `skipped` when the corresponding `<SERVICE>_SERVICE_ENABLED=false` in the staged `.env`:
|
||||
|
||||
- `oidc`: baseline admin OIDC round-trip plus the three RBAC scenarios (subscriber/editor/administrator).
|
||||
- `ldap`: the three RBAC scenarios additionally depend on LDAP group sync; disabling LDAP skips them alongside OIDC.
|
||||
- `discourse`: the WP->Discourse post round-trip scenario (requirement 007). Disabling Discourse skips it; front-page reachability and CSP baselines stay active.
|
||||
- `discourse`: the WP->Discourse post round-trip scenario. Disabling Discourse skips it; front-page reachability and CSP baselines stay active.
|
||||
|
||||
The front-page CSP + canonical-domain baseline is ungated and always runs.
|
||||
|
||||
|
||||
@@ -16,20 +16,24 @@ See the [WordPress Must-Use Plugins handbook](https://developer.wordpress.org/ad
|
||||
|
||||
Put a file in this directory only when the behavior it implements is part of the security or integration contract of this role and MUST remain in effect for every request. Everyday feature plugins SHOULD stay in normal `wp-content/plugins/` and be managed through [05_enable_plugin.yml](../../tasks/05_enable_plugin.yml) so operators can disable them per site if needed.
|
||||
|
||||
Each file in this directory SHOULD declare its purpose, hooks, and source requirement at the top of the file so readers can understand why it cannot be switched off.
|
||||
Each file in this directory SHOULD declare its purpose and the hooks it registers at the top of the file so readers can understand why it cannot be switched off.
|
||||
|
||||
## OIDC -> RBAC mapping (infinito-oidc-rbac-mapper.php) 🎫
|
||||
|
||||
[infinito-oidc-rbac-mapper.php](infinito-oidc-rbac-mapper.php) is the only file shipped here today. It implements the OIDC -> WordPress role contract from requirements 004 and 005:
|
||||
[infinito-oidc-rbac-mapper.php](infinito-oidc-rbac-mapper.php) implements the OIDC -> WordPress role contract:
|
||||
|
||||
- **Single-Site path**: the claim MUST contain `/roles/web-app-wordpress/<role>` entries. The highest-privilege role across all matches wins (`administrator > editor > author > contributor > subscriber`). When no entry matches, the user's role is set to `subscriber` as a deterministic fallback.
|
||||
- **Multisite path** (auto-detected via `is_multisite()`): per-site roles come from `/roles/web-app-wordpress/<canonical-domain>/<role>` entries; the super-admin capability comes from `/roles/web-app-wordpress/network-administrator`. The mapper adds the user to any site they have a role for (`add_user_to_blog`) and removes them from sites it previously added them to (`remove_user_from_blog`) when a role for that site disappears from the claim. A user-meta marker (`_infinito_oidc_added_blog_ids`) records which blog memberships the mapper owns, so memberships added through `wp-admin` or the REST API outside the OIDC flow are never touched.
|
||||
|
||||
For the broader RBAC contract (LDAP layout, `rbac.tenancy` schema, the `rbac_group_path` lookup plugin), see [rbac.md](../../../../docs/contributing/design/iam/rbac.md).
|
||||
|
||||
## HTTP CA trust (infinito-http-ca-trust.php) 🔐
|
||||
|
||||
[infinito-http-ca-trust.php](infinito-http-ca-trust.php) points WordPress's HTTP API at the deployment's CA bundle so outbound calls to internal HTTPS endpoints (OIDC discovery, REST loopback) verify instead of failing.
|
||||
|
||||
## Deployment 🚚
|
||||
|
||||
Files in this directory are copied into the running container by [04_mu_plugins.yml](../../tasks/04_mu_plugins.yml). The task runs on every deploy, so dropping a new file here is enough to have it picked up on the next playbook run.
|
||||
Every file here is declared as an addon under [meta/addons/](../../meta/addons/) with `mechanism: mu_plugin` and `source: vendored`, and the addon id is the file stem. [04_mu_plugins.yml](../../tasks/04_mu_plugins.yml) loops over those declarations and copies `<addon_id>.php` into the container on every deploy; a new file therefore needs its `meta/addons/<addon_id>.yml` entry and a Playwright spec before it is installed.
|
||||
|
||||
## Credits 🙏
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
const { test, expect } = require("@playwright/test");
|
||||
const { skipUnlessAddonEnabled } = require("../addon-gating");
|
||||
const shared = require("../_shared");
|
||||
|
||||
test("addon infinito-http-ca-trust: the CA-trust mu-plugin is loaded by WordPress and outbound HTTPS calls resolve", async ({
|
||||
browser,
|
||||
}) => {
|
||||
skipUnlessAddonEnabled("infinito-http-ca-trust");
|
||||
test.setTimeout(120_000);
|
||||
|
||||
const context = await browser.newContext({ ignoreHTTPSErrors: true });
|
||||
const page = await context.newPage();
|
||||
|
||||
try {
|
||||
await shared.wpAdminLoginViaOidc(
|
||||
page,
|
||||
shared.env.wpBaseUrl,
|
||||
shared.env.adminUsername,
|
||||
shared.env.adminPassword
|
||||
);
|
||||
|
||||
await page.goto(
|
||||
`${shared.env.wpBaseUrl}/wp-admin/plugins.php?plugin_status=mustuse`,
|
||||
{ waitUntil: "domcontentloaded", timeout: 60_000 }
|
||||
);
|
||||
|
||||
await expect(
|
||||
page.locator("#the-list"),
|
||||
"the Must-Use plugins screen must list infinito-http-ca-trust.php — a mu-plugin that is absent from this screen was never copied into wp-content/mu-plugins"
|
||||
).toContainText("infinito-http-ca-trust.php", { timeout: 30_000 });
|
||||
|
||||
const restResponse = await page.request.get(
|
||||
`${shared.env.wpBaseUrl}/wp-json/`,
|
||||
{ failOnStatusCode: false }
|
||||
);
|
||||
expect(
|
||||
restResponse.status(),
|
||||
"the REST root must answer over HTTPS: WordPress reaching its own TLS endpoint is what this mu-plugin's CA bundle enables"
|
||||
).toBe(200);
|
||||
} finally {
|
||||
await page.close().catch(() => {});
|
||||
await context.close().catch(() => {});
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
required: true
|
||||
mechanism: mu_plugin
|
||||
source: vendored
|
||||
@@ -1,46 +1,15 @@
|
||||
---
|
||||
# Install mu-plugins into the running WordPress container so they are
|
||||
# always active (mu-plugins cannot be deactivated from the admin UI).
|
||||
# Currently installs the OIDC→RBAC role mapper.
|
||||
|
||||
- name: "✅ Ensure mu-plugins directory exists inside the container"
|
||||
shell: >
|
||||
container exec -u {{ WORDPRESS_USER }} {{ WORDPRESS_CONTAINER_ADDRESS }}
|
||||
mkdir -p {{ WORDPRESS_DOCKER_HTML_PATH }}/wp-content/mu-plugins
|
||||
mkdir -p {{ WORDPRESS_MU_PLUGINS_DIR }}
|
||||
changed_when: false
|
||||
|
||||
- name: "📋 Copy infinito-oidc-rbac-mapper.php into the container"
|
||||
shell: >
|
||||
container exec -i {{ WORDPRESS_CONTAINER_ADDRESS }}
|
||||
tee {{ WORDPRESS_DOCKER_HTML_PATH }}/wp-content/mu-plugins/infinito-oidc-rbac-mapper.php
|
||||
> /dev/null
|
||||
args:
|
||||
stdin: "{{ lookup('file', [role_path, 'files/mu-plugins/infinito-oidc-rbac-mapper.php'] | path_join) }}"
|
||||
register: cp_mu_plugin
|
||||
changed_when: cp_mu_plugin.rc == 0
|
||||
failed_when: cp_mu_plugin.rc != 0
|
||||
|
||||
- name: "✅ Ensure WordPress user owns the mu-plugin file"
|
||||
shell: >
|
||||
container exec {{ WORDPRESS_CONTAINER_ADDRESS }}
|
||||
chown {{ WORDPRESS_USER }}:{{ WORDPRESS_USER }}
|
||||
{{ WORDPRESS_DOCKER_HTML_PATH }}/wp-content/mu-plugins/infinito-oidc-rbac-mapper.php
|
||||
changed_when: false
|
||||
|
||||
- name: "📋 Copy {{ WORDPRESS_CA_TRUST_MU_PLUGIN_FILE }} into the container"
|
||||
shell: >
|
||||
container exec -i {{ WORDPRESS_CONTAINER_ADDRESS }}
|
||||
tee {{ WORDPRESS_CA_TRUST_MU_PLUGIN_PATH }}
|
||||
> /dev/null
|
||||
args:
|
||||
stdin: "{{ lookup('file', [role_path, 'files/mu-plugins', WORDPRESS_CA_TRUST_MU_PLUGIN_FILE] | path_join) }}"
|
||||
register: cp_ca_trust_mu_plugin
|
||||
changed_when: cp_ca_trust_mu_plugin.rc == 0
|
||||
failed_when: cp_ca_trust_mu_plugin.rc != 0
|
||||
|
||||
- name: "✅ Ensure WordPress user owns the CA-trust mu-plugin file"
|
||||
shell: >
|
||||
container exec {{ WORDPRESS_CONTAINER_ADDRESS }}
|
||||
chown {{ WORDPRESS_USER }}:{{ WORDPRESS_USER }}
|
||||
{{ WORDPRESS_CA_TRUST_MU_PLUGIN_PATH }}
|
||||
changed_when: false
|
||||
- name: "🔁 Iterate through declared WordPress mu-plugins"
|
||||
include_tasks: utils/install_mu_plugin.yml
|
||||
loop: "{{ WORDPRESS_MU_PLUGINS }}"
|
||||
loop_control:
|
||||
loop_var: wordpress_mu_plugin
|
||||
label: "{{ wordpress_mu_plugin.key }}"
|
||||
vars:
|
||||
mu_plugin_name: "{{ wordpress_mu_plugin.key }}"
|
||||
@@ -10,8 +10,5 @@
|
||||
include_tasks: "{{ plugin_task_path }}"
|
||||
when: plugin_task_file.stat.exists
|
||||
|
||||
- name: "📦 Install and activate WordPress plugin via WP CLI"
|
||||
ansible.builtin.shell: >
|
||||
container exec -u {{ WORDPRESS_USER }} {{ WORDPRESS_CONTAINER_ADDRESS }}
|
||||
wp plugin install {{ plugin_name }} --activate
|
||||
--path={{ WORDPRESS_DOCKER_HTML_PATH }}
|
||||
- name: "📦 Ensure WordPress plugin is installed and active"
|
||||
ansible.builtin.include_tasks: utils/install_plugin.yml
|
||||
@@ -19,7 +19,17 @@
|
||||
(lookup('config', application_id, 'services.wordpress.multisite.enabled')
|
||||
| default(false) | bool)
|
||||
|
||||
- name: "📦 Install mu-plugins (OIDC→RBAC role mapper)"
|
||||
- name: "🛑 Assert every required addon is claimed by an install path"
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- WORDPRESS_ADDONS_REQUIRED | difference(WORDPRESS_ADDONS_INSTALLED) | length == 0
|
||||
fail_msg: >-
|
||||
Required addon(s)
|
||||
{{ WORDPRESS_ADDONS_REQUIRED | difference(WORDPRESS_ADDONS_INSTALLED) | join(', ') }}
|
||||
are declared in meta/addons/ but no install loop selects them; their
|
||||
`mechanism` must be `plugin` or `mu_plugin` to be installed at all.
|
||||
|
||||
- name: "📦 Install declared mu-plugins"
|
||||
include_tasks: 04_mu_plugins.yml
|
||||
|
||||
- name: "🔁 Iterate through WordPress plugins"
|
||||
|
||||
@@ -21,10 +21,9 @@
|
||||
changed_when: false
|
||||
|
||||
- name: "📦 Install WP Discourse plugin"
|
||||
ansible.builtin.shell: >
|
||||
container exec -u {{ WORDPRESS_USER }} {{ WORDPRESS_CONTAINER_ADDRESS }}
|
||||
wp plugin install wp-discourse --activate
|
||||
--path={{ WORDPRESS_DOCKER_HTML_PATH }}
|
||||
ansible.builtin.include_tasks: ../utils/install_plugin.yml
|
||||
vars:
|
||||
plugin_name: wp-discourse
|
||||
when: IS_STACK_HOST | bool
|
||||
|
||||
- name: "🔧 Configure WP Discourse settings"
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
---
|
||||
- name: "📦 Normalise the install contract for '{{ mu_plugin_name }}'"
|
||||
ansible.builtin.set_fact:
|
||||
wp_mu_plugin_required: "{{ lookup('config', application_id, 'addons.' ~ mu_plugin_name ~ '.required', false) | bool }}"
|
||||
changed_when: false
|
||||
|
||||
- name: "📋 Copy mu-plugin '{{ mu_plugin_name }}' into the container"
|
||||
ansible.builtin.shell: >
|
||||
container exec -i {{ WORDPRESS_CONTAINER_ADDRESS }}
|
||||
tee {{ [WORDPRESS_MU_PLUGINS_DIR, mu_plugin_name ~ '.php'] | path_join }}
|
||||
> /dev/null
|
||||
args:
|
||||
stdin: "{{ lookup('file', [role_path, 'files/mu-plugins', mu_plugin_name ~ '.php'] | path_join) }}"
|
||||
register: wp_mu_plugin_copy
|
||||
changed_when: wp_mu_plugin_copy.rc == 0
|
||||
failed_when: wp_mu_plugin_copy.rc != 0 and wp_mu_plugin_required | bool
|
||||
|
||||
- name: "🟡 Skip optional mu-plugin '{{ mu_plugin_name }}' after a failed copy"
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
::warning title=addon install failed::{{ application_id }} addon
|
||||
'{{ mu_plugin_name }}' was not copied into the container
|
||||
(rc={{ wp_mu_plugin_copy.rc }}); deploy continues because it is not required
|
||||
when: wp_mu_plugin_copy.rc != 0
|
||||
|
||||
- name: "✅ Ensure WordPress user owns mu-plugin '{{ mu_plugin_name }}'"
|
||||
ansible.builtin.shell: >
|
||||
container exec {{ WORDPRESS_CONTAINER_ADDRESS }}
|
||||
chown {{ WORDPRESS_USER }}:{{ WORDPRESS_USER }}
|
||||
{{ [WORDPRESS_MU_PLUGINS_DIR, mu_plugin_name ~ '.php'] | path_join }}
|
||||
changed_when: false
|
||||
when: wp_mu_plugin_copy.rc == 0
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
- name: "📦 Normalise the install contract for '{{ plugin_name }}'"
|
||||
ansible.builtin.set_fact:
|
||||
wp_plugin_required: "{{ lookup('config', application_id, 'addons.' ~ plugin_name ~ '.required', false) | bool }}"
|
||||
changed_when: false
|
||||
|
||||
- name: "🔎 Check if WordPress plugin '{{ plugin_name }}' is installed"
|
||||
ansible.builtin.shell: >
|
||||
container exec -u {{ WORDPRESS_USER }} {{ WORDPRESS_CONTAINER_ADDRESS }}
|
||||
wp plugin is-installed {{ plugin_name }}
|
||||
--path={{ WORDPRESS_DOCKER_HTML_PATH }}
|
||||
register: wp_plugin_present
|
||||
changed_when: false
|
||||
failed_when: wp_plugin_present.rc not in [0, 1]
|
||||
|
||||
- name: "📦 Install WordPress plugin '{{ plugin_name }}' via WP CLI"
|
||||
ansible.builtin.shell: >
|
||||
container exec -u {{ WORDPRESS_USER }} {{ WORDPRESS_CONTAINER_ADDRESS }}
|
||||
wp plugin install {{ plugin_name }}
|
||||
--path={{ WORDPRESS_DOCKER_HTML_PATH }}
|
||||
register: wp_plugin_install
|
||||
when: wp_plugin_present.rc != 0
|
||||
failed_when: wp_plugin_install.rc != 0 and wp_plugin_required | bool
|
||||
|
||||
- name: "🟡 Skip optional WordPress plugin '{{ plugin_name }}' after a failed install"
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
::warning title=addon install failed::{{ application_id }} addon
|
||||
'{{ plugin_name }}' did not install (rc={{ wp_plugin_install.rc }});
|
||||
activation skipped, deploy continues because the addon is not required
|
||||
when: (wp_plugin_install.rc | default(0)) != 0
|
||||
|
||||
- name: "✅ Activate WordPress plugin '{{ plugin_name }}' via WP CLI"
|
||||
ansible.builtin.shell: >
|
||||
container exec -u {{ WORDPRESS_USER }} {{ WORDPRESS_CONTAINER_ADDRESS }}
|
||||
wp plugin activate {{ plugin_name }}
|
||||
--path={{ WORDPRESS_DOCKER_HTML_PATH }}
|
||||
register: wp_plugin_activate
|
||||
when: (wp_plugin_install.rc | default(0)) == 0
|
||||
changed_when: >
|
||||
wp_plugin_activate.rc == 0
|
||||
and ('already' not in (wp_plugin_activate.stderr | lower))
|
||||
@@ -14,6 +14,9 @@ WORDPRESS_CONTAINER_ADDRESS: "{{ lookup('container_address', application_id, ent
|
||||
WORDPRESS_DISCOURSE_CONTAINER_ADDRESS: "{{ lookup('config', 'web-app-discourse', 'services.discourse.name') }}"
|
||||
WORDPRESS_DOMAINS: "{{ lookup('config', application_id, 'domains.canonical') }}"
|
||||
WORDPRESS_PLUGINS: "{{ lookup('config', application_id, 'addons') | dict2items | selectattr('value.mechanism', 'equalto', 'plugin') | list }}"
|
||||
WORDPRESS_MU_PLUGINS: "{{ lookup('config', application_id, 'addons') | dict2items | selectattr('value.mechanism', 'equalto', 'mu_plugin') | list }}"
|
||||
WORDPRESS_ADDONS_INSTALLED: "{{ (WORDPRESS_PLUGINS + WORDPRESS_MU_PLUGINS) | map(attribute='key') | list }}"
|
||||
WORDPRESS_ADDONS_REQUIRED: "{{ lookup('config', application_id, 'addons') | dict2items | selectattr('value.required', 'defined') | selectattr('value.required') | map(attribute='key') | list }}"
|
||||
WORDPRESS_USER: "www-data"
|
||||
|
||||
WORDPRESS_MSMTP_SRC: "{{ lookup('path_absolute', 'roles/sys-svc-mail-msmtp/templates/msmtprc.conf.j2') }}"
|
||||
@@ -24,8 +27,7 @@ WORDPRESS_CONFIG_PATH: "{{ [WORDPRESS_DOCKER_HTML_PATH, WORDPRESS_CONFIG_FILE] |
|
||||
WORDPRESS_CONFIG_UPLD_REL: "config/upload.ini"
|
||||
WORDPRESS_CONFIG_UPLD_ABS: "{{ [lookup('container', application_id, 'directories.instance'), WORDPRESS_CONFIG_UPLD_REL ] | path_join }}"
|
||||
|
||||
WORDPRESS_CA_TRUST_MU_PLUGIN_FILE: "infinito-http-ca-trust.php"
|
||||
WORDPRESS_CA_TRUST_MU_PLUGIN_PATH: "{{ [WORDPRESS_DOCKER_HTML_PATH, 'wp-content/mu-plugins', WORDPRESS_CA_TRUST_MU_PLUGIN_FILE] | path_join }}"
|
||||
WORDPRESS_MU_PLUGINS_DIR: "{{ [WORDPRESS_DOCKER_HTML_PATH, 'wp-content/mu-plugins'] | path_join }}"
|
||||
|
||||
WORDPRESS_ADMIN_EMAIL: "{{ lookup('config', application_id, 'users.administrator.email') }}"
|
||||
WORDPRESS_ADMIN_USER: "{{ lookup('config', application_id, 'users.administrator.username') }}"
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""A vendored addon and its shipped file must find each other.
|
||||
|
||||
``source: vendored`` means the payload is committed under the role. The
|
||||
install path resolves it by convention - ``files/mu-plugins/<addon_id>.php``
|
||||
for a WordPress mu-plugin - so a rename on either side silently breaks the
|
||||
deploy: the copy step fails on a path nobody typed, and a file with no
|
||||
declaration is never installed at all. That second direction is how
|
||||
``infinito-http-ca-trust`` shipped for months while the loop that installs
|
||||
mu-plugins never saw it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import unittest
|
||||
|
||||
from utils.cache.yaml import load_yaml_any
|
||||
from utils.roles.mapping import ROLE_DIR_META_ADDONS
|
||||
|
||||
from . import PROJECT_ROOT
|
||||
|
||||
ROLES_DIR = PROJECT_ROOT / "roles"
|
||||
VENDORED_DIRS = {"mu_plugin": ("files/mu-plugins", ".php")}
|
||||
|
||||
|
||||
def _vendored_addons():
|
||||
for role in sorted(ROLES_DIR.iterdir()):
|
||||
addons_dir = role / ROLE_DIR_META_ADDONS
|
||||
if not addons_dir.is_dir():
|
||||
continue
|
||||
for spec_file in sorted(addons_dir.glob("*.yml")):
|
||||
spec = load_yaml_any(str(spec_file), default_if_missing={}) or {}
|
||||
layout = VENDORED_DIRS.get(spec.get("mechanism"))
|
||||
if layout and spec.get("source") == "vendored":
|
||||
yield role, spec_file.stem, layout
|
||||
|
||||
|
||||
class TestAddonsVendoredFiles(unittest.TestCase):
|
||||
def test_every_vendored_addon_ships_its_file(self) -> None:
|
||||
missing = [
|
||||
f"{role.name}: addon '{addon_id}' declares source: vendored "
|
||||
f"but {subdir}/{addon_id}{suffix} does not exist"
|
||||
for role, addon_id, (subdir, suffix) in _vendored_addons()
|
||||
if not (role / subdir / f"{addon_id}{suffix}").is_file()
|
||||
]
|
||||
if missing:
|
||||
self.fail("\n".join(missing))
|
||||
|
||||
def test_every_shipped_file_is_declared(self) -> None:
|
||||
declared = {
|
||||
(role.name, subdir, addon_id)
|
||||
for role, addon_id, (subdir, _) in _vendored_addons()
|
||||
}
|
||||
orphans = []
|
||||
for role in sorted(ROLES_DIR.iterdir()):
|
||||
for subdir, suffix in VENDORED_DIRS.values():
|
||||
for shipped in sorted((role / subdir).glob(f"*{suffix}")):
|
||||
if (role.name, subdir, shipped.stem) not in declared:
|
||||
orphans.append(
|
||||
f"{role.name}: {subdir}/{shipped.name} has no "
|
||||
f"meta/addons/{shipped.stem}.yml, so nothing installs it"
|
||||
)
|
||||
if orphans:
|
||||
self.fail("\n".join(orphans))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user