The role declared `platform_administrator` as its administrator and GitLab
disagreed. Community Edition maps no OIDC group onto admin, templates/config/
gitlab.yml.j2 sets no `admin_groups`, and nothing promoted the account after
its first sign-in, so `/admin` answered 404 to the one identity the
deployment calls the administrator. The rights lived on `root` instead,
which carries that same person's password, so one secret opened two doors
with different privileges and an audit of "who administers this" named an
account that belongs to nobody.
tasks/utils/platform_admin.yml provisions the account instead of deriving
it: files/ruby/ensure_platform_admin.rb creates it when absent, sets the
admin flag when missing, and realigns the password, reporting [CHANGED],
[UNCHANGED] or [FAIL] the way the root realign beside it already does.
Three details decide whether this works, and each is read off the role
rather than assumed. `skip_confirmation: true` is what lets
`auto_link_user: ["openid_connect"]`, already set in gitlab.yml.j2, bind the
first OIDC sign-in to this account by e-mail instead of creating a second
one; the role's own MCP provisioning uses that flag for the same reason.
Clearing `password_automatically_set` is what keeps an account OmniAuth
created earlier from rejecting every password, a trap the root script next
door documents. And the account is looked up by username or by any e-mail,
so a pre-existing OIDC identity is promoted rather than duplicated.
`root` stays the break-glass account and keeps its realign. Giving the
declared administrator its own rights is what lets root stop being the
everyday admin; blocking it, as the designer branch proposes, would lock out
the recovery path this branch relies on.
The sharpened persona check from 989bf86374 should go green with this, which
is the point: it was red because the deployment was wrong, not the test.
mobilizon and port-ui share the shape and are handled separately.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GitLab reads its logos, favicon, PWA icon and title from the Appearance
model, so tasks/03_design.yml renders the logo set and runs
files/ruby/appearance.rb through rails runner after the MCP surface.
The script uploads logo, header logo, favicon and PWA icon, sets the
title, PWA name and short name, and removes only the fields the role
set earlier when design is off or a field is missing.
templates/style.css.j2 maps GitLab's colors onto the design tokens,
templates/design.js.j2 mirrors GitLab's own color mode, and
meta/services.yml declares the logo slots.
files/playwright/test-design.js signs in natively as GitLab's root
account with ADMIN_PASSWORD, which holds root's password on this branch
and on feature/store-i18n alike, checks the tokens in both modes, a
color mode picked in the preferences, the sign-in page, the signed-in
interface, top-bar focus, logos and title, and root's admin area, and
captures the gallery. The pass's own administrator provisioning stays out of
this commit: role logic is owned by feature/store-i18n, which keeps
root as its break-glass account.
TODO.md records the open self sign-up, the MCP owner password in argv,
Gravatar and the stale MCP section of the README.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
3c72c2002a and 5366788ab1 moved application roles from the host account
administrator onto platform_administrator. administrator never rotates: it
declares ansible_become_password in roles/user-administrator/meta/users.yml, so
the generator pins nothing into host_vars, and rotate.py:12 exempts it anyway.
platform_administrator rotates with every --users pass, between PASS 1 and
PASS 2 of a matrix-deploy round. The roles below wrote the password only when
they created the account, so from the second pass on the instance kept the
previous value while the deploy rendered the new one, and the sign-in failed.
Each one now re-applies the declared password through its own handle. baserow
drops the 'if created' guard and compares with check_password. gitea and
mattermost gain a change-password call beside the create they already had.
gitlab realigns its native root break-glass through the Rails runner and clears
password_automatically_set, which would otherwise make valid_password? return
false whatever the password is. magento and snipe-it get a script because
neither ships a CLI for an existing account; magento's runs under AREA_GLOBAL so
the adminhtml-only admin_user_save_after observer never loads, at the price of
the admin_passwords history row. matomo moves its realign out from behind the
token gate a rotation never opens. The matrix ansible flavor gets the realign
its compose flavor already had. bluesky resets the native PDS account through
the admin API when the create reports the handle taken.
Every new path follows the contract web-app-nextcloud and sys-svc-rdbms already
use: [CHANGED] / [UNCHANGED] / [FAIL] on the wire, failed_when on rc or [FAIL],
and an absent account is a failure rather than a silent zero-row success. The
password reaches the container through container exec -e NAME plus a task
environment, never the host-side argv.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GitLab 19 places a new user's personal namespace in an organization, so the
MCP token owner cannot be created without one. The script looked the default
organization up and aborted when it was missing, on the assumption that an
installed instance always has it.
A CNG install does not. On a fully migrated v19.3.1, whose migrations log ends
"Performing custom instance setup" and "Registering OAuth applications", the
table is empty:
ORGS=0
DEFAULT=nil
Not a row under another id: no organizations at all. Upstream still resolves
the default as find_by(id: DEFAULT_ORGANIZATION_ID), so the lookup is right
and simply finds nothing, and nothing on a headless deploy creates the row.
The first account provisioned here therefore has to.
Create it from GitLab's own constants rather than literals, and report a
failed create through the model's errors, keeping the abort markers this
script already speaks in. Instances that have an organization are untouched:
the branch runs only when the user is absent and the lookup came back nil.
Verified against a live instance before and after: the create yields
"CREATED id=1 path=default" and the lookup then resolves, Users::CreateService
builds the personal namespace, and the deploy's mint task reports changed
where it previously aborted.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The merged branch moved every role-local script out of files/ into a folder
named for its language (files/python/, files/php/, files/shell/), but the
moves lost their staging and never landed. HEAD still carried the old paths
while the worktree had the new ones.
Move the 24 remaining scripts and update the six places that resolve them:
the mcp-adapter instance/isolation/probe tasks, the litellm key task, the
adapter README and the endpoint-path lint. Every changed line in those
files is a path.
Also drop roles/sys-svc-compose/files/swarm/stack_ready.sh, a stale
duplicate of the copy already tracked under files/shell/swarm/.
The added comments are the branch's own `nocheck: mirrored-unit-test`
directives, which state why a script whose measurement only exists inside
its container cannot have a mirrored unit test.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>