diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d43f7759e5..988f844aaf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,6 +92,9 @@ jobs: lint: name: Lint runs-on: ubuntu-latest + # P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam + # drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados). + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} env: # tsx gates below (known-symbols, route-guard-membership) import modules that # open SQLite on load; provide DB env so a fresh CI DB initializes cleanly. @@ -148,7 +151,7 @@ jobs: # release PR #4854, where the drift cascade only surfaced post-merge in #5029). # The coverage.* metrics degrade gracefully: the download is continue-on-error and # the ratchet runs with --allow-missing, so absent coverage is skipped, not failed. - if: ${{ !cancelled() }} + if: ${{ !cancelled() && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }} # security-events: read lets the CodeQL ratchet read open code-scanning alerts # via `gh api .../code-scanning/alerts`. contents: read keeps checkout working. permissions: @@ -240,6 +243,9 @@ jobs: quality-extended: name: Quality Gates (Extended) runs-on: ubuntu-latest + # P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam + # drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados). + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} steps: # fetch-depth: 0 — the OpenAPI breaking-change gate (oasdiff) reads the base # spec via `git show :docs/openapi.yaml`; a shallow clone @@ -347,6 +353,9 @@ jobs: docs-sync-strict: name: Docs Sync (Strict) runs-on: ubuntu-latest + # P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam + # drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados). + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} steps: - uses: actions/checkout@v7 with: @@ -376,6 +385,9 @@ jobs: docs-lint: name: Docs Lint (prose — advisory) runs-on: ubuntu-latest + # P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam + # drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados). + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} # Advisory (warning-first): prose/markdown style must not block merges while the # existing doc corpus is brought up to style. Promote to blocking once it converges. continue-on-error: true @@ -402,6 +414,9 @@ jobs: i18n-ui-coverage: name: i18n UI Coverage runs-on: ubuntu-latest + # P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam + # drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados). + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} steps: - uses: actions/checkout@v7 with: @@ -413,30 +428,18 @@ jobs: - uses: ./.github/actions/npm-ci-retry - run: node scripts/i18n/check-ui-keys-coverage.mjs --threshold=65 - i18n-matrix: - name: Build language matrix - runs-on: ubuntu-latest - outputs: - langs: ${{ steps.langs.outputs.langs }} - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - id: langs - run: | - LANG_DIR="src/i18n/messages" - LANGS=$(ls "$LANG_DIR"/*.json | xargs -n1 basename | sed 's/.json$//' | grep -v '^en$' | jq -R . | jq -s . | jq -c .) - echo "langs=${LANGS}" >> "$GITHUB_OUTPUT" - + # D4 (plano mestre testes+CI): a matrix de ~40 jobs de <1min por idioma saturava sozinha + # a concorrência de jobs da conta (Free = 20 slots, compartilhados entre TODOS os repos) + # e pagava spin-up + arredondamento de billing por idioma. Um único job itera os idiomas + # (mesmo script), com grupo de log por idioma e artifact único de resultados nomeados por + # idioma (a matrix antiga subia 40 artifacts cujo result.txt colidia no merge-multiple). i18n: - name: i18n Validation + name: i18n Validation (all languages) runs-on: ubuntu-latest + # P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam + # drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados). + if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }} continue-on-error: true - strategy: - fail-fast: false - matrix: - lang: ${{ fromJson(needs.i18n-matrix.outputs.langs) }} - needs: i18n-matrix steps: - uses: actions/checkout@v7 with: @@ -444,27 +447,35 @@ jobs: - uses: actions/setup-python@v6 with: python-version: "3.12" - - - name: Validate ${{ matrix.lang }} - env: - # Pass the matrix value via env (never interpolate ${{ ... }} straight - # into the run: script body) so the shell receives a variable, not - # inlined text — zizmor template-injection mitigation. Named MATRIX_LANG - # to avoid clobbering the POSIX `LANG` locale variable. - MATRIX_LANG: ${{ matrix.lang }} + - name: Validate all languages run: | - python3 scripts/i18n/validate_translation.py quick -l "$MATRIX_LANG" > result.txt - - - name: Upload result + set -uo pipefail + mkdir -p i18n-results + FAIL=0 + for f in src/i18n/messages/*.json; do + lang=$(basename "$f" .json) + [ "$lang" = "en" ] && continue + echo "::group::i18n $lang" + if python3 scripts/i18n/validate_translation.py quick -l "$lang" > "i18n-results/$lang.txt" 2>&1; then + echo "OK $lang" + else + FAIL=1 + echo "FAIL $lang" + cat "i18n-results/$lang.txt" + fi + echo "::endgroup::" + done + exit "$FAIL" + - name: Upload results if: always() uses: actions/upload-artifact@v7 with: - name: i18n-${{ matrix.lang }} - path: result.txt + name: i18n-results + path: i18n-results/ pr-test-policy: name: PR Test Policy - if: ${{ github.event_name == 'pull_request' }} + if: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == false }} runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 @@ -497,7 +508,7 @@ jobs: name: Build runs-on: ubuntu-latest needs: changes - if: ${{ github.event_name != 'pull_request' || needs.changes.outputs.code == 'true' }} + if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }} steps: - uses: actions/checkout@v7 with: @@ -604,7 +615,7 @@ jobs: test-unit: name: Unit Tests (${{ matrix.shard }}/8) runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 25 needs: build strategy: fail-fast: false @@ -626,9 +637,28 @@ jobs: - run: npm run check:node-runtime # QW-d (plano mestre): fonte única — o MESMO npm script dos runs locais (adiciona o # setupPolyfill que o comando inline omitia; o glob canônico vive só no package.json). - - run: npm run test:unit:ci:shard + # D3 (plano mestre): a coverage é coletada NESTE mesmo run (c8/NODE_V8_COVERAGE propaga + # aos filhos através do npm) — elimina a matrix Coverage Shard ×8, que re-executava a + # suíte inteira só para medir o gate. Padrão usado pelo CI do próprio nodejs/node. + - name: Unit tests (shard ${{ matrix.shard }}/8) with V8 coverage env: TEST_SHARD: ${{ matrix.shard }}/8 + run: | + rm -rf coverage-shard coverage-shard-report + npx c8 \ + --temp-directory=coverage-shard \ + --reports-dir=coverage-shard-report \ + --reporter=json \ + --exclude=tests/** \ + --exclude=**/*.test.* \ + npm run test:unit:ci:shard + - name: Upload raw shard coverage + if: always() + uses: actions/upload-artifact@v7 + with: + name: coverage-shard-${{ matrix.shard }} + path: coverage-shard/*.json + if-no-files-found: error test-vitest: name: Vitest (MCP / autoCombo / UI components) @@ -657,147 +687,16 @@ jobs: - run: npm run test:vitest:ui continue-on-error: true - node-24-compat: - name: Node 24 Compatibility Tests (${{ matrix.shard }}/4) - runs-on: ubuntu-latest - timeout-minutes: 20 - needs: build - strategy: - fail-fast: false - matrix: - shard: [1, 2, 3, 4] - env: - JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation - API_KEY_SECRET: ci-test-api-key-secret-long - DISABLE_SQLITE_AUTO_BACKUP: "true" - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: ${{ env.CI_NODE_24_VERSION }} - cache: npm - - uses: ./.github/actions/npm-ci-retry - - run: npm run check:node-runtime - # QW-d: fonte única — mesmo npm script dos runs locais (inclui setupPolyfill; glob no package.json). - - run: npm run test:unit:ci:shard - env: - TEST_SHARD: ${{ matrix.shard }}/4 - - node-26-compat-build: - name: Node 26 Compatibility Build - runs-on: ubuntu-latest - timeout-minutes: 25 - needs: build - env: - JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation - API_KEY_SECRET: ci-test-api-key-secret-long - DISABLE_SQLITE_AUTO_BACKUP: "true" - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: ${{ env.CI_NODE_26_VERSION }} - cache: npm - - uses: ./.github/actions/npm-ci-retry - - run: npm run check:node-runtime - - name: Cache Next.js build cache - uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 - with: - path: .build/next/cache - key: nextjs-${{ runner.os }}-node-${{ env.CI_NODE_26_VERSION }}-${{ hashFiles('package-lock.json') }}-${{ hashFiles('src/**/*', 'open-sse/**/*', 'db/**/*', 'next.config.mjs', 'tsconfig*.json', 'postcss.config.*', 'tailwind.config.*') }} - restore-keys: | - nextjs-${{ runner.os }}-node-${{ env.CI_NODE_26_VERSION }}-${{ hashFiles('package-lock.json') }}- - - run: npm run build - - node-26-compat: - name: Node 26 Compatibility Tests (${{ matrix.shard }}/4) - runs-on: ubuntu-latest - timeout-minutes: 20 - needs: node-26-compat-build - strategy: - fail-fast: false - matrix: - shard: [1, 2, 3, 4] - env: - JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation - API_KEY_SECRET: ci-test-api-key-secret-long - DISABLE_SQLITE_AUTO_BACKUP: "true" - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: ${{ env.CI_NODE_26_VERSION }} - cache: npm - - uses: ./.github/actions/npm-ci-retry - - run: npm run check:node-runtime - # QW-d: fonte única — mesmo npm script dos runs locais (inclui setupPolyfill; glob no package.json). - - run: npm run test:unit:ci:shard - env: - TEST_SHARD: ${{ matrix.shard }}/4 - - test-coverage-shard: - name: Coverage Shard (${{ matrix.shard }}/8) - runs-on: ubuntu-latest - timeout-minutes: 25 - needs: build - strategy: - fail-fast: false - matrix: - shard: [1, 2, 3, 4, 5, 6, 7, 8] - env: - JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation - API_KEY_SECRET: ci-test-api-key-secret-long - DISABLE_SQLITE_AUTO_BACKUP: "true" - steps: - - uses: actions/checkout@v7 - with: - persist-credentials: false - - uses: actions/setup-node@v6 - with: - node-version: ${{ env.CI_NODE_VERSION }} - cache: npm - - uses: ./.github/actions/npm-ci-retry - - run: npm run check:node-runtime - - name: Run c8 over shard ${{ matrix.shard }}/8 - env: - TEST_SHARD: ${{ matrix.shard }}/8 - run: | - rm -rf coverage-shard coverage-shard-report - # `--temp-directory` (writable via NODE_V8_COVERAGE) is what the merge - # job reads with `c8 report --temp-directory ...`. Using `--output-dir` - # only produces the final json *report* and leaves the raw v8 files in - # `coverage/tmp`, so uploading `coverage-shard/` was empty. Pin the temp - # dir so the raw coverage files live there and the artifact upload picks - # them up regardless of `--test-force-exit` timing. - # QW-d: o comando de teste é o MESMO npm script dos runs locais (c8 propaga - # NODE_V8_COVERAGE aos filhos através do npm; node_modules é excluído por default). - npx c8 \ - --temp-directory=coverage-shard \ - --reports-dir=coverage-shard-report \ - --reporter=json \ - --exclude=tests/** \ - --exclude=**/*.test.* \ - npm run test:unit:ci:shard - - name: Upload raw shard coverage - if: always() - uses: actions/upload-artifact@v7 - with: - name: coverage-shard-${{ matrix.shard }} - path: coverage-shard/*.json - if-no-files-found: error - + # Node 24/26 compatibility matrices moved to .github/workflows/nightly-compat.yml + # (plano mestre testes+CI, Eixo D2 — they cost ~28% of every heavy run to catch a + # failure class that rarely originates in a PR; nightly catches it within 24h and + # the release gate can still exercise them via workflow_dispatch when needed). test-coverage: name: Coverage runs-on: ubuntu-latest timeout-minutes: 10 - needs: test-coverage-shard - if: ${{ !cancelled() && needs.test-coverage-shard.result == 'success' }} + needs: test-unit + if: ${{ !cancelled() && needs.test-unit.result == 'success' }} env: JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation API_KEY_SECRET: ci-test-api-key-secret-long @@ -928,7 +827,7 @@ jobs: coverage-pr-comment: name: PR Coverage Comment runs-on: ubuntu-latest - if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false && needs.changes.outputs.code == 'true' }} + if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.draft == false && github.event.pull_request.head.repo.fork == false && needs.changes.outputs.code == 'true' }} needs: - changes - pr-test-policy @@ -1112,9 +1011,6 @@ jobs: - package-artifact - electron-package-smoke - test-unit - - node-24-compat - - node-26-compat-build - - node-26-compat - test-coverage - sonarqube - coverage-pr-comment @@ -1164,15 +1060,12 @@ jobs: echo "| Build Matrix | $(status '${{ needs.build.result }}') |" >> "$GITHUB_STEP_SUMMARY" echo "| Package Artifact | $(status '${{ needs.package-artifact.result }}') |" >> "$GITHUB_STEP_SUMMARY" echo "| Electron Package Smoke | $(status '${{ needs.electron-package-smoke.result }}') |" >> "$GITHUB_STEP_SUMMARY" - echo "| Node 26 Compatibility Build | $(status '${{ needs.node-26-compat-build.result }}') |" >> "$GITHUB_STEP_SUMMARY" echo "" >> "$GITHUB_STEP_SUMMARY" echo "## 🧪 Tests" >> "$GITHUB_STEP_SUMMARY" echo "| Suite | Status |" >> "$GITHUB_STEP_SUMMARY" echo "|-------|--------|" >> "$GITHUB_STEP_SUMMARY" echo "| Unit | $(status '${{ needs.test-unit.result }}') |" >> "$GITHUB_STEP_SUMMARY" - echo "| Node 24 Compatibility Tests | $(status '${{ needs.node-24-compat.result }}') |" >> "$GITHUB_STEP_SUMMARY" - echo "| Node 26 Compatibility Tests | $(status '${{ needs.node-26-compat.result }}') |" >> "$GITHUB_STEP_SUMMARY" echo "| Coverage | $(status '${{ needs.test-coverage.result }}') |" >> "$GITHUB_STEP_SUMMARY" echo "| PR Coverage Comment | $(status '${{ needs.coverage-pr-comment.result }}') |" >> "$GITHUB_STEP_SUMMARY" echo "| E2E | $(status '${{ needs.test-e2e.result }}') |" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/nightly-compat.yml b/.github/workflows/nightly-compat.yml new file mode 100644 index 0000000000..2abbb149d9 --- /dev/null +++ b/.github/workflows/nightly-compat.yml @@ -0,0 +1,126 @@ +name: Nightly Node Compat + +# Plano mestre testes+CI (Eixo D2, aprovado 2026-07-04): as matrizes de compatibilidade +# Node 24/26 custavam ~28% de CADA run do CI pesado (2 execuções completas da suíte por +# sync da release-PR) para pegar uma classe de quebra que raramente nasce num PR típico. +# Elas rodam aqui 1×/dia contra o tip da release ativa (mesmo alvo do nightly-release-green) +# e continuam obrigatórias no gate de release via workflow_dispatch do ci.yml se preciso. +# fail-fast desligado: numa quebra queremos saber TODAS as versões afetadas de uma vez. + +on: + schedule: + - cron: "47 6 * * *" # 06:47 UTC diário — slot distinto dos demais nightlies + workflow_dispatch: + inputs: + branch: + description: "Branch to validate (default: highest release/vX.Y.Z)" + required: false + type: string + +permissions: + contents: read + issues: write + +concurrency: + group: nightly-compat + cancel-in-progress: true + +jobs: + resolve-branch: + name: Resolve active release branch + runs-on: ubuntu-latest + outputs: + target: ${{ steps.branch.outputs.target }} + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + persist-credentials: false + - name: Resolve active release branch + id: branch + env: + INPUT_BRANCH: ${{ github.event.inputs.branch }} + run: | + set -euo pipefail + if [ -n "${INPUT_BRANCH:-}" ]; then + TARGET="$INPUT_BRANCH" + else + TARGET=$(git for-each-ref --format='%(refname:short)' 'refs/remotes/origin/release/v*' \ + | sed 's#origin/##' \ + | sort -t/ -k2 -V \ + | tail -1) + fi + case "$TARGET" in + release/v[0-9]*.[0-9]*.[0-9]*) ;; + *) echo "Refusing non-canonical branch name: $TARGET"; exit 1 ;; + esac + echo "target=$TARGET" >> "$GITHUB_OUTPUT" + + compat-build-26: + name: Node 26 Compatibility Build + runs-on: ubuntu-latest + timeout-minutes: 25 + needs: resolve-branch + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ needs.resolve-branch.outputs.target }} + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: "26" + cache: npm + - uses: ./.github/actions/npm-ci-retry + - run: npm run build + + compat-tests: + name: Node ${{ matrix.node }} Compat Tests (${{ matrix.shard }}/4) + runs-on: ubuntu-latest + timeout-minutes: 25 + needs: resolve-branch + strategy: + fail-fast: false + matrix: + node: [24, 26] + shard: [1, 2, 3, 4] + env: + JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation + API_KEY_SECRET: ci-nightly-api-key-secret-long + DISABLE_SQLITE_AUTO_BACKUP: "true" + TEST_SHARD: ${{ matrix.shard }}/4 + steps: + - uses: actions/checkout@v7 + with: + ref: ${{ needs.resolve-branch.outputs.target }} + persist-credentials: false + - uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node }} + cache: npm + - uses: ./.github/actions/npm-ci-retry + - run: npm run check:node-runtime + - run: npm run test:unit:ci:shard + + report: + name: Open / update tracking issue on failure + runs-on: ubuntu-latest + if: ${{ !cancelled() && (needs.compat-tests.result == 'failure' || needs.compat-build-26.result == 'failure') }} + needs: [resolve-branch, compat-build-26, compat-tests] + permissions: + issues: write + steps: + - name: Open or update issue + env: + GH_TOKEN: ${{ github.token }} + TARGET: ${{ needs.resolve-branch.outputs.target }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + set -euo pipefail + TITLE="🌙 nightly-compat: Node 24/26 failures on $TARGET" + EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "$TITLE in:title" --json number --jq '.[0].number') + BODY="Nightly Node-compat run failed on \`$TARGET\`: $RUN_URL — triage which Node version/shard broke (fail-fast off, all versions reported)." + if [ -n "$EXISTING" ]; then + gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" --body "$BODY" + else + gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --body "$BODY" + fi