Skip to content

feat(cli): port universal-portals setup and audit to Go (Beta) - #1756

Open
jacobovidal wants to merge 27 commits into
betafrom
feat/universal-portals-go
Open

jacobovidal wants to merge 27 commits into
betafrom
feat/universal-portals-go

Conversation

@jacobovidal

@jacobovidal jacobovidal commented Nov 6, 2026 •

Copy link
Copy Markdown

🔧 Changes

Replaces the basic auth0 universal-portals setup with a full native Go implementation, and adds auth0 universal-portals audit. Nothing here needs Node or an external tool: everything runs inside the CLI on top of the existing go-auth0 clients. go.mod and go.sum are unchanged.

setup

  • Two templates: consumer (end users manage their account) and business (organization administrators manage their organization). Interactive by default; --no-input plus flags for scripts.
  • Checks the logged-in session's permissions before it creates anything, and says which scopes are missing and how to add them (15 probes for consumer, 23 for business).
  • Creates or reuses the application, grants, forms vault, forms and the portal. A rerun after a failure resumes, and a rerun after success changes nothing.
  • --dry-run runs the whole flow against the tenant's real state, keeps the writes instead of sending them (placeholder ids, secrets redacted) and prints them.
  • --json output is the same content and key order as the previous tool's, with 4-space indentation.

audit <portal-id>

  • 24 rules for the portal and its application, each an error or a warning. Fails on errors, or on warnings too with --strict.
  • Interactive fix menu, or --fix to apply every fix the CLI can make. Warnings are never changed because they may be intentional.
  • --organization also checks that the portal and its application share a connection with the organization.

Options are checked before anything is sent. Setup checks the template, the name, the slug and, for a business portal that cannot ask, the user, before the permission check. An empty slug is made from the name, and a name made of spaces is refused. The cobra command only maps flags to options, and keeps the one rule that is about flags: the flags a run that cannot ask must be given.

Behavior change (beta only). The previous setup created three resources from --name and --slug. The new one keeps both flags (-n, -s) and does a superset of the work, but it also asks for --template when interactive and fails with a usage error under --no-input without it. Exit codes follow the CLI: 1 for errors including usage errors, 130 on interrupt.

Structure

  • internal/portals is the domain package: the portal model, 12 small store interfaces, setup, business setup, audit, the rules and fixes, the terminal-agnostic Reporter/Prompter interfaces, and an in-memory fake tenant (portalstest). It imports no cobra, renderer or SDK. Precedent for a domain package: internal/openapi, internal/auth.
  • internal/cli/universal_portals_*.go is the thin layer: cobra commands, the survey prompter, the terminal renderer, the error mapping, the dry-run transport, and the tenant adapters that implement the stores over cli.api and cli.apiv3. Typed SDK calls are used wherever one exists; raw requests only for portals, form import and read, and the permission probes.
  • No new dependencies. The UI uses survey, the spinner and internal/ansi already in the module.

Shared files touched (outside the new command):

  • internal/cli/management.go: the client builders accept an HTTP client, so the dry-run transport can wrap them.
  • internal/cli/cli.go, internal/cli/auth_env.go: keep the domain, token and metadata needed by the portals adapters.
  • internal/cli/utils_shared.go: removes three helpers only the old setup used.
  • internal/auth0: new wrappers (UserAPI.Organizations, organization add and assign, ClientConnectionAPIV3) and regenerated mocks.
  • docs/: regenerated with make docs.

Known difference between setup and audit. On a rerun, setup resets the callback and logout URLs of the application to its own list, while audit P012 only adds the callback that is missing. Both are pinned by tests and left as they are. If a person adds a URL by hand, a rerun of setup removes it. Say if you want setup to keep extra URLs, with the catch that the old tenant-domain callback then stays after a custom domain goes live.

Deviations from the previous tool, all deliberate: a slug that another portal uses is reported as a validation error with the reason slug_taken; P010 on an application whose only grant is client_credentials prints the command to run, because the CLI has no grant to leave it with; P008 says the tenant has "no custom domain", which is what it checks; usage errors exit 1 (CLI convention); agent mode without --json now emits JSON; client-credentials scope advice with no named scope lists every required scope; a failed audit also prints "The audit failed: N error(s)." from the root error handler, which is how the CLI exits 1.

📚 References

🔬 Testing

Unit tests run with no credentials and no network: go test ./... -count=1 and make lint both pass.

Area What it covers
internal/portals Setup for both templates (create, reuse, resume, failure paths); permission probes and scope advice, including a test that the declared scopes cover every request; all 24 audit rules, the fixes, and the fix rounds; error classes; embedded form and template data; the portal model keeping unknown JSON fields.
internal/portals/portalstest In-memory tenant and scripted prompter/reporter used by the tests above.
internal/cli Flags, usage errors and the fix menu through the real cobra commands; the tenant adapters against local HTTP servers; the dry-run transport (reads pass through, writes are kept, secrets redacted); the exact --json output; the terminal renderer.

A contract test runs the same "a setting left empty is not sent" cases against the in-memory tenant and the adapter over the SDK, so the fake the flow tests run on cannot drift from the requests the API receives. A test requires a probe for every write that a first run, a rerun and a business run with a new organization and role make. The business dry run is tested end to end through the transport.

Because the go-auth0 client mutates a package-level map when it is built, tests that build CLI clients do not run in parallel.

Run against a real tenant, twice: once on the first version of this branch, and again after the review fixes (the last commits). It was a throwaway test tenant with a ready custom domain. Everything created was deleted afterwards and each delete was checked with a 404, and a before/after snapshot of applications, resource servers, grants, vault connections, forms, portals, organizations, roles and flows matched both times.

On the first version:

  • Usage errors, a slug clash and a missing portal exit 1 and write nothing.
  • --dry-run for consumer writes nothing.
  • setup for consumer and business created the application, grants and portal, reused the existing resource servers, forms and (for business) the vault; a rerun reused everything with the same ids.
  • audit of the new portals reported no findings. After breaking the application and sidebar by hand (refresh token rotation on, client_credentials added, a legacy sidebar link), audit reported the three errors and two warnings, audit --fix cleared them, and a final audit was clean.
  • The organization member roles request was rejected for its empty payload (400) and not for permissions (403), so the token had create:organization_member_roles. That scope is not in the required-scope list, which was carried over from the previous tool. Left as is; say if you want it added.

After the review fixes, the second run covered what changed:

  • Slug clash, blank name, bad slug and unknown template stop with the new messages and reasons and write nothing. The clash only gets as far as slug_taken after the permission check, so the live API accepted the new update client probe.
  • --dry-run for consumer and for business passed the permission check, including the business-only probes, and wrote nothing. The only value of a secret field in the output is the placeholder.
  • setup and a rerun for consumer created and then reused the same resources.
  • After the same breaks as before, audit --fix cleared the findings. With client_credentials as the only grant, audit printed a command instead of offering a fix, --fix left the grant alone, and running the printed command restored the application and a clean audit.

Not tested

  • The form import path and its flow against the real API: the tenant already had forms with the same names, which setup reuses.
  • A real business setup after the review fixes. Only its dry run ran against the tenant, which covers the new member-role probe, but not the role assignment itself.
  • Interactive prompts, the fix menu, Ctrl+C mid-run and the interactive role assignment, which need a person at a terminal. The renderer and prompts are covered only by tests of their output.

📝 Checklist

  • All new/changed/fixed functionality is covered by tests (or N/A)
  • I have added documentation for all new/changed functionality (or N/A)

Diagrams

Tan is the cobra layer, blue is the domain package, purple is the Management API side, gray is test support.

%%{init: { 'theme': 'base', 'themeVariables': { 'primaryColor': '#eef6f7', 'primaryTextColor': '#2f3a3d', 'primaryBorderColor': '#b8d8dd', 'lineColor': '#7f9ea6', 'background': '#ffffff' } } }%%
flowchart LR
    subgraph CLI["internal/cli"]
        Setup["setup command"]
        Audit["audit command"]
        Adapters["tenant adapters"]
        DryRun["dry-run transport"]
        Terminal["terminal and prompter"]
    end
    subgraph Domain["internal/portals"]
        Run["setup and business setup"]
        Rules["24 audit rules and fixes"]
        Stores["12 store interfaces"]
        UI["Reporter and Prompter interfaces"]
    end
    subgraph Remote["Management API"]
        SDK["go-auth0 v1 and v3"]
        Raw["raw requests: portals, forms, probes"]
    end
    subgraph Support["test support"]
        Fake[("portalstest fake tenant")]
    end
    Setup --> Run
    Audit --> Rules
    Run --> Stores
    Rules --> Stores
    Run --> UI
    Terminal -.implements.-> UI
    Adapters -.implements.-> Stores
    Adapters --> SDK
    Adapters --> Raw
    DryRun -.wraps.-> SDK
    DryRun -.wraps.-> Raw
    Fake -.implements.-> Stores
    classDef entry fill:#f7efe5,stroke:#d8b98f,color:#2f3a3d
    classDef domain fill:#e8f4f8,stroke:#a8d4e6,color:#2f3a3d
    classDef remote fill:#f6eff9,stroke:#cfb5dc,color:#2f3a3d
    classDef support fill:#f3f3f3,stroke:#cfcfcf,color:#2f3a3d
    class Setup,Audit,Adapters,DryRun,Terminal entry
    class Run,Rules,Stores,UI domain
    class SDK,Raw remote
    class Fake support
    style CLI fill:#fdfaf5,stroke:#d8b98f
    style Domain fill:#f4fafc,stroke:#a8d4e6
    style Remote fill:#fbf8fc,stroke:#cfb5dc
    style Support fill:#fafafa,stroke:#cfcfcf
Loading

The domain package depends only on interfaces. The same flow runs against the real tenant, the dry-run transport, or the fake tenant, depending on what is plugged into the stores.

%%{init: { 'sequence': {'mirrorActors':false}, 'theme': 'base', 'themeVariables': { 'primaryColor': '#eef6f7', 'primaryTextColor': '#2f3a3d', 'primaryBorderColor': '#b8d8dd', 'lineColor': '#7f9ea6', 'background': '#ffffff', 'actorBkg': '#f7efe5', 'actorBorder': '#d8b98f', 'actorTextColor': '#2f3a3d', 'noteBkgColor': '#fff8d8', 'noteTextColor': '#2f3a3d', 'noteBorderColor': '#e7d28a' } } }%%
sequenceDiagram
    actor User
    box rgb(247,239,229) CLI layer
        participant Cmd as setup command
        participant Ad as tenant adapters
    end
    box rgb(232,244,248) Domain
        participant Pf as preflight
        participant Su as setup
    end
    box rgb(246,239,249) Management API
        participant API as tenant
    end
    User->>Cmd: setup --template consumer --name ...
    Cmd->>Su: run(stores, reporter, prompter)
    Su->>Pf: check permissions
    Pf->>Ad: probes, 4 at a time
    Ad->>API: requests that are rejected or empty
    API-->>Pf: 400 ok or 403 missing scope
    Pf-->>Su: ready, or MissingScopes with advice
    Su->>Ad: find or create app, grants, vault, forms, portal
    Ad->>API: GET and POST
    Su-->>Cmd: resources with created or reused status
    Cmd-->>User: summary, or the JSON result
Loading

With --dry-run the adapters are built on the recording transport: the GETs above go to the tenant, and the POSTs are kept with placeholder ids instead of sent.

🤖 Generated with Claude Code

jacobovidal and others added 13 commits November 5, 2026 23:55
Universal Portals setup and audit need a few Management API calls that
the SDK has but the CLI wrappers did not expose:

- UserAPI.Organizations lists the organizations of a user.
- OrganizationAPI.AddConnection, AddMembers and AssignMemberRoles enable
  a connection, add members and give a member roles.
- ClientConnectionAPIV3.Get lists the connections enabled for a client
  with checkpoint paging, which the page and per_page query rejects.

The mocks are regenerated with make test-mocks.

Co-Authored-By: Claude <noreply@anthropic.com>
Universal Portals setup and audit will run on small stores, one per
kind of resource, so the order of the work lives in internal/portals
and the SDK stays in the CLI. A failed request is an *APIError with
the status and the errorCode, and the package decides what it means.

portalstest is an in-memory tenant that implements every store. It
names requests like the Management API routes ("POST clients"), so a
test can deny a scope or fail a request once, and it keeps the rules
setup relies on: conflicts on duplicate identifiers, slugs and names,
secrets only on create and on a read of the secret, and validation
errors for write probes with an empty body. IDs come from one counter
with the shape of the real IDs, so output can be compared exactly.

Co-Authored-By: Claude <noreply@anthropic.com>
portalsTenant implements portals.API on the typed v1 and v3 clients of
the CLI. Only the requests with no typed call go through the raw HTTP
client: portals, the import and the read of a form (a typed read fails
on a node the SDK does not know), and the permission probes.

Every failure becomes a *portals.APIError with the status, the
errorCode and the message, whichever client made the request. Listings
stop after 100 pages of 100 items, and the connections of an
application page by checkpoint because that endpoint rejects page and
per_page. A raw request times out after 30 seconds.

