fix(server): align agent runtime with Pi 0.84.3 #3950
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/CD | |
| on: | |
| workflow_dispatch: | |
| push: | |
| branches: ["main"] | |
| pull_request: | |
| branches: ["**"] | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| detect-changes: | |
| name: "Detect changes" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pull-requests: read | |
| contents: read | |
| outputs: | |
| webapp: ${{ steps.filter.outputs.webapp }} | |
| webapp-image: ${{ steps.filter.outputs.webapp-image == 'true' || steps.webapp_image_source.outputs.webapp-image-source == 'true' }} | |
| application-server: ${{ steps.filter.outputs.application-server }} | |
| application-server-image: ${{ steps.filter.outputs.application-server-image }} | |
| e2e: ${{ steps.filter.outputs.e2e }} | |
| agent-images: ${{ steps.filter.outputs.agent-images }} | |
| release-pin-fetcher: ${{ steps.filter.outputs.release-pin-fetcher }} | |
| postgres-image: ${{ steps.filter.outputs.postgres-image }} | |
| docs: ${{ steps.filter.outputs.docs }} | |
| ci-config: ${{ steps.filter.outputs.ci-config }} | |
| docker-config: ${{ steps.filter.outputs.docker-config }} | |
| pmd-canary: ${{ steps.filter.outputs.pmd-canary }} | |
| any-code: ${{ steps.filter.outputs.webapp == 'true' || steps.filter.outputs.application-server == 'true' || steps.filter.outputs.agent-images == 'true' }} | |
| should_skip: ${{ steps.skip_check.outputs.should_skip }} | |
| timeout-minutes: 5 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - id: skip_check | |
| uses: fkirc/skip-duplicate-actions@b974a9395958c231af965b70070979a577efa578 # v5.3.2 | |
| with: | |
| do_not_skip: '["workflow_dispatch", "push", "merge_group"]' | |
| - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 | |
| id: filter | |
| with: | |
| filters: | | |
| webapp: | |
| - 'webapp/**' | |
| - 'docs/images/readme/**' | |
| # webapp/.oxlintrc.json carries no `options` block and webapp/biome.jsonc declares | |
| # `root: false`, so `typeAware`, `reportUnusedDisableDirectives` and the Biome graph | |
| # root all come from these two files — they decide the webapp's verdict from outside | |
| # webapp/. | |
| - '.oxlintrc.json' | |
| - 'biome.jsonc' | |
| # check:components, check:stories and check:story-sort live here, run on the Webapp leg. | |
| - 'scripts/**' | |
| - 'package.json' | |
| - 'pnpm-lock.yaml' | |
| - 'pnpm-workspace.yaml' | |
| - '.npmrc' | |
| - '.node-version' | |
| webapp-image: | |
| - 'webapp/public/**' | |
| - 'webapp/docker/**' | |
| - 'webapp/Dockerfile' | |
| - 'webapp/index.html' | |
| - 'webapp/package.json' | |
| - 'webapp/tsconfig.json' | |
| - 'webapp/vite.config.ts' | |
| - 'webapp/vite.shared.ts' | |
| - 'docs/package.json' | |
| - 'package.json' | |
| - 'pnpm-lock.yaml' | |
| - 'pnpm-workspace.yaml' | |
| - '.npmrc' | |
| application-server: | |
| - 'server/**' | |
| # check:env, check:contracts, check:diagrams and db-utils.sh all live here and run on | |
| # the App Server or Database leg. | |
| - 'scripts/**' | |
| # test:agents and typecheck:agents cover the precompute tree and are configured here; | |
| # the Bun they run on is read from the agent image. | |
| - 'docker/agents/**' | |
| - 'tsconfig.agents.json' | |
| # The lint/format gate for every non-webapp TypeScript tree is assembled from all five: | |
| # oxlint's rules, Biome's rules, the `:agents` scripts that invoke them, the pinned | |
| # binaries, and the root tsconfig — which exists only to give the Bun trees, configured | |
| # by tsconfig.agents.json, the project that oxlint's type-aware rules require. | |
| - '.oxlintrc.json' | |
| - 'biome.jsonc' | |
| - 'tsconfig.json' | |
| - 'package.json' | |
| - 'pnpm-lock.yaml' | |
| # Linted by the same leg (docs/.oxlintrc.json decides that verdict), markdown-linted by | |
| # docs:lint, and parsed by check:diagrams. The `docs` filter below feeds no job, so this | |
| # entry is the only thing that puts a docs-only PR into `any-code`. | |
| - 'docs/**' | |
| - '.changeset/*.cjs' | |
| - 'commitlint.config.ts' | |
| - '.github/scripts/**' | |
| # check:biome-pin compares this file's $schema against the pinned version. | |
| - 'webapp/biome.jsonc' | |
| # check:env compares application.yml against the deployment that has to deliver it. | |
| - 'docker/compose.app.yaml' | |
| - 'docker/compose.core.yaml' | |
| # check:instructions runs on this leg and reads every one of these. The two markdown | |
| # globs are named even though webapp/** and server/** already carry them, because | |
| # webapp/** is behind the *webapp* filter — a PR touching only webapp/CLAUDE.md would | |
| # otherwise run the leg that does not hold this gate. | |
| - '**/AGENTS.md' | |
| - '**/CLAUDE.md' | |
| - '.claude/**' | |
| - '.opencode/**' | |
| - '.agents/**' | |
| - 'opencode.json' | |
| application-server-image: | |
| - 'server/src/main/**' | |
| - 'server/pom.xml' | |
| - 'server/mvnw' | |
| - 'server/.mvn/**' | |
| e2e: | |
| - 'webapp/e2e/**' | |
| - 'webapp/src/**' | |
| - 'webapp/public/**' | |
| - 'webapp/index.html' | |
| - 'webapp/package.json' | |
| - 'webapp/playwright.config.ts' | |
| - 'server/src/main/**' | |
| - 'server/pom.xml' | |
| - 'server/compose.yaml' | |
| - 'docker/postgres/**' | |
| - 'package.json' | |
| - 'pnpm-lock.yaml' | |
| - 'pnpm-workspace.yaml' | |
| - '.npmrc' | |
| - '.node-version' | |
| - '.github/workflows/cicd.yml' | |
| - '.github/workflows/ci-tests.yml' | |
| - '.github/actions/setup-caches/**' | |
| - '.github/actions/setup-pnpm-node/**' | |
| agent-images: | |
| - 'docker/agents/**' | |
| release-pin-fetcher: | |
| - 'docker/release-pin-fetcher/**' | |
| postgres-image: | |
| - 'docker/postgres/**' | |
| docs: | |
| - 'docs/**' | |
| ci-config: | |
| - '.github/workflows/**' | |
| - '.github/actions/**' | |
| docker-config: | |
| - '.github/workflows/ci-docker-build.yml' | |
| - '.github/workflows/reusable-docker-build.yml' | |
| pmd-canary: | |
| - 'server/pmd-ruleset.xml' | |
| - 'server/pom.xml' | |
| - 'package.json' | |
| - 'scripts/run-mvnw.ts' | |
| - '.github/workflows/ci-quality-gates.yml' | |
| - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 | |
| id: webapp_image_source | |
| with: | |
| filters: | | |
| webapp-image-source: | |
| - 'webapp/src/**' | |
| - '!webapp/src/**/*.test.*' | |
| - '!webapp/src/**/*.stories.*' | |
| - '!webapp/src/test/**' | |
| # Coolify creates previews for trusted same-repository pull requests. This job waits for the | |
| # immutable application-server image, then updates the preview to the exact head commit. Coolify's | |
| # deploy API only accepts a pull request that already has a preview, so a missing preview remains a | |
| # safe no-op. Instance identifiers come from repository variables; forks never receive credentials. | |
| preview: | |
| name: "Preview / Coolify" | |
| runs-on: ubuntu-latest | |
| # Wait for Docker so SOURCE_COMMIT always names an image that already exists in GHCR. `always` | |
| # preserves the link/no-op behavior when the Docker workflow is legitimately skipped. | |
| needs: [detect-changes, Docker] | |
| if: >- | |
| always() && | |
| github.event_name == 'pull_request' && | |
| vars.COOLIFY_URL != '' && | |
| vars.COOLIFY_APP_UUID != '' && | |
| (needs.Docker.result == 'success' || needs.Docker.result == 'skipped') | |
| permissions: | |
| statuses: write | |
| timeout-minutes: 2 | |
| env: | |
| COOLIFY_URL: ${{ vars.COOLIFY_URL }} | |
| COOLIFY_APP_UUID: ${{ vars.COOLIFY_APP_UUID }} | |
| COOLIFY_PROJECT_UUID: ${{ vars.COOLIFY_PROJECT_UUID }} | |
| COOLIFY_ENVIRONMENT_UUID: ${{ vars.COOLIFY_ENVIRONMENT_UUID }} | |
| steps: | |
| - name: Link the preview deployments page on the PR | |
| if: vars.COOLIFY_PROJECT_UUID != '' && vars.COOLIFY_ENVIRONMENT_UUID != '' | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const { COOLIFY_URL, COOLIFY_PROJECT_UUID, COOLIFY_ENVIRONMENT_UUID, COOLIFY_APP_UUID } = process.env; | |
| await github.rest.repos.createCommitStatus({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| sha: context.payload.pull_request.head.sha, | |
| state: 'success', | |
| target_url: `${COOLIFY_URL}/project/${COOLIFY_PROJECT_UUID}/environment/${COOLIFY_ENVIRONMENT_UUID}/application/${COOLIFY_APP_UUID}/preview-deployments`, | |
| description: 'Click Details to view Coolify preview deployments', | |
| context: 'Preview / Coolify', | |
| }); | |
| # Forks are skipped deliberately: a preview runs with the instance's real credentials. | |
| - name: Update this PR's preview deployment, if it has one | |
| if: >- | |
| (github.event.action == 'opened' || | |
| github.event.action == 'reopened' || | |
| github.event.action == 'synchronize') && | |
| github.event.pull_request.head.repo.full_name == github.repository | |
| env: | |
| COOLIFY_TOKEN: ${{ secrets.COOLIFY_API_TOKEN }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| APP_SERVER_PUBLISHED: ${{ needs.Docker.outputs.application-server-published }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${COOLIFY_TOKEN}" ]; then | |
| echo "::notice::COOLIFY_API_TOKEN is not configured; skipping preview update." | |
| exit 0 | |
| fi | |
| # Coolify pins the preview to SOURCE_COMMIT, so without that tag the deployment can only | |
| # fail on `manifest unknown` — under a green check, since queueing one always succeeds. | |
| if [ "${APP_SERVER_PUBLISHED}" != 'true' ]; then | |
| echo "::notice::No application-server image for this commit; skipping preview update." | |
| exit 0 | |
| fi | |
| body=$(mktemp) | |
| status=$(curl -sS -o "${body}" -w '%{http_code}' -X POST \ | |
| -H "Authorization: Bearer ${COOLIFY_TOKEN}" \ | |
| -H 'Accept: application/json' \ | |
| --retry 3 --retry-connrefused --max-time 30 \ | |
| "${COOLIFY_URL}/api/v1/deploy?uuid=${COOLIFY_APP_UUID}&pr=${PR_NUMBER}") | |
| if [ "${status}" -ge 400 ]; then | |
| echo "::error::Coolify returned HTTP ${status}: $(cat "${body}")" | |
| exit 1 | |
| fi | |
| deployment=$(jq -r '.deployments[0].deployment_uuid // empty' "${body}") | |
| if [ -z "${deployment}" ]; then | |
| echo "::notice::PR #${PR_NUMBER} has no preview deployment ($(jq -r '.deployments[0].message // "no deployment queued"' "${body}"))." | |
| exit 0 | |
| fi | |
| echo "::notice::Queued Coolify deployment ${deployment} for PR #${PR_NUMBER}." | |
| Quality: | |
| uses: ./.github/workflows/ci-quality-gates.yml | |
| needs: [detect-changes] | |
| if: | | |
| needs.detect-changes.outputs.should_skip != 'true' && ( | |
| needs.detect-changes.outputs.any-code == 'true' || | |
| needs.detect-changes.outputs.ci-config == 'true' || | |
| github.event_name != 'pull_request' | |
| ) | |
| secrets: inherit | |
| with: | |
| should_skip: ${{ needs.detect-changes.outputs.should_skip }} | |
| webapp_changed: ${{ (needs.detect-changes.outputs.webapp == 'true' || needs.detect-changes.outputs.ci-config == 'true' || github.event_name != 'pull_request') && 'true' || 'false' }} | |
| application_server_changed: ${{ (needs.detect-changes.outputs.application-server == 'true' || needs.detect-changes.outputs.ci-config == 'true' || github.event_name != 'pull_request') && 'true' || 'false' }} | |
| pmd_canary: ${{ (needs.detect-changes.outputs.pmd-canary == 'true' || github.event_name != 'pull_request') && 'true' || 'false' }} | |
| Security: | |
| uses: ./.github/workflows/ci-security-scan.yml | |
| needs: [detect-changes] | |
| if: | | |
| needs.detect-changes.outputs.should_skip != 'true' && ( | |
| needs.detect-changes.outputs.any-code == 'true' || | |
| needs.detect-changes.outputs.ci-config == 'true' || | |
| github.event_name != 'pull_request' | |
| ) | |
| secrets: inherit | |
| with: | |
| should_skip: ${{ needs.detect-changes.outputs.should_skip }} | |
| Test: | |
| uses: ./.github/workflows/ci-tests.yml | |
| needs: [detect-changes] | |
| if: | | |
| needs.detect-changes.outputs.should_skip != 'true' && ( | |
| needs.detect-changes.outputs.any-code == 'true' || | |
| needs.detect-changes.outputs.ci-config == 'true' || | |
| github.event_name != 'pull_request' | |
| ) | |
| secrets: inherit | |
| with: | |
| should_skip: ${{ needs.detect-changes.outputs.should_skip }} | |
| webapp_changed: ${{ (needs.detect-changes.outputs.webapp == 'true' || needs.detect-changes.outputs.ci-config == 'true' || github.event_name != 'pull_request') && 'true' || 'false' }} | |
| application_server_changed: ${{ (needs.detect-changes.outputs.application-server == 'true' || needs.detect-changes.outputs.ci-config == 'true' || github.event_name != 'pull_request') && 'true' || 'false' }} | |
| e2e_changed: ${{ (needs.detect-changes.outputs.e2e == 'true' || github.event_name != 'pull_request') && 'true' || 'false' }} | |
| Docker: | |
| uses: ./.github/workflows/ci-docker-build.yml | |
| # Image builds consume the detected source tree, not Quality outputs. Running both branches at | |
| # once keeps image and preview confidence without adding Docker as a second CI stage. | |
| needs: [detect-changes] | |
| # Every same-repository pull request gets a Coolify preview pinned to SOURCE_COMMIT, so it needs | |
| # an application-server tag at its head commit even when it touches only docs or the preview | |
| # stack. The path filters below therefore gate fork pull requests only — forks get no preview. | |
| if: | | |
| needs.detect-changes.outputs.should_skip != 'true' && ( | |
| github.event_name != 'pull_request' || | |
| github.event.pull_request.head.repo.full_name == github.repository || | |
| needs.detect-changes.outputs.any-code == 'true' || | |
| needs.detect-changes.outputs.ci-config == 'true' | |
| ) | |
| secrets: inherit | |
| with: | |
| should_skip: ${{ needs.detect-changes.outputs.should_skip }} | |
| webapp_changed: ${{ (needs.detect-changes.outputs.webapp-image == 'true' || needs.detect-changes.outputs.docker-config == 'true' || github.event_name != 'pull_request') && 'true' || 'false' }} | |
| application_server_changed: ${{ (needs.detect-changes.outputs.application-server-image == 'true' || needs.detect-changes.outputs.docker-config == 'true' || github.event_name != 'pull_request') && 'true' || 'false' }} | |
| agent_images_changed: ${{ (needs.detect-changes.outputs.agent-images == 'true' || needs.detect-changes.outputs.docker-config == 'true' || github.event_name != 'pull_request') && 'true' || 'false' }} | |
| release_pin_fetcher_changed: ${{ (needs.detect-changes.outputs.release-pin-fetcher == 'true' || needs.detect-changes.outputs.docker-config == 'true' || github.event_name != 'pull_request') && 'true' || 'false' }} | |
| postgres_image_changed: ${{ (needs.detect-changes.outputs.postgres-image == 'true' || needs.detect-changes.outputs.docker-config == 'true' || github.event_name != 'pull_request') && 'true' || 'false' }} | |
| all-ci-passed: | |
| name: "CI Status Gate" | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: read | |
| statuses: write | |
| needs: [detect-changes, Quality, Security, Test, Docker] | |
| if: always() | |
| steps: | |
| - name: Generate workflow timeline | |
| uses: Kesin11/actions-timeline@7c7e0821d38f27460f4a71ef874a1c8cf23602e4 # v2 | |
| with: | |
| show-waiting-runner: true | |
| - name: Summarize workflow performance | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const run = await github.rest.actions.getWorkflowRun({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| run_id: context.runId, | |
| }); | |
| const jobs = await github.paginate(github.rest.actions.listJobsForWorkflowRun, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| run_id: context.runId, | |
| per_page: 100, | |
| filter: 'latest', | |
| }); | |
| const finished = jobs.filter((job) => job.started_at && job.completed_at); | |
| const seconds = (start, end) => (Date.parse(end) - Date.parse(start)) / 1000; | |
| const elapsed = seconds(run.data.created_at, new Date().toISOString()); | |
| const runnerSeconds = finished.reduce( | |
| (total, job) => total + seconds(job.started_at, job.completed_at), | |
| 0, | |
| ); | |
| const startDelays = finished.map((job) => seconds(job.created_at, job.started_at)); | |
| const format = (value) => `${Math.floor(value / 60)}m ${Math.round(value % 60)}s`; | |
| await core.summary | |
| .addHeading('Workflow performance', 3) | |
| .addTable([ | |
| [{ data: 'Metric', header: true }, { data: 'Value', header: true }], | |
| ['Workflow-to-gate sample', format(elapsed)], | |
| ['Completed runner time', format(runnerSeconds)], | |
| ['Longest job creation-to-start delay', format(Math.max(0, ...startDelays))], | |
| ]) | |
| .addRaw( | |
| 'The creation-to-start delay includes dependency waiting; use the timeline above to distinguish dependency time from runner waiting. Cache hits and misses remain visible in each native cache action log.', | |
| ) | |
| .write(); | |
| - name: Evaluate CI results | |
| id: evaluate | |
| run: | | |
| echo "detect-changes: ${{ needs.detect-changes.result }}" | |
| echo "Quality: ${{ needs.Quality.result }}" | |
| echo "Security: ${{ needs.Security.result }}" | |
| echo "Test: ${{ needs.Test.result }}" | |
| echo "Docker: ${{ needs.Docker.result }}" | |
| if [[ "${{ contains(needs.*.result, 'failure') }}" == "true" ]]; then | |
| echo "status=failure" >> $GITHUB_OUTPUT | |
| exit 1 | |
| fi | |
| if [[ "${{ contains(needs.*.result, 'cancelled') }}" == "true" ]]; then | |
| echo "status=cancelled" >> $GITHUB_OUTPUT | |
| exit 1 | |
| fi | |
| echo "status=success" >> $GITHUB_OUTPUT | |
| - name: Generate CI Summary | |
| if: always() | |
| run: | | |
| echo "## 🔍 CI Pipeline Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [[ "${{ steps.evaluate.outputs.status }}" == "success" ]]; then | |
| echo "✅ **All checks passed!**" >> $GITHUB_STEP_SUMMARY | |
| elif [[ "${{ steps.evaluate.outputs.status }}" == "failure" ]]; then | |
| echo "❌ **Some checks failed.** See details below." >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "⚠️ **CI was cancelled or encountered an issue.**" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Workflow Results" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Workflow | Status |" >> $GITHUB_STEP_SUMMARY | |
| echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY | |
| result_to_emoji() { | |
| case "$1" in | |
| success) echo "✅ Passed" ;; | |
| failure) echo "❌ Failed" ;; | |
| skipped) echo "⏭️ Skipped" ;; | |
| cancelled) echo "🚫 Cancelled" ;; | |
| *) echo "❓ Unknown" ;; | |
| esac | |
| } | |
| echo "| Quality | $(result_to_emoji '${{ needs.Quality.result }}') |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Test | $(result_to_emoji '${{ needs.Test.result }}') |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Security | $(result_to_emoji '${{ needs.Security.result }}') |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Docker | $(result_to_emoji '${{ needs.Docker.result }}') |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### 📁 Components Changed" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Component | Changed |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-----------|---------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Webapp | ${{ needs.detect-changes.outputs.webapp == 'true' && '✅ Yes' || '➖ No' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Application Server | ${{ needs.detect-changes.outputs.application-server == 'true' && '✅ Yes' || '➖ No' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Agent Images | ${{ needs.detect-changes.outputs.agent-images == 'true' && '✅ Yes' || '➖ No' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| CI Config | ${{ needs.detect-changes.outputs.ci-config == 'true' && '⚙️ Yes' || '➖ No' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [[ "${{ steps.evaluate.outputs.status }}" == "failure" ]]; then | |
| echo "### 💡 Troubleshooting Guide" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| if [[ "${{ needs.Quality.result }}" == "failure" ]]; then | |
| echo "<details><summary><b>❌ Quality Failed</b></summary>" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Issue | Fix Command |" >> $GITHUB_STEP_SUMMARY | |
| echo "|-------|-------------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Formatting errors | \`pnpm run format\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| TypeScript errors | \`pnpm run typecheck\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Lint errors (webapp) | \`pnpm run check:webapp:fix\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Lint errors (outside webapp) | \`pnpm run check:agents:fix\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Java formatting | \`pnpm run format:java\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Java lint (PMD) | \`pnpm run lint:java:report\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Storybook sidebar order | \`pnpm run check:story-sort\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Mermaid diagram parse errors | \`pnpm run check:diagrams\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Stale \`routeTree.gen.ts\` | \`cd webapp && pnpm run build\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| OpenAPI out of sync | \`pnpm run generate:api\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Database schema drift | \`pnpm run db:draft-changelog\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "</details>" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [[ "${{ needs.Test.result }}" == "failure" ]]; then | |
| echo "<details><summary><b>❌ Tests Failed</b></summary>" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| Test Suite | Run Locally |" >> $GITHUB_STEP_SUMMARY | |
| echo "|------------|-------------|" >> $GITHUB_STEP_SUMMARY | |
| echo "| Webapp unit | \`pnpm run test:webapp\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Webapp Storybook | \`pnpm --filter webapp run test:storybook\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Application server | \`cd server && ./mvnw test\` |" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Tip:** Check the **Test Results** tab above for specific failures." >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "</details>" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [[ "${{ needs.Docker.result }}" == "failure" ]]; then | |
| echo "<details><summary><b>❌ Docker Failed</b></summary>" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "Common causes:" >> $GITHUB_STEP_SUMMARY | |
| echo "- Build errors in the application code" >> $GITHUB_STEP_SUMMARY | |
| echo "- Missing dependencies" >> $GITHUB_STEP_SUMMARY | |
| echo "- Dockerfile syntax errors" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "Test locally: \`docker build -f <component>/Dockerfile .\`" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "</details>" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [[ "${{ needs.Security.result }}" == "failure" ]]; then | |
| echo "<details><summary><b>❌ Security Failed</b></summary>" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "Check the **Security** tab for details on vulnerabilities." >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "</details>" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| echo "---" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "**Quick fix:** Run \`pnpm run format && pnpm run check\` before pushing." >> $GITHUB_STEP_SUMMARY | |
| fi | |
| if [[ "${{ steps.evaluate.outputs.status }}" == "success" ]]; then | |
| echo "### ⚡ Performance" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| SKIPPED_COUNT=0 | |
| [[ "${{ needs.Quality.result }}" == "skipped" ]] && SKIPPED_COUNT=$((SKIPPED_COUNT + 1)) || true | |
| [[ "${{ needs.Test.result }}" == "skipped" ]] && SKIPPED_COUNT=$((SKIPPED_COUNT + 1)) || true | |
| [[ "${{ needs.Security.result }}" == "skipped" ]] && SKIPPED_COUNT=$((SKIPPED_COUNT + 1)) || true | |
| [[ "${{ needs.Docker.result }}" == "skipped" ]] && SKIPPED_COUNT=$((SKIPPED_COUNT + 1)) || true | |
| if [[ $SKIPPED_COUNT -gt 0 ]]; then | |
| echo "🚀 **Path-based filtering saved time!** $SKIPPED_COUNT workflow(s) skipped because no relevant files changed." >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "All workflows ran (CI config or main branch push)." >> $GITHUB_STEP_SUMMARY | |
| fi | |
| fi | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "---" >> $GITHUB_STEP_SUMMARY | |
| echo "*Generated by CI Status Gate • [View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})*" >> $GITHUB_STEP_SUMMARY | |
| - name: Create commit status | |
| if: always() | |
| uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 | |
| with: | |
| script: | | |
| const sha = context.payload.pull_request?.head?.sha || context.sha; | |
| const outcome = '${{ steps.evaluate.outputs.status }}' || 'failure'; | |
| // createCommitStatus accepts error, failure, pending or success and 422s on anything | |
| // else, so a cancelled run has to be reported as one of them rather than by its name. | |
| const REPORTED = { | |
| success: ['success', 'All CI checks passed'], | |
| failure: ['failure', 'One or more CI checks failed'], | |
| cancelled: ['error', 'CI was cancelled before it finished'], | |
| }; | |
| const [state, description] = REPORTED[outcome] ?? REPORTED.failure; | |
| await github.rest.repos.createCommitStatus({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| sha: sha, | |
| state: state, | |
| description: description, | |
| context: 'All CI Passed' | |
| }); |