Skip to content

feat: Advanced Query Explorer (issue #10) #1344

feat: Advanced Query Explorer (issue #10)

feat: Advanced Query Explorer (issue #10) #1344

Workflow file for this run

# CI / CD Pipeline for Claude Code Agent Monitor
#
# This workflow runs on every push and pull request, executing the following stages:
# 1. Format Check: Ensures code adheres to formatting standards.
# 2. Tests: Runs server and client tests to validate functionality.
# 3. Build: Builds the client application and uploads the artifact.
# 4. Detect Changed Paths: A `dorny/paths-filter` job that flags `desktop/**` (or workflow) changes so the desktop job below stays
# path-filtered without hard-coding a branch name.
# 5. Desktop Apps (DMG + EXE): Builds the Electron desktop app for macOS on `macos-latest` (uploads two
# `ClaudeCodeMonitor-*.dmg` artifacts, arm64 + x64) and for Windows on `windows-latest` (uploads `ClaudeCodeMonitor-win`:
# an NSIS installer + a no-install portable `.exe`). Universal macOS builds were dropped because @electron/universal's
# merge step hangs/errors on this repo's extraResources layout (parent-path sources); two single-arch DMGs ship the same
# payload without the merge step. Both jobs run on any push, when the changes job flagged `desktop/**`, or when a PR
# carries the `desktop` label. The macOS build is resilient to flaky `hdiutil detach` (retries up to 3 times,
# force-detaching stale volumes between attempts).
# 6. Deployment and OCI supply chain: validates Docker/Compose/Nginx/Helm/Kustomize/Terraform,
# scans app + MCP images, publishes multi-architecture images with SBOM and provenance,
# and keyless-signs pushed digests on the default branch.
# 7. Release: On a successful push to the default branch, publishes a GitHub Release tagged vX.Y.Z (matching the existing v1.0.0 /
# v1.1.0 convention, drawn from the root package.json `version`) with the macOS DMGs and Windows EXEs attached — but only if no
# release exists for that version yet, so bumping the version is what cuts a new release and re-running master with an unchanged
# version is a no-op.
# 8. Pipeline Summary: Compiles results from all stages and posts a summary to the GitHub Actions step summary, including status
# icons, commit details, and links to artifacts if applicable.
# Any failure in the stages will cause the pipeline to fail, ensuring that only well-formatted, tested, and built code is merged and deployed.
#
# Author: Son Nguyen <hoangson091104@gmail.com>
name: 🚀 CI / CD Pipeline for Claude Code Agent Monitor
on:
push:
branches: ["**"]
# CLA signature commits (made by github-actions[bot] via cla.yml) only touch
# this path — skip the full CI/CD pipeline for them.
paths-ignore:
- "signatures/**"
pull_request:
branches: ["**"]
env:
NODE_VERSION: "24"
IMAGE_NAME: claude-code-agent-monitor
jobs:
# ────────────────────────────────────────────────────────────────
# 🧹 Format Check #
# ────────────────────────────────────────────────────────────────
format:
name: "🧹 Check Formatting"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- name: Install root dependencies
run: npm ci
- name: Check formatting
run: npm run format:check
# ────────────────────────────────────────────────────────────────
# 🧪 Tests #
# ────────────────────────────────────────────────────────────────
test:
name: "🧪 Run Tests"
runs-on: ubuntu-latest
needs: format
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- name: Install root dependencies
run: npm ci
- name: Install client dependencies
run: cd client && npm ci
- name: Run server tests
run: npm run test:server
- name: Run client tests
run: npm run test:client
- name: Install MCP dependencies
run: npm run mcp:install
- name: Typecheck and test MCP
run: npm run mcp:typecheck && npm run test:mcp
# ────────────────────────────────────────────────────────────────
# 🏗️ Build Client #
# ────────────────────────────────────────────────────────────────
build:
name: "🏗️ Build & Upload Artifact"
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- name: Install root dependencies
run: npm ci
- name: Install client dependencies
run: cd client && npm ci
- name: Build client
run: npm run build
- name: Upload build artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: client-dist
path: client/dist/
retention-days: 7
# ────────────────────────────────────────────────────────────────
# 🔎 Detect changed paths #
# Drives the path-filtered desktop job below without relying #
# on a hard-coded branch name. #
# ────────────────────────────────────────────────────────────────
changes:
name: "🔎 Detect changed paths"
runs-on: ubuntu-latest
outputs:
desktop: ${{ steps.filter.outputs.desktop }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: dorny/paths-filter@6852f92c20ea7fd3b0c25de3b5112db3a98da050 # v3
id: filter
with:
filters: |
desktop:
- 'desktop/**'
- '.github/workflows/ci.yml'
# ────────────────────────────────────────────────────────────────
# 🍎 macOS Desktop App (Electron DMG) #
# Runs when desktop/** changes, when the PR carries the #
# 'desktop' label, or on any push. Produces a downloadable #
# .dmg artifact for QA. #
# ────────────────────────────────────────────────────────────────
desktop:
name: "🍎 macOS Desktop (DMG)"
runs-on: macos-latest
needs: [build, changes]
if: |
github.event_name == 'push' ||
needs.changes.outputs.desktop == 'true' ||
contains(toJSON(github.event.pull_request.labels.*.name), 'desktop')
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- name: Install root dependencies
run: npm ci
- name: Install client dependencies & build client
run: |
cd client && npm ci
cd .. && npm run build
- name: Install desktop dependencies
run: cd desktop && npm ci
- name: Ensure Electron binary
# Electron 43 downloads its platform archive lazily on first require.
# Resolve it before the smoke test. Retry the primary release endpoint,
# then Electron's documented npm mirror, so a GitHub CDN outage does not
# fail an otherwise healthy desktop build.
run: |
cd desktop
for source in primary mirror; do
for attempt in 1 2 3; do
if [ "$source" = mirror ]; then
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ \
node node_modules/electron/install.js && exit 0
elif node node_modules/electron/install.js; then
exit 0
fi
echo "Electron $source download failed (attempt $attempt/3)" >&2
sleep $((attempt * 5))
done
done
echo "Electron binary download failed from primary and mirror" >&2
exit 1
- name: Build desktop (tsc)
run: cd desktop && npm run build
- name: Smoke test
run: cd desktop && npm test
- name: Quiet Spotlight (reduces flaky hdiutil detach)
# Spotlight indexing a freshly-mounted DMG volume is the usual cause of
# electron-builder's `hdiutil detach` failing as "busy". Disabling it on
# the ephemeral runner is harmless.
run: sudo mdutil -a -i off || true
- name: Build DMG
# electron-builder finalizes the DMG by running `hdiutil detach` on the
# temp build volume, which is intermittently flaky on GitHub macOS
# runners ("unable to execute hdiutil ... detach"). Retry the whole
# build, force-detaching any stale volume between attempts.
run: |
cd desktop
# GitHub Actions resolves missing secrets to empty strings, which
# makes electron-builder think CSC_LINK is set to "". It then tries
# to stat that empty path against cwd and bombs with
# "<repo>/desktop not a file" before reaching the DMG step. Unset
# any sign/notarize env var that's empty so electron-builder
# cleanly falls back to ad-hoc signing.
for v in CSC_LINK CSC_KEY_PASSWORD APPLE_ID APPLE_TEAM_ID APPLE_APP_SPECIFIC_PASSWORD; do
if [ -z "${!v:-}" ]; then unset "$v"; fi
done
for attempt in 1 2 3; do
echo "::group::DMG build attempt ${attempt}/3"
if npm run dmg; then
echo "::endgroup::"
exit 0
fi
echo "::endgroup::"
echo "DMG build attempt ${attempt} failed; detaching stale volumes"
for vol in "/Volumes/Claude Code Monitor"*; do
if [ -d "${vol}" ]; then
hdiutil detach -force "${vol}" || true
fi
done
sleep 10
done
echo "DMG build failed after 3 attempts" >&2
exit 1
env:
# If these are set as repo secrets, electron-builder will sign +
# notarize automatically via desktop/scripts/notarize.js. When the
# secret is missing, GitHub resolves it to "" — the run step above
# strips empty values before invoking electron-builder so an empty
# CSC_LINK doesn't get interpreted as a path to a (non-existent)
# certificate file.
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
- name: Upload DMG artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ClaudeCodeMonitor-dmg
path: desktop/release/*.dmg
retention-days: 14
# ────────────────────────────────────────────────────────────────
# 🪟 Windows Desktop App (Electron NSIS / portable .exe) #
# Mirrors the macOS desktop job on windows-latest. Runs on any #
# push, when the changes job flagged desktop/**, or when a PR #
# carries the 'desktop' label. Produces an NSIS installer and a #
# no-install portable .exe, uploaded as ClaudeCodeMonitor-win. #
# ────────────────────────────────────────────────────────────────
desktop-win:
name: "🪟 Windows Desktop (EXE)"
runs-on: windows-latest
needs: [build, changes]
if: |
github.event_name == 'push' ||
needs.changes.outputs.desktop == 'true' ||
contains(toJSON(github.event.pull_request.labels.*.name), 'desktop')
defaults:
run:
# Git Bash ships on windows-latest; using it lets the `cd a && b` steps
# mirror the macOS desktop job verbatim (the indirect-expansion unset
# trick below is bash-only).
shell: bash
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- name: Install root dependencies
run: npm ci
- name: Install client dependencies & build client
run: |
cd client && npm ci
cd .. && npm run build
- name: Install desktop dependencies
# postinstall runs `electron-builder install-app-deps`, fetching the
# prebuilt better-sqlite3 binary for Electron's ABI on win32-x64 (no
# Visual Studio C++ toolchain needed in the common case).
run: cd desktop && npm ci
- name: Ensure Electron binary
# Electron 43 downloads electron.exe lazily on first require. Resolve
# it before the smoke test. Retry the primary release endpoint, then
# Electron's documented npm mirror; master at 1d5c645 failed on a
# transient GitHub fetch, and one later runner saw a sustained 503.
run: |
cd desktop
for source in primary mirror; do
for attempt in 1 2 3; do
if [ "$source" = mirror ]; then
ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ \
node node_modules/electron/install.js && exit 0
elif node node_modules/electron/install.js; then
exit 0
fi
echo "Electron $source download failed (attempt $attempt/3)" >&2
sleep $((attempt * 5))
done
done
echo "Electron binary download failed from primary and mirror" >&2
exit 1
- name: Build desktop (tsc)
run: cd desktop && npm run build
- name: Smoke test
run: cd desktop && npm test
- name: Build Windows installer + portable
# electron-builder packages for the host OS and embeds the committed
# desktop/assets/icon.ico. Windows builds stay unsigned unless an
# explicit cert is provided; GitHub resolves a missing secret to "",
# which electron-builder would treat as a bogus cert path — so strip
# empty sign vars before invoking it. Packaging downloads auxiliary
# tools such as NSIS lazily, so retry each target on transient CDN
# failures without rerunning the whole job.
run: |
cd desktop
for v in CSC_LINK CSC_KEY_PASSWORD; do
if [ -z "${!v:-}" ]; then unset "$v"; fi
done
for target in win win:portable; do
packaged=false
for source in primary mirror; do
for attempt in 1 2 3; do
if [ "$source" = mirror ]; then
if ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ \
npm run "$target"; then
packaged=true
break 2
fi
elif npm run "$target"; then
packaged=true
break 2
fi
echo "Windows $target $source packaging failed (attempt $attempt/3)" >&2
sleep $((attempt * 10))
done
done
if [ "$packaged" != true ]; then
echo "Windows $target packaging failed with primary and mirror downloads" >&2
exit 1
fi
done
env:
CSC_LINK: ${{ secrets.CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
- name: Upload Windows artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: ClaudeCodeMonitor-win
path: desktop/release/*.exe
retention-days: 14
deployment:
name: "☁️ Validate Deployment Stack"
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: "npm"
- uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4
with:
version: v4.2.3
- uses: azure/setup-kubectl@776406bce94f63e41d621b960d78ee25c8b76ede # v4
with:
version: v1.36.1
- name: Install root and MCP dependencies
run: npm ci && npm run mcp:install
- name: Validate Docker, Compose, Nginx, Helm, Kustomize, Terraform, and dependencies
run: npm run deploy:validate
# ────────────────────────────────────────────────────────────────
# 🐳 OCI images, attestations, and GHCR publish #
# ────────────────────────────────────────────────────────────────
docker:
name: "🐳 OCI Supply Chain"
runs-on: ubuntu-latest
needs: [test, build, deployment]
permissions:
contents: read
packages: write
id-token: write
outputs:
app-digest: ${{ steps.build-app.outputs.digest }}
mcp-digest: ${{ steps.build-mcp.outputs.digest }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Set up QEMU
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
- name: Log in to GHCR
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Read package version
id: package-version
run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"
- name: Extract app metadata
id: meta-app
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}
tags: |
type=sha,prefix=
type=ref,event=branch
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=${{ steps.package-version.outputs.version }},enable={{is_default_branch}}
- name: Extract MCP metadata
id: meta-mcp
uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0
with:
images: ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}-mcp
tags: |
type=sha,prefix=
type=ref,event=branch
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=${{ steps.package-version.outputs.version }},enable={{is_default_branch}}
- name: Build app image
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
file: ./Dockerfile
target: runtime
load: true
tags: ccam-dashboard:ci
cache-from: type=gha,scope=app
cache-to: type=gha,mode=max,scope=app
- name: Build MCP image
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
file: ./mcp/Dockerfile
target: runtime
load: true
tags: ccam-mcp:ci
cache-from: type=gha,scope=mcp
cache-to: type=gha,mode=max,scope=mcp
- name: Build and push multi-architecture app image
id: build-app
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
file: ./Dockerfile
target: runtime
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta-app.outputs.tags }}
labels: ${{ steps.meta-app.outputs.labels }}
cache-from: type=gha,scope=app
cache-to: type=gha,mode=max,scope=app
provenance: mode=max
sbom: true
- name: Build and push multi-architecture MCP image
id: build-mcp
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
file: ./mcp/Dockerfile
target: runtime
push: true
platforms: linux/amd64,linux/arm64
tags: ${{ steps.meta-mcp.outputs.tags }}
labels: ${{ steps.meta-mcp.outputs.labels }}
cache-from: type=gha,scope=mcp
cache-to: type=gha,mode=max,scope=mcp
provenance: mode=max
sbom: true
- name: Install Cosign
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')
uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2
- name: Keyless-sign pushed image digests
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')
env:
APP_DIGEST: ${{ steps.build-app.outputs.digest }}
MCP_DIGEST: ${{ steps.build-mcp.outputs.digest }}
run: |
cosign sign --yes "ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}@${APP_DIGEST}"
cosign sign --yes "ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}-mcp@${MCP_DIGEST}"
# ────────────────────────────────────────────────────────────────
# 🚢 Publish GitHub Release #
# On a successful push to the default branch, publish a Release #
# for the current package.json version (tagged vX.Y.Z, matching #
# the existing v1.0.0 / v1.1.0 convention) and attach the macOS #
# DMG built by the desktop job. The release is created only when #
# no release exists for that version yet — so bumping `version` #
# in package.json is what cuts a new release, and re-running #
# master with an unchanged version is a safe no-op. #
# ────────────────────────────────────────────────────────────────
release:
name: "🚢 Publish Release"
runs-on: ubuntu-latest
needs: [desktop, desktop-win, docker]
if: |
github.event_name == 'push' &&
(github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main')
permissions:
contents: write
concurrency:
# Serialize release publishing per branch so two close pushes can't race
# to create the same tag.
group: publish-release-${{ github.ref }}
cancel-in-progress: false
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- name: Resolve version and release tag
id: ver
run: |
VERSION="$(node -p "require('./package.json').version")"
if [ -z "${VERSION}" ]; then
echo "::error::Could not read a version from package.json"
exit 1
fi
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
echo "Resolved release tag: v${VERSION}"
- name: Check whether the release already exists
id: check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if gh release view "${{ steps.ver.outputs.tag }}" >/dev/null 2>&1; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Release ${{ steps.ver.outputs.tag }} already exists — nothing to publish."
else
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "Release ${{ steps.ver.outputs.tag }} does not exist — it will be published."
fi
- name: Download macOS DMG artifact
if: steps.check.outputs.exists == 'false'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ClaudeCodeMonitor-dmg
path: dmg
- name: Download Windows EXE artifact
if: steps.check.outputs.exists == 'false'
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: ClaudeCodeMonitor-win
path: win
- name: Publish release
if: steps.check.outputs.exists == 'false'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${{ steps.ver.outputs.tag }}"
echo "Publishing ${TAG} with assets:"
ls -la dmg win
gh release create "${TAG}" \
--target "${{ github.sha }}" \
--title "${TAG}" \
--generate-notes \
--latest \
dmg/*.dmg win/*.exe
echo "Published ${TAG} → ${{ github.server_url }}/${{ github.repository }}/releases/tag/${TAG}"
# ────────────────────────────────────────────────────────────────
# 🎉 Pipeline Summary #
# Runs after all jobs — reports final status to step summary #
# ────────────────────────────────────────────────────────────────
pipeline-status:
name: "🎉 Pipeline Status"
if: always()
runs-on: ubuntu-latest
needs: [format, test, build, deployment, desktop, desktop-win, docker, release]
steps:
- name: Determine overall result
id: result
run: |
# Map each job result
FORMAT="${{ needs.format.result }}"
TEST="${{ needs.test.result }}"
BUILD="${{ needs.build.result }}"
DEPLOYMENT="${{ needs.deployment.result }}"
DESKTOP="${{ needs.desktop.result }}"
DESKTOP_WIN="${{ needs['desktop-win'].result }}"
DOCKER="${{ needs.docker.result }}"
RELEASE="${{ needs.release.result }}"
{
echo "format=$FORMAT"
echo "test=$TEST"
echo "build=$BUILD"
echo "deployment=$DEPLOYMENT"
echo "desktop=$DESKTOP"
echo "desktop_win=$DESKTOP_WIN"
echo "docker=$DOCKER"
echo "release=$RELEASE"
} >> "$GITHUB_OUTPUT"
# Overall: fail if any required job failed. Desktop jobs and release
# are treated as non-blocking when skipped (the desktop jobs are
# path-filtered out for non-desktop PRs; release runs only on the
# default branch).
if [[ "$FORMAT" == "failure" || "$TEST" == "failure" || "$BUILD" == "failure" || "$DEPLOYMENT" == "failure" || "$DESKTOP" == "failure" || "$DESKTOP_WIN" == "failure" || "$DOCKER" == "failure" || "$RELEASE" == "failure" ]]; then
echo "overall=failure" >> "$GITHUB_OUTPUT"
elif [[ "$FORMAT" == "cancelled" || "$TEST" == "cancelled" || "$BUILD" == "cancelled" || "$DEPLOYMENT" == "cancelled" || "$DESKTOP" == "cancelled" || "$DESKTOP_WIN" == "cancelled" || "$DOCKER" == "cancelled" || "$RELEASE" == "cancelled" ]]; then
echo "overall=cancelled" >> "$GITHUB_OUTPUT"
else
echo "overall=success" >> "$GITHUB_OUTPUT"
fi
- name: Status icon helper
id: icons
run: |
icon() {
case "$1" in
success) echo "✅" ;;
failure) echo "❌" ;;
cancelled) echo "⚪" ;;
skipped) echo "⏭️" ;;
*) echo "❓" ;;
esac
}
{
echo "format=$(icon ${{ steps.result.outputs.format }})"
echo "test=$(icon ${{ steps.result.outputs.test }})"
echo "build=$(icon ${{ steps.result.outputs.build }})"
echo "deployment=$(icon ${{ steps.result.outputs.deployment }})"
echo "desktop=$(icon ${{ steps.result.outputs.desktop }})"
echo "desktop_win=$(icon ${{ steps.result.outputs.desktop_win }})"
echo "docker=$(icon ${{ steps.result.outputs.docker }})"
echo "release=$(icon ${{ steps.result.outputs.release }})"
echo "overall=$(icon ${{ steps.result.outputs.overall }})"
} >> "$GITHUB_OUTPUT"
- name: Write pipeline summary
run: |
PUSH_INFO=""
if [[ "${{ github.event_name }}" == "push" && ( "${{ github.ref }}" == "refs/heads/main" || "${{ github.ref }}" == "refs/heads/master" ) ]]; then
PUSH_INFO="| **Docker Image** | \`ghcr.io/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}:latest\` |"
fi
cat >> "$GITHUB_STEP_SUMMARY" <<EOF
## ${{ steps.icons.outputs.overall }} CI / CD Pipeline — ${GITHUB_REF_NAME}
| Stage | Status | Job |
|-------|--------|-----|
| Formatting | ${{ steps.icons.outputs.format }} \`${{ steps.result.outputs.format }}\` | \`format\` |
| Tests | ${{ steps.icons.outputs.test }} \`${{ steps.result.outputs.test }}\` | \`test\` |
| Client Build | ${{ steps.icons.outputs.build }} \`${{ steps.result.outputs.build }}\` | \`build\` |
| Deployment Stack | ${{ steps.icons.outputs.deployment }} \`${{ steps.result.outputs.deployment }}\` | \`deployment\` |
| macOS Desktop | ${{ steps.icons.outputs.desktop }} \`${{ steps.result.outputs.desktop }}\` | \`desktop\` |
| Windows Desktop | ${{ steps.icons.outputs.desktop_win }} \`${{ steps.result.outputs.desktop_win }}\` | \`desktop-win\` |
| Docker | ${{ steps.icons.outputs.docker }} \`${{ steps.result.outputs.docker }}\` | \`docker\` |
| Release | ${{ steps.icons.outputs.release }} \`${{ steps.result.outputs.release }}\` | \`release\` |
| Detail | Value |
|--------|-------|
| **Commit** | [\`${GITHUB_SHA::7}\`](${{ github.server_url }}/${{ github.repository }}/commit/${GITHUB_SHA}) |
| **Branch** | \`${GITHUB_REF_NAME}\` |
| **Trigger** | \`${{ github.event_name }}\` by \`${{ github.actor }}\` |
| **Runner** | \`ubuntu-latest\` · Node ${{ env.NODE_VERSION }} |
${PUSH_INFO}
| **Completed** | $(date -u +"%Y-%m-%d %H:%M:%S UTC") |
EOF
- name: Fail pipeline if any job failed
if: steps.result.outputs.overall != 'success'
run: |
echo "::error::Pipeline finished with status: ${{ steps.result.outputs.overall }}"
exit 1