The tests run the real v1 and v3 clients against a local server, so
they check the requests the SDK really sends: the body of each create
and update, the query of each list, and the escaping of IDs.

Co-Authored-By: Claude <noreply@anthropic.com>
Setup sends one check per capability it needs before it creates
anything, so a missing scope is reported once and nothing is left half
done. The checks run four at a time and are read in order. A write
check sends an empty body the API rejects with a 400, and the checks on
a resource that does not exist also accept a 404, so they cover what a
rerun updates and what the business template adds: the grants, the
vault connection, the organization, its connection and members, and
the role and its permissions.

The advice for a missing scope depends on how the CLI is signed in. A
user login lists every scope the tool uses together with the missing
ones, because auth0 login --scopes replaces the scopes of the last
login. A client-credentials login only needs the missing scopes added
to the grant. The advice is also attached to any request refused for a
scope, and the message of a refusal that names no scope points at
Universal Portals not being enabled.

Co-authored-by: ramya18101 <anusriankisetty@gmail.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Setup and audit show their progress through a Reporter and ask their
questions through a Prompter, two small interfaces in internal/portals,
so the flows can run in tests without a terminal.

The terminal implementation draws the output on the stream for
messages, with a rail on the left and a mark for each kind of line, a
rounded title box that shows the tenant before any question, sections
with items and results, a table, links to the dashboard on the IDs,
and the URL of the portal in a note. A spinner shows while work runs
when there is a terminal. Its output for the same calls is the same,
byte for byte, as the output of the tool it replaces, which the tests
compare against.

The questions use survey, and a Ctrl+C comes back as
portals.ErrCancelled so a command can say what was and was not done.
portalstest gets a scripted prompter and a reporter that records what
it was shown.

Co-Authored-By: Claude <noreply@anthropic.com>
Setup creates a portal with the resources it needs, in an order where
the portal comes last, and finds each resource by a stable key before
it creates one: the resource server, the application (named after the
portal, with its slug in its metadata), its grants, the forms vault
application, grant and connection, the three forms, and the portal.
Rerunning the same command after a failure reuses what exists, a
change of the scopes of the vault grant refreshes the vault, and the
settings of an existing application are brought back without touching
a session transfer setting that was changed on purpose.

An application or a portal that setup did not create is never reused,
a taken slug is fixed before anything is created, and the questions for
the kind, the name and the slug are asked first. The result is the same
JSON as before, in the same order, and the tests compare it with the
output of the tool this replaces.

The forms and the pages of a portal are JSON in internal/portals/data,
generated from the tool this replaces. Only the IDs that are known when
setup runs are put in.

The fake tenant answers a write probe with an empty body before it
applies a failure a test set for the real request, as the API does.

Co-Authored-By: Claude <noreply@anthropic.com>
A business portal opens in an organization, so the user who will test
it has to belong to one, with a connection to sign in with and a role.
Setup settles that before it creates anything, so a user who cannot
sign in stops the run with nothing to clean up.

The test user is given as an email or an ID, and an email keeps only
the exact address. When several users match, the person picks one or
looks for another. The organization is the only one of the user, the one
they name or pick, or one they join or create after a confirmation, with
a database connection enabled on it. A missing role is a warning that
names the missing permissions, and the person can be offered a role of
the tenant that has them, or the role of the portal, created with only
the organization permissions the components use. Without a terminal the
run stops with the exact command to fix what is missing.

Skipping the test user asks for the organization of the link instead,
and keeps a placeholder in it when none is given.

Co-Authored-By: Claude <noreply@anthropic.com>
The setup command now runs the setup of internal/portals: it asks for
the kind of portal, its name and its slug, checks the permissions of
the session, and creates the portal with its application, forms and
pages, for a consumer or a business portal. --template, --name,
--slug, --user and --organization give the answers, --no-input fails
on what is missing, and --json prints the result. The old command,
which created a portal application with a Management API grant and the
client credentials grant, is replaced, and --name and --slug still
work.

Errors are classified as the other commands do. A missing scope is an
auth error that lists how to get it for each way of signing in, and a
failure after a write adds how to resume. The steps are printed after
the message and listed in the details of the JSON error.

The command takes the interrupt over for the time it runs, so a Ctrl+C
says whether anything was created, and the helpers that only the old
command used are removed.

