From 8ac0d2336946cd1198ec28d0a7582cb27a06151f Mon Sep 17 00:00:00 2001 From: Martin Othamar Date: Sat, 22 Aug 2026 12:22:26 +0200 Subject: [PATCH 1/3] ci: refactor Cypress sharding due to cloud limits --- .github/workflows/app-frontend-cypress.yml | 7 +- src/App/frontend/cypress.config.js | 51 +++++---- src/App/frontend/scripts/cypress-shard.js | 85 +++++++++++++++ .../frontend/test/e2e/cypress-timings.json | 100 ++++++++++++++++++ 4 files changed, 220 insertions(+), 23 deletions(-) create mode 100644 src/App/frontend/scripts/cypress-shard.js create mode 100644 src/App/frontend/test/e2e/cypress-timings.json diff --git a/.github/workflows/app-frontend-cypress.yml b/.github/workflows/app-frontend-cypress.yml index ac87cbbfe11..995940dc360 100644 --- a/.github/workflows/app-frontend-cypress.yml +++ b/.github/workflows/app-frontend-cypress.yml @@ -277,12 +277,13 @@ jobs: PERCY_BRANCH: ${{ github.head_ref || github.ref_name }} PERCY_TARGET_BRANCH: ${{ github.base_ref || 'main' }} IS_PR_DRAFT: ${{ github.event.pull_request.draft }} + E2E_SHARD_TOTAL: 6 + E2E_SHARD_NUMBER: ${{ matrix.containers }} run: | cypress_args=( run --record - --parallel - --group altinn-app-frontend + --group "altinn-app-frontend-shard-${{ matrix.containers }}" --tag altinn-app-frontend --browser "${CHROME_PATH:-chrome}" --ci-build-id "${{ github.run_id }}-${{ github.run_attempt }}" @@ -299,7 +300,7 @@ jobs: - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 if: failure() with: - name: app-frontend-logs + name: app-frontend-logs-${{ matrix.containers }} path: | src/App/frontend/test/logs/* src/App/frontend/test/screenshots/** diff --git a/src/App/frontend/cypress.config.js b/src/App/frontend/cypress.config.js index 22171b7e296..92973a29920 100644 --- a/src/App/frontend/cypress.config.js +++ b/src/App/frontend/cypress.config.js @@ -5,6 +5,7 @@ const path = require('node:path'); const fs = require('node:fs/promises'); const { existsSync } = require('node:fs'); const env = require('dotenv').config(); +const configureCypressShard = require('./scripts/cypress-shard'); const CYPRESS_WINDOW_WIDTH = env.parsed?.CYPRESS_WINDOW_WIDTH || 1920; const CYPRESS_WINDOW_HEIGHT = env.parsed?.CYPRESS_WINDOW_HEIGHT || 1080; @@ -80,26 +81,36 @@ module.exports = defineConfig({ const validEnvironments = ['localtest', 'tt02']; if (validEnvironments.includes(config.env.environment)) { - return getConfigurationByFile(config.env.environment).then((fileConfig) => ({ - ...fileConfig, - env: { - ...config.env, - ...fileConfig.env, - }, - expose: { - ...config.expose, - ...fileConfig.expose, - // Specs that assert on backend-local date/time values need the backend's timezone. - // Only in localtest does the app backend run on the same machine as Cypress, so only - // then is the machine timezone valid - read it here in the Node process, since the - // browser's timezone may be emulated via CDP and cannot be trusted. Against remote - // environments (tt02) this is deliberately left unset; specs fall back to UTC, which - // is what those backends run in. - ...(config.env.environment === 'localtest' - ? { machineTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone } - : {}), - }, - })); + return getConfigurationByFile(config.env.environment).then((fileConfig) => { + const configured = { + ...config, + ...fileConfig, + env: { + ...config.env, + ...fileConfig.env, + }, + expose: { + ...config.expose, + ...fileConfig.expose, + // Specs that assert on backend-local date/time values need the backend's timezone. + // Only in localtest does the app backend run on the same machine as Cypress, so only + // then is the machine timezone valid - read it here in the Node process, since the + // browser's timezone may be emulated via CDP and cannot be trusted. Against remote + // environments (tt02) this is deliberately left unset; specs fall back to UTC, which + // is what those backends run in. + ...(config.env.environment === 'localtest' + ? { machineTimezone: Intl.DateTimeFormat().resolvedOptions().timeZone } + : {}), + }, + }; + + return configureCypressShard(configured, { + specRoot: path.resolve(__dirname, 'test/e2e/integration'), + timingsFile: path.resolve(__dirname, 'test/e2e/cypress-timings.json'), + total: process.env.E2E_SHARD_TOTAL, + number: process.env.E2E_SHARD_NUMBER, + }); + }); } throw new Error(`Unknown environment "${config.env.environment}" diff --git a/src/App/frontend/scripts/cypress-shard.js b/src/App/frontend/scripts/cypress-shard.js new file mode 100644 index 00000000000..ddd27074f52 --- /dev/null +++ b/src/App/frontend/scripts/cypress-shard.js @@ -0,0 +1,85 @@ +/* eslint-disable @typescript-eslint/no-require-imports, no-console */ +const fs = require('node:fs'); +const path = require('node:path'); + +function configureCypressShard(config, { specRoot, timingsFile, total, number }) { + if (total === undefined && number === undefined) { + return config; + } + + const shardTotal = parseInteger(total, 'E2E_SHARD_TOTAL'); + const shardNumber = parseInteger(number, 'E2E_SHARD_NUMBER'); + const shardIndex = shardNumber - 1; + if (shardTotal < 1) { + throw new Error('E2E_SHARD_TOTAL must be greater than zero.'); + } + if (shardNumber < 1 || shardNumber > shardTotal) { + throw new Error(`E2E_SHARD_NUMBER must be between 1 and ${shardTotal}.`); + } + + const specs = findSpecs(specRoot); + if (shardTotal > specs.length) { + throw new Error(`Cannot split ${specs.length} Cypress specs across ${shardTotal} shards.`); + } + + const timingData = JSON.parse(fs.readFileSync(timingsFile, 'utf8')).durations; + const durations = new Map(timingData.map(({ spec, duration }) => [spec, duration])); + const averageDuration = timingData.reduce((sum, { duration }) => sum + duration, 0) / timingData.length; + const shards = Array.from({ length: shardTotal }, () => ({ duration: 0, specs: [] })); + let specsWithoutTimings = 0; + + const weightedSpecs = specs + .map((spec) => { + const relativeSpec = toPosixPath(path.relative(config.projectRoot, spec)); + const duration = durations.get(relativeSpec); + if (duration === undefined) { + specsWithoutTimings += 1; + } + return { spec, duration: duration ?? averageDuration }; + }) + .sort((left, right) => right.duration - left.duration || left.spec.localeCompare(right.spec)); + + for (const spec of weightedSpecs) { + const shard = shards.reduce((shortest, candidate) => + candidate.duration < shortest.duration ? candidate : shortest, + ); + shard.specs.push(spec.spec); + shard.duration += spec.duration; + } + + const selectedShard = shards[shardIndex]; + config.specPattern = selectedShard.specs.sort(); + const missingTimingMessage = + specsWithoutTimings > 0 + ? ` (${specsWithoutTimings} ${specsWithoutTimings === 1 ? 'spec uses' : 'specs use'} the historical average)` + : ''; + console.log( + `Cypress shard ${shardNumber}/${shardTotal}: ${selectedShard.specs.length} specs, ${Math.round(selectedShard.duration / 1000)} estimated seconds${missingTimingMessage}`, + ); + + return config; +} + +function findSpecs(directory) { + return fs + .readdirSync(directory, { withFileTypes: true }) + .flatMap((entry) => { + const entryPath = path.join(directory, entry.name); + return entry.isDirectory() ? findSpecs(entryPath) : entryPath.endsWith('.ts') ? [entryPath] : []; + }) + .sort(); +} + +function parseInteger(value, name) { + const parsed = Number(value); + if (!Number.isInteger(parsed)) { + throw new Error(`${name} must be an integer.`); + } + return parsed; +} + +function toPosixPath(value) { + return value.split(path.sep).join('/'); +} + +module.exports = configureCypressShard; diff --git a/src/App/frontend/test/e2e/cypress-timings.json b/src/App/frontend/test/e2e/cypress-timings.json new file mode 100644 index 00000000000..342131782b4 --- /dev/null +++ b/src/App/frontend/test/e2e/cypress-timings.json @@ -0,0 +1,100 @@ +{ + "source": "GitHub Actions run 29900669595 on 2026-07-22", + "unit": "milliseconds", + "durations": [ + { "spec": "test/e2e/integration/anonymous-stateless-app/anonymous.ts", "duration": 10000 }, + { "spec": "test/e2e/integration/anonymous-stateless-app/auto-save-behavior.ts", "duration": 7000 }, + { "spec": "test/e2e/integration/anonymous-stateless-app/options.ts", "duration": 4000 }, + { "spec": "test/e2e/integration/anonymous-stateless-app/validation.ts", "duration": 6000 }, + { "spec": "test/e2e/integration/component-library/accordion-group.ts", "duration": 6000 }, + { "spec": "test/e2e/integration/component-library/accordion.ts", "duration": 6000 }, + { "spec": "test/e2e/integration/component-library/address.ts", "duration": 20000 }, + { "spec": "test/e2e/integration/component-library/button.ts", "duration": 6000 }, + { "spec": "test/e2e/integration/component-library/checkboxes.ts", "duration": 47000 }, + { "spec": "test/e2e/integration/component-library/date-formatDate.ts", "duration": 19000 }, + { "spec": "test/e2e/integration/component-library/date-picker.ts", "duration": 5000 }, + { "spec": "test/e2e/integration/component-library/divider.ts", "duration": 6000 }, + { "spec": "test/e2e/integration/component-library/dropdown.ts", "duration": 7000 }, + { "spec": "test/e2e/integration/component-library/fileupload-attachmentlist.ts", "duration": 5000 }, + { "spec": "test/e2e/integration/component-library/grid.ts", "duration": 16000 }, + { "spec": "test/e2e/integration/component-library/group.ts", "duration": 22000 }, + { "spec": "test/e2e/integration/component-library/image-upload.ts", "duration": 18000 }, + { "spec": "test/e2e/integration/component-library/input.ts", "duration": 5000 }, + { "spec": "test/e2e/integration/component-library/link.ts", "duration": 6000 }, + { "spec": "test/e2e/integration/component-library/list.ts", "duration": 26000 }, + { "spec": "test/e2e/integration/component-library/map.ts", "duration": 5000 }, + { "spec": "test/e2e/integration/component-library/multiple-select.ts", "duration": 43000 }, + { "spec": "test/e2e/integration/component-library/organizationlookup.ts", "duration": 6000 }, + { "spec": "test/e2e/integration/component-library/pdf.ts", "duration": 17000 }, + { "spec": "test/e2e/integration/component-library/personlookup.ts", "duration": 7000 }, + { "spec": "test/e2e/integration/component-library/radio-buttons.ts", "duration": 4000 }, + { "spec": "test/e2e/integration/component-library/repeating-group.ts", "duration": 38000 }, + { "spec": "test/e2e/integration/component-library/summary-of-previous-task.ts", "duration": 27000 }, + { "spec": "test/e2e/integration/component-library/text-area.ts", "duration": 8000 }, + { "spec": "test/e2e/integration/component-library/title-tag-updates.ts", "duration": 9000 }, + { "spec": "test/e2e/integration/expression-validation-test/expression-validation.ts", "duration": 88000 }, + { "spec": "test/e2e/integration/expression-validation-test/tags-validation.ts", "duration": 19000 }, + { "spec": "test/e2e/integration/frontend-test/accordion.ts", "duration": 8000 }, + { "spec": "test/e2e/integration/frontend-test/all-process-steps.ts", "duration": 59000 }, + { "spec": "test/e2e/integration/frontend-test/attachments-in-group.ts", "duration": 44000 }, + { "spec": "test/e2e/integration/frontend-test/auto-save-behavior.ts", "duration": 81000 }, + { "spec": "test/e2e/integration/frontend-test/cards.ts", "duration": 11000 }, + { "spec": "test/e2e/integration/frontend-test/components.ts", "duration": 267000 }, + { "spec": "test/e2e/integration/frontend-test/custom-button.ts", "duration": 25000 }, + { "spec": "test/e2e/integration/frontend-test/custom-confirm.ts", "duration": 18000 }, + { "spec": "test/e2e/integration/frontend-test/dynamics.ts", "duration": 59000 }, + { "spec": "test/e2e/integration/frontend-test/footer.ts", "duration": 5000 }, + { "spec": "test/e2e/integration/frontend-test/formatting.ts", "duration": 42000 }, + { "spec": "test/e2e/integration/frontend-test/grid.ts", "duration": 30000 }, + { "spec": "test/e2e/integration/frontend-test/group-pets.ts", "duration": 55000 }, + { "spec": "test/e2e/integration/frontend-test/group.ts", "duration": 213000 }, + { "spec": "test/e2e/integration/frontend-test/hash-router-redirect.ts", "duration": 7000 }, + { "spec": "test/e2e/integration/frontend-test/hide-row-in-group.ts", "duration": 49000 }, + { "spec": "test/e2e/integration/frontend-test/instantiation.ts", "duration": 6000 }, + { "spec": "test/e2e/integration/frontend-test/language.ts", "duration": 61000 }, + { "spec": "test/e2e/integration/frontend-test/likert.ts", "duration": 13000 }, + { "spec": "test/e2e/integration/frontend-test/list-component.ts", "duration": 29000 }, + { "spec": "test/e2e/integration/frontend-test/message.ts", "duration": 4000 }, + { "spec": "test/e2e/integration/frontend-test/mobile.ts", "duration": 54000 }, + { "spec": "test/e2e/integration/frontend-test/navigation.ts", "duration": 57000 }, + { "spec": "test/e2e/integration/frontend-test/number-text-date.ts", "duration": 22000 }, + { "spec": "test/e2e/integration/frontend-test/on-entry.ts", "duration": 19000 }, + { "spec": "test/e2e/integration/frontend-test/options.ts", "duration": 104000 }, + { "spec": "test/e2e/integration/frontend-test/page-order-with-dynamics.ts", "duration": 6000 }, + { "spec": "test/e2e/integration/frontend-test/party-selection.ts", "duration": 21000 }, + { "spec": "test/e2e/integration/frontend-test/pdf.ts", "duration": 208000 }, + { "spec": "test/e2e/integration/frontend-test/prefill.ts", "duration": 6000 }, + { "spec": "test/e2e/integration/frontend-test/redirect.ts", "duration": 3000 }, + { "spec": "test/e2e/integration/frontend-test/return-url.ts", "duration": 12000 }, + { "spec": "test/e2e/integration/frontend-test/rules.ts", "duration": 16000 }, + { "spec": "test/e2e/integration/frontend-test/self-identified-user.ts", "duration": 1000 }, + { "spec": "test/e2e/integration/frontend-test/sort-order.ts", "duration": 15000 }, + { "spec": "test/e2e/integration/frontend-test/summary.ts", "duration": 199000 }, + { "spec": "test/e2e/integration/frontend-test/tabbing.ts", "duration": 7000 }, + { "spec": "test/e2e/integration/frontend-test/tabs.ts", "duration": 10000 }, + { "spec": "test/e2e/integration/frontend-test/texts.ts", "duration": 8000 }, + { "spec": "test/e2e/integration/frontend-test/ui-settings.ts", "duration": 13000 }, + { "spec": "test/e2e/integration/frontend-test/validation.ts", "duration": 215000 }, + { "spec": "test/e2e/integration/multiple-datamodels-test/fetching.ts", "duration": 10000 }, + { "spec": "test/e2e/integration/multiple-datamodels-test/readonly.ts", "duration": 25000 }, + { "spec": "test/e2e/integration/multiple-datamodels-test/saving.ts", "duration": 33000 }, + { "spec": "test/e2e/integration/multiple-datamodels-test/validation.ts", "duration": 25000 }, + { "spec": "test/e2e/integration/navigation-test-subform/navigation.ts", "duration": 169000 }, + { "spec": "test/e2e/integration/payment-test/payment.ts", "duration": 37000 }, + { "spec": "test/e2e/integration/process-transition-test/workflow-status.ts", "duration": 104000 }, + { "spec": "test/e2e/integration/service-task/service-task.ts", "duration": 61000 }, + { "spec": "test/e2e/integration/signering-brukerstyrt/signing.ts", "duration": 31000 }, + { "spec": "test/e2e/integration/signing-test/double-signing.ts", "duration": 27000 }, + { "spec": "test/e2e/integration/signing-test/reject.ts", "duration": 19000 }, + { "spec": "test/e2e/integration/signing-test/validation.ts", "duration": 9000 }, + { "spec": "test/e2e/integration/stateless-app/feedback.ts", "duration": 20000 }, + { "spec": "test/e2e/integration/stateless-app/instantiate-from-query-params.ts", "duration": 4000 }, + { "spec": "test/e2e/integration/stateless-app/party-selection.ts", "duration": 5000 }, + { "spec": "test/e2e/integration/stateless-app/receipt.ts", "duration": 21000 }, + { "spec": "test/e2e/integration/stateless-app/stateless.ts", "duration": 12000 }, + { "spec": "test/e2e/integration/subform-test/attachments.ts", "duration": 21000 }, + { "spec": "test/e2e/integration/subform-test/pdf.ts", "duration": 82000 }, + { "spec": "test/e2e/integration/subform-test/subform.ts", "duration": 18000 }, + { "spec": "test/e2e/integration/subform-test/tableEditButton.ts", "duration": 18000 } + ] +} From 5bc39f238a93307ae3c853a2c05133d4ffaac358 Mon Sep 17 00:00:00 2001 From: Martin Othamar Date: Sat, 22 Aug 2026 20:38:28 +0200 Subject: [PATCH 2/3] updates --- .../actions/app-stop-local-env/action.yaml | 108 ++++++++++++++---- .github/workflows/app-frontend-cypress.yml | 32 ++++-- .../frontend/test/e2e/cypress-timings.json | 6 +- 3 files changed, 109 insertions(+), 37 deletions(-) diff --git a/.github/actions/app-stop-local-env/action.yaml b/.github/actions/app-stop-local-env/action.yaml index fd4cd4b05d4..3760671edcc 100644 --- a/.github/actions/app-stop-local-env/action.yaml +++ b/.github/actions/app-stop-local-env/action.yaml @@ -1,38 +1,98 @@ name: "App Stop Local Environment" description: "Stops localtest and app containers" +inputs: + collect-diagnostics: + description: "Whether to collect local environment diagnostics before stopping it" + required: false + default: "false" runs: using: "composite" steps: - - name: Log localtest diagnostics - if: failure() + - name: Collect local environment diagnostics + if: ${{ inputs.collect-diagnostics == 'true' }} shell: bash run: | - if command -v studioctl > /dev/null 2>&1; then - studioctl server status || true - studioctl app ps || true - - apps_json="$(mktemp)" - if studioctl app ps --json > "$apps_json"; then - node -e ' - const fs = require("fs"); - const data = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); - for (const app of data.apps ?? []) { - if (app.id) console.log(app.id); - } - ' "$apps_json" | while IFS= read -r id; do - echo "::group::studioctl app logs --id $id" - studioctl app logs --id "$id" --tail 200 --follow=false || true - echo "::endgroup::" - done || true + diagnostics_dir="${GITHUB_WORKSPACE}/src/App/frontend" + diagnostics_log="${diagnostics_dir}/local-env-diagnostics.log" + localtest_log="${diagnostics_dir}/localtest-container.log" + frontend_pid_file="${diagnostics_dir}/frontend-server.pid" + + mkdir -p "$diagnostics_dir" + set +e + { + echo "Collected at: $(date --iso-8601=seconds)" + echo "Runner: $(uname -a)" + + run_diagnostic() { + echo + echo "### $*" + timeout 30 "$@" + echo "Exit code: $?" + } + + run_diagnostic curl -v --max-time 10 --output /dev/null \ + --write-out $'HTTP %{http_code}; connect %{time_connect}s; first byte %{time_starttransfer}s; total %{time_total}s\n' \ + http://localhost:8080/ + run_diagnostic curl -v --max-time 10 --output /dev/null \ + --write-out $'HTTP %{http_code}; connect %{time_connect}s; first byte %{time_starttransfer}s; total %{time_total}s\n' \ + http://app-frontend.local.altinn.cloud:8000/ + run_diagnostic curl -v --max-time 10 --output /dev/null \ + --write-out $'HTTP %{http_code}; connect %{time_connect}s; first byte %{time_starttransfer}s; total %{time_total}s\n' \ + http://local.altinn.cloud:8000/ttd/frontend-test + + if [[ -f "$frontend_pid_file" ]]; then + frontend_pid="$(<"$frontend_pid_file")" + run_diagnostic ps -o pid,ppid,nlwp,rss,stat,lstart,cmd -p "$frontend_pid" + run_diagnostic ls -l "/proc/${frontend_pid}/fd" + run_diagnostic cat "/proc/${frontend_pid}/limits" + else + echo + echo "Frontend PID file not found: $frontend_pid_file" fi - rm -f "$apps_json" - echo "::group::studioctl-server logs" - studioctl server logs --tail 200 --follow=false || true - echo "::endgroup::" - fi + run_diagnostic ss -antp + run_diagnostic docker ps --no-trunc + run_diagnostic docker inspect --format '{{json .State}}' localtest + run_diagnostic docker stats --no-stream + run_diagnostic df -h + run_diagnostic free -h + run_diagnostic ps -eLf + run_diagnostic sh -c 'printf "Runner thread count: "; ps -eLf --no-headers | wc -l' + run_diagnostic sh -c 'printf "Open system file handles: "; cat /proc/sys/fs/file-nr' + run_diagnostic sh -c 'printf "PID limit: "; cat /proc/sys/kernel/pid_max' + + if command -v studioctl > /dev/null 2>&1; then + run_diagnostic studioctl doctor + run_diagnostic studioctl server status + run_diagnostic studioctl app ps + + apps_json="$(mktemp)" + if timeout 30 studioctl app ps --json > "$apps_json"; then + node -e ' + const fs = require("fs"); + const data = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); + for (const app of data.apps ?? []) { + if (app.id) console.log(app.id); + } + ' "$apps_json" | while IFS= read -r id; do + echo + echo "### studioctl app logs --id $id" + timeout 30 studioctl app logs --id "$id" --tail 200 --follow=false + done + fi + rm -f "$apps_json" + + echo + echo "### studioctl server logs" + timeout 30 studioctl server logs --tail 200 --follow=false + fi + } 2>&1 | tee "$diagnostics_log" + + timeout 30 docker logs --timestamps localtest > "$localtest_log" 2>&1 || true + exit 0 - name: Stop localtest + if: always() shell: bash run: | if command -v studioctl > /dev/null 2>&1; then diff --git a/.github/workflows/app-frontend-cypress.yml b/.github/workflows/app-frontend-cypress.yml index 995940dc360..663398251cc 100644 --- a/.github/workflows/app-frontend-cypress.yml +++ b/.github/workflows/app-frontend-cypress.yml @@ -266,6 +266,7 @@ jobs: run: npx cypress install - name: Cypress run + id: cypress shell: bash env: CYPRESS_PROJECT_ID: o7mikf @@ -297,19 +298,24 @@ jobs: npx cypress "${cypress_args[@]}" fi + - name: Stop localtest + if: always() + uses: ./.github/actions/app-stop-local-env + with: + collect-diagnostics: ${{ steps.cypress.outcome == 'failure' }} + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - if: failure() + if: ${{ always() && steps.cypress.outcome == 'failure' }} with: name: app-frontend-logs-${{ matrix.containers }} path: | + src/App/frontend/frontend-server.log + src/App/frontend/local-env-diagnostics.log + src/App/frontend/localtest-container.log src/App/frontend/test/logs/* src/App/frontend/test/screenshots/** ${{ env.STUDIOCTL_HOME }}/logs/** - - name: Stop localtest - if: always() - uses: ./.github/actions/app-stop-local-env - cypress-run-external: name: Cypress Run External if: | @@ -359,19 +365,25 @@ jobs: run: npx cypress install - name: Cypress run + id: cypress shell: bash run: npx cypress run --browser "${CHROME_PATH:-chrome}" --spec test/e2e/integration --env environment=localtest + - name: Stop localtest + if: always() + uses: ./.github/actions/app-stop-local-env + with: + collect-diagnostics: ${{ steps.cypress.outcome == 'failure' }} + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 - if: failure() + if: ${{ always() && steps.cypress.outcome == 'failure' }} with: name: app-frontend-external-logs path: | + src/App/frontend/frontend-server.log + src/App/frontend/local-env-diagnostics.log + src/App/frontend/localtest-container.log src/App/frontend/test/logs/* src/App/frontend/test/screenshots/* src/App/frontend/test/videos/* ${{ env.STUDIOCTL_HOME }}/logs/** - - - name: Stop localtest - if: always() - uses: ./.github/actions/app-stop-local-env diff --git a/src/App/frontend/test/e2e/cypress-timings.json b/src/App/frontend/test/e2e/cypress-timings.json index 342131782b4..4319a07f48f 100644 --- a/src/App/frontend/test/e2e/cypress-timings.json +++ b/src/App/frontend/test/e2e/cypress-timings.json @@ -1,5 +1,5 @@ { - "source": "GitHub Actions run 29900669595 on 2026-07-22", + "source": "GitHub Actions run 29900669595 on 2026-07-22; summary and workflow-status averaged from run 32588244974 attempts 1-2 on 2026-08-22", "unit": "milliseconds", "durations": [ { "spec": "test/e2e/integration/anonymous-stateless-app/anonymous.ts", "duration": 10000 }, @@ -69,7 +69,7 @@ { "spec": "test/e2e/integration/frontend-test/rules.ts", "duration": 16000 }, { "spec": "test/e2e/integration/frontend-test/self-identified-user.ts", "duration": 1000 }, { "spec": "test/e2e/integration/frontend-test/sort-order.ts", "duration": 15000 }, - { "spec": "test/e2e/integration/frontend-test/summary.ts", "duration": 199000 }, + { "spec": "test/e2e/integration/frontend-test/summary.ts", "duration": 153000 }, { "spec": "test/e2e/integration/frontend-test/tabbing.ts", "duration": 7000 }, { "spec": "test/e2e/integration/frontend-test/tabs.ts", "duration": 10000 }, { "spec": "test/e2e/integration/frontend-test/texts.ts", "duration": 8000 }, @@ -81,7 +81,7 @@ { "spec": "test/e2e/integration/multiple-datamodels-test/validation.ts", "duration": 25000 }, { "spec": "test/e2e/integration/navigation-test-subform/navigation.ts", "duration": 169000 }, { "spec": "test/e2e/integration/payment-test/payment.ts", "duration": 37000 }, - { "spec": "test/e2e/integration/process-transition-test/workflow-status.ts", "duration": 104000 }, + { "spec": "test/e2e/integration/process-transition-test/workflow-status.ts", "duration": 282000 }, { "spec": "test/e2e/integration/service-task/service-task.ts", "duration": 61000 }, { "spec": "test/e2e/integration/signering-brukerstyrt/signing.ts", "duration": 31000 }, { "spec": "test/e2e/integration/signing-test/double-signing.ts", "duration": 27000 }, From 6973a2317512b76e5f8c7608aa33184cbd8ec120 Mon Sep 17 00:00:00 2001 From: Martin Othamar Date: Sat, 22 Aug 2026 21:33:39 +0200 Subject: [PATCH 3/3] update timings --- .../frontend/test/e2e/cypress-timings.json | 181 +++++++++--------- 1 file changed, 91 insertions(+), 90 deletions(-) diff --git a/src/App/frontend/test/e2e/cypress-timings.json b/src/App/frontend/test/e2e/cypress-timings.json index 4319a07f48f..6c017141e3d 100644 --- a/src/App/frontend/test/e2e/cypress-timings.json +++ b/src/App/frontend/test/e2e/cypress-timings.json @@ -1,100 +1,101 @@ { - "source": "GitHub Actions run 29900669595 on 2026-07-22; summary and workflow-status averaged from run 32588244974 attempts 1-2 on 2026-08-22", + "source": "Mean spec durations from successful GitHub Actions run 32573184955 and run 32591337996 attempts 1-2 on 2026-08-22", "unit": "milliseconds", "durations": [ - { "spec": "test/e2e/integration/anonymous-stateless-app/anonymous.ts", "duration": 10000 }, - { "spec": "test/e2e/integration/anonymous-stateless-app/auto-save-behavior.ts", "duration": 7000 }, - { "spec": "test/e2e/integration/anonymous-stateless-app/options.ts", "duration": 4000 }, - { "spec": "test/e2e/integration/anonymous-stateless-app/validation.ts", "duration": 6000 }, - { "spec": "test/e2e/integration/component-library/accordion-group.ts", "duration": 6000 }, - { "spec": "test/e2e/integration/component-library/accordion.ts", "duration": 6000 }, - { "spec": "test/e2e/integration/component-library/address.ts", "duration": 20000 }, - { "spec": "test/e2e/integration/component-library/button.ts", "duration": 6000 }, - { "spec": "test/e2e/integration/component-library/checkboxes.ts", "duration": 47000 }, - { "spec": "test/e2e/integration/component-library/date-formatDate.ts", "duration": 19000 }, - { "spec": "test/e2e/integration/component-library/date-picker.ts", "duration": 5000 }, - { "spec": "test/e2e/integration/component-library/divider.ts", "duration": 6000 }, - { "spec": "test/e2e/integration/component-library/dropdown.ts", "duration": 7000 }, - { "spec": "test/e2e/integration/component-library/fileupload-attachmentlist.ts", "duration": 5000 }, - { "spec": "test/e2e/integration/component-library/grid.ts", "duration": 16000 }, - { "spec": "test/e2e/integration/component-library/group.ts", "duration": 22000 }, - { "spec": "test/e2e/integration/component-library/image-upload.ts", "duration": 18000 }, - { "spec": "test/e2e/integration/component-library/input.ts", "duration": 5000 }, - { "spec": "test/e2e/integration/component-library/link.ts", "duration": 6000 }, - { "spec": "test/e2e/integration/component-library/list.ts", "duration": 26000 }, - { "spec": "test/e2e/integration/component-library/map.ts", "duration": 5000 }, - { "spec": "test/e2e/integration/component-library/multiple-select.ts", "duration": 43000 }, - { "spec": "test/e2e/integration/component-library/organizationlookup.ts", "duration": 6000 }, - { "spec": "test/e2e/integration/component-library/pdf.ts", "duration": 17000 }, - { "spec": "test/e2e/integration/component-library/personlookup.ts", "duration": 7000 }, - { "spec": "test/e2e/integration/component-library/radio-buttons.ts", "duration": 4000 }, - { "spec": "test/e2e/integration/component-library/repeating-group.ts", "duration": 38000 }, - { "spec": "test/e2e/integration/component-library/summary-of-previous-task.ts", "duration": 27000 }, - { "spec": "test/e2e/integration/component-library/text-area.ts", "duration": 8000 }, - { "spec": "test/e2e/integration/component-library/title-tag-updates.ts", "duration": 9000 }, - { "spec": "test/e2e/integration/expression-validation-test/expression-validation.ts", "duration": 88000 }, - { "spec": "test/e2e/integration/expression-validation-test/tags-validation.ts", "duration": 19000 }, - { "spec": "test/e2e/integration/frontend-test/accordion.ts", "duration": 8000 }, - { "spec": "test/e2e/integration/frontend-test/all-process-steps.ts", "duration": 59000 }, - { "spec": "test/e2e/integration/frontend-test/attachments-in-group.ts", "duration": 44000 }, - { "spec": "test/e2e/integration/frontend-test/auto-save-behavior.ts", "duration": 81000 }, - { "spec": "test/e2e/integration/frontend-test/cards.ts", "duration": 11000 }, - { "spec": "test/e2e/integration/frontend-test/components.ts", "duration": 267000 }, - { "spec": "test/e2e/integration/frontend-test/custom-button.ts", "duration": 25000 }, - { "spec": "test/e2e/integration/frontend-test/custom-confirm.ts", "duration": 18000 }, - { "spec": "test/e2e/integration/frontend-test/dynamics.ts", "duration": 59000 }, + { "spec": "test/e2e/integration/anonymous-stateless-app/anonymous.ts", "duration": 14333 }, + { "spec": "test/e2e/integration/anonymous-stateless-app/auto-save-behavior.ts", "duration": 6667 }, + { "spec": "test/e2e/integration/anonymous-stateless-app/options.ts", "duration": 2667 }, + { "spec": "test/e2e/integration/anonymous-stateless-app/validation.ts", "duration": 7667 }, + { "spec": "test/e2e/integration/component-library/accordion-group.ts", "duration": 15333 }, + { "spec": "test/e2e/integration/component-library/accordion.ts", "duration": 10667 }, + { "spec": "test/e2e/integration/component-library/address.ts", "duration": 22667 }, + { "spec": "test/e2e/integration/component-library/button.ts", "duration": 14667 }, + { "spec": "test/e2e/integration/component-library/checkboxes.ts", "duration": 73000 }, + { "spec": "test/e2e/integration/component-library/date-formatDate.ts", "duration": 25667 }, + { "spec": "test/e2e/integration/component-library/date-picker.ts", "duration": 8333 }, + { "spec": "test/e2e/integration/component-library/divider.ts", "duration": 8333 }, + { "spec": "test/e2e/integration/component-library/dropdown.ts", "duration": 9667 }, + { "spec": "test/e2e/integration/component-library/fileupload-attachmentlist.ts", "duration": 21333 }, + { "spec": "test/e2e/integration/component-library/grid.ts", "duration": 23000 }, + { "spec": "test/e2e/integration/component-library/group.ts", "duration": 20000 }, + { "spec": "test/e2e/integration/component-library/image-upload.ts", "duration": 23667 }, + { "spec": "test/e2e/integration/component-library/input.ts", "duration": 3000 }, + { "spec": "test/e2e/integration/component-library/link.ts", "duration": 5333 }, + { "spec": "test/e2e/integration/component-library/list.ts", "duration": 43333 }, + { "spec": "test/e2e/integration/component-library/map.ts", "duration": 11667 }, + { "spec": "test/e2e/integration/component-library/multiple-select.ts", "duration": 46000 }, + { "spec": "test/e2e/integration/component-library/organizationlookup.ts", "duration": 5000 }, + { "spec": "test/e2e/integration/component-library/pdf.ts", "duration": 16667 }, + { "spec": "test/e2e/integration/component-library/personlookup.ts", "duration": 5000 }, + { "spec": "test/e2e/integration/component-library/radio-buttons.ts", "duration": 3667 }, + { "spec": "test/e2e/integration/component-library/repeating-group.ts", "duration": 64667 }, + { "spec": "test/e2e/integration/component-library/summary-of-previous-task.ts", "duration": 29000 }, + { "spec": "test/e2e/integration/component-library/text-area.ts", "duration": 9000 }, + { "spec": "test/e2e/integration/component-library/title-tag-updates.ts", "duration": 10333 }, + { "spec": "test/e2e/integration/expression-validation-test/expression-validation.ts", "duration": 91000 }, + { "spec": "test/e2e/integration/expression-validation-test/tags-validation.ts", "duration": 17667 }, + { "spec": "test/e2e/integration/frontend-test/accordion.ts", "duration": 8667 }, + { "spec": "test/e2e/integration/frontend-test/all-process-steps.ts", "duration": 67667 }, + { "spec": "test/e2e/integration/frontend-test/attachments-in-group.ts", "duration": 62667 }, + { "spec": "test/e2e/integration/frontend-test/auto-save-behavior.ts", "duration": 91667 }, + { "spec": "test/e2e/integration/frontend-test/cards.ts", "duration": 13667 }, + { "spec": "test/e2e/integration/frontend-test/components.ts", "duration": 312333 }, + { "spec": "test/e2e/integration/frontend-test/custom-button.ts", "duration": 23000 }, + { "spec": "test/e2e/integration/frontend-test/custom-confirm.ts", "duration": 17000 }, + { "spec": "test/e2e/integration/frontend-test/dynamics.ts", "duration": 72667 }, { "spec": "test/e2e/integration/frontend-test/footer.ts", "duration": 5000 }, - { "spec": "test/e2e/integration/frontend-test/formatting.ts", "duration": 42000 }, - { "spec": "test/e2e/integration/frontend-test/grid.ts", "duration": 30000 }, - { "spec": "test/e2e/integration/frontend-test/group-pets.ts", "duration": 55000 }, - { "spec": "test/e2e/integration/frontend-test/group.ts", "duration": 213000 }, - { "spec": "test/e2e/integration/frontend-test/hash-router-redirect.ts", "duration": 7000 }, - { "spec": "test/e2e/integration/frontend-test/hide-row-in-group.ts", "duration": 49000 }, - { "spec": "test/e2e/integration/frontend-test/instantiation.ts", "duration": 6000 }, - { "spec": "test/e2e/integration/frontend-test/language.ts", "duration": 61000 }, - { "spec": "test/e2e/integration/frontend-test/likert.ts", "duration": 13000 }, - { "spec": "test/e2e/integration/frontend-test/list-component.ts", "duration": 29000 }, - { "spec": "test/e2e/integration/frontend-test/message.ts", "duration": 4000 }, - { "spec": "test/e2e/integration/frontend-test/mobile.ts", "duration": 54000 }, - { "spec": "test/e2e/integration/frontend-test/navigation.ts", "duration": 57000 }, - { "spec": "test/e2e/integration/frontend-test/number-text-date.ts", "duration": 22000 }, - { "spec": "test/e2e/integration/frontend-test/on-entry.ts", "duration": 19000 }, - { "spec": "test/e2e/integration/frontend-test/options.ts", "duration": 104000 }, + { "spec": "test/e2e/integration/frontend-test/formatting.ts", "duration": 59000 }, + { "spec": "test/e2e/integration/frontend-test/grid.ts", "duration": 33667 }, + { "spec": "test/e2e/integration/frontend-test/group-pets.ts", "duration": 59667 }, + { "spec": "test/e2e/integration/frontend-test/group.ts", "duration": 307667 }, + { "spec": "test/e2e/integration/frontend-test/hash-router-redirect.ts", "duration": 5000 }, + { "spec": "test/e2e/integration/frontend-test/hide-row-in-group.ts", "duration": 49333 }, + { "spec": "test/e2e/integration/frontend-test/instantiation.ts", "duration": 5667 }, + { "spec": "test/e2e/integration/frontend-test/language.ts", "duration": 56333 }, + { "spec": "test/e2e/integration/frontend-test/likert.ts", "duration": 14333 }, + { "spec": "test/e2e/integration/frontend-test/list-component.ts", "duration": 38333 }, + { "spec": "test/e2e/integration/frontend-test/message.ts", "duration": 3333 }, + { "spec": "test/e2e/integration/frontend-test/mobile.ts", "duration": 66333 }, + { "spec": "test/e2e/integration/frontend-test/navigation.ts", "duration": 54667 }, + { "spec": "test/e2e/integration/frontend-test/number-text-date.ts", "duration": 17667 }, + { "spec": "test/e2e/integration/frontend-test/on-entry.ts", "duration": 18667 }, + { "spec": "test/e2e/integration/frontend-test/options.ts", "duration": 107000 }, { "spec": "test/e2e/integration/frontend-test/page-order-with-dynamics.ts", "duration": 6000 }, - { "spec": "test/e2e/integration/frontend-test/party-selection.ts", "duration": 21000 }, - { "spec": "test/e2e/integration/frontend-test/pdf.ts", "duration": 208000 }, - { "spec": "test/e2e/integration/frontend-test/prefill.ts", "duration": 6000 }, - { "spec": "test/e2e/integration/frontend-test/redirect.ts", "duration": 3000 }, - { "spec": "test/e2e/integration/frontend-test/return-url.ts", "duration": 12000 }, - { "spec": "test/e2e/integration/frontend-test/rules.ts", "duration": 16000 }, - { "spec": "test/e2e/integration/frontend-test/self-identified-user.ts", "duration": 1000 }, - { "spec": "test/e2e/integration/frontend-test/sort-order.ts", "duration": 15000 }, - { "spec": "test/e2e/integration/frontend-test/summary.ts", "duration": 153000 }, - { "spec": "test/e2e/integration/frontend-test/tabbing.ts", "duration": 7000 }, - { "spec": "test/e2e/integration/frontend-test/tabs.ts", "duration": 10000 }, + { "spec": "test/e2e/integration/frontend-test/party-selection.ts", "duration": 24667 }, + { "spec": "test/e2e/integration/frontend-test/pdf.ts", "duration": 167000 }, + { "spec": "test/e2e/integration/frontend-test/prefill.ts", "duration": 7333 }, + { "spec": "test/e2e/integration/frontend-test/redirect.ts", "duration": 1667 }, + { "spec": "test/e2e/integration/frontend-test/return-url.ts", "duration": 11667 }, + { "spec": "test/e2e/integration/frontend-test/rules.ts", "duration": 13333 }, + { "spec": "test/e2e/integration/frontend-test/self-identified-user.ts", "duration": 0 }, + { "spec": "test/e2e/integration/frontend-test/sort-order.ts", "duration": 12333 }, + { "spec": "test/e2e/integration/frontend-test/summary.ts", "duration": 189000 }, + { "spec": "test/e2e/integration/frontend-test/tabbing.ts", "duration": 7333 }, + { "spec": "test/e2e/integration/frontend-test/tabs.ts", "duration": 8667 }, { "spec": "test/e2e/integration/frontend-test/texts.ts", "duration": 8000 }, - { "spec": "test/e2e/integration/frontend-test/ui-settings.ts", "duration": 13000 }, - { "spec": "test/e2e/integration/frontend-test/validation.ts", "duration": 215000 }, - { "spec": "test/e2e/integration/multiple-datamodels-test/fetching.ts", "duration": 10000 }, - { "spec": "test/e2e/integration/multiple-datamodels-test/readonly.ts", "duration": 25000 }, - { "spec": "test/e2e/integration/multiple-datamodels-test/saving.ts", "duration": 33000 }, - { "spec": "test/e2e/integration/multiple-datamodels-test/validation.ts", "duration": 25000 }, - { "spec": "test/e2e/integration/navigation-test-subform/navigation.ts", "duration": 169000 }, - { "spec": "test/e2e/integration/payment-test/payment.ts", "duration": 37000 }, - { "spec": "test/e2e/integration/process-transition-test/workflow-status.ts", "duration": 282000 }, - { "spec": "test/e2e/integration/service-task/service-task.ts", "duration": 61000 }, - { "spec": "test/e2e/integration/signering-brukerstyrt/signing.ts", "duration": 31000 }, - { "spec": "test/e2e/integration/signing-test/double-signing.ts", "duration": 27000 }, - { "spec": "test/e2e/integration/signing-test/reject.ts", "duration": 19000 }, - { "spec": "test/e2e/integration/signing-test/validation.ts", "duration": 9000 }, + { "spec": "test/e2e/integration/frontend-test/ui-settings.ts", "duration": 11667 }, + { "spec": "test/e2e/integration/frontend-test/validation.ts", "duration": 201000 }, + { "spec": "test/e2e/integration/multiple-datamodels-test/fetching.ts", "duration": 11667 }, + { "spec": "test/e2e/integration/multiple-datamodels-test/readonly.ts", "duration": 27667 }, + { "spec": "test/e2e/integration/multiple-datamodels-test/saving.ts", "duration": 37667 }, + { "spec": "test/e2e/integration/multiple-datamodels-test/validation.ts", "duration": 21667 }, + { "spec": "test/e2e/integration/navigation-test-subform/navigation.ts", "duration": 140000 }, + { "spec": "test/e2e/integration/payment-test/payment.ts", "duration": 35333 }, + { "spec": "test/e2e/integration/process-transition-test/workflow-status.ts", "duration": 312333 }, + { "spec": "test/e2e/integration/service-task/service-task.ts", "duration": 53333 }, + { "spec": "test/e2e/integration/signering-brukerstyrt/existing-instance-access.ts", "duration": 6333 }, + { "spec": "test/e2e/integration/signering-brukerstyrt/signing.ts", "duration": 25000 }, + { "spec": "test/e2e/integration/signing-test/double-signing.ts", "duration": 22667 }, + { "spec": "test/e2e/integration/signing-test/reject.ts", "duration": 14667 }, + { "spec": "test/e2e/integration/signing-test/validation.ts", "duration": 7667 }, { "spec": "test/e2e/integration/stateless-app/feedback.ts", "duration": 20000 }, { "spec": "test/e2e/integration/stateless-app/instantiate-from-query-params.ts", "duration": 4000 }, - { "spec": "test/e2e/integration/stateless-app/party-selection.ts", "duration": 5000 }, - { "spec": "test/e2e/integration/stateless-app/receipt.ts", "duration": 21000 }, - { "spec": "test/e2e/integration/stateless-app/stateless.ts", "duration": 12000 }, - { "spec": "test/e2e/integration/subform-test/attachments.ts", "duration": 21000 }, - { "spec": "test/e2e/integration/subform-test/pdf.ts", "duration": 82000 }, - { "spec": "test/e2e/integration/subform-test/subform.ts", "duration": 18000 }, - { "spec": "test/e2e/integration/subform-test/tableEditButton.ts", "duration": 18000 } + { "spec": "test/e2e/integration/stateless-app/party-selection.ts", "duration": 5667 }, + { "spec": "test/e2e/integration/stateless-app/receipt.ts", "duration": 22667 }, + { "spec": "test/e2e/integration/stateless-app/stateless.ts", "duration": 13333 }, + { "spec": "test/e2e/integration/subform-test/attachments.ts", "duration": 17667 }, + { "spec": "test/e2e/integration/subform-test/pdf.ts", "duration": 60333 }, + { "spec": "test/e2e/integration/subform-test/subform.ts", "duration": 13667 }, + { "spec": "test/e2e/integration/subform-test/tableEditButton.ts", "duration": 15333 } ] }