Skip to content

chore(release): version packages #3557

chore(release): version packages

chore(release): version packages #3557

Workflow file for this run

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 }}
application-server: ${{ steps.filter.outputs.application-server }}
agent-images: ${{ steps.filter.outputs.agent-images }}
docs: ${{ steps.filter.outputs.docs }}
ci-config: ${{ steps.filter.outputs.ci-config }}
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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- id: skip_check
uses: fkirc/skip-duplicate-actions@f75f66ce1886f00957d99748a42c724f4330bdcf # v5
with:
do_not_skip: '["workflow_dispatch", "push", "merge_group"]'
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
id: filter
with:
filters: |
webapp:
- 'webapp/**'
- 'docs/images/readme/**'
- 'package.json'
- 'pnpm-lock.yaml'
- 'pnpm-workspace.yaml'
- '.npmrc'
- '.node-version'
application-server:
- 'server/**'
# The contract validator and the immutability guard live here; a PR that edits only a
# guard would otherwise skip the workflow that runs it.
- 'scripts/**'
agent-images:
- 'docker/agents/**'
docs:
- 'docs/**'
ci-config:
- '.github/workflows/**'
- '.github/actions/**'
# 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@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
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 }}
DOCKER_RESULT: ${{ needs.Docker.result }}
run: |
set -euo pipefail
if [ -z "${COOLIFY_TOKEN}" ]; then
echo "::notice::COOLIFY_API_TOKEN is not configured; skipping preview update."
exit 0
fi
# No image workflow, no SOURCE_COMMIT tag. Requesting the deployment anyway would only
# queue one that fails on `manifest unknown`, under a green check.
if [ "${DOCKER_RESULT}" != 'success' ]; then
echo "::notice::No application-server image was published for this commit (Docker: ${DOCKER_RESULT}); 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' }}
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' }}
Docker:
uses: ./.github/workflows/ci-docker-build.yml
needs: [detect-changes]
# Coolify gives every same-repository pull request a preview and pins it to SOURCE_COMMIT, so
# every such pull request needs an application-server tag at its head commit — including one that
# touches only the preview stack or documentation. Without it the preview deployment fails with
# `manifest unknown` for an image that will never be published. application-server-build is a
# cached rebuild when server code did not change; the webapp and agent images stay gated by their
# own inputs, so this costs one cached build, not the whole matrix.
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' ||
github.event.pull_request.head.repo.full_name == github.repository
)
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' }}
agent_images_changed: ${{ (needs.detect-changes.outputs.agent-images == 'true' || needs.detect-changes.outputs.ci-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: 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: |
# Header
echo "## 🔍 CI Pipeline Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Overall status
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
# Workflow results table
echo "### Workflow Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Workflow | Status |" >> $GITHUB_STEP_SUMMARY
echo "|----------|--------|" >> $GITHUB_STEP_SUMMARY
# Map result to emoji
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
# Path filtering info
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
# Tips section for failures - specific to what failed
if [[ "${{ steps.evaluate.outputs.status }}" == "failure" ]]; then
echo "### 💡 Troubleshooting Guide" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
# Quality failures
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:webapp\` |" >> $GITHUB_STEP_SUMMARY
echo "| Biome lint errors | \`pnpm run lint:fix\` |" >> $GITHUB_STEP_SUMMARY
echo "| Java formatting | \`pnpm run format:java\` |" >> $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
# Test failures
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
# Docker failures
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
# Security failures
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
# Performance note for successful runs
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@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
with:
script: |
const sha = context.payload.pull_request?.head?.sha || context.sha;
const state = '${{ steps.evaluate.outputs.status }}' || 'failure';
await github.rest.repos.createCommitStatus({
owner: context.repo.owner,
repo: context.repo.repo,
sha: sha,
state: state,
description: state === 'success' ? 'All CI checks passed' : 'One or more CI checks failed',
context: 'All CI Passed'
});