Co-Authored-By: Claude <noreply@anthropic.com>
A dry run plans the setup without writing to the tenant. Setup runs as
it does for real, with the same questions, the same flag checks and the
same decisions about what to create, reuse or update, on clients whose
HTTP transport sends the reads to the tenant and keeps every other
request. The kept requests are printed in the order a real run would
send them, with every client secret replaced, and the resources a real
run would create have placeholder IDs such as <new:clients:2>. With
--json the output is the result of a real run plus dryRun and the
requests, and otherwise a short summary. A dry run needs a login, and
it opens no portal.

The checks of the permissions still go to the tenant, because they are
rejected and change nothing, so a dry run finds a missing scope that a
real run would stop on.

The Management API clients are now built by functions that take the HTTP
client, which the dry run uses, and the CLI keeps the tenant and the
token it built them with.

Co-authored-by: Bharath Natarajan <bharath.lalgudinatarajan@okta.com>
Co-Authored-By: Claude <noreply@anthropic.com>
The audit reads a portal and what it depends on at the same time: its
application with the grants and the connections, the resource servers,
whether each form exists, the custom domain and, when asked, an
organization. It checks them against 24 rules, each an error that breaks
the portal or a warning that is degraded or differs from the setup
guide. A portal made by setup passes with no finding.

The CLI fixes the errors it can: the home page, the shape of the
sidebar, a missing application, the settings of the application and of
its refresh token, the Management API grant, and the grants, resource
servers and refresh token policies of the APIs the components call.
Warnings are never changed. Every fix reads the portal again first and
finds its finding by ID and subject, so a fix never works on a stale
copy and is skipped when it is already in place. A fix can expose errors
that the first audit could not see, so up to three rounds are made, and
the errors a fix exposes are asked about again.

--fix applies the errors without asking. With a terminal the person
fixes all of them, chooses, or skips. A fix that fails is shown with its
finding, the scopes the failed fixes needed are summed up once with how
to get them, and a failed fix fails the audit even when the next audit
no longer sees the error.

The pages of a portal are sent back whole when one is fixed, so a page
and a component keep the fields the package does not know.

Co-authored-by: ramya18101 <anusriankisetty@gmail.com>
Co-Authored-By: Claude <noreply@anthropic.com>
audit checks a Universal Portal and its application and shows what it
found in a table, with the command to run for the findings the CLI
cannot fix. It fails when there is an error, with --strict when there
is a warning, and when a fix failed. With a terminal it offers to fix
the errors, all of them or the ones that are chosen, and --fix fixes
them without asking. --organization checks that the organization a
business portal opens in shares a connection with the application, and
--json prints the result.

A failed request keeps the error of the client in it, so the CLI
classifies the failure of an audit as it does the others: a portal that
does not exist is not found, and a missing scope is an auth error with
the steps to get it.

Like setup, audit takes the interrupt over while it runs, so a Ctrl+C
says that the fixes already applied stay in the tenant.

Co-authored-by: ramya18101 <anusriankisetty@gmail.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Every request that setup, its checks, the audit and its fixes make needs
a scope, and the advice for a missing scope asks for the scopes of the
tool. A test runs a consumer and a business setup, the checks, and an
audit that fixes a broken portal, and fails when a request needs a
scope that the advice does not ask for, so a call added later cannot
leave a person who followed the advice refused.

The result of setup no longer has a method of its own to print JSON,
because the command prints it like the other commands do.

Co-Authored-By: Claude <noreply@anthropic.com>
The blank line after the table of the audit was drawn with two trailing
spaces after the rail, where the tool this replaces draws the rail alone.

Co-Authored-By: Claude <noreply@anthropic.com>
jacobovidal and others added 12 commits November 6, 2026 13:16
The up-front slug check returned a plain UsageError, so a slug clash
in a run that cannot ask was reported as invalid_input and the
slug_taken branch of the CLI classification was never reached.

Return SlugTakenError instead, with the hint to rerun with a different
--slug. The error is now a validation error, like name_in_use and
slug_race, so a JSON consumer can tell it from a malformed flag.

Co-Authored-By: Claude <noreply@anthropic.com>
rawJSON cut its path on "?" and "/" before the SDK escaped it, so an id
given on the command line, such as audit "up_x?y/../z", changed the
route and added a query instead of reaching the API as one segment.

Add rawJSONAt, which takes the route as segments, and use it for the
three calls that carry an id: read portal, update portal and read form.

Co-Authored-By: Claude <noreply@anthropic.com>
The audit reads the application, resource servers, forms and custom
domain in parallel, but kept only the first failure. A user with two
broken reads had to fix one, rerun, and only then see the other.

Join the failures so one run shows all of them.

Co-Authored-By: Claude <noreply@anthropic.com>
ClientSettings says that on an update the settings left empty or nil
are not sent. The SDK adapter does that, but the in-memory tenant let
an empty, non-nil list clear the current callbacks, logout URLs or
grant types. A flow test could pass on a change the real API never
receives.

Treat an empty list as not set in the in-memory tenant, and add a
contract test that runs the same cases against both it and the
adapter, so the two cannot drift apart again.

Co-Authored-By: Claude <noreply@anthropic.com>
P010 removes client_credentials by sending the grants that remain.
When it is the only grant nothing remains, and an update never sends an
empty list, so the fix reported success and changed nothing.

In that case offer the command to set the grants the portal signs in
with, and do not offer the automatic fix.

Co-Authored-By: Claude <noreply@anthropic.com>
The class and reason of an error are what a script reads from the JSON
envelope. business_access, slug_taken and permission_check_failed were
not pinned by any test, so a change to the classification would pass.

Check each error of the portals package against its class, reason and
fix hints in one table.

Co-Authored-By: Claude <noreply@anthropic.com>
A dry run answers every read about something it would create, such as
an organization, with an empty object. For the roles of a member, the
connections of an organization and the permissions of a role the SDK
returns no list at all for that answer, and the adapters dereferenced
it, so a dry run of a business portal that has to create an
organization crashed.

Treat the missing list as an empty one in those three adapters, and
check all the listings a dry run can ask about in one table.

Co-Authored-By: Claude <noreply@anthropic.com>
The dry-run transport has a special case for the connections of an
organization that the plan creates, and only the consumer template was
ever run through it, so the business flow was never exercised.

Run a business setup that creates an organization, enables a
connection and adds the user through the transport. Check the order of
the planned writes and that none of them reaches the tenant.

Co-Authored-By: Claude <noreply@anthropic.com>
The check of the permissions exists so that setup does not stop half
way for a permission it could have found at the start. Two writes had
no probe: updating the application, which a rerun does, and giving a
member of an organization a role, which a business run does. A session
without update:clients passed the check and failed on the rerun.

Add both probes, and a test that runs a first run, a rerun and a
business run with a new organization and role, and requires a probe of
the same shape for every write. The counts of the probes are no longer
asserted, because they say nothing about what is checked.

Co-Authored-By: Claude <noreply@anthropic.com>
Finding the roles that fix a missing-permission warning read the
permissions of every role in the tenant one after the other, so a
tenant with many roles waited for each request in turn.

Send the reads with the same bounded parallelism the audit and the
checks of the permissions use, and keep the order of the roles.

Co-Authored-By: Claude <noreply@anthropic.com>
The rules for the options of setup were spread over the cobra command
and Setup, with different messages and error types. Setup also checked
the slug and the user of a business portal only after the check of the
permissions, so a session that lacked a scope was told about a typo in
the slug only after it had fixed the scope. A name made of spaces was
accepted and would have created an application with an empty name.

Setup now checks the template, the name, the slug and the user before
it sends anything, and makes an empty slug from the name. The command
only maps the flags to the options, and keeps the one rule that is
about flags, the ones a run that cannot ask must be given. The messages
and reasons of the errors are unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
Whether the custom domain serves the portal was decided three ways:
setup needed it to be ready and to have a name, while the audit and
its fixes only needed it to be ready, so a ready domain with no name
would have made the audit expect a callback on "https:///".

Add CustomDomain.IsReady and use it in all three. P008 only needs the
tenant to have a custom domain, as its test says, so its message no
longer claims the domain must be ready.

Co-Authored-By: Claude <noreply@anthropic.com>
jacobovidal and others added 2 commits November 6, 2026 13:43
A portal or a page the API returned in a shape the CLI does not read
failed with the plain error of the JSON decoder, such as "cannot
unmarshal number into Go value of type string", with no field. The
fields were also read in map order, so which error came first changed
from run to run.

Read the fields in the order of their names and say which one failed.

Co-Authored-By: Claude <noreply@anthropic.com>
Replace containsString with slices.Contains, count pages with range
over an int, and drop the named results of the snapshot reads that
were never used. Fix the doc comment of the PortalTemplate alias.

The test of forEachParallel waited for its calls with no limit, so a
function that ran them one by one would have hung the suite. Fail the
test after a timeout instead.

Co-Authored-By: Claude <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant