🧬 Quality: Mutation Testing — Monthly schedule #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: "🧬 Quality: Mutation Testing" | |
| run-name: >- | |
| ${{ | |
| github.event_name == 'schedule' && '🧬 Quality: Mutation Testing — Monthly schedule' || | |
| format('🧬 Quality: Mutation Testing — Manual by {0}', github.actor) | |
| }} | |
| # Keep mutation testing out of the push/PR fast path. | |
| # This workflow is a monthly/manual quality signal used to find weak assertions, | |
| # dead code, and refactor candidates after merges land on the integration branch. | |
| on: | |
| workflow_dispatch: | |
| schedule: | |
| - cron: '15 6 1 * *' # Monthly on day 1 at 06:15 UTC | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: mutation-testing-${{ github.workflow }} | |
| cancel-in-progress: true | |
| jobs: | |
| stryker: | |
| name: "🧬 Quality: Stryker Advisory (${{ matrix.name }})" | |
| runs-on: ubuntu-latest | |
| environment: mutation | |
| timeout-minutes: 360 # GitHub Actions maximum; mutation testing is expensive and advisory-only | |
| continue-on-error: true # advisory by policy; review results, then file focused follow-up work | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: app-core-api-container | |
| package: app | |
| incremental_file: reports/stryker-incremental-app-core-api-container.json | |
| mutate: >- | |
| api/container/**/*.ts,api/container.ts,api/container-actions.ts,!**/*.d.ts,!**/*.test.ts,!**/*.fuzz.test.ts,!**/*.typecheck.ts,!test/**,!dist/**,!coverage/** | |
| - name: app-core-api-openapi | |
| package: app | |
| incremental_file: reports/stryker-incremental-app-core-api-openapi.json | |
| mutate: >- | |
| api/openapi/**/*.ts,api/openapi-contract.ts,api/openapi.ts,!**/*.d.ts,!**/*.test.ts,!**/*.fuzz.test.ts,!**/*.typecheck.ts,!test/**,!dist/**,!coverage/** | |
| - name: app-core-api-webhooks | |
| package: app | |
| incremental_file: reports/stryker-incremental-app-core-api-webhooks.json | |
| mutate: >- | |
| api/webhooks/**/*.ts,api/webhook.ts,api/webhooks.ts,!**/*.d.ts,!**/*.test.ts,!**/*.fuzz.test.ts,!**/*.typecheck.ts,!test/**,!dist/**,!coverage/** | |
| - name: app-core-api-auth | |
| package: app | |
| incremental_file: reports/stryker-incremental-app-core-api-auth.json | |
| mutate: >- | |
| authentications/providers/**/*.ts,api/auth*.ts,api/csrf.ts,api/destructive-confirmation.ts,api/json-content-type.ts,!**/*.d.ts,!**/*.test.ts,!**/*.fuzz.test.ts,!**/*.typecheck.ts,!test/**,!dist/**,!coverage/** | |
| - name: app-core-agent-config | |
| package: app | |
| incremental_file: reports/stryker-incremental-app-core-agent-config.json | |
| mutate: >- | |
| agent/**/*.ts,configuration/**/*.ts,index.ts,!**/*.d.ts,!**/*.test.ts,!**/*.fuzz.test.ts,!**/*.typecheck.ts,!test/**,!dist/**,!coverage/** | |
| - name: app-core-api-streaming | |
| package: app | |
| incremental_file: reports/stryker-incremental-app-core-api-streaming.json | |
| mutate: >- | |
| event/**/*.ts,api/sse*.ts,api/ws-upgrade-utils.ts,api/log-stream*.ts,log/**/*.ts,!**/*.d.ts,!**/*.test.ts,!**/*.fuzz.test.ts,!**/*.typecheck.ts,!test/**,!dist/**,!coverage/** | |
| - name: app-core-api-routes | |
| package: app | |
| incremental_file: reports/stryker-incremental-app-core-api-routes.json | |
| mutate: >- | |
| api/agent.ts,api/api.ts,api/app.ts,api/audit*.ts,api/backup.ts,api/component.ts,api/debug.ts,api/docker-trigger.ts,api/error-response.ts,api/group.ts,api/header-value.ts,api/health.ts,api/helpers.ts,api/icons.ts,api/icons/**/*.ts,api/index.ts,api/internal-self-update.ts,api/log.ts,api/notification.ts,api/pagination-links.ts,api/preview.ts,api/prometheus.ts,api/rate-limit-key.ts,api/registry.ts,api/server.ts,api/settings.ts,api/store.ts,api/trigger.ts,api/ui.ts,api/watcher.ts,!**/*.d.ts,!**/*.test.ts,!**/*.fuzz.test.ts,!**/*.typecheck.ts,!test/**,!dist/**,!coverage/** | |
| - name: app-core-domain-support | |
| package: app | |
| incremental_file: reports/stryker-incremental-app-core-domain-support.json | |
| mutate: >- | |
| debug/**/*.ts,model/**/*.ts,notifications/trigger-policy.ts,docker/legacy-label.ts,!**/*.d.ts,!**/*.test.ts,!**/*.fuzz.test.ts,!**/*.typecheck.ts,!test/**,!dist/**,!coverage/** | |
| - name: app-orch-trigger-docker | |
| package: app | |
| incremental_file: reports/stryker-incremental-app-orch-trigger-docker.json | |
| mutate: >- | |
| triggers/providers/docker/**/*.ts,!**/*.d.ts,!**/*.test.ts,!**/*.fuzz.test.ts,!**/*.typecheck.ts,!test/**,!dist/**,!coverage/** | |
| - name: app-orch-trigger-dockercompose | |
| package: app | |
| incremental_file: reports/stryker-incremental-app-orch-trigger-dockercompose.json | |
| mutate: >- | |
| triggers/providers/dockercompose/**/*.ts,!**/*.d.ts,!**/*.test.ts,!**/*.fuzz.test.ts,!**/*.typecheck.ts,!test/**,!dist/**,!coverage/** | |
| - name: app-orch-trigger-runtime | |
| package: app | |
| incremental_file: reports/stryker-incremental-app-orch-trigger-runtime.json | |
| mutate: >- | |
| triggers/providers/Trigger.ts,triggers/providers/trigger-*.ts,triggers/hooks/**/*.ts,!**/*.d.ts,!**/*.test.ts,!**/*.fuzz.test.ts,!**/*.typecheck.ts,!test/**,!dist/**,!coverage/** | |
| - name: app-orch-trigger-notify-primary | |
| package: app | |
| incremental_file: reports/stryker-incremental-app-orch-trigger-notify-primary.json | |
| mutate: >- | |
| triggers/providers/apprise/**/*.ts,triggers/providers/discord/**/*.ts,triggers/providers/googlechat/**/*.ts,triggers/providers/gotify/**/*.ts,triggers/providers/http/**/*.ts,triggers/providers/ifttt/**/*.ts,triggers/providers/kafka/**/*.ts,triggers/providers/matrix/**/*.ts,triggers/providers/mattermost/**/*.ts,!**/*.d.ts,!**/*.test.ts,!**/*.fuzz.test.ts,!**/*.typecheck.ts,!test/**,!dist/**,!coverage/** | |
| - name: app-orch-trigger-notify-secondary | |
| package: app | |
| incremental_file: reports/stryker-incremental-app-orch-trigger-notify-secondary.json | |
| mutate: >- | |
| triggers/providers/command/**/*.ts,triggers/providers/mock/**/*.ts,triggers/providers/mqtt/**/*.ts,triggers/providers/ntfy/**/*.ts,triggers/providers/pushover/**/*.ts,triggers/providers/rocketchat/**/*.ts,triggers/providers/slack/**/*.ts,triggers/providers/smtp/**/*.ts,triggers/providers/teams/**/*.ts,triggers/providers/telegram/**/*.ts,release-notes/**/*.ts,registry/**/*.ts,runtime/**/*.ts,util/**/*.ts,tag/**/*.ts,!**/*.d.ts,!**/*.test.ts,!**/*.fuzz.test.ts,!**/*.typecheck.ts,!test/**,!dist/**,!coverage/** | |
| - name: app-ops-watchers-runtime | |
| package: app | |
| incremental_file: reports/stryker-incremental-app-ops-watchers-runtime.json | |
| mutate: >- | |
| watchers/providers/docker/Docker.ts,watchers/providers/docker/Docker.containers.test.helpers.ts,watchers/providers/docker/container-*.ts,watchers/providers/docker/container-processing.ts,watchers/providers/docker/docker-helpers.ts,watchers/providers/docker/docker-image-details-orchestration.ts,watchers/providers/docker/image-comparison.ts,watchers/providers/docker/label.ts,watchers/providers/docker/runtime-details.ts,watchers/providers/docker/tag-candidates.ts,!**/*.d.ts,!**/*.test.ts,!**/*.fuzz.test.ts,!**/*.typecheck.ts,!test/**,!dist/**,!coverage/** | |
| - name: app-ops-watchers-events | |
| package: app | |
| incremental_file: reports/stryker-incremental-app-ops-watchers-events.json | |
| mutate: >- | |
| watchers/Watcher.ts,watchers/registry-webhook-fresh.ts,watchers/providers/docker/digest-cache-lifecycle.ts,watchers/providers/docker/disable-socket-redirects.ts,watchers/providers/docker/docker-event-orchestration.ts,watchers/providers/docker/docker-events.ts,watchers/providers/docker/docker-remote-auth.ts,watchers/providers/docker/fallback-logger.ts,watchers/providers/docker/maintenance.ts,watchers/providers/docker/oidc.ts,watchers/providers/docker/recent-events.ts,watchers/providers/docker/release-notes-enrichment.ts,watchers/providers/docker/socket-version-probe.ts,!**/*.d.ts,!**/*.test.ts,!**/*.fuzz.test.ts,!**/*.typecheck.ts,!test/**,!dist/**,!coverage/** | |
| - name: app-ops-store-security | |
| package: app | |
| incremental_file: reports/stryker-incremental-app-ops-store-security.json | |
| mutate: >- | |
| store/**/*.ts,security/**/*.ts,!**/*.d.ts,!**/*.test.ts,!**/*.fuzz.test.ts,!**/*.typecheck.ts,!test/**,!dist/**,!coverage/** | |
| - name: app-ops-registries-core | |
| package: app | |
| incremental_file: reports/stryker-incremental-app-ops-registries-core.json | |
| mutate: >- | |
| registries/BaseRegistry.ts,registries/Registry.ts,registries/configuration.ts,registries/providers/custom/**/*.ts,registries/providers/shared/**/*.ts,!**/*.d.ts,!**/*.test.ts,!**/*.fuzz.test.ts,!**/*.typecheck.ts,!test/**,!dist/**,!coverage/** | |
| - name: app-ops-registries-remote | |
| package: app | |
| incremental_file: reports/stryker-incremental-app-ops-registries-remote.json | |
| mutate: >- | |
| registries/providers/acr/**/*.ts,registries/providers/alicr/**/*.ts,registries/providers/artifactory/**/*.ts,registries/providers/codeberg/**/*.ts,registries/providers/dhi/**/*.ts,registries/providers/docr/**/*.ts,registries/providers/ecr/**/*.ts,registries/providers/forgejo/**/*.ts,registries/providers/gar/**/*.ts,registries/providers/gcr/**/*.ts,registries/providers/ghcr/**/*.ts,registries/providers/gitea/**/*.ts,registries/providers/gitlab/**/*.ts,registries/providers/harbor/**/*.ts,registries/providers/hub/**/*.ts,registries/providers/ibmcr/**/*.ts,registries/providers/lscr/**/*.ts,registries/providers/mau/**/*.ts,registries/providers/nexus/**/*.ts,registries/providers/ocir/**/*.ts,registries/providers/quay/**/*.ts,registries/providers/trueforge/**/*.ts,stats/**/*.ts,prometheus/**/*.ts,!**/*.d.ts,!**/*.test.ts,!**/*.fuzz.test.ts,!**/*.typecheck.ts,!test/**,!dist/**,!coverage/** | |
| - name: ui-shell-and-data | |
| package: ui | |
| incremental_file: reports/stryker-incremental-ui-shell-and-data.json | |
| mutate: >- | |
| src/boot/**/*.ts,src/components/**/*.ts,src/composables/**/*.ts,src/layouts/**/*.ts,src/preferences/**/*.ts,src/services/**/*.ts,src/theme/**/*.ts,src/types/**/*.ts,src/utils/**/*.ts,src/main.ts,!**/*.d.ts,!**/*.test.ts,!**/*.fuzz.test.ts,!**/*.typecheck.ts,!dist/**,!coverage/** | |
| - name: ui-views-and-navigation | |
| package: ui | |
| incremental_file: reports/stryker-incremental-ui-views-and-navigation.json | |
| mutate: >- | |
| src/views/**/*.ts,src/directives/**/*.ts,src/router/**/*.ts,src/icons.ts,!**/*.d.ts,!**/*.test.ts,!**/*.fuzz.test.ts,!**/*.typecheck.ts,!dist/**,!coverage/** | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Node.js | |
| uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 | |
| with: | |
| node-version: 24 | |
| package-manager-cache: false | |
| - name: Install dependencies | |
| run: npm ci | |
| working-directory: ${{ matrix.package }} | |
| - name: Restore Stryker incremental cache | |
| uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 | |
| with: | |
| path: ${{ matrix.package }}/${{ matrix.incremental_file }} | |
| key: stryker-incremental-${{ matrix.name }}-${{ github.ref_name }} | |
| restore-keys: | | |
| stryker-incremental-${{ matrix.name }}- | |
| - name: Run Stryker | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| args=(run --incrementalFile "${{ matrix.incremental_file }}") | |
| if [[ -n "${{ matrix.mutate }}" ]]; then | |
| args+=(--mutate "${{ matrix.mutate }}") | |
| fi | |
| ./node_modules/.bin/stryker "${args[@]}" | |
| working-directory: ${{ matrix.package }} | |
| - name: Summarize advisory mode | |
| if: always() | |
| run: | | |
| { | |
| echo "### Mutation Testing" | |
| echo "- Mode: ADVISORY (non-blocking)." | |
| echo "- Trigger policy: monthly schedule plus manual dispatch only; not part of push or PR CI." | |
| echo "- Scope: ${{ matrix.name }}." | |
| echo "- Expected use: review surviving mutants and initial dry-run failures, then create focused follow-up work for real test gaps or brittle logic." | |
| echo "- Dashboard badge publish: handled by the aggregate post-processing job after all shard artifacts are available." | |
| echo "- Merge policy: do not block merges on mutation score." | |
| echo "- Promotion criteria to stricter enforcement: clean dry-runs in every package, stable CI variance, and explicit team agreement." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| - name: Upload mutation report | |
| if: always() | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: mutation-report-${{ matrix.name }}-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: ${{ matrix.package }}/reports/mutation | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| aggregate: | |
| name: "🧬 Quality: Aggregate Mutation Score" | |
| needs: stryker | |
| if: always() | |
| runs-on: ubuntu-latest | |
| environment: mutation | |
| continue-on-error: true | |
| env: | |
| STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} | |
| steps: | |
| - name: Harden Runner | |
| uses: step-security/harden-runner@fa2e9d605c4eeb9fcad4c99c224cee0c6c7f3594 # v2.16.0 | |
| with: | |
| egress-policy: audit | |
| - name: Checkout | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Setup Node.js | |
| uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 | |
| with: | |
| node-version: 24 | |
| package-manager-cache: false | |
| - name: Download mutation artifacts | |
| uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | |
| with: | |
| path: artifacts/mutation | |
| - name: Aggregate shard scores | |
| run: | | |
| node scripts/aggregate-stryker-score.mjs \ | |
| --input artifacts/mutation \ | |
| --expected-count 20 \ | |
| --summary-out artifacts/aggregate/summary.json \ | |
| --score-out artifacts/aggregate/mutation-score.json | |
| - name: Summarize aggregate score | |
| if: always() | |
| shell: bash | |
| run: | | |
| if [[ -f artifacts/aggregate/summary.json ]]; then | |
| node scripts/aggregate-stryker-score.mjs \ | |
| --summarize artifacts/aggregate/summary.json \ | |
| >> "$GITHUB_STEP_SUMMARY" | |
| else | |
| { | |
| echo "### Aggregate Mutation Score" | |
| echo "- Aggregate score unavailable because one or more shard reports were missing." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| - name: Publish aggregate mutation score to dashboard | |
| if: success() && env.STRYKER_DASHBOARD_API_KEY != '' | |
| env: | |
| REPO_SLUG: ${{ github.repository }} | |
| REF_NAME: ${{ github.ref_name }} | |
| run: | | |
| curl \ | |
| --fail-with-body \ | |
| --request PUT \ | |
| --header "X-Api-Key: ${STRYKER_DASHBOARD_API_KEY}" \ | |
| --header "Content-Type: application/json" \ | |
| --data @artifacts/aggregate/mutation-score.json \ | |
| "https://dashboard.stryker-mutator.io/api/reports/github.com/${REPO_SLUG}/${REF_NAME}" | |
| - name: Upload aggregate mutation summary | |
| if: always() | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: mutation-aggregate-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: artifacts/aggregate | |
| if-no-files-found: warn | |
| retention-days: 14 |