[v3] Run ExternalMode (stand) suite in CI in addition to LocalMode - #63

Merged
Drownek merged 6 commits into
Drownek:v3-devfrom
monikon22:feat/61-stand-ci-testing
Aug 28, 2026
Merged

[v3] Run ExternalMode (stand) suite in CI in addition to LocalMode#63
Drownek merged 6 commits into
Drownek:v3-devfrom
monikon22:feat/61-stand-ci-testing

Conversation

@monikon22

@monikon22monikon22 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Closes#61.

test-example-plugin only ever ran LocalMode — stand (ExternalMode) is includeInMatrix: false because it needs an already-running server, so plugwrightTest skips it and the RCON console channel, account-pool leasing and justCreated registration flow had zero CI coverage.

What changed

  • example_plugin/src/test/e2e/stand-run/start.sh (new, tracked): the launcher example_plugin/README.md already told developers to hand-write under generated/local/run/. That directory is gitignored, so CI had nothing to copy in — this commits the same script README documents, outside generated/.

  • .gitattributes (new): pins *.sh to LF so a Windows checkout doesn't corrupt the shebang.

  • example_plugin/README.md: one-line pointer to the tracked copy.

  • .github/workflows/ci.yml: stand now runs as its own job, test-example-plugin-stand, in parallel with test-example-plugin (unchanged, keeps its name so an existing required-status-check on it still matches). Running on a separate runner means it can't reuse the local job's generated/local/run/, so it:

    1. Provisions its own Paper server via plugwright-action's gradle-args: plugwrightProvisionLocal plugwrightCompileTests (provisioning alone isn't enough — plugwrightPingStand/plugwrightTestStand don't depend on plugwrightCompileTests themselves, they just expect node_modules already has what console { rcon {} } and the npm(...) plugin refs need; the old sequential job got this for free as a side effect of plugwrightTest's own dependency chain).
    2. Installs a real JDK 21 (actions/setup-java@v4) before touching start.sh — Paper 1.21.11 needs it, but the Gradle daemon above runs on 17, and start.sh just execs whatever java is on PATH.
    3. Copies the tracked start.sh into the run dir, launches it backgrounded, saves its PID.
    4. Retry-loop ./gradlew plugwrightPingStand (up to 30×2s) until the server answers — the built-in readiness probe (connects, probes the RCON console channel, leases + authenticates an account), not a raw port check.
    5. ./gradlew plugwrightTestStand — blocking, same as LocalMode.
    6. if: always(): kill the backgrounded server by PID.
    7. if: failure(): upload the server log + build/reports/plugwright/stand.* as an artifact.

    PLUGWRIGHT_RCON_PASSWORD / PLUGWRIGHT_BOT_PASSWORD are set at job level as literal plugwright — test-only credentials for a server the job itself starts and tears down within the same run, matching local's existing default.

No change to plugwright-action itself — its existing gradle-args input covered everything needed.

Testing

Ran the sequence locally first: plugwrightProvisionLocal → copy tracked start.sh in → background start → ping retry-loop (ready on attempt 3) → plugwrightTestStand: 44/44 passed, 6 expected skips, 0 failed → kill by PID confirmed the process exits.

CI itself went through a few iterations once the two jobs actually ran on separate runners (each caught something the single-job version had been masking as a side effect of task ordering):

  • JDK 17-only PATH → UnsupportedClassVersionError starting Paper 1.21.11 → fixed by installing JDK 21 before start.sh.
  • plugwrightProvisionLocal alone doesn't install @plugwright/console-rcon → every ping failed with "no console channel could be reached" → fixed by also running plugwrightCompileTests.

Final green run: test-example-pluginpass in 4m58s, test-example-plugin-standpass in 4m52s, running concurrently — down from ~7m13s sequential.

…ted/
generated/ is gitignored, so CI has nothing to launch the stand Paper
server with. Commit the launcher example_plugin/README.md already
told developers to write by hand, under stand-run/, and pin *.sh to
LF via .gitattributes so a Windows checkout doesn't break the
shebang.
test-example-plugin only ever exercised LocalMode - the stand
(ExternalMode) env is excluded from the matrix since it needs an
already-running server, so the RCON console channel, account-pool
leasing and the justCreated registration flow had zero CI coverage.
The LocalMode run plugwright-action already does leaves Paper, cache
and libraries under generated/local/run/ - stand points at the same
localhost:25565. Copy in the tracked launcher, start it in the
background, retry plugwrightPingStand until it answers, then run
plugwrightTestStand. Kill the server and upload its log on failure
regardless of outcome.
ClosesDrownek#61.
@monikon22
monikon22 marked this pull request as draft August 28, 2026 12:47
start.sh just runs whatever "java" is on PATH, which the earlier
plugwright-action step pinned to 17 for the Gradle daemon. Paper
1.21.11 needs 21 (example_plugin/build.gradle.kts pins the toolchain
there) - LocalMode never hit this because Gradle resolves and
downloads that toolchain JDK itself for its own server launch, but a
plain shell script has no such resolution.
CI run 33172008840 confirmed the crash: UnsupportedClassVersionError,
class file version 65.0 vs runtime's 61.0.
test-example-plugin and test-example-plugin-stand now run on separate
runners concurrently instead of stand chaining off the end of a single
sequential job. Stand can no longer reuse the local job's
generated/local/run/ (different runner, different filesystem), so it
provisions its own Paper server via plugwright-action's gradle-args
input (plugwrightProvisionLocal only - not the full plugwrightTest,
which would also run and duplicate the local suite). Everything after
that (JDK 21 setup, start.sh, ping retry-loop, plugwrightTestStand,
teardown, failure log upload) is unchanged, just moved into the new
job. Trades a duplicate Paper/plugin download for roughly half the
wall-clock time versus running sequentially in one job.
job names: test-example-plugin keeps its existing name (a maintainer
required-status-check on it, if any, keeps matching); the new job is
test-example-plugin-stand.
plugwrightPingStand/plugwrightTestStand don't depend on
plugwrightCompileTests (ExternalMode registers no prepareTask -
registerTasks in ExternalMode.kt assumes the stand is already up and
node_modules already has what it needs). The old sequential job got
this for free as a side effect of plugwrightTest's dependsOn chain
running first; the new parallel stand job only ran
plugwrightProvisionLocal, so @plugwright/console-rcon was never
installed and every ping failed with 'no console channel could be
reached'.
Verified locally: a clean node_modules, then ./gradlew
plugwrightCompileTests, installs console-rcon/auth-authme/runner as
expected.
The console-rcon fix's replace missed the pre-existing gradle-args
line, leaving two under the same 'with:' block. Plain YAML parsers
silently keep the last one (which is why local yaml.safe_load passed),
but GitHub Actions' own parser rejects it outright - the run failed
in 0s with zero jobs registered, no logs at all.
@monikon22
monikon22 marked this pull request as ready for review August 28, 2026 13:22
@monikon22monikon22 changed the title [v3] Run ExternalMode (stand) suite in CI after LocalMode[v3] Run ExternalMode (stand) suite in CI in addition to LocalModeAug 28, 2026
@Drownek
Drownek merged commit 1e58e04 into Drownek:v3-devAug 28, 2026
2 checks passed
Sign up for freeto 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.

2 participants

@monikon22@Drownek
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

[v3] Run ExternalMode (stand) suite in CI in addition to LocalMode - #63

Merged
Drownek merged 6 commits into
Drownek:v3-devfrom
monikon22:feat/61-stand-ci-testing
Aug 28, 2026
Merged

[v3] Run ExternalMode (stand) suite in CI in addition to LocalMode#63
Drownek merged 6 commits into
Drownek:v3-devfrom
monikon22:feat/61-stand-ci-testing

Conversation

@monikon22

@monikon22monikon22 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Closes#61.

test-example-plugin only ever ran LocalMode — stand (ExternalMode) is includeInMatrix: false because it needs an already-running server, so plugwrightTest skips it and the RCON console channel, account-pool leasing and justCreated registration flow had zero CI coverage.

What changed

  • example_plugin/src/test/e2e/stand-run/start.sh (new, tracked): the launcher example_plugin/README.md already told developers to hand-write under generated/local/run/. That directory is gitignored, so CI had nothing to copy in — this commits the same script README documents, outside generated/.

  • .gitattributes (new): pins *.sh to LF so a Windows checkout doesn't corrupt the shebang.

  • example_plugin/README.md: one-line pointer to the tracked copy.

  • .github/workflows/ci.yml: stand now runs as its own job, test-example-plugin-stand, in parallel with test-example-plugin (unchanged, keeps its name so an existing required-status-check on it still matches). Running on a separate runner means it can't reuse the local job's generated/local/run/, so it:

    1. Provisions its own Paper server via plugwright-action's gradle-args: plugwrightProvisionLocal plugwrightCompileTests (provisioning alone isn't enough — plugwrightPingStand/plugwrightTestStand don't depend on plugwrightCompileTests themselves, they just expect node_modules already has what console { rcon {} } and the npm(...) plugin refs need; the old sequential job got this for free as a side effect of plugwrightTest's own dependency chain).
    2. Installs a real JDK 21 (actions/setup-java@v4) before touching start.sh — Paper 1.21.11 needs it, but the Gradle daemon above runs on 17, and start.sh just execs whatever java is on PATH.
    3. Copies the tracked start.sh into the run dir, launches it backgrounded, saves its PID.
    4. Retry-loop ./gradlew plugwrightPingStand (up to 30×2s) until the server answers — the built-in readiness probe (connects, probes the RCON console channel, leases + authenticates an account), not a raw port check.
    5. ./gradlew plugwrightTestStand — blocking, same as LocalMode.
    6. if: always(): kill the backgrounded server by PID.
    7. if: failure(): upload the server log + build/reports/plugwright/stand.* as an artifact.

    PLUGWRIGHT_RCON_PASSWORD / PLUGWRIGHT_BOT_PASSWORD are set at job level as literal plugwright — test-only credentials for a server the job itself starts and tears down within the same run, matching local's existing default.

No change to plugwright-action itself — its existing gradle-args input covered everything needed.

Testing

Ran the sequence locally first: plugwrightProvisionLocal → copy tracked start.sh in → background start → ping retry-loop (ready on attempt 3) → plugwrightTestStand: 44/44 passed, 6 expected skips, 0 failed → kill by PID confirmed the process exits.

CI itself went through a few iterations once the two jobs actually ran on separate runners (each caught something the single-job version had been masking as a side effect of task ordering):

  • JDK 17-only PATH → UnsupportedClassVersionError starting Paper 1.21.11 → fixed by installing JDK 21 before start.sh.
  • plugwrightProvisionLocal alone doesn't install @plugwright/console-rcon → every ping failed with "no console channel could be reached" → fixed by also running plugwrightCompileTests.

Final green run: test-example-pluginpass in 4m58s, test-example-plugin-standpass in 4m52s, running concurrently — down from ~7m13s sequential.

…ted/
generated/ is gitignored, so CI has nothing to launch the stand Paper
server with. Commit the launcher example_plugin/README.md already
told developers to write by hand, under stand-run/, and pin *.sh to
LF via .gitattributes so a Windows checkout doesn't break the
shebang.
test-example-plugin only ever exercised LocalMode - the stand
(ExternalMode) env is excluded from the matrix since it needs an
already-running server, so the RCON console channel, account-pool
leasing and the justCreated registration flow had zero CI coverage.
The LocalMode run plugwright-action already does leaves Paper, cache
and libraries under generated/local/run/ - stand points at the same
localhost:25565. Copy in the tracked launcher, start it in the
background, retry plugwrightPingStand until it answers, then run
plugwrightTestStand. Kill the server and upload its log on failure
regardless of outcome.
ClosesDrownek#61.
@monikon22
monikon22 marked this pull request as draft August 28, 2026 12:47
start.sh just runs whatever "java" is on PATH, which the earlier
plugwright-action step pinned to 17 for the Gradle daemon. Paper
1.21.11 needs 21 (example_plugin/build.gradle.kts pins the toolchain
there) - LocalMode never hit this because Gradle resolves and
downloads that toolchain JDK itself for its own server launch, but a
plain shell script has no such resolution.
CI run 33172008840 confirmed the crash: UnsupportedClassVersionError,
class file version 65.0 vs runtime's 61.0.
test-example-plugin and test-example-plugin-stand now run on separate
runners concurrently instead of stand chaining off the end of a single
sequential job. Stand can no longer reuse the local job's
generated/local/run/ (different runner, different filesystem), so it
provisions its own Paper server via plugwright-action's gradle-args
input (plugwrightProvisionLocal only - not the full plugwrightTest,
which would also run and duplicate the local suite). Everything after
that (JDK 21 setup, start.sh, ping retry-loop, plugwrightTestStand,
teardown, failure log upload) is unchanged, just moved into the new
job. Trades a duplicate Paper/plugin download for roughly half the
wall-clock time versus running sequentially in one job.
job names: test-example-plugin keeps its existing name (a maintainer
required-status-check on it, if any, keeps matching); the new job is
test-example-plugin-stand.
plugwrightPingStand/plugwrightTestStand don't depend on
plugwrightCompileTests (ExternalMode registers no prepareTask -
registerTasks in ExternalMode.kt assumes the stand is already up and
node_modules already has what it needs). The old sequential job got
this for free as a side effect of plugwrightTest's dependsOn chain
running first; the new parallel stand job only ran
plugwrightProvisionLocal, so @plugwright/console-rcon was never
installed and every ping failed with 'no console channel could be
reached'.
Verified locally: a clean node_modules, then ./gradlew
plugwrightCompileTests, installs console-rcon/auth-authme/runner as
expected.
The console-rcon fix's replace missed the pre-existing gradle-args
line, leaving two under the same 'with:' block. Plain YAML parsers
silently keep the last one (which is why local yaml.safe_load passed),
but GitHub Actions' own parser rejects it outright - the run failed
in 0s with zero jobs registered, no logs at all.
@monikon22
monikon22 marked this pull request as ready for review August 28, 2026 13:22
@monikon22monikon22 changed the title [v3] Run ExternalMode (stand) suite in CI after LocalMode[v3] Run ExternalMode (stand) suite in CI in addition to LocalModeAug 28, 2026
@Drownek
Drownek merged commit 1e58e04 into Drownek:v3-devAug 28, 2026
2 checks passed
Sign up for freeto 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.

2 participants

@monikon22@Drownek
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[v3] Run ExternalMode (stand) suite in CI in addition to LocalMode - #63

Merged
Drownek merged 6 commits into
Drownek:v3-devfrom
monikon22:feat/61-stand-ci-testing
Aug 28, 2026
Merged

[v3] Run ExternalMode (stand) suite in CI in addition to LocalMode#63
Drownek merged 6 commits into
Drownek:v3-devfrom
monikon22:feat/61-stand-ci-testing

Conversation

@monikon22

@monikon22monikon22 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Closes#61.

test-example-plugin only ever ran LocalMode — stand (ExternalMode) is includeInMatrix: false because it needs an already-running server, so plugwrightTest skips it and the RCON console channel, account-pool leasing and justCreated registration flow had zero CI coverage.

What changed

  • example_plugin/src/test/e2e/stand-run/start.sh (new, tracked): the launcher example_plugin/README.md already told developers to hand-write under generated/local/run/. That directory is gitignored, so CI had nothing to copy in — this commits the same script README documents, outside generated/.

  • .gitattributes (new): pins *.sh to LF so a Windows checkout doesn't corrupt the shebang.

  • example_plugin/README.md: one-line pointer to the tracked copy.

  • .github/workflows/ci.yml: stand now runs as its own job, test-example-plugin-stand, in parallel with test-example-plugin (unchanged, keeps its name so an existing required-status-check on it still matches). Running on a separate runner means it can't reuse the local job's generated/local/run/, so it:

    1. Provisions its own Paper server via plugwright-action's gradle-args: plugwrightProvisionLocal plugwrightCompileTests (provisioning alone isn't enough — plugwrightPingStand/plugwrightTestStand don't depend on plugwrightCompileTests themselves, they just expect node_modules already has what console { rcon {} } and the npm(...) plugin refs need; the old sequential job got this for free as a side effect of plugwrightTest's own dependency chain).
    2. Installs a real JDK 21 (actions/setup-java@v4) before touching start.sh — Paper 1.21.11 needs it, but the Gradle daemon above runs on 17, and start.sh just execs whatever java is on PATH.
    3. Copies the tracked start.sh into the run dir, launches it backgrounded, saves its PID.
    4. Retry-loop ./gradlew plugwrightPingStand (up to 30×2s) until the server answers — the built-in readiness probe (connects, probes the RCON console channel, leases + authenticates an account), not a raw port check.
    5. ./gradlew plugwrightTestStand — blocking, same as LocalMode.
    6. if: always(): kill the backgrounded server by PID.
    7. if: failure(): upload the server log + build/reports/plugwright/stand.* as an artifact.

    PLUGWRIGHT_RCON_PASSWORD / PLUGWRIGHT_BOT_PASSWORD are set at job level as literal plugwright — test-only credentials for a server the job itself starts and tears down within the same run, matching local's existing default.

No change to plugwright-action itself — its existing gradle-args input covered everything needed.

Testing

Ran the sequence locally first: plugwrightProvisionLocal → copy tracked start.sh in → background start → ping retry-loop (ready on attempt 3) → plugwrightTestStand: 44/44 passed, 6 expected skips, 0 failed → kill by PID confirmed the process exits.

CI itself went through a few iterations once the two jobs actually ran on separate runners (each caught something the single-job version had been masking as a side effect of task ordering):

  • JDK 17-only PATH → UnsupportedClassVersionError starting Paper 1.21.11 → fixed by installing JDK 21 before start.sh.
  • plugwrightProvisionLocal alone doesn't install @plugwright/console-rcon → every ping failed with "no console channel could be reached" → fixed by also running plugwrightCompileTests.

Final green run: test-example-pluginpass in 4m58s, test-example-plugin-standpass in 4m52s, running concurrently — down from ~7m13s sequential.

…ted/
generated/ is gitignored, so CI has nothing to launch the stand Paper
server with. Commit the launcher example_plugin/README.md already
told developers to write by hand, under stand-run/, and pin *.sh to
LF via .gitattributes so a Windows checkout doesn't break the
shebang.
test-example-plugin only ever exercised LocalMode - the stand
(ExternalMode) env is excluded from the matrix since it needs an
already-running server, so the RCON console channel, account-pool
leasing and the justCreated registration flow had zero CI coverage.
The LocalMode run plugwright-action already does leaves Paper, cache
and libraries under generated/local/run/ - stand points at the same
localhost:25565. Copy in the tracked launcher, start it in the
background, retry plugwrightPingStand until it answers, then run
plugwrightTestStand. Kill the server and upload its log on failure
regardless of outcome.
ClosesDrownek#61.
@monikon22
monikon22 marked this pull request as draft August 28, 2026 12:47
start.sh just runs whatever "java" is on PATH, which the earlier
plugwright-action step pinned to 17 for the Gradle daemon. Paper
1.21.11 needs 21 (example_plugin/build.gradle.kts pins the toolchain
there) - LocalMode never hit this because Gradle resolves and
downloads that toolchain JDK itself for its own server launch, but a
plain shell script has no such resolution.
CI run 33172008840 confirmed the crash: UnsupportedClassVersionError,
class file version 65.0 vs runtime's 61.0.
test-example-plugin and test-example-plugin-stand now run on separate
runners concurrently instead of stand chaining off the end of a single
sequential job. Stand can no longer reuse the local job's
generated/local/run/ (different runner, different filesystem), so it
provisions its own Paper server via plugwright-action's gradle-args
input (plugwrightProvisionLocal only - not the full plugwrightTest,
which would also run and duplicate the local suite). Everything after
that (JDK 21 setup, start.sh, ping retry-loop, plugwrightTestStand,
teardown, failure log upload) is unchanged, just moved into the new
job. Trades a duplicate Paper/plugin download for roughly half the
wall-clock time versus running sequentially in one job.
job names: test-example-plugin keeps its existing name (a maintainer
required-status-check on it, if any, keeps matching); the new job is
test-example-plugin-stand.
plugwrightPingStand/plugwrightTestStand don't depend on
plugwrightCompileTests (ExternalMode registers no prepareTask -
registerTasks in ExternalMode.kt assumes the stand is already up and
node_modules already has what it needs). The old sequential job got
this for free as a side effect of plugwrightTest's dependsOn chain
running first; the new parallel stand job only ran
plugwrightProvisionLocal, so @plugwright/console-rcon was never
installed and every ping failed with 'no console channel could be
reached'.
Verified locally: a clean node_modules, then ./gradlew
plugwrightCompileTests, installs console-rcon/auth-authme/runner as
expected.
The console-rcon fix's replace missed the pre-existing gradle-args
line, leaving two under the same 'with:' block. Plain YAML parsers
silently keep the last one (which is why local yaml.safe_load passed),
but GitHub Actions' own parser rejects it outright - the run failed
in 0s with zero jobs registered, no logs at all.
@monikon22
monikon22 marked this pull request as ready for review August 28, 2026 13:22
@monikon22monikon22 changed the title [v3] Run ExternalMode (stand) suite in CI after LocalMode[v3] Run ExternalMode (stand) suite in CI in addition to LocalModeAug 28, 2026
@Drownek
Drownek merged commit 1e58e04 into Drownek:v3-devAug 28, 2026
2 checks passed
Sign up for freeto 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.

2 participants

@monikon22@Drownek
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[v3] Run ExternalMode (stand) suite in CI in addition to LocalMode - #63

Merged
Drownek merged 6 commits into
Drownek:v3-devfrom
monikon22:feat/61-stand-ci-testing
Aug 28, 2026
Merged

[v3] Run ExternalMode (stand) suite in CI in addition to LocalMode#63
Drownek merged 6 commits into
Drownek:v3-devfrom
monikon22:feat/61-stand-ci-testing

Conversation

@monikon22

@monikon22monikon22 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Closes#61.

test-example-plugin only ever ran LocalMode — stand (ExternalMode) is includeInMatrix: false because it needs an already-running server, so plugwrightTest skips it and the RCON console channel, account-pool leasing and justCreated registration flow had zero CI coverage.

What changed

  • example_plugin/src/test/e2e/stand-run/start.sh (new, tracked): the launcher example_plugin/README.md already told developers to hand-write under generated/local/run/. That directory is gitignored, so CI had nothing to copy in — this commits the same script README documents, outside generated/.

  • .gitattributes (new): pins *.sh to LF so a Windows checkout doesn't corrupt the shebang.

  • example_plugin/README.md: one-line pointer to the tracked copy.

  • .github/workflows/ci.yml: stand now runs as its own job, test-example-plugin-stand, in parallel with test-example-plugin (unchanged, keeps its name so an existing required-status-check on it still matches). Running on a separate runner means it can't reuse the local job's generated/local/run/, so it:

    1. Provisions its own Paper server via plugwright-action's gradle-args: plugwrightProvisionLocal plugwrightCompileTests (provisioning alone isn't enough — plugwrightPingStand/plugwrightTestStand don't depend on plugwrightCompileTests themselves, they just expect node_modules already has what console { rcon {} } and the npm(...) plugin refs need; the old sequential job got this for free as a side effect of plugwrightTest's own dependency chain).
    2. Installs a real JDK 21 (actions/setup-java@v4) before touching start.sh — Paper 1.21.11 needs it, but the Gradle daemon above runs on 17, and start.sh just execs whatever java is on PATH.
    3. Copies the tracked start.sh into the run dir, launches it backgrounded, saves its PID.
    4. Retry-loop ./gradlew plugwrightPingStand (up to 30×2s) until the server answers — the built-in readiness probe (connects, probes the RCON console channel, leases + authenticates an account), not a raw port check.
    5. ./gradlew plugwrightTestStand — blocking, same as LocalMode.
    6. if: always(): kill the backgrounded server by PID.
    7. if: failure(): upload the server log + build/reports/plugwright/stand.* as an artifact.

    PLUGWRIGHT_RCON_PASSWORD / PLUGWRIGHT_BOT_PASSWORD are set at job level as literal plugwright — test-only credentials for a server the job itself starts and tears down within the same run, matching local's existing default.

No change to plugwright-action itself — its existing gradle-args input covered everything needed.

Testing

Ran the sequence locally first: plugwrightProvisionLocal → copy tracked start.sh in → background start → ping retry-loop (ready on attempt 3) → plugwrightTestStand: 44/44 passed, 6 expected skips, 0 failed → kill by PID confirmed the process exits.

CI itself went through a few iterations once the two jobs actually ran on separate runners (each caught something the single-job version had been masking as a side effect of task ordering):

  • JDK 17-only PATH → UnsupportedClassVersionError starting Paper 1.21.11 → fixed by installing JDK 21 before start.sh.
  • plugwrightProvisionLocal alone doesn't install @plugwright/console-rcon → every ping failed with "no console channel could be reached" → fixed by also running plugwrightCompileTests.

Final green run: test-example-pluginpass in 4m58s, test-example-plugin-standpass in 4m52s, running concurrently — down from ~7m13s sequential.

…ted/
generated/ is gitignored, so CI has nothing to launch the stand Paper
server with. Commit the launcher example_plugin/README.md already
told developers to write by hand, under stand-run/, and pin *.sh to
LF via .gitattributes so a Windows checkout doesn't break the
shebang.
test-example-plugin only ever exercised LocalMode - the stand
(ExternalMode) env is excluded from the matrix since it needs an
already-running server, so the RCON console channel, account-pool
leasing and the justCreated registration flow had zero CI coverage.
The LocalMode run plugwright-action already does leaves Paper, cache
and libraries under generated/local/run/ - stand points at the same
localhost:25565. Copy in the tracked launcher, start it in the
background, retry plugwrightPingStand until it answers, then run
plugwrightTestStand. Kill the server and upload its log on failure
regardless of outcome.
ClosesDrownek#61.
@monikon22
monikon22 marked this pull request as draft August 28, 2026 12:47
start.sh just runs whatever "java" is on PATH, which the earlier
plugwright-action step pinned to 17 for the Gradle daemon. Paper
1.21.11 needs 21 (example_plugin/build.gradle.kts pins the toolchain
there) - LocalMode never hit this because Gradle resolves and
downloads that toolchain JDK itself for its own server launch, but a
plain shell script has no such resolution.
CI run 33172008840 confirmed the crash: UnsupportedClassVersionError,
class file version 65.0 vs runtime's 61.0.
test-example-plugin and test-example-plugin-stand now run on separate
runners concurrently instead of stand chaining off the end of a single
sequential job. Stand can no longer reuse the local job's
generated/local/run/ (different runner, different filesystem), so it
provisions its own Paper server via plugwright-action's gradle-args
input (plugwrightProvisionLocal only - not the full plugwrightTest,
which would also run and duplicate the local suite). Everything after
that (JDK 21 setup, start.sh, ping retry-loop, plugwrightTestStand,
teardown, failure log upload) is unchanged, just moved into the new
job. Trades a duplicate Paper/plugin download for roughly half the
wall-clock time versus running sequentially in one job.
job names: test-example-plugin keeps its existing name (a maintainer
required-status-check on it, if any, keeps matching); the new job is
test-example-plugin-stand.
plugwrightPingStand/plugwrightTestStand don't depend on
plugwrightCompileTests (ExternalMode registers no prepareTask -
registerTasks in ExternalMode.kt assumes the stand is already up and
node_modules already has what it needs). The old sequential job got
this for free as a side effect of plugwrightTest's dependsOn chain
running first; the new parallel stand job only ran
plugwrightProvisionLocal, so @plugwright/console-rcon was never
installed and every ping failed with 'no console channel could be
reached'.
Verified locally: a clean node_modules, then ./gradlew
plugwrightCompileTests, installs console-rcon/auth-authme/runner as
expected.
The console-rcon fix's replace missed the pre-existing gradle-args
line, leaving two under the same 'with:' block. Plain YAML parsers
silently keep the last one (which is why local yaml.safe_load passed),
but GitHub Actions' own parser rejects it outright - the run failed
in 0s with zero jobs registered, no logs at all.
@monikon22
monikon22 marked this pull request as ready for review August 28, 2026 13:22
@monikon22monikon22 changed the title [v3] Run ExternalMode (stand) suite in CI after LocalMode[v3] Run ExternalMode (stand) suite in CI in addition to LocalModeAug 28, 2026
@Drownek
Drownek merged commit 1e58e04 into Drownek:v3-devAug 28, 2026
2 checks passed
Sign up for freeto 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.

2 participants

@monikon22@Drownek
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

[v3] Run ExternalMode (stand) suite in CI in addition to LocalMode - #63

Merged
Drownek merged 6 commits into
Drownek:v3-devfrom
monikon22:feat/61-stand-ci-testing
Aug 28, 2026
Merged

[v3] Run ExternalMode (stand) suite in CI in addition to LocalMode#63
Drownek merged 6 commits into
Drownek:v3-devfrom
monikon22:feat/61-stand-ci-testing

Conversation

@monikon22

@monikon22monikon22 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Closes#61.

test-example-plugin only ever ran LocalMode — stand (ExternalMode) is includeInMatrix: false because it needs an already-running server, so plugwrightTest skips it and the RCON console channel, account-pool leasing and justCreated registration flow had zero CI coverage.

What changed

  • example_plugin/src/test/e2e/stand-run/start.sh (new, tracked): the launcher example_plugin/README.md already told developers to hand-write under generated/local/run/. That directory is gitignored, so CI had nothing to copy in — this commits the same script README documents, outside generated/.

  • .gitattributes (new): pins *.sh to LF so a Windows checkout doesn't corrupt the shebang.

  • example_plugin/README.md: one-line pointer to the tracked copy.

  • .github/workflows/ci.yml: stand now runs as its own job, test-example-plugin-stand, in parallel with test-example-plugin (unchanged, keeps its name so an existing required-status-check on it still matches). Running on a separate runner means it can't reuse the local job's generated/local/run/, so it:

    1. Provisions its own Paper server via plugwright-action's gradle-args: plugwrightProvisionLocal plugwrightCompileTests (provisioning alone isn't enough — plugwrightPingStand/plugwrightTestStand don't depend on plugwrightCompileTests themselves, they just expect node_modules already has what console { rcon {} } and the npm(...) plugin refs need; the old sequential job got this for free as a side effect of plugwrightTest's own dependency chain).
    2. Installs a real JDK 21 (actions/setup-java@v4) before touching start.sh — Paper 1.21.11 needs it, but the Gradle daemon above runs on 17, and start.sh just execs whatever java is on PATH.
    3. Copies the tracked start.sh into the run dir, launches it backgrounded, saves its PID.
    4. Retry-loop ./gradlew plugwrightPingStand (up to 30×2s) until the server answers — the built-in readiness probe (connects, probes the RCON console channel, leases + authenticates an account), not a raw port check.
    5. ./gradlew plugwrightTestStand — blocking, same as LocalMode.
    6. if: always(): kill the backgrounded server by PID.
    7. if: failure(): upload the server log + build/reports/plugwright/stand.* as an artifact.

    PLUGWRIGHT_RCON_PASSWORD / PLUGWRIGHT_BOT_PASSWORD are set at job level as literal plugwright — test-only credentials for a server the job itself starts and tears down within the same run, matching local's existing default.

No change to plugwright-action itself — its existing gradle-args input covered everything needed.

Testing

Ran the sequence locally first: plugwrightProvisionLocal → copy tracked start.sh in → background start → ping retry-loop (ready on attempt 3) → plugwrightTestStand: 44/44 passed, 6 expected skips, 0 failed → kill by PID confirmed the process exits.

CI itself went through a few iterations once the two jobs actually ran on separate runners (each caught something the single-job version had been masking as a side effect of task ordering):

  • JDK 17-only PATH → UnsupportedClassVersionError starting Paper 1.21.11 → fixed by installing JDK 21 before start.sh.
  • plugwrightProvisionLocal alone doesn't install @plugwright/console-rcon → every ping failed with "no console channel could be reached" → fixed by also running plugwrightCompileTests.

Final green run: test-example-pluginpass in 4m58s, test-example-plugin-standpass in 4m52s, running concurrently — down from ~7m13s sequential.

…ted/
generated/ is gitignored, so CI has nothing to launch the stand Paper
server with. Commit the launcher example_plugin/README.md already
told developers to write by hand, under stand-run/, and pin *.sh to
LF via .gitattributes so a Windows checkout doesn't break the
shebang.
test-example-plugin only ever exercised LocalMode - the stand
(ExternalMode) env is excluded from the matrix since it needs an
already-running server, so the RCON console channel, account-pool
leasing and the justCreated registration flow had zero CI coverage.
The LocalMode run plugwright-action already does leaves Paper, cache
and libraries under generated/local/run/ - stand points at the same
localhost:25565. Copy in the tracked launcher, start it in the
background, retry plugwrightPingStand until it answers, then run
plugwrightTestStand. Kill the server and upload its log on failure
regardless of outcome.
ClosesDrownek#61.
@monikon22
monikon22 marked this pull request as draft August 28, 2026 12:47
start.sh just runs whatever "java" is on PATH, which the earlier
plugwright-action step pinned to 17 for the Gradle daemon. Paper
1.21.11 needs 21 (example_plugin/build.gradle.kts pins the toolchain
there) - LocalMode never hit this because Gradle resolves and
downloads that toolchain JDK itself for its own server launch, but a
plain shell script has no such resolution.
CI run 33172008840 confirmed the crash: UnsupportedClassVersionError,
class file version 65.0 vs runtime's 61.0.
test-example-plugin and test-example-plugin-stand now run on separate
runners concurrently instead of stand chaining off the end of a single
sequential job. Stand can no longer reuse the local job's
generated/local/run/ (different runner, different filesystem), so it
provisions its own Paper server via plugwright-action's gradle-args
input (plugwrightProvisionLocal only - not the full plugwrightTest,
which would also run and duplicate the local suite). Everything after
that (JDK 21 setup, start.sh, ping retry-loop, plugwrightTestStand,
teardown, failure log upload) is unchanged, just moved into the new
job. Trades a duplicate Paper/plugin download for roughly half the
wall-clock time versus running sequentially in one job.
job names: test-example-plugin keeps its existing name (a maintainer
required-status-check on it, if any, keeps matching); the new job is
test-example-plugin-stand.
plugwrightPingStand/plugwrightTestStand don't depend on
plugwrightCompileTests (ExternalMode registers no prepareTask -
registerTasks in ExternalMode.kt assumes the stand is already up and
node_modules already has what it needs). The old sequential job got
this for free as a side effect of plugwrightTest's dependsOn chain
running first; the new parallel stand job only ran
plugwrightProvisionLocal, so @plugwright/console-rcon was never
installed and every ping failed with 'no console channel could be
reached'.
Verified locally: a clean node_modules, then ./gradlew
plugwrightCompileTests, installs console-rcon/auth-authme/runner as
expected.
The console-rcon fix's replace missed the pre-existing gradle-args
line, leaving two under the same 'with:' block. Plain YAML parsers
silently keep the last one (which is why local yaml.safe_load passed),
but GitHub Actions' own parser rejects it outright - the run failed
in 0s with zero jobs registered, no logs at all.
@monikon22
monikon22 marked this pull request as ready for review August 28, 2026 13:22
@monikon22monikon22 changed the title [v3] Run ExternalMode (stand) suite in CI after LocalMode[v3] Run ExternalMode (stand) suite in CI in addition to LocalModeAug 28, 2026
@Drownek
Drownek merged commit 1e58e04 into Drownek:v3-devAug 28, 2026
2 checks passed
Sign up for freeto 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.

2 participants

@monikon22@Drownek
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[v3] Run ExternalMode (stand) suite in CI in addition to LocalMode - #63

Merged
Drownek merged 6 commits into
Drownek:v3-devfrom
monikon22:feat/61-stand-ci-testing
Aug 28, 2026
Merged

[v3] Run ExternalMode (stand) suite in CI in addition to LocalMode#63
Drownek merged 6 commits into
Drownek:v3-devfrom
monikon22:feat/61-stand-ci-testing

Conversation

@monikon22

@monikon22monikon22 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Closes#61.

test-example-plugin only ever ran LocalMode — stand (ExternalMode) is includeInMatrix: false because it needs an already-running server, so plugwrightTest skips it and the RCON console channel, account-pool leasing and justCreated registration flow had zero CI coverage.

What changed

  • example_plugin/src/test/e2e/stand-run/start.sh (new, tracked): the launcher example_plugin/README.md already told developers to hand-write under generated/local/run/. That directory is gitignored, so CI had nothing to copy in — this commits the same script README documents, outside generated/.

  • .gitattributes (new): pins *.sh to LF so a Windows checkout doesn't corrupt the shebang.

  • example_plugin/README.md: one-line pointer to the tracked copy.

  • .github/workflows/ci.yml: stand now runs as its own job, test-example-plugin-stand, in parallel with test-example-plugin (unchanged, keeps its name so an existing required-status-check on it still matches). Running on a separate runner means it can't reuse the local job's generated/local/run/, so it:

    1. Provisions its own Paper server via plugwright-action's gradle-args: plugwrightProvisionLocal plugwrightCompileTests (provisioning alone isn't enough — plugwrightPingStand/plugwrightTestStand don't depend on plugwrightCompileTests themselves, they just expect node_modules already has what console { rcon {} } and the npm(...) plugin refs need; the old sequential job got this for free as a side effect of plugwrightTest's own dependency chain).
    2. Installs a real JDK 21 (actions/setup-java@v4) before touching start.sh — Paper 1.21.11 needs it, but the Gradle daemon above runs on 17, and start.sh just execs whatever java is on PATH.
    3. Copies the tracked start.sh into the run dir, launches it backgrounded, saves its PID.
    4. Retry-loop ./gradlew plugwrightPingStand (up to 30×2s) until the server answers — the built-in readiness probe (connects, probes the RCON console channel, leases + authenticates an account), not a raw port check.
    5. ./gradlew plugwrightTestStand — blocking, same as LocalMode.
    6. if: always(): kill the backgrounded server by PID.
    7. if: failure(): upload the server log + build/reports/plugwright/stand.* as an artifact.

    PLUGWRIGHT_RCON_PASSWORD / PLUGWRIGHT_BOT_PASSWORD are set at job level as literal plugwright — test-only credentials for a server the job itself starts and tears down within the same run, matching local's existing default.

No change to plugwright-action itself — its existing gradle-args input covered everything needed.

Testing

Ran the sequence locally first: plugwrightProvisionLocal → copy tracked start.sh in → background start → ping retry-loop (ready on attempt 3) → plugwrightTestStand: 44/44 passed, 6 expected skips, 0 failed → kill by PID confirmed the process exits.

CI itself went through a few iterations once the two jobs actually ran on separate runners (each caught something the single-job version had been masking as a side effect of task ordering):

  • JDK 17-only PATH → UnsupportedClassVersionError starting Paper 1.21.11 → fixed by installing JDK 21 before start.sh.
  • plugwrightProvisionLocal alone doesn't install @plugwright/console-rcon → every ping failed with "no console channel could be reached" → fixed by also running plugwrightCompileTests.

Final green run: test-example-pluginpass in 4m58s, test-example-plugin-standpass in 4m52s, running concurrently — down from ~7m13s sequential.

…ted/
generated/ is gitignored, so CI has nothing to launch the stand Paper
server with. Commit the launcher example_plugin/README.md already
told developers to write by hand, under stand-run/, and pin *.sh to
LF via .gitattributes so a Windows checkout doesn't break the
shebang.
test-example-plugin only ever exercised LocalMode - the stand
(ExternalMode) env is excluded from the matrix since it needs an
already-running server, so the RCON console channel, account-pool
leasing and the justCreated registration flow had zero CI coverage.
The LocalMode run plugwright-action already does leaves Paper, cache
and libraries under generated/local/run/ - stand points at the same
localhost:25565. Copy in the tracked launcher, start it in the
background, retry plugwrightPingStand until it answers, then run
plugwrightTestStand. Kill the server and upload its log on failure
regardless of outcome.
ClosesDrownek#61.
@monikon22
monikon22 marked this pull request as draft August 28, 2026 12:47
start.sh just runs whatever "java" is on PATH, which the earlier
plugwright-action step pinned to 17 for the Gradle daemon. Paper
1.21.11 needs 21 (example_plugin/build.gradle.kts pins the toolchain
there) - LocalMode never hit this because Gradle resolves and
downloads that toolchain JDK itself for its own server launch, but a
plain shell script has no such resolution.
CI run 33172008840 confirmed the crash: UnsupportedClassVersionError,
class file version 65.0 vs runtime's 61.0.
test-example-plugin and test-example-plugin-stand now run on separate
runners concurrently instead of stand chaining off the end of a single
sequential job. Stand can no longer reuse the local job's
generated/local/run/ (different runner, different filesystem), so it
provisions its own Paper server via plugwright-action's gradle-args
input (plugwrightProvisionLocal only - not the full plugwrightTest,
which would also run and duplicate the local suite). Everything after
that (JDK 21 setup, start.sh, ping retry-loop, plugwrightTestStand,
teardown, failure log upload) is unchanged, just moved into the new
job. Trades a duplicate Paper/plugin download for roughly half the
wall-clock time versus running sequentially in one job.
job names: test-example-plugin keeps its existing name (a maintainer
required-status-check on it, if any, keeps matching); the new job is
test-example-plugin-stand.
plugwrightPingStand/plugwrightTestStand don't depend on
plugwrightCompileTests (ExternalMode registers no prepareTask -
registerTasks in ExternalMode.kt assumes the stand is already up and
node_modules already has what it needs). The old sequential job got
this for free as a side effect of plugwrightTest's dependsOn chain
running first; the new parallel stand job only ran
plugwrightProvisionLocal, so @plugwright/console-rcon was never
installed and every ping failed with 'no console channel could be
reached'.
Verified locally: a clean node_modules, then ./gradlew
plugwrightCompileTests, installs console-rcon/auth-authme/runner as
expected.
The console-rcon fix's replace missed the pre-existing gradle-args
line, leaving two under the same 'with:' block. Plain YAML parsers
silently keep the last one (which is why local yaml.safe_load passed),
but GitHub Actions' own parser rejects it outright - the run failed
in 0s with zero jobs registered, no logs at all.
@monikon22
monikon22 marked this pull request as ready for review August 28, 2026 13:22
@monikon22monikon22 changed the title [v3] Run ExternalMode (stand) suite in CI after LocalMode[v3] Run ExternalMode (stand) suite in CI in addition to LocalModeAug 28, 2026
@Drownek
Drownek merged commit 1e58e04 into Drownek:v3-devAug 28, 2026
2 checks passed
Sign up for freeto 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.

2 participants

@monikon22@Drownek
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

[v3] Run ExternalMode (stand) suite in CI in addition to LocalMode - #63

Merged
Drownek merged 6 commits into
Drownek:v3-devfrom
monikon22:feat/61-stand-ci-testing
Aug 28, 2026
Merged

[v3] Run ExternalMode (stand) suite in CI in addition to LocalMode#63
Drownek merged 6 commits into
Drownek:v3-devfrom
monikon22:feat/61-stand-ci-testing

Conversation

@monikon22

@monikon22monikon22 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Closes#61.

test-example-plugin only ever ran LocalMode — stand (ExternalMode) is includeInMatrix: false because it needs an already-running server, so plugwrightTest skips it and the RCON console channel, account-pool leasing and justCreated registration flow had zero CI coverage.

What changed

  • example_plugin/src/test/e2e/stand-run/start.sh (new, tracked): the launcher example_plugin/README.md already told developers to hand-write under generated/local/run/. That directory is gitignored, so CI had nothing to copy in — this commits the same script README documents, outside generated/.

  • .gitattributes (new): pins *.sh to LF so a Windows checkout doesn't corrupt the shebang.

  • example_plugin/README.md: one-line pointer to the tracked copy.

  • .github/workflows/ci.yml: stand now runs as its own job, test-example-plugin-stand, in parallel with test-example-plugin (unchanged, keeps its name so an existing required-status-check on it still matches). Running on a separate runner means it can't reuse the local job's generated/local/run/, so it:

    1. Provisions its own Paper server via plugwright-action's gradle-args: plugwrightProvisionLocal plugwrightCompileTests (provisioning alone isn't enough — plugwrightPingStand/plugwrightTestStand don't depend on plugwrightCompileTests themselves, they just expect node_modules already has what console { rcon {} } and the npm(...) plugin refs need; the old sequential job got this for free as a side effect of plugwrightTest's own dependency chain).
    2. Installs a real JDK 21 (actions/setup-java@v4) before touching start.sh — Paper 1.21.11 needs it, but the Gradle daemon above runs on 17, and start.sh just execs whatever java is on PATH.
    3. Copies the tracked start.sh into the run dir, launches it backgrounded, saves its PID.
    4. Retry-loop ./gradlew plugwrightPingStand (up to 30×2s) until the server answers — the built-in readiness probe (connects, probes the RCON console channel, leases + authenticates an account), not a raw port check.
    5. ./gradlew plugwrightTestStand — blocking, same as LocalMode.
    6. if: always(): kill the backgrounded server by PID.
    7. if: failure(): upload the server log + build/reports/plugwright/stand.* as an artifact.

    PLUGWRIGHT_RCON_PASSWORD / PLUGWRIGHT_BOT_PASSWORD are set at job level as literal plugwright — test-only credentials for a server the job itself starts and tears down within the same run, matching local's existing default.

No change to plugwright-action itself — its existing gradle-args input covered everything needed.

Testing

Ran the sequence locally first: plugwrightProvisionLocal → copy tracked start.sh in → background start → ping retry-loop (ready on attempt 3) → plugwrightTestStand: 44/44 passed, 6 expected skips, 0 failed → kill by PID confirmed the process exits.

CI itself went through a few iterations once the two jobs actually ran on separate runners (each caught something the single-job version had been masking as a side effect of task ordering):

  • JDK 17-only PATH → UnsupportedClassVersionError starting Paper 1.21.11 → fixed by installing JDK 21 before start.sh.
  • plugwrightProvisionLocal alone doesn't install @plugwright/console-rcon → every ping failed with "no console channel could be reached" → fixed by also running plugwrightCompileTests.

Final green run: test-example-pluginpass in 4m58s, test-example-plugin-standpass in 4m52s, running concurrently — down from ~7m13s sequential.

…ted/
generated/ is gitignored, so CI has nothing to launch the stand Paper
server with. Commit the launcher example_plugin/README.md already
told developers to write by hand, under stand-run/, and pin *.sh to
LF via .gitattributes so a Windows checkout doesn't break the
shebang.
test-example-plugin only ever exercised LocalMode - the stand
(ExternalMode) env is excluded from the matrix since it needs an
already-running server, so the RCON console channel, account-pool
leasing and the justCreated registration flow had zero CI coverage.
The LocalMode run plugwright-action already does leaves Paper, cache
and libraries under generated/local/run/ - stand points at the same
localhost:25565. Copy in the tracked launcher, start it in the
background, retry plugwrightPingStand until it answers, then run
plugwrightTestStand. Kill the server and upload its log on failure
regardless of outcome.
ClosesDrownek#61.
@monikon22
monikon22 marked this pull request as draft August 28, 2026 12:47
start.sh just runs whatever "java" is on PATH, which the earlier
plugwright-action step pinned to 17 for the Gradle daemon. Paper
1.21.11 needs 21 (example_plugin/build.gradle.kts pins the toolchain
there) - LocalMode never hit this because Gradle resolves and
downloads that toolchain JDK itself for its own server launch, but a
plain shell script has no such resolution.
CI run 33172008840 confirmed the crash: UnsupportedClassVersionError,
class file version 65.0 vs runtime's 61.0.
test-example-plugin and test-example-plugin-stand now run on separate
runners concurrently instead of stand chaining off the end of a single
sequential job. Stand can no longer reuse the local job's
generated/local/run/ (different runner, different filesystem), so it
provisions its own Paper server via plugwright-action's gradle-args
input (plugwrightProvisionLocal only - not the full plugwrightTest,
which would also run and duplicate the local suite). Everything after
that (JDK 21 setup, start.sh, ping retry-loop, plugwrightTestStand,
teardown, failure log upload) is unchanged, just moved into the new
job. Trades a duplicate Paper/plugin download for roughly half the
wall-clock time versus running sequentially in one job.
job names: test-example-plugin keeps its existing name (a maintainer
required-status-check on it, if any, keeps matching); the new job is
test-example-plugin-stand.
plugwrightPingStand/plugwrightTestStand don't depend on
plugwrightCompileTests (ExternalMode registers no prepareTask -
registerTasks in ExternalMode.kt assumes the stand is already up and
node_modules already has what it needs). The old sequential job got
this for free as a side effect of plugwrightTest's dependsOn chain
running first; the new parallel stand job only ran
plugwrightProvisionLocal, so @plugwright/console-rcon was never
installed and every ping failed with 'no console channel could be
reached'.
Verified locally: a clean node_modules, then ./gradlew
plugwrightCompileTests, installs console-rcon/auth-authme/runner as
expected.
The console-rcon fix's replace missed the pre-existing gradle-args
line, leaving two under the same 'with:' block. Plain YAML parsers
silently keep the last one (which is why local yaml.safe_load passed),
but GitHub Actions' own parser rejects it outright - the run failed
in 0s with zero jobs registered, no logs at all.
@monikon22
monikon22 marked this pull request as ready for review August 28, 2026 13:22
@monikon22monikon22 changed the title [v3] Run ExternalMode (stand) suite in CI after LocalMode[v3] Run ExternalMode (stand) suite in CI in addition to LocalModeAug 28, 2026
@Drownek
Drownek merged commit 1e58e04 into Drownek:v3-devAug 28, 2026
2 checks passed
Sign up for freeto 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.

2 participants

@monikon22@Drownek
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

[v3] Run ExternalMode (stand) suite in CI in addition to LocalMode - #63

Merged
Drownek merged 6 commits into
Drownek:v3-devfrom
monikon22:feat/61-stand-ci-testing
Aug 28, 2026
Merged

[v3] Run ExternalMode (stand) suite in CI in addition to LocalMode#63
Drownek merged 6 commits into
Drownek:v3-devfrom
monikon22:feat/61-stand-ci-testing

Conversation

@monikon22

@monikon22monikon22 commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Closes#61.

test-example-plugin only ever ran LocalMode — stand (ExternalMode) is includeInMatrix: false because it needs an already-running server, so plugwrightTest skips it and the RCON console channel, account-pool leasing and justCreated registration flow had zero CI coverage.

What changed

  • example_plugin/src/test/e2e/stand-run/start.sh (new, tracked): the launcher example_plugin/README.md already told developers to hand-write under generated/local/run/. That directory is gitignored, so CI had nothing to copy in — this commits the same script README documents, outside generated/.

  • .gitattributes (new): pins *.sh to LF so a Windows checkout doesn't corrupt the shebang.

  • example_plugin/README.md: one-line pointer to the tracked copy.

  • .github/workflows/ci.yml: stand now runs as its own job, test-example-plugin-stand, in parallel with test-example-plugin (unchanged, keeps its name so an existing required-status-check on it still matches). Running on a separate runner means it can't reuse the local job's generated/local/run/, so it:

    1. Provisions its own Paper server via plugwright-action's gradle-args: plugwrightProvisionLocal plugwrightCompileTests (provisioning alone isn't enough — plugwrightPingStand/plugwrightTestStand don't depend on plugwrightCompileTests themselves, they just expect node_modules already has what console { rcon {} } and the npm(...) plugin refs need; the old sequential job got this for free as a side effect of plugwrightTest's own dependency chain).
    2. Installs a real JDK 21 (actions/setup-java@v4) before touching start.sh — Paper 1.21.11 needs it, but the Gradle daemon above runs on 17, and start.sh just execs whatever java is on PATH.
    3. Copies the tracked start.sh into the run dir, launches it backgrounded, saves its PID.
    4. Retry-loop ./gradlew plugwrightPingStand (up to 30×2s) until the server answers — the built-in readiness probe (connects, probes the RCON console channel, leases + authenticates an account), not a raw port check.
    5. ./gradlew plugwrightTestStand — blocking, same as LocalMode.
    6. if: always(): kill the backgrounded server by PID.
    7. if: failure(): upload the server log + build/reports/plugwright/stand.* as an artifact.

    PLUGWRIGHT_RCON_PASSWORD / PLUGWRIGHT_BOT_PASSWORD are set at job level as literal plugwright — test-only credentials for a server the job itself starts and tears down within the same run, matching local's existing default.

No change to plugwright-action itself — its existing gradle-args input covered everything needed.

Testing

Ran the sequence locally first: plugwrightProvisionLocal → copy tracked start.sh in → background start → ping retry-loop (ready on attempt 3) → plugwrightTestStand: 44/44 passed, 6 expected skips, 0 failed → kill by PID confirmed the process exits.

CI itself went through a few iterations once the two jobs actually ran on separate runners (each caught something the single-job version had been masking as a side effect of task ordering):

  • JDK 17-only PATH → UnsupportedClassVersionError starting Paper 1.21.11 → fixed by installing JDK 21 before start.sh.
  • plugwrightProvisionLocal alone doesn't install @plugwright/console-rcon → every ping failed with "no console channel could be reached" → fixed by also running plugwrightCompileTests.

Final green run: test-example-pluginpass in 4m58s, test-example-plugin-standpass in 4m52s, running concurrently — down from ~7m13s sequential.

…ted/
generated/ is gitignored, so CI has nothing to launch the stand Paper
server with. Commit the launcher example_plugin/README.md already
told developers to write by hand, under stand-run/, and pin *.sh to
LF via .gitattributes so a Windows checkout doesn't break the
shebang.
test-example-plugin only ever exercised LocalMode - the stand
(ExternalMode) env is excluded from the matrix since it needs an
already-running server, so the RCON console channel, account-pool
leasing and the justCreated registration flow had zero CI coverage.
The LocalMode run plugwright-action already does leaves Paper, cache
and libraries under generated/local/run/ - stand points at the same
localhost:25565. Copy in the tracked launcher, start it in the
background, retry plugwrightPingStand until it answers, then run
plugwrightTestStand. Kill the server and upload its log on failure
regardless of outcome.
ClosesDrownek#61.
@monikon22
monikon22 marked this pull request as draft August 28, 2026 12:47
start.sh just runs whatever "java" is on PATH, which the earlier
plugwright-action step pinned to 17 for the Gradle daemon. Paper
1.21.11 needs 21 (example_plugin/build.gradle.kts pins the toolchain
there) - LocalMode never hit this because Gradle resolves and
downloads that toolchain JDK itself for its own server launch, but a
plain shell script has no such resolution.
CI run 33172008840 confirmed the crash: UnsupportedClassVersionError,
class file version 65.0 vs runtime's 61.0.
test-example-plugin and test-example-plugin-stand now run on separate
runners concurrently instead of stand chaining off the end of a single
sequential job. Stand can no longer reuse the local job's
generated/local/run/ (different runner, different filesystem), so it
provisions its own Paper server via plugwright-action's gradle-args
input (plugwrightProvisionLocal only - not the full plugwrightTest,
which would also run and duplicate the local suite). Everything after
that (JDK 21 setup, start.sh, ping retry-loop, plugwrightTestStand,
teardown, failure log upload) is unchanged, just moved into the new
job. Trades a duplicate Paper/plugin download for roughly half the
wall-clock time versus running sequentially in one job.
job names: test-example-plugin keeps its existing name (a maintainer
required-status-check on it, if any, keeps matching); the new job is
test-example-plugin-stand.
plugwrightPingStand/plugwrightTestStand don't depend on
plugwrightCompileTests (ExternalMode registers no prepareTask -
registerTasks in ExternalMode.kt assumes the stand is already up and
node_modules already has what it needs). The old sequential job got
this for free as a side effect of plugwrightTest's dependsOn chain
running first; the new parallel stand job only ran
plugwrightProvisionLocal, so @plugwright/console-rcon was never
installed and every ping failed with 'no console channel could be
reached'.
Verified locally: a clean node_modules, then ./gradlew
plugwrightCompileTests, installs console-rcon/auth-authme/runner as
expected.
The console-rcon fix's replace missed the pre-existing gradle-args
line, leaving two under the same 'with:' block. Plain YAML parsers
silently keep the last one (which is why local yaml.safe_load passed),
but GitHub Actions' own parser rejects it outright - the run failed
in 0s with zero jobs registered, no logs at all.
@monikon22
monikon22 marked this pull request as ready for review August 28, 2026 13:22
@monikon22monikon22 changed the title [v3] Run ExternalMode (stand) suite in CI after LocalMode[v3] Run ExternalMode (stand) suite in CI in addition to LocalModeAug 28, 2026
@Drownek
Drownek merged commit 1e58e04 into Drownek:v3-devAug 28, 2026
2 checks passed
Sign up for freeto 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.

2 participants

@monikon22@Drownek