Skip to content

docs(run-once): fix the examples that violate the key rule - #149

Merged
subhashkhileri merged 6 commits into
mainfrom
fix/run-once-project-scope
Aug 24, 2026
Merged

docs(run-once): fix the examples that violate the key rule#149
subhashkhileri merged 6 commits into
mainfrom
fix/run-once-project-scope

Conversation

@gustavolira

@gustavolira gustavolira commented Aug 20, 2026

Copy link
Copy Markdown
Member

Documentation only after review — the API change is reverted. git diff origin/main -- src/ package.json is empty.

The guide already had the right rule:

The key must be globally unique across all spec files and projects. Use a prefix that includes the workspace or project name.

Every example under it then used a literal key around configure() + deploy(), breaking the rule the paragraph had just given. That is what was worth fixing.

What changed

  • The key section says why a shared key breaks — the flag directory is keyed on the runner PID alone, nothing in the path comes from the project — and shows the fix as `${key}-${rhdh.deploymentConfig.namespace}`, which is what deploy() does internally and why deploy() was never affected.
  • Both intents, side by side: namespaced when the setup belongs to one project, literal when it is genuinely shared (an operator in a fixed namespace every project uses).
  • 12 examples fixed across the guide, API reference, deployment guide and the three overlay pages.
  • Nesting is no rescue: a shared outer key skips before deploy() is reached.

Not here: the warning that found affected call sites needed test.info(), which is the coupling being reverted. That moved to redhat-developer/rhdh-plugin-export-overlays#3375 as a static check.

yarn check clean, 111 tests, docs build clean.

gustavolira and others added 5 commits August 20, 2026 17:56
… silently

runOnce keys its flag file by the string alone, in a directory shared by every
project in the run:

    const flagDir = path.join(os.tmpdir(), `playwright-once-${process.ppid}`);
    const flagFile = path.join(flagDir, `${key}.done`);

Nothing in the key comes from the project. So when one spec is matched by more
than one project -- which is what adding an `-app-next` lane does -- the first
project's setup satisfies the second, and the second skips configure() and
deploy() entirely. It then fails much later on a missing element, with nothing
pointing at the cause. That happened on rhdh-plugin-export-overlays#3318 and
was caught in review; four workspaces still pass literal keys today.

Automatic per-project scoping would be wrong, because both intents are real:
installing an operator into a fixed namespace that every project then uses
genuinely wants once per run, while anything touching a project's own namespace
wants once per project. The API cannot guess, so this makes the caller say:

    await test.runOnce("my-setup", fn, { scope: "project" });

Default behaviour is unchanged.

For the keys that stay run-scoped, the dangerous case is no longer silent. The
flag file now records which project satisfied it, and a skip on behalf of a
*different* project logs a warning naming both and pointing at the option --
because that is nearly always the mistake rather than the intent.

Tests are new; there were none for this helper. Each was checked by mutation:
collapsing the scoped key back to `key` fails the per-project test, dropping the
warning guard fails the warning test, and warning unconditionally fails the
same-project test. Full suite 117/117, lint clean.

Part of RHIDP-16456.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Version Bump Check requires both for any change under src/, and it caught
that I had pushed neither. Documents the new scope option and the silent skip
it fixes.

Part of RHIDP-16456.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CI runs `yarn check`, which is typecheck + lint:check + prettier:check. I had
run lint and the tests separately and missed the formatting gate, so the new
test file failed it. `yarn check` is the command to run locally.

Only the test file changed: prettier:check excludes docs/, so the changelog
needed no reformatting, and running --write over it had touched two unrelated
entries.

Part of RHIDP-16456.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e applied

The guides still said a runOnce key must be unique across "all spec files
and projects", and their two flagship examples wrapped configure() and
deploy() with no scope — which is exactly the shape that leaves a second
project with no deployment. Someone reading the guide rather than the
changelog would have written the bug.

Both examples now pass { scope: "project" }, the key section explains what
scope decides instead of asserting the old rule, and a new Scope section
gives the run-vs-project table plus the reason deploy() needs no scope of
its own: its internal key already carries the namespace.

Also close the one silent path the option still had. With scope "project"
and no resolvable project the key fell back to the bare form and behaved as
run-scoped again, without a word. It now warns, and the unit test for it
fails when the warning is removed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…project scope cannot be honoured

Review of the first two commits found the warning firing on the one case the
docs call correct, and the scope option still able to degrade quietly.

The warning fired on every cross-project skip of a run-scoped key, including
"install an operator into a fixed namespace every project then uses" — the
example the docs give for choosing "run". In an N-project run that printed N-1
warnings telling the author to switch to { scope: "project" }, which for that
setup would mean N operator installs, and there was no way to opt out because
the guard tested the resolved scope. It now tests `options.scope` as passed:
undefined means the author never thought about it, which is who the advice is
for; an explicit "run" is an answer and is left alone.

{ scope: "project" } with no resolvable project used to warn and then fall back
to the bare key — which is the shared-key bug it exists to prevent, reported by
one line easy to lose in CI output. It throws now. That only reaches callers
outside a Playwright context, since test.info() resolves inside beforeAll.

The flag file records which project satisfied a key, and the lock-free fast
path reads it from another process. writeFileSync truncates before it writes,
so a reader landing between the two steps saw an empty file and dropped the
warning — in the two-projects-one-spec case, the exact case it exists for.
Written through a temp file and renamed.

Docs: the earlier pass missed four files, including the whole overlay section,
which is the one rhdh-plugin-export-overlays follows and where the bug was
reported. Every documented runOnce block that wraps deploy() now passes the
scope — audited rather than spot-fixed — and the API reference documents the
third argument at all, which it did not. Also noted that nesting does not
rescue a missing scope: a run-scoped outer call skips before deploy() is
reached, so its internal protection never gets a say.

The changelog now leads with the fact that upgrading alone changes nothing:
the default is unchanged by design, so each affected call site has to opt in.

Both new behaviours were mutation-tested — reverting each turns exactly one
test red.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gustavolira

Copy link
Copy Markdown
Member Author

Two review rounds landed on this branch since it opened. Summary for whoever picks it up.

What changed in the behaviour

{ scope: "project" } with no resolvable project now throws. It used to warn and fall back to the bare key — which is the shared-key bug the option exists to prevent, reported by one line that is easy to lose in CI output. This only reaches callers outside a Playwright context; test.info() resolves inside beforeAll, verified with a two-project probe.

The warning no longer fires on a deliberate { scope: "run" }. It was gating on the resolved scope, so the case the docs call correct — installing an operator into a fixed namespace every project then uses — printed N-1 warnings in an N-project run, advising a switch to { scope: "project" } that would mean N operator installs. There was no opt-out. It now gates on options.scope as passed: undefined means the author never considered it, which is who the advice is for.

The flag file is written atomically. Its content is what names the project in the warning, and the lock-free fast path reads it from another process. writeFileSync truncates before it writes, so a reader landing between the two steps saw an empty file and dropped the warning — in the two-projects-one-spec case, the exact case it exists for. Temp file plus renameSync.

Documentation

The first pass missed four files, including the entire docs/overlay/ section — the one rhdh-plugin-export-overlays follows, and where the bug was reported. Audited rather than spot-fixed: every documented runOnce block that wraps deploy() now passes the scope (18 blocks across 7 files), and docs/api/playwright/test-fixtures.md documents the third argument at all, which it did not.

Also recorded that nesting does not rescue a missing scope — a run-scoped outer call skips before deploy() is reached, so its internal protection never gets a say.

Verification

yarn check    typecheck + eslint (0 errors) + prettier clean
yarn test     119 passed, 0 failed
docs build    clean (validates internal links)

Both new behaviours are mutation-tested — reverting each turns exactly one test red:

Mutation Test that goes red
gate the warning on the resolved scope again leaves an explicitly chosen run scope alone
fall back instead of throwing throws when scope is project but no project can be determined

And end to end, one spec under two projects:

[ws]           project-scope executed=true   default executed=true   explicit-run executed=true
[ws-app-next]  project-scope executed=true   default executed=false  explicit-run executed=false
[runOnce] "k-default" was already run by project "ws", so project "ws-app-next" is skipping it. …

Note the third column: an explicit run skips silently, a defaulted one warns.

One thing worth stating plainly

Upgrading to 2.1.10 does not fix an affected call site. The default is unchanged by design, so every existing runOnce in the overlays repo still skips the second project until it opts in. What this release adds is the fix and a way to find the call sites that need it. The changelog now leads with that.

Also: 2.1.10 is free on npm today and no other open PR claims it, but the Version Bump Check only compares against main — if another version-bumping PR merges first, this one publishes nothing.

@subhashkhileri

subhashkhileri commented Aug 24, 2026

Copy link
Copy Markdown
Member

Hey, I’m not sure we need this in the helper.

In 2.1, the default one is the new front-end system, then why we are still running the old test cases in that way. If we have migrated successfully, then we can just remove the older one and use the new one, right?

Overlay CI still runs both shells on the same spec (plugin and plugin-app-next), which is the collision we already hit (rhdh-plugin-export-overlays#3318). If we drop the legacy project once app-next is the default, that skip goes away. Until then two projects in one Playwright run still share one runOnce flag dir.

Could we just put namespace in the key like deploy() already does (deploy-${namespace})? Something like my-plugin-setup-${rhdh.deploymentConfig.namespace} as the unique key is expected to come from the caller as per the desgin. Bulk-import already fixed the spec that way

runOnce isn’t tied to Playwright today, which is why deploy() and the unit tests can use it. { scope: "project" } plus test.info() couples it to a test, and defaulting to "run" means existing overlay keys stay broken until every caller opts in (a warning doesn’t actually run setup).

What if we keep runOnce generic, update the examples, and let call sites do what deploy() already does? its easy and no worries of regression.

Review is right and this reverts the code change entirely.

The rule this guide already stated was correct — "the key must be globally
unique across all spec files and projects... use a prefix that includes the
workspace or project name". What was wrong is that every example underneath it
then used a literal key around configure() and deploy(), violating the rule the
paragraph had just given. Someone reading the examples rather than the sentence
wrote the bug; the earlier version of this branch responded by changing the
sentence, which was the wrong half to move.

So runOnce goes back to what it was. The { scope: "project" } option, the
import of @playwright/test into a module that had no test-runner dependency,
and the version bump are all gone. deploy() and the unit tests can keep using
runOnce outside a Playwright context, which was the point of it being generic.

What lands is the documentation:

- the key section now says why a project-shared key breaks, quoting the flag
  directory that is keyed on the runner PID alone, and shows the fix as
  `${key}-${rhdh.deploymentConfig.namespace}` — which is what deploy() has
  always done internally, and why deploy() was never affected;
- both intents are spelled out side by side, because both are real: a namespaced
  key for setup that belongs to one project, a literal one for an operator
  installed into a fixed namespace every project then uses;
- every example that wraps deploy() carries the namespace — twelve of them,
  across the guide, the API reference, the deployment guide and all three
  overlay pages;
- nesting is noted as no rescue: a project-shared outer key skips before
  deploy() is reached, so its internal protection never gets a say.

Not carried over: the warning when a shared key is skipped for another project.
It needed test.info(), which is the coupling being reverted. That gap is better
closed in rhdh-plugin-export-overlays, where a static check can find a literal
key in a workspace whose config declares two projects over one spec — before a
deployment rather than after one.

RHIDP-16456.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gustavolira added a commit that referenced this pull request Aug 24, 2026
#149 was reduced to a documentation change, so it no longer bumps the version and
2.1.10 is free. The note about the two PRs colliding goes with it — there is only
one version-bumping PR open now.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gustavolira gustavolira changed the title fix(run-once): let callers scope a key per project, and stop skipping silently docs(run-once): fix the examples that violate the key rule Aug 24, 2026
gustavolira added a commit that referenced this pull request Aug 24, 2026
#151 merged today and claimed 2.1.10, so the renumber in the previous commit
collided with main and the Version Bump Check would have refused it. Same number
as before, different reason: this is no longer about #149, which now bumps
nothing at all.

Worth noting separately: main says 2.1.10 but the publish workflow for it failed,
so npm latest is still 2.1.9. If that is not resolved before this merges, 2.1.11
publishes over a 2.1.10 that never existed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@gustavolira

Copy link
Copy Markdown
Member Author

You're right — reverted the whole code change. runOnce is identical to main again: no scope, no @playwright/test import, no version bump.

I had it backwards. The rule in the guide was already correct; what was broken is that every example under it used a literal key around configure() + deploy(). I changed the sentence when the examples were the wrong half. Fixed 12 of them, and the guide now says why it breaks and shows both cases side by side — a literal key is right for something like the orchestrator install into a fixed namespace.

Small thing, not to reopen it: "existing keys stay broken until callers opt in" is equally true of adding -${namespace} — that's also a per-call-site change. Just so it doesn't get filed under a reason that doesn't separate the two options.

What does get lost is the warning that found the bad call sites, since it needed test.info(). Moved that half to redhat-developer/rhdh-plugin-export-overlays#3375 — a static check that finds a literal key in a workspace where two projects run the same spec. Green on that repo today, red if you reconstruct the #3318 shape.

One back at you: dropping the legacy lanes once app-next is default is RHIDP-16460, still New and unassigned. Until someone owns it the two-project window stays open.

@subhashkhileri

Copy link
Copy Markdown
Member

/lgtm

@openshift-ci openshift-ci Bot added the lgtm label Aug 24, 2026
@subhashkhileri
subhashkhileri merged commit e5ac2b1 into main Aug 24, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants