feat(agent-actions): auto-close a contributor's PR over the open-PR cap (#2270) #5726
Workflow file for this run
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: CI | |
| on: | |
| pull_request: | |
| push: | |
| branches: | |
| - main | |
| permissions: | |
| contents: read | |
| concurrency: | |
| # Keep pull requests in one ref-scoped group so newer commits cancel superseded PR runs. | |
| # Add github.sha for push runs so distinct main commits do not cancel each other's validation. | |
| group: ci-${{ github.ref }}-${{ github.event_name == 'push' && github.sha || 'pr' }} | |
| # NB: keep this a literal boolean. An expression here (cancel-in-progress: ${{ ... }}) made GitHub | |
| # fail the workflow at startup (startup_failure), so `validate` never reported. | |
| cancel-in-progress: true | |
| jobs: | |
| # Detect which areas a PR touches so the heavy jobs can skip when irrelevant. | |
| # On push to main everything runs regardless (keeps the coverage baseline solid). | |
| changes: | |
| name: changes | |
| runs-on: ${{ fromJSON((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) && '["ubuntu-latest"]' || '["self-hosted","gittensory"]') }} | |
| timeout-minutes: 5 | |
| outputs: | |
| backend: ${{ steps.filter.outputs.backend }} | |
| ui: ${{ steps.filter.outputs.ui }} | |
| uiContract: ${{ steps.filter.outputs.uiContract }} | |
| mcp: ${{ steps.filter.outputs.mcp }} | |
| mcpCliHarness: ${{ steps.filter.outputs.mcpCliHarness }} | |
| rees: ${{ steps.filter.outputs.rees }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 | |
| with: | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| - name: Check whitespace | |
| run: git diff --check | |
| - name: Filter changed paths | |
| id: filter | |
| uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4 | |
| with: | |
| filters: | | |
| backend: | |
| - 'src/**' | |
| - 'test/**' | |
| - 'vitest*.config.ts' | |
| - 'tsconfig*.json' | |
| - 'package.json' | |
| - 'package-lock.json' | |
| - 'scripts/**' | |
| - 'migrations/**' | |
| - '.github/workflows/**' | |
| # The UI's own app/extension code -- triggers the FULL toolchain (lint/typecheck/test/build). | |
| # A dependency bump (package.json/package-lock.json) stays here too since it can break the UI | |
| # build or types in ways only that full toolchain would catch. | |
| ui: | |
| - 'apps/gittensory-ui/**' | |
| - 'apps/gittensory-extension/**' | |
| - 'scripts/build-extension.mjs' | |
| - 'package.json' | |
| - 'package-lock.json' | |
| # Backend changes that can drift the OpenAPI contract the UI type-checks against, but say | |
| # nothing about the UI app's OWN code -- only the lightweight drift check needs to run, not | |
| # ui:lint/typecheck/test/build (#ci-scope). If a backend PR's change IS UI-relevant, regenerating | |
| # `apps/gittensory-ui/public/openapi.json` per CONTRIBUTING.md lands a change under `ui` above, | |
| # which correctly re-triggers the full toolchain. | |
| uiContract: | |
| - 'src/**' | |
| - 'scripts/write-ui-openapi.ts' | |
| mcp: | |
| # Self-contained package: build is `node --check` on its own files | |
| # and the pack check only inspects the tarball. Root src/ cannot | |
| # affect it, so it is intentionally NOT a trigger here. | |
| - 'packages/gittensory-mcp/**' | |
| - 'scripts/check-mcp-package.mjs' | |
| - 'package-lock.json' | |
| # 6 of the 7 MCP CLI-cluster test files, and ONLY these 6, are truly self-contained w.r.t. root | |
| # src/**: verified by direct-import inspection that test/unit/mcp-cli-*.test.ts (5 files) and | |
| # their shared test/unit/support/mcp-cli-harness.ts import nothing but node:* builtins + vitest, | |
| # and mcp-discovery.test.ts spawns packages/gittensory-mcp/bin/gittensory-mcp.js as a real | |
| # subprocess via StdioClientTransport -- none of the 6 ever load root src/ in-process. This mirrors | |
| # the `mcp` filter's own established trust boundary above (a self-contained package build). | |
| # test/unit/mcp-output-schemas.test.ts is DELIBERATELY NOT in this filter: unlike the other 6, it | |
| # imports src/mcp/server.ts in-process, and server.ts alone directly imports ~40 other src/ modules | |
| # (src/github/app.ts, src/signals/slop.ts, src/settings/autonomy.ts, src/orb/analytics.ts, and | |
| # most of src/services/** + src/signals/**), so its real dependency surface is practically all of | |
| # `backend`. An earlier version of this filter hand-picked a handful of "the src files that affect | |
| # it" for that file too -- that guess missed dozens of server.ts's actual direct imports, which was | |
| # a reachable CI blind spot (a backend change outside the guessed list could regress MCP server | |
| # behavior with zero test rerun to catch it). Enumerating that surface by hand is not safely | |
| # possible, so mcp-output-schemas.test.ts is simply never excluded -- it always runs whenever | |
| # `backend` does, at the cost of keeping its slice of the suite's runtime unconditional. | |
| mcpCliHarness: | |
| - 'vitest*.config.ts' | |
| - 'tsconfig*.json' | |
| - 'package.json' | |
| - 'package-lock.json' | |
| - 'scripts/**' | |
| - 'migrations/**' | |
| - '.github/workflows/**' | |
| - 'packages/gittensory-mcp/**' | |
| - 'test/helpers/**' | |
| - 'test/unit/mcp-cli-*.test.ts' | |
| - 'test/unit/mcp-discovery.test.ts' | |
| - 'test/unit/support/mcp-cli-harness.ts' | |
| rees: | |
| - 'review-enrichment/**' | |
| - '.github/workflows/ci.yml' | |
| # Path-aware validation. Keep this as one self-hosted job so a PR uses one | |
| # runner slot and one dependency install instead of fanning out into several | |
| # competing installs on the same VPS. Fork PRs still run on GitHub-hosted | |
| # runners because their code is untrusted. | |
| validate-code: | |
| name: validate-code | |
| needs: changes | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' || needs.changes.outputs.mcp == 'true' || needs.changes.outputs.rees == 'true' || needs.changes.outputs.ui == 'true' }} | |
| runs-on: ${{ fromJSON((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) && '["ubuntu-latest"]' || '["self-hosted","gittensory"]') }} | |
| timeout-minutes: 45 | |
| env: | |
| VITE_GITTENSORY_API_ORIGIN: https://gittensory-api.aethereal.dev | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 | |
| with: | |
| # Full history so Codecov can resolve the merge base for patch coverage. | |
| fetch-depth: 0 | |
| ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| - name: Setup Node | |
| uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: npm | |
| # actions/checkout wipes node_modules (git clean -ffdx) on every run regardless of the self-hosted | |
| # runner's own persistence, and npm ci always deletes+reinstalls node_modules by design -- so | |
| # neither the runner nor npm ci gives node_modules any real cross-run reuse on its own. This | |
| # explicit restore/save pair (via GitHub's own cache service, not the wiped local disk) fills that | |
| # gap: an exact manifest+lockfile match skips npm ci entirely. Keyed separately per fork/trusted | |
| # (see the runs-on expression above) because self-hosted's Docker image and GitHub's ubuntu-latest | |
| # image are not guaranteed binary-compatible for native modules (sharp, workerd, fsevents, ...) -- | |
| # crossing them could load an incompatible native binary. Fork PRs get read-only cache tokens (a | |
| # documented actions/cache behavior), so a "fork"-keyed entry can never actually be written; that's | |
| # fine, it just means fork PRs keep doing a full npm ci exactly as before -- no regression, no risk | |
| # on the highest-stakes (no-retry) population. | |
| - name: Restore node_modules cache | |
| id: node-modules-cache | |
| uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: | | |
| node_modules | |
| apps/gittensory-ui/node_modules | |
| # hashFiles('.nvmrc') matters as much as the package manifests and lockfile: a Node bump | |
| # with no lockfile change would otherwise still hit and silently reuse node_modules whose | |
| # native addons (sharp, workerd, fsevents) were compiled against the OLD Node's ABI. The | |
| # manifests matter too because npm ci validates package.json/package-lock.json consistency | |
| # and runs lifecycle scripts from package.json; a package.json-only change must not skip it. | |
| key: npm-${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) && 'fork' || 'trusted' }}-${{ hashFiles('.nvmrc') }}-${{ hashFiles('package.json', 'apps/*/package.json', 'packages/*/package.json', 'package-lock.json') }} | |
| - name: Install dependencies (retry on transient failures) | |
| if: ${{ steps.node-modules-cache.outputs.cache-hit != 'true' }} | |
| run: | | |
| for attempt in 1 2 3; do | |
| if npm ci --prefer-offline --no-audit --no-fund; then | |
| exit 0 | |
| fi | |
| echo "::warning::npm ci failed (attempt ${attempt}/3); retrying in 10s" | |
| sleep 10 | |
| done | |
| echo "::error::npm ci failed after 3 attempts" | |
| exit 1 | |
| # Placed immediately after install (not as an automatic post-job hook) so a cache is only ever | |
| # saved once npm ci has actually succeeded -- a job that fails here never reaches this step, so a | |
| # broken/partial node_modules can never get written to the cache for a future run to inherit. | |
| - name: Save node_modules cache | |
| if: ${{ steps.node-modules-cache.outputs.cache-hit != 'true' }} | |
| uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: | | |
| node_modules | |
| apps/gittensory-ui/node_modules | |
| key: ${{ steps.node-modules-cache.outputs.cache-primary-key }} | |
| - name: Lint workflows | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }} | |
| run: npm run actionlint | |
| - name: Check migrations | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }} | |
| run: npm run db:migrations:check | |
| - name: Typecheck | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }} | |
| run: npm run typecheck | |
| - name: Prepare test reports dir | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }} | |
| run: mkdir -p reports/junit | |
| - name: Test with coverage | |
| id: coverage | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }} | |
| env: | |
| VITEST_JUNIT_PATH: reports/junit/vitest.xml | |
| # Push always runs the full suite (keeps the coverage baseline solid); a PR skips only the 6 | |
| # self-contained MCP CLI-harness files when their real dependency surface (the mcpCliHarness filter | |
| # above) didn't change. mcp-output-schemas.test.ts is never in the exclude list -- see that filter's | |
| # comment for why it can't be safely narrowed. | |
| SKIP_MCP_CLI_HARNESS: ${{ github.event_name == 'pull_request' && needs.changes.outputs.mcpCliHarness != 'true' }} | |
| # Pinned to the self-hosted runner's actual Docker CPU quota (4), not "100%": Node's os.cpus() | |
| # reports the HOST's full core count inside the container, ignoring the cgroup CPU limit, so | |
| # --maxWorkers=100% oversubscribed the worker pool far past what the runner can actually execute | |
| # in parallel, causing thrashing. A GitHub-hosted fork-PR runner has no such limit, but the | |
| # standard 4-core GitHub runner makes 4 the right number there too. | |
| run: | | |
| EXCLUDE_ARGS=() | |
| if [ "$SKIP_MCP_CLI_HARNESS" = "true" ]; then | |
| echo "Skipping the self-contained MCP CLI-harness tests (no packages/gittensory-mcp-relevant paths changed)." | |
| EXCLUDE_ARGS=( | |
| --exclude "test/unit/mcp-cli-*.test.ts" | |
| --exclude "test/unit/mcp-discovery.test.ts" | |
| ) | |
| fi | |
| npm run test:coverage -- --maxWorkers=4 "${EXCLUDE_ARGS[@]}" | |
| - name: Test failure guidance | |
| if: ${{ failure() && steps.coverage.conclusion == 'failure' }} | |
| run: | | |
| echo "::error title=Tests::The backend test coverage suite failed." | |
| echo "Coverage itself is gated by Codecov on changed lines (codecov/patch), computed from the complete lcov generated by this job." | |
| echo "Reproduce locally with: 'npm run test:coverage'." | |
| - name: Verify coverage report exists | |
| if: ${{ success() && (github.event_name == 'push' || needs.changes.outputs.backend == 'true') }} | |
| run: | | |
| if [ ! -s coverage/lcov.info ]; then | |
| echo "::error title=Coverage::coverage/lcov.info is missing or empty" | |
| exit 1 | |
| fi | |
| # Direct upload for trusted contexts (push + same-repo PRs), where secrets.CODECOV_TOKEN is | |
| # available. | |
| - name: Upload coverage to Codecov | |
| if: ${{ success() && (github.event_name == 'push' || needs.changes.outputs.backend == 'true') && github.event.pull_request.head.repo.fork != true }} | |
| uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: ./coverage/lcov.info | |
| disable_search: true | |
| override_branch: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref_name }} | |
| override_commit: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| override_pr: ${{ github.event_name == 'pull_request' && github.event.pull_request.number || '' }} | |
| # Coverage upload is part of the hard gate: upload or service errors | |
| # should fail CI instead of allowing a PR to merge without patch data. | |
| fail_ci_if_error: true | |
| # Fork PRs run without secrets, so they cannot use the token path above. codecov-action v4+ supports | |
| # tokenless upload for public repos, but its commit auto-detection cannot be trusted here: for | |
| # pull_request events GITHUB_SHA is the ephemeral auto-merge commit, and codecov-cli's fallback to | |
| # recover the real head sha assumes HEAD is that 2-parent merge commit -- our checkout step above | |
| # deliberately checks out github.event.pull_request.head.sha directly (so tests run the contributor's | |
| # actual commit, not a synthetic merge), so HEAD has one parent and that recovery can't fire. Pass | |
| # the same explicit overrides as the trusted upload above so the report attaches to the real PR head, | |
| # not a merge sha GitHub's PR checks list has no reason to display. | |
| # | |
| # override_branch is prefixed with the fork owner (owner:branch) -- per Codecov's own docs, only a | |
| # branch string containing a colon is recognized as "unprotected" and eligible for a tokenless | |
| # upload; a bare branch name looks like it could be a real (possibly protected) branch on the base | |
| # repo and gets rejected with "Token required because branch is protected" even with no token | |
| # configured at all. codecov-cli's own auto-detection never adds this prefix either (verified in its | |
| # source), so this must be supplied explicitly. | |
| - name: Upload coverage to Codecov (fork PR tokenless) | |
| if: ${{ success() && needs.changes.outputs.backend == 'true' && github.event.pull_request.head.repo.fork == true }} | |
| uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 | |
| with: | |
| files: ./coverage/lcov.info | |
| disable_search: true | |
| override_branch: ${{ github.event.pull_request.head.repo.owner.login }}:${{ github.event.pull_request.head.ref }} | |
| override_commit: ${{ github.event.pull_request.head.sha }} | |
| override_pr: ${{ github.event.pull_request.number }} | |
| fail_ci_if_error: true | |
| # Test results are useful Codecov annotations, not the coverage gate. Keep | |
| # their upload non-blocking so a JUnit ingestion hiccup does not fail CI | |
| # after the tests and hard coverage upload have already passed. | |
| - name: Upload Vitest results to Codecov | |
| if: ${{ !cancelled() && (github.event_name == 'push' || needs.changes.outputs.backend == 'true') && github.event.pull_request.head.repo.fork != true }} | |
| uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: ./reports/junit/vitest.xml | |
| report_type: test_results | |
| disable_search: true | |
| override_branch: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || github.ref_name }} | |
| override_commit: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} | |
| override_pr: ${{ github.event_name == 'pull_request' && github.event.pull_request.number || '' }} | |
| fail_ci_if_error: false | |
| - name: Upload Vitest results to Codecov (fork PR tokenless) | |
| if: ${{ !cancelled() && needs.changes.outputs.backend == 'true' && github.event.pull_request.head.repo.fork == true }} | |
| uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 | |
| with: | |
| files: ./reports/junit/vitest.xml | |
| report_type: test_results | |
| disable_search: true | |
| override_branch: ${{ github.event.pull_request.head.repo.owner.login }}:${{ github.event.pull_request.head.ref }} | |
| override_commit: ${{ github.event.pull_request.head.sha }} | |
| override_pr: ${{ github.event.pull_request.number }} | |
| fail_ci_if_error: false | |
| - name: Worker runtime tests | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.backend == 'true' }} | |
| run: npm run test:workers | |
| - name: Build MCP | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.mcp == 'true' }} | |
| run: npm run build:mcp | |
| - name: MCP package check | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.mcp == 'true' }} | |
| run: npm run test:mcp-pack | |
| # review-enrichment is not an npm workspace member (its own package-lock.json), so it needs its own | |
| # cache entry -- same restore/save-after-success pattern and fork/trusted key split as the root | |
| # install above, for the same reasons. | |
| - name: Restore review-enrichment node_modules cache | |
| id: rees-node-modules-cache | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.rees == 'true' }} | |
| uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: review-enrichment/node_modules | |
| # Same Node-version and manifest guards as the root cache key above -- REES runs on the same | |
| # pinned .nvmrc and has its own package.json lifecycle/install validation. | |
| key: npm-rees-${{ (github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) && 'fork' || 'trusted' }}-${{ hashFiles('.nvmrc') }}-${{ hashFiles('review-enrichment/package.json', 'review-enrichment/package-lock.json') }} | |
| - name: REES install | |
| if: ${{ (github.event_name == 'push' || needs.changes.outputs.rees == 'true') && steps.rees-node-modules-cache.outputs.cache-hit != 'true' }} | |
| run: npm run rees:install | |
| - name: Save review-enrichment node_modules cache | |
| if: ${{ (github.event_name == 'push' || needs.changes.outputs.rees == 'true') && steps.rees-node-modules-cache.outputs.cache-hit != 'true' }} | |
| uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: review-enrichment/node_modules | |
| key: ${{ steps.rees-node-modules-cache.outputs.cache-primary-key }} | |
| - name: REES build, source-map validation, and tests | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.rees == 'true' }} | |
| run: npm --prefix review-enrichment test | |
| - name: OpenAPI drift check | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' || needs.changes.outputs.uiContract == 'true' }} | |
| run: npm run ui:openapi:check | |
| # Checks apps/gittensory-ui/src' known-latest MCP version string against the published package, so | |
| # its dependency is `ui` (the file it scans) + `mcp` (the package it checks against) -- NOT the | |
| # OpenAPI contract, which this script never reads. | |
| - name: UI/MCP version audit | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' || needs.changes.outputs.mcp == 'true' }} | |
| run: npm run ui:version-audit | |
| - name: UI lint | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' }} | |
| run: npm run ui:lint | |
| - name: UI typecheck | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' }} | |
| run: npm run ui:typecheck | |
| - name: UI tests | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' }} | |
| run: npm run ui:test | |
| # `npm run ui:build` also regenerates apps/gittensory-ui/public/openapi.json (needed for a | |
| # standalone build), but this step's trigger condition is a strict subset of "OpenAPI drift | |
| # check" above (push || ui==true, vs. push || ui==true || uiContract==true), so whenever this | |
| # step runs, that check already ran and passed in this same job -- the committed spec is | |
| # already byte-identical to what regenerating it here would produce. Run ui:build's other two | |
| # steps directly instead of the aggregate script, skipping that redundant regen. | |
| - name: UI build | |
| if: ${{ github.event_name == 'push' || needs.changes.outputs.ui == 'true' }} | |
| run: npm run extension:build && npm --workspace @jsonbored/gittensory-ui run build | |
| # Diff-scoped security gate: fails only on vulnerabilities this PR introduces. | |
| # Ambient advisories in untouched deps are handled by Renovate + the scheduled | |
| # audit workflow, so one upstream CVE never blocks unrelated PRs. | |
| security: | |
| name: security | |
| if: ${{ github.event_name == 'pull_request' }} | |
| runs-on: ${{ fromJSON((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) && '["ubuntu-latest"]' || '["self-hosted","gittensory"]') }} | |
| timeout-minutes: 5 | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 | |
| - name: Dependency review | |
| uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 | |
| with: | |
| fail-on-severity: moderate | |
| comment-summary-in-pr: on-failure | |
| # Single required status check. Branch protection points at "validate"; this | |
| # aggregates the path-aware job and the PR-only dependency review gate so that | |
| # requirement keeps working unchanged. | |
| # Path-filtered jobs report "skipped", which is treated as success. | |
| validate: | |
| name: validate | |
| needs: [changes, validate-code, security] | |
| if: ${{ always() }} | |
| runs-on: ${{ fromJSON((github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true) && '["ubuntu-latest"]' || '["self-hosted","gittensory"]') }} | |
| timeout-minutes: 2 | |
| steps: | |
| - name: All required jobs passed | |
| if: ${{ !(contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled')) }} | |
| run: echo "All required CI jobs passed (path-filtered jobs reported skipped, which is OK)." | |
| - name: A required job failed | |
| if: ${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }} | |
| run: | | |
| echo "::error title=CI::A required CI job failed or was cancelled." | |
| echo "Job results: ${{ join(needs.*.result, ', ') }}" | |
| exit 1 |