Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/e2e-tests-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ jobs:
pmm_client_version: ${{ inputs.pmm_client_version || github.event.inputs.pmm_client_version || 'latest-tarball' }}
pmm_qa_branch: ${{ github.event_name == 'pull_request' && github.head_ref || (inputs.pmm_qa_branch || github.event.inputs.pmm_qa_branch || 'main') }}
setup_services: '--database ps=8.4 --database psmdb --database valkey'
pmm_test_flag: '@new-navigation'
pmm_test_flag: '@new-navigation|@menu'

# valkey_integration:
# name: Valkey integration tests
Expand Down
42 changes: 40 additions & 2 deletions .github/workflows/nightly-e2e-tests-matrix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,20 @@ jobs:
pmm_server_image: ${{ inputs.pmm_server_image || 'perconalab/pmm-server:3-dev-latest' }}
setup_services: ${{ matrix.setup_services }}
shard_name: ${{ matrix.shard_name }}
expected_test_jobs: 2
# 3: the two CodeceptJS `test_execution / ...` jobs plus the Playwright
# `test_execution / playwright ...` job below. The setup shards poll by
# job-name prefix "test execution / " to know when it's safe to finish
# waiting -- undercounting here lets a shard finish (and its runner
# terminate) while a still-running consumer job needs it.
expected_test_jobs: 3

test_execution:
name: "test execution / ${{ matrix.tags_for_tests }}"
strategy:
fail-fast: false
matrix:
include:
- tags_for_tests: '@qan|@menu|@valkey-nightly|@permissions-nightly|@pt-summary-nightly|@pbm-nightly'
- tags_for_tests: '@qan|@valkey-nightly|@permissions-nightly|@pt-summary-nightly|@pbm-nightly'
- tags_for_tests: '@nightly'
uses: ./.github/workflows/runner-e2e-tests-codeceptjs-remote-nightly-tests.yml
secrets:
Expand All @@ -148,3 +153,36 @@ jobs:
workers: 1
launchable_confidence: ${{ inputs.launchable_confidence || '100%' }}
installation_type: ${{ inputs.installation_type || 'docker' }}

# Migrated CodeceptJS scenarios keep their original tag bucket here (per
# .claude/skills/codeceptjs-migration/branch-workflow.md "Workflow coverage")
# so this workflow's coverage stays equivalent instead of silently dropping
# once the CodeceptJS source retires. Add a migrated tag to tags_for_tests
# below as each one lands; the test itself must keep the matching tag
# alongside its own file's execution tag.
test_execution_playwright:
# Name must start with "test execution / " -- the setup shards' own wait
# step (runner-e2e-tests-codeceptjs-remote-nightly-setup.yml) polls jobs
# by that literal prefix to count consumers; anything else is invisible
# to it and the shard can finish/terminate while this job still needs it.
name: "test execution / playwright ${{ matrix.tags_for_tests }}"
strategy:
fail-fast: false
matrix:
include:
- tags_for_tests: '@menu'
uses: ./.github/workflows/runner-e2e-tests-playwright-remote-nightly-tests.yml
secrets:
LAUNCHABLE_TOKEN: ${{ secrets.LAUNCHABLE_TOKEN }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
with:
pmm_server_address: ${{ inputs.pmm_server_address }}
admin_password: ${{ inputs.admin_password || 'admin' }}
pmm_qa_branch: ${{ inputs.pmm_qa_branch || 'main' }}
pmm_client_version: ${{ inputs.pmm_client_version || 'latest-tarball' }}
pmm_server_image: ${{ inputs.pmm_server_image || 'perconalab/pmm-server:3-dev-latest' }}
tags_for_tests: ${{ matrix.tags_for_tests }}
expected_setup_jobs: 14
workers: 1
launchable_confidence: ${{ inputs.launchable_confidence || '100%' }}
installation_type: ${{ inputs.installation_type || 'docker' }}
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
name: _runner-e2e-tests-playwright-remote-nightly-tests

on:
workflow_call:
inputs:
pmm_server_address:
required: true
type: string
admin_password:
required: false
default: 'admin'
type: string
pmm_qa_branch:
required: false
default: 'main'
type: string
pmm_client_version:
required: false
default: 'latest-tarball'
type: string
pmm_server_image:
required: false
default: 'perconalab/pmm-server:3-dev-latest'
type: string
tags_for_tests:
required: true
type: string
expected_setup_jobs:
required: false
default: 14
type: number
workers:
required: false
default: 1
type: number
launchable_confidence:
required: false
default: '100%'
type: string
installation_type:
required: false
default: 'docker'
type: string
secrets:
LAUNCHABLE_TOKEN:
required: false
GH_TOKEN:
required: false

permissions:
actions: read
contents: read

jobs:
tests:
name: "test execution / ${{ inputs.tags_for_tests }}"
runs-on: ubuntu-22.04
timeout-minutes: 120
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
ADMIN_PASSWORD: ${{ inputs.admin_password || 'admin' }}
PMM_SERVER_ADDRESS: ${{ inputs.pmm_server_address }}
PMM_QA_BRANCH: ${{ inputs.pmm_qa_branch || 'main' }}
PMM_QA_GIT_BRANCH: ${{ inputs.pmm_qa_branch || 'main' }}
PMM_CLIENT_VERSION: ${{ inputs.pmm_client_version || 'latest-tarball' }}
CLIENT_VERSION: ${{ inputs.pmm_client_version || 'latest-tarball' }}
DOCKER_VERSION: ${{ inputs.pmm_server_image || 'perconalab/pmm-server:3-dev-latest' }}
PMM_TEST_FLAG: ${{ inputs.tags_for_tests }}
TAGS_FOR_TESTS: ${{ inputs.tags_for_tests }}
WORKERS: ${{ inputs.workers || 1 }}
LAUNCHABLE_CONFIDENCE: ${{ inputs.launchable_confidence || '100%' }}
INSTALLATION_TYPE: ${{ inputs.installation_type || 'docker' }}
LAUNCHABLE_TOKEN: ${{ secrets.LAUNCHABLE_TOKEN }}
SUBSET_FILE_NAME: 'launchable-subset.txt'
SETUP_WAIT_STEP: 'Waiting for tests execution'
SETUP_JOB_PREFIX: 'setup / '
EXPECTED_SETUP_JOBS: ${{ inputs.expected_setup_jobs || 14 }}
WAIT_POLL_INTERVAL_SECONDS: 600
WAIT_TIMEOUT_SECONDS: 10800

steps:
- name: Checkout pmm-qa repo
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: percona/pmm-qa
ref: ${{ env.PMM_QA_BRANCH }}
path: ./pmm-qa

- name: Cleanup disk space on a worker according to https://github.com/actions/runner-images/issues/2840#issuecomment-790492173
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
sudo rm -rf "/usr/local/share/boost"
sudo rm -rf "$AGENT_TOOLSDIRECTORY"

- name: Resolve PMM Server URLs
id: pmm_urls
run: |
ADDR="${{ env.PMM_SERVER_ADDRESS }}"
ADDR_NO_SCHEME=${ADDR#http://}
ADDR_NO_SCHEME=${ADDR_NO_SCHEME#https://}
ADDR_NO_SCHEME=${ADDR_NO_SCHEME%/}
echo "PMM_UI_URL=https://${ADDR_NO_SCHEME}/" >> "$GITHUB_ENV"
echo "PMM_URL=https://admin:${{ env.ADMIN_PASSWORD }}@${ADDR_NO_SCHEME}" >> "$GITHUB_ENV"
echo "SERVER_IP=${ADDR_NO_SCHEME}" >> "$GITHUB_ENV"

- name: Sanity check remote PMM Server reachability
run: |
set -eux
for i in $(seq 1 30); do
code=$(curl -ksS -o /dev/null -w "%{http_code}" "https://${SERVER_IP}/v1/server/readyz" || echo "000")
if [ "$code" = "200" ]; then
echo "Remote PMM Server is ready"
exit 0
fi
echo "Attempt $i: PMM Server not ready (http=$code), sleeping 5s"
sleep 5
done
echo "Remote PMM Server did not become ready in time" >&2
exit 1

- name: Setup npm modules for e2e tests
working-directory: pmm-qa/e2e_tests
run: |
npm ci
npx playwright install-deps
npx playwright install chromium

- uses: actions/github-script@v7
id: tags_processed
with:
script: |
return `${process.env.TAGS_FOR_TESTS.replaceAll('|', '-')}`
result-encoding: string

- name: Prepare launchable
working-directory: pmm-qa/e2e_tests
run: |
pip3 install --user --upgrade launchable~=1.0
launchable verify || true

docker pull ${{ env.DOCKER_VERSION }} || true
export DOCKER_IMAGE_ID=$(docker inspect -f '{{index .RepoDigests 0}}' ${{ env.DOCKER_VERSION }} | cut -d@ -f2) || true
echo "Launchable build: ${DOCKER_IMAGE_ID}"
echo "Launchable confidence: ${{ env.LAUNCHABLE_CONFIDENCE }}"

launchable record session --build ${DOCKER_IMAGE_ID} --test-suite "${{ env.INSTALLATION_TYPE }}-nightly-ui-tests-${{ steps.tags_processed.outputs.result }}" > launchable-session.txt || true
node launchable-prepare.js "${{ env.PMM_TEST_FLAG }}"
cat test_list.txt | launchable subset --session $(cat launchable-session.txt) --confidence ${{ env.LAUNCHABLE_CONFIDENCE }} --use-case feature-branch playwright > ${{ env.SUBSET_FILE_NAME }} || true
echo "$(cat ${{ env.SUBSET_FILE_NAME }})" || true

- name: Check if launchable subset is empty
id: check_launchable_subset
working-directory: pmm-qa/e2e_tests
run: |
if [ -s "${{ env.SUBSET_FILE_NAME }}" ]; then
echo "has_subset=true" >> "$GITHUB_OUTPUT"
echo "Launchable subset is not empty. Continue with tests."
else
echo "has_subset=false" >> "$GITHUB_OUTPUT"
echo "Launchable subset is empty. Test execution will be skipped."
fi

- name: Skip notice
if: ${{ steps.check_launchable_subset.outputs.has_subset != 'true' }}
run: echo "No tests selected by Launchable subset. Skipping setup wait and test execution."

- name: Wait for all setup jobs to be ready
if: ${{ steps.check_launchable_subset.outputs.has_subset == 'true' }}
uses: actions/github-script@v7
with:
script: |
const owner = context.repo.owner;
const repo = context.repo.repo;
const run_id = context.runId;
const expectedSetupJobs = Number(process.env.EXPECTED_SETUP_JOBS);
const pollIntervalMs = Number(process.env.WAIT_POLL_INTERVAL_SECONDS) * 1000;
const timeoutMs = Number(process.env.WAIT_TIMEOUT_SECONDS) * 1000;
const setupPrefix = process.env.SETUP_JOB_PREFIX;
const setupWaitStep = process.env.SETUP_WAIT_STEP;
const startedAt = Date.now();

async function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

async function listJobs() {
return github.paginate(github.rest.actions.listJobsForWorkflowRun, {
owner,
repo,
run_id,
filter: 'latest',
per_page: 100,
headers: {
'X-GitHub-Api-Version': '2026-03-10',
},
});
}

async function getJob(job) {
const response = await github.rest.actions.getJobForWorkflowRun({
owner,
repo,
job_id: job.id,
headers: {
'X-GitHub-Api-Version': '2026-03-10',
},
});
return response.data;
}

function findSetupWaitStep(job) {
return (job.steps || []).find((step) => step.name === setupWaitStep);
}

function isWaitingForTests(job) {
const step = findSetupWaitStep(job);
return step?.status === 'in_progress';
}

function finishedWaitingSuccessfully(job) {
const step = findSetupWaitStep(job);
return step?.status === 'completed' && step?.conclusion === 'success';
}

function setupJobFailed(job) {
return ['failure', 'cancelled'].includes(job.conclusion);
}

while (true) {
const jobs = await listJobs();
const setupJobs = jobs.filter((job) => job.name.startsWith(setupPrefix) || job.name.includes(setupPrefix));
const detailedSetupJobs = await Promise.all(setupJobs.map(getJob));
const setupJobsWithWaitStep = detailedSetupJobs.filter(findSetupWaitStep);
const readySetupJobs = setupJobsWithWaitStep.filter(isWaitingForTests);
const failedSetupJobs = detailedSetupJobs.filter(setupJobFailed);
const finishedEarlyJobs = setupJobsWithWaitStep.filter(finishedWaitingSuccessfully);

core.info(`Setup jobs: discovered=${setupJobsWithWaitStep.length}/${expectedSetupJobs}, ready=${readySetupJobs.length}/${expectedSetupJobs}`);
for (const job of setupJobsWithWaitStep) {
const step = findSetupWaitStep(job);
core.info(`- ${job.name}: job_status=${job.status}, step_status=${step.status}, step_conclusion=${step.conclusion || 'null'}, job_conclusion=${job.conclusion || 'null'}`);
}

if (failedSetupJobs.length > 0) {
throw new Error(`Setup job(s) failed or were cancelled: ${failedSetupJobs.map((job) => `${job.name} (${job.conclusion})`).join(', ')}`);
}

if (finishedEarlyJobs.length > 0) {
throw new Error(`Setup wait step completed before this test job was ready: ${finishedEarlyJobs.map((job) => job.name).join(', ')}`);
}

if (setupJobsWithWaitStep.length >= expectedSetupJobs && readySetupJobs.length >= expectedSetupJobs) {
core.info('All setup jobs are waiting for test execution.');
return;
}

if (Date.now() - startedAt > timeoutMs) {
throw new Error(`Timed out waiting for ${expectedSetupJobs} setup jobs to reach '${setupWaitStep}' with status=in_progress.`);
}

await sleep(pollIntervalMs);
}

- name: Wait for metrics to be collected
if: ${{ steps.check_launchable_subset.outputs.has_subset == 'true' }}
working-directory: pmm-qa/e2e_tests
run: |
sleep 60

- name: 'Run UI tests: ${{ env.PMM_TEST_FLAG }} with launchable'
if: ${{ steps.check_launchable_subset.outputs.has_subset == 'true' }}
working-directory: pmm-qa/e2e_tests
run: |
npx playwright test --grep "${{ env.PMM_TEST_FLAG }}" $(cat "${{ env.SUBSET_FILE_NAME }}") || true
Comment on lines +271 to +275

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make required failures fail the workflow.

Make it so || true does not convert required failures into success. A Launchable error can leave launchable-subset.txt empty, and Lines 152-166 then skip required @menu coverage. A Playwright failure also leaves the step successful, so failure-only diagnostics and artifacts do not run.

  • .github/workflows/runner-e2e-tests-playwright-remote-nightly-tests.yml#L271-L275: remove || true and preserve the Playwright exit status.
  • .github/workflows/runner-e2e-tests-playwright-remote-nightly-tests.yml#L147-L150: fail Launchable preparation, or implement an explicit full-test fallback when Launchable is unavailable.
🧰 Tools
🪛 zizmor (1.29.0)

[warning] 275-275: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

📍 Affects 1 file
  • .github/workflows/runner-e2e-tests-playwright-remote-nightly-tests.yml#L271-L275 (this comment)
  • .github/workflows/runner-e2e-tests-playwright-remote-nightly-tests.yml#L147-L150
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/runner-e2e-tests-playwright-remote-nightly-tests.yml
around lines 271 - 275, Update
.github/workflows/runner-e2e-tests-playwright-remote-nightly-tests.yml lines
271-275 to remove || true so the Run UI tests step preserves the Playwright exit
status. At lines 147-150, make Launchable subset preparation fail when
unavailable, or explicitly fall back to running the full required test suite so
`@menu` coverage is not skipped.


- name: Download PMM Server logs
if: ${{ steps.check_launchable_subset.outputs.has_subset == 'true' && always() }}
working-directory: pmm-qa/e2e_tests
continue-on-error: true
run: |
curl --insecure "https://admin:${{ env.ADMIN_PASSWORD }}@${SERVER_IP}/logs.zip" --output logs.zip
unzip logs.zip -d logs || true

- name: Record launchable test results
if: ${{ steps.check_launchable_subset.outputs.has_subset == 'true' && always() }}
working-directory: pmm-qa/e2e_tests
run: |
launchable record tests --session $(cat launchable-session.txt) playwright --json output/results.json || true
launchable gate --session "$(cat launchable-session.txt)"

- name: Record launchable logs on failure
if: ${{ steps.check_launchable_subset.outputs.has_subset == 'true' && failure() }}
working-directory: pmm-qa/e2e_tests
continue-on-error: true
run: |
launchable record attachment --session $(cat launchable-session.txt) --include "*.log" logs.zip || true

- uses: actions/upload-artifact@v4
if: ${{ failure() }}
continue-on-error: true
with:
name: "artifacts_${{ steps.tags_processed.outputs.result }}"
path: |
pmm-qa/e2e_tests/playwright-report
pmm-qa/e2e_tests/screenshots
pmm-qa/e2e_tests/logs

- uses: actions/upload-artifact@v4
if: ${{ steps.check_launchable_subset.outputs.has_subset == 'true' && always() }}
continue-on-error: true
with:
name: "junit_${{ steps.tags_processed.outputs.result }}"
path: pmm-qa/e2e_tests/output/results.json
Loading