Files
wehub-resource-sync 161ef94b4f
Check engine pin consistency / Dockerfile / CI pin consistency (push) Successful in 8s
Sirius CI/CD Pipeline / Detect Changes (push) Successful in 23s
Validate Docker Configuration / Validate Docker Compose Configuration (push) Successful in 47s
Sirius CI/CD Pipeline / Build API (${{ matrix.platform }}) (push) Has been cancelled
Sirius CI/CD Pipeline / Build UI (${{ matrix.platform }}) (push) Has been cancelled
Sirius CI/CD Pipeline / Merge Engine Manifest (push) Has been cancelled
Sirius CI/CD Pipeline / Merge API Manifest (push) Has been cancelled
Sirius CI/CD Pipeline / Merge UI Manifest (push) Has been cancelled
Sirius CI/CD Pipeline / Build Engine (${{ matrix.platform }}) (push) Has been cancelled
Sirius CI/CD Pipeline / Build Infra (${{ matrix.service }}, ${{ matrix.platform }}) (push) Has been cancelled
Sirius CI/CD Pipeline / Merge Infra Manifest (sirius-postgres) (push) Has been cancelled
Sirius CI/CD Pipeline / Merge Infra Manifest (sirius-rabbitmq) (push) Has been cancelled
Sirius CI/CD Pipeline / Merge Infra Manifest (sirius-valkey) (push) Has been cancelled
Sirius CI/CD Pipeline / Integration Test (push) Has been cancelled
Sirius CI/CD Pipeline / Public Stack Contract (push) Has been cancelled
Sirius CI/CD Pipeline / Dispatch Demo Deployment (sirius-demo branch) (push) Has been cancelled
Sirius CI/CD Pipeline / Dispatch Demo Canary (main branch) (push) Has been cancelled
Sirius CI/CD Pipeline / Guard Registry Namespace (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:25 +08:00

1067 lines
45 KiB
YAML

name: Sirius CI/CD Pipeline
# Canonical CI pipeline. Handles all builds, tests, and canary dispatch.
# deploy.yml is now deployment-only (workflow_dispatch) and does not build images.
#
# Multi-arch strategy: each container is built per-platform (amd64 native,
# arm64 via QEMU) on GitHub-hosted runners. After both arch builds succeed,
# arch-specific tags are merged into a single multi-arch manifest.
on:
push:
branches: [main, sirius-demo]
pull_request:
branches: [main]
repository_dispatch:
types: [submodule-update]
env:
REGISTRY: ghcr.io
IMAGE_NAMESPACE: siriusscan
jobs:
# ─────────────────────────────────────────────────────────────────────────────
# Detect which services actually changed so we only build what's needed
# ─────────────────────────────────────────────────────────────────────────────
detect-changes:
name: Detect Changes
runs-on: ubuntu-latest
outputs:
sirius_ui_changes: ${{ steps.changes.outputs.sirius_ui_changes }}
sirius_api_changes: ${{ steps.changes.outputs.sirius_api_changes }}
sirius_engine_changes: ${{ steps.changes.outputs.sirius_engine_changes }}
sirius_infra_changes: ${{ steps.changes.outputs.sirius_infra_changes }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Determine Changed Files
id: changes
run: |
if [ "${{ github.event_name }}" == "repository_dispatch" ]; then
SUBMODULE="${{ github.event.client_payload.submodule }}"
echo "submodule_changes=true" >> $GITHUB_OUTPUT
echo "Submodule update detected: $SUBMODULE"
REPO_NAME=$(echo $SUBMODULE | awk -F'/' '{print $NF}')
case $REPO_NAME in
"go-api")
echo "Affects both sirius-api and sirius-engine"
echo "sirius_api_changes=true" >> $GITHUB_OUTPUT
echo "sirius_engine_changes=true" >> $GITHUB_OUTPUT
;;
"app-scanner"|"app-terminal"|"sirius-nse"|"app-agent")
echo "Affects sirius-engine"
echo "sirius_engine_changes=true" >> $GITHUB_OUTPUT
;;
*)
echo "Unknown submodule, rebuilding everything"
echo "sirius_ui_changes=true" >> $GITHUB_OUTPUT
echo "sirius_api_changes=true" >> $GITHUB_OUTPUT
echo "sirius_engine_changes=true" >> $GITHUB_OUTPUT
echo "sirius_infra_changes=true" >> $GITHUB_OUTPUT
;;
esac
else
if [ "${{ github.event_name }}" == "pull_request" ]; then
BASE_SHA=${{ github.event.pull_request.base.sha }}
HEAD_SHA=${{ github.event.pull_request.head.sha }}
else
BASE_SHA=${{ github.event.before }}
HEAD_SHA=${{ github.event.after }}
fi
git diff --name-only $BASE_SHA $HEAD_SHA > changed_files.txt
if grep -q "sirius-ui/" changed_files.txt; then
echo "UI changes detected"
echo "sirius_ui_changes=true" >> $GITHUB_OUTPUT
fi
if grep -q "sirius-api/" changed_files.txt; then
echo "API changes detected"
echo "sirius_api_changes=true" >> $GITHUB_OUTPUT
fi
if grep -q "sirius-engine/" changed_files.txt || grep -q "rabbitmq/" changed_files.txt; then
echo "Engine or RabbitMQ changes detected"
echo "sirius_engine_changes=true" >> $GITHUB_OUTPUT
fi
if grep -q -E "^(sirius-postgres/|sirius-rabbitmq/|sirius-valkey/|rabbitmq/)" changed_files.txt; then
echo "Infrastructure changes detected"
echo "sirius_infra_changes=true" >> $GITHUB_OUTPUT
fi
# Dockerfile or CI changes trigger a full rebuild
if grep -q -E "(Dockerfile|\.github/|docker-compose)" changed_files.txt; then
echo "Docker or CI changes detected, rebuilding everything"
echo "sirius_ui_changes=true" >> $GITHUB_OUTPUT
echo "sirius_api_changes=true" >> $GITHUB_OUTPUT
echo "sirius_engine_changes=true" >> $GITHUB_OUTPUT
echo "sirius_infra_changes=true" >> $GITHUB_OUTPUT
fi
# No specific changes but something did change: full rebuild
if [ ! -s changed_files.txt ]; then
echo "General changes detected, rebuilding everything"
echo "sirius_ui_changes=true" >> $GITHUB_OUTPUT
echo "sirius_api_changes=true" >> $GITHUB_OUTPUT
echo "sirius_engine_changes=true" >> $GITHUB_OUTPUT
echo "sirius_infra_changes=true" >> $GITHUB_OUTPUT
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
# Guard against accidental image namespace typos in CI/build paths.
# ─────────────────────────────────────────────────────────────────────────────
guard-registry-namespace:
name: "Guard Registry Namespace"
needs: detect-changes
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Fail on typoed GHCR namespace
run: |
set -e
TARGETS=".github/workflows docker-compose.yaml docker-stack.swarm.yaml scripts sirius-api sirius-engine sirius-postgres sirius-rabbitmq sirius-ui sirius-valkey"
BAD_NAMESPACE="${{ env.REGISTRY }}/sirius""cam/"
if grep -R --line-number "$BAD_NAMESPACE" $TARGETS; then
echo "::error::Found typoed GHCR namespace '${BAD_NAMESPACE}'. Use '${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/'."
exit 1
fi
echo "Registry namespace guard passed."
# ─────────────────────────────────────────────────────────────────────────────
# Build sirius-ui
# PRs build amd64 only; main/dispatch builds both arches (arm64 via QEMU).
# ─────────────────────────────────────────────────────────────────────────────
build-ui:
name: "Build UI (${{ matrix.platform }})"
needs: [detect-changes, guard-registry-namespace]
if: needs.detect-changes.outputs.sirius_ui_changes == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
platform: ${{ github.event_name == 'pull_request' && fromJSON('["amd64"]') || fromJSON('["amd64","arm64"]') }}
steps:
- name: Checkout Sirius repository
uses: actions/checkout@v4
- name: Generate image tag
id: meta
run: |
if [ "${{ github.event_name }}" == "pull_request" ]; then
TAG="pr-${{ github.event.number }}"
elif [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == "refs/heads/main" ]; then
TAG="latest"
echo "also_tag_beta=true" >> $GITHUB_OUTPUT
elif [ "${{ github.event_name }}" == "repository_dispatch" ]; then
TAG="latest"
echo "also_tag_beta=true" >> $GITHUB_OUTPUT
else
TAG="dev"
fi
echo "image_tag=$TAG" >> $GITHUB_OUTPUT
- name: Install UI dependencies
run: npm ci
working-directory: sirius-ui
- name: Lint UI code
continue-on-error: true
run: npm run lint
working-directory: sirius-ui
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.GHCR_PUSH_USER }}
password: ${{ secrets.GHCR_PUSH_TOKEN }}
- name: Build and push sirius-ui (${{ matrix.platform }})
uses: docker/build-push-action@v6
with:
context: ./sirius-ui
platforms: linux/${{ matrix.platform }}
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/sirius-ui:${{ steps.meta.outputs.image_tag }}-${{ matrix.platform }}
# Merge arch-specific UI images into a single multi-arch manifest
merge-ui:
name: "Merge UI Manifest"
needs: build-ui
if: needs.detect-changes.outputs.sirius_ui_changes == 'true' && github.event_name != 'pull_request'
runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.meta.outputs.image_tag }}
steps:
- name: Generate image tag
id: meta
run: |
if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == "refs/heads/main" ]; then
echo "image_tag=latest" >> $GITHUB_OUTPUT
echo "also_tag_beta=true" >> $GITHUB_OUTPUT
elif [ "${{ github.event_name }}" == "repository_dispatch" ]; then
echo "image_tag=latest" >> $GITHUB_OUTPUT
echo "also_tag_beta=true" >> $GITHUB_OUTPUT
else
echo "image_tag=dev" >> $GITHUB_OUTPUT
fi
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.GHCR_PUSH_USER }}
password: ${{ secrets.GHCR_PUSH_TOKEN }}
- name: Create and push multi-arch manifest for sirius-ui
run: |
TAG="${{ steps.meta.outputs.image_tag }}"
BASE="${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/sirius-ui"
docker buildx imagetools create -t ${BASE}:${TAG} \
${BASE}:${TAG}-amd64 \
${BASE}:${TAG}-arm64
if [ "${{ steps.meta.outputs.also_tag_beta }}" == "true" ]; then
docker buildx imagetools create -t ${BASE}:beta \
${BASE}:${TAG}-amd64 \
${BASE}:${TAG}-arm64
fi
echo "Published ${BASE}:${TAG} (amd64 + arm64)"
# ─────────────────────────────────────────────────────────────────────────────
# Build sirius-api
# ─────────────────────────────────────────────────────────────────────────────
build-api:
name: "Build API (${{ matrix.platform }})"
needs: [detect-changes, guard-registry-namespace]
if: needs.detect-changes.outputs.sirius_api_changes == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
platform: ${{ github.event_name == 'pull_request' && fromJSON('["amd64"]') || fromJSON('["amd64","arm64"]') }}
steps:
- name: Checkout Sirius repository
uses: actions/checkout@v4
- name: Generate image tag
id: meta
run: |
if [ "${{ github.event_name }}" == "repository_dispatch" ]; then
SUBMODULE="${{ github.event.client_payload.submodule }}"
REPO_NAME=$(echo $SUBMODULE | awk -F'/' '{print $NF}')
COMMIT_SHA="${{ github.event.client_payload.commit_sha }}"
if [ "$REPO_NAME" == "go-api" ]; then
echo "GO_API_COMMIT_SHA=$COMMIT_SHA" >> $GITHUB_ENV
fi
fi
if [ "${{ github.event_name }}" == "pull_request" ]; then
TAG="pr-${{ github.event.number }}"
elif [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == "refs/heads/main" ]; then
TAG="latest"
echo "also_tag_beta=true" >> $GITHUB_OUTPUT
elif [ "${{ github.event_name }}" == "repository_dispatch" ]; then
TAG="latest"
echo "also_tag_beta=true" >> $GITHUB_OUTPUT
else
TAG="dev"
fi
echo "image_tag=$TAG" >> $GITHUB_OUTPUT
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Compile-check API code
run: |
go mod download
go vet ./...
working-directory: sirius-api
- name: Run API tests (non-blocking)
continue-on-error: true
run: go test ./... -short -count=1
working-directory: sirius-api
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.GHCR_PUSH_USER }}
password: ${{ secrets.GHCR_PUSH_TOKEN }}
- name: Build and push sirius-api (${{ matrix.platform }})
uses: docker/build-push-action@v6
with:
context: ./sirius-api
platforms: linux/${{ matrix.platform }}
push: true
# Fallback SHAs MUST match sirius-engine/Dockerfile ARG defaults.
# Enforced by .github/workflows/check-pin-consistency.yml.
build-args: |
GO_API_COMMIT_SHA=${{ env.GO_API_COMMIT_SHA || 'v0.0.18' }}
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/sirius-api:${{ steps.meta.outputs.image_tag }}-${{ matrix.platform }}
# Merge arch-specific API images into a single multi-arch manifest
merge-api:
name: "Merge API Manifest"
needs: build-api
if: needs.detect-changes.outputs.sirius_api_changes == 'true' && github.event_name != 'pull_request'
runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.meta.outputs.image_tag }}
steps:
- name: Generate image tag
id: meta
run: |
if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == "refs/heads/main" ]; then
echo "image_tag=latest" >> $GITHUB_OUTPUT
echo "also_tag_beta=true" >> $GITHUB_OUTPUT
elif [ "${{ github.event_name }}" == "repository_dispatch" ]; then
echo "image_tag=latest" >> $GITHUB_OUTPUT
echo "also_tag_beta=true" >> $GITHUB_OUTPUT
else
echo "image_tag=dev" >> $GITHUB_OUTPUT
fi
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.GHCR_PUSH_USER }}
password: ${{ secrets.GHCR_PUSH_TOKEN }}
- name: Create and push multi-arch manifest for sirius-api
run: |
TAG="${{ steps.meta.outputs.image_tag }}"
BASE="${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/sirius-api"
docker buildx imagetools create -t ${BASE}:${TAG} \
${BASE}:${TAG}-amd64 \
${BASE}:${TAG}-arm64
if [ "${{ steps.meta.outputs.also_tag_beta }}" == "true" ]; then
docker buildx imagetools create -t ${BASE}:beta \
${BASE}:${TAG}-amd64 \
${BASE}:${TAG}-arm64
fi
echo "Published ${BASE}:${TAG} (amd64 + arm64)"
# ─────────────────────────────────────────────────────────────────────────────
# Build sirius-engine
# ─────────────────────────────────────────────────────────────────────────────
build-engine:
name: "Build Engine (${{ matrix.platform }})"
needs: [detect-changes, guard-registry-namespace]
if: needs.detect-changes.outputs.sirius_engine_changes == 'true'
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
platform: ${{ github.event_name == 'pull_request' && fromJSON('["amd64"]') || fromJSON('["amd64","arm64"]') }}
steps:
- name: Checkout Sirius repository
uses: actions/checkout@v4
- name: Generate image tag and submodule SHAs
id: meta
run: |
if [ "${{ github.event_name }}" == "repository_dispatch" ]; then
SUBMODULE="${{ github.event.client_payload.submodule }}"
REPO_NAME=$(echo $SUBMODULE | awk -F'/' '{print $NF}')
COMMIT_SHA="${{ github.event.client_payload.commit_sha }}"
REPO_NAME_UPPER=$(echo $REPO_NAME | tr '-' '_' | tr 'a-z' 'A-Z')
echo "${REPO_NAME_UPPER}_COMMIT_SHA=$COMMIT_SHA" >> $GITHUB_ENV
fi
if [ "${{ github.event_name }}" == "pull_request" ]; then
TAG="pr-${{ github.event.number }}"
elif [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == "refs/heads/main" ]; then
TAG="latest"
echo "also_tag_beta=true" >> $GITHUB_OUTPUT
elif [ "${{ github.event_name }}" == "repository_dispatch" ]; then
TAG="latest"
echo "also_tag_beta=true" >> $GITHUB_OUTPUT
else
TAG="dev"
fi
echo "image_tag=$TAG" >> $GITHUB_OUTPUT
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
- name: Compile-check Engine code
run: |
go mod download
go vet ./...
working-directory: sirius-engine
- name: Run Engine tests (non-blocking)
continue-on-error: true
run: go test ./... -short -count=1
working-directory: sirius-engine
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.GHCR_PUSH_USER }}
password: ${{ secrets.GHCR_PUSH_TOKEN }}
- name: Build and push sirius-engine (${{ matrix.platform }})
uses: docker/build-push-action@v6
with:
context: ./sirius-engine
platforms: linux/${{ matrix.platform }}
push: true
# Fallback SHAs MUST match sirius-engine/Dockerfile ARG defaults.
# Floating refs (main/master) are forbidden; use a full SHA or a tag.
# Enforced by .github/workflows/check-pin-consistency.yml.
# See documentation/dev/architecture/README.engine-component-pinning.md.
build-args: |
GO_API_COMMIT_SHA=${{ env.GO_API_COMMIT_SHA || 'v0.0.18' }}
APP_SCANNER_COMMIT_SHA=${{ env.APP_SCANNER_COMMIT_SHA || 'a031480cc7e0dc4a1cb09bdcfafedd89fbc61dfd' }}
APP_TERMINAL_COMMIT_SHA=${{ env.APP_TERMINAL_COMMIT_SHA || '5745e43ca1f2ad3936a02a51cc77bfcf33ee95c1' }}
SIRIUS_NSE_COMMIT_SHA=${{ env.SIRIUS_NSE_COMMIT_SHA || 'a58e8c5330b49bd8f4e93c27e340c43c1fa89fa9' }}
APP_AGENT_COMMIT_SHA=${{ env.APP_AGENT_COMMIT_SHA || '9311f3805db9cc08740f1b588b6f3bff9191c9dd' }}
PINGPP_COMMIT_SHA=${{ env.PINGPP_COMMIT_SHA || '9508a16c40a49746feb7cab4b8c1c358246169b0' }}
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/sirius-engine:${{ steps.meta.outputs.image_tag }}-${{ matrix.platform }}
# Merge arch-specific Engine images into a single multi-arch manifest
merge-engine:
name: "Merge Engine Manifest"
needs: build-engine
if: needs.detect-changes.outputs.sirius_engine_changes == 'true' && github.event_name != 'pull_request'
runs-on: ubuntu-latest
outputs:
image_tag: ${{ steps.meta.outputs.image_tag }}
steps:
- name: Generate image tag
id: meta
run: |
if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == "refs/heads/main" ]; then
echo "image_tag=latest" >> $GITHUB_OUTPUT
echo "also_tag_beta=true" >> $GITHUB_OUTPUT
elif [ "${{ github.event_name }}" == "repository_dispatch" ]; then
echo "image_tag=latest" >> $GITHUB_OUTPUT
echo "also_tag_beta=true" >> $GITHUB_OUTPUT
else
echo "image_tag=dev" >> $GITHUB_OUTPUT
fi
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.GHCR_PUSH_USER }}
password: ${{ secrets.GHCR_PUSH_TOKEN }}
- name: Create and push multi-arch manifest for sirius-engine
run: |
TAG="${{ steps.meta.outputs.image_tag }}"
BASE="${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/sirius-engine"
docker buildx imagetools create -t ${BASE}:${TAG} \
${BASE}:${TAG}-amd64 \
${BASE}:${TAG}-arm64
if [ "${{ steps.meta.outputs.also_tag_beta }}" == "true" ]; then
docker buildx imagetools create -t ${BASE}:beta \
${BASE}:${TAG}-amd64 \
${BASE}:${TAG}-arm64
fi
echo "Published ${BASE}:${TAG} (amd64 + arm64)"
# ─────────────────────────────────────────────────────────────────────────────
# Build infrastructure images used by the default public stack.
# ─────────────────────────────────────────────────────────────────────────────
build-infra:
name: "Build Infra (${{ matrix.service }}, ${{ matrix.platform }})"
needs: [detect-changes, guard-registry-namespace]
if: >
needs.detect-changes.outputs.sirius_infra_changes == 'true' ||
github.event_name == 'repository_dispatch' ||
(github.event_name == 'push' && github.ref == 'refs/heads/main')
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
service: [sirius-postgres, sirius-rabbitmq, sirius-valkey]
platform: ${{ github.event_name == 'pull_request' && fromJSON('["amd64"]') || fromJSON('["amd64","arm64"]') }}
steps:
- name: Checkout Sirius repository
uses: actions/checkout@v4
- name: Generate image tag
id: meta
run: |
if [ "${{ github.event_name }}" == "pull_request" ]; then
TAG="pr-${{ github.event.number }}"
elif [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == "refs/heads/main" ]; then
TAG="latest"
echo "also_tag_beta=true" >> $GITHUB_OUTPUT
elif [ "${{ github.event_name }}" == "repository_dispatch" ]; then
TAG="latest"
echo "also_tag_beta=true" >> $GITHUB_OUTPUT
else
TAG="dev"
fi
echo "image_tag=$TAG" >> $GITHUB_OUTPUT
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.GHCR_PUSH_USER }}
password: ${{ secrets.GHCR_PUSH_TOKEN }}
- name: Build and push infra image (${{ matrix.service }}, ${{ matrix.platform }})
uses: docker/build-push-action@v6
with:
context: ./${{ matrix.service }}
platforms: linux/${{ matrix.platform }}
push: true
tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.service }}:${{ steps.meta.outputs.image_tag }}-${{ matrix.platform }}
merge-infra:
name: "Merge Infra Manifest (${{ matrix.service }})"
needs: [detect-changes, build-infra]
if: >
github.event_name != 'pull_request' &&
(
needs.detect-changes.outputs.sirius_infra_changes == 'true' ||
github.event_name == 'repository_dispatch' ||
(github.event_name == 'push' && github.ref == 'refs/heads/main')
)
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
service: [sirius-postgres, sirius-rabbitmq, sirius-valkey]
steps:
- name: Generate image tag
id: meta
run: |
if [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == "refs/heads/main" ]; then
echo "image_tag=latest" >> $GITHUB_OUTPUT
echo "also_tag_beta=true" >> $GITHUB_OUTPUT
elif [ "${{ github.event_name }}" == "repository_dispatch" ]; then
echo "image_tag=latest" >> $GITHUB_OUTPUT
echo "also_tag_beta=true" >> $GITHUB_OUTPUT
else
echo "image_tag=dev" >> $GITHUB_OUTPUT
fi
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.GHCR_PUSH_USER }}
password: ${{ secrets.GHCR_PUSH_TOKEN }}
- name: Create and push multi-arch manifest for infra image
run: |
TAG="${{ steps.meta.outputs.image_tag }}"
BASE="${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/${{ matrix.service }}"
docker buildx imagetools create -t ${BASE}:${TAG} \
${BASE}:${TAG}-amd64 \
${BASE}:${TAG}-arm64
if [ "${{ steps.meta.outputs.also_tag_beta }}" == "true" ]; then
docker buildx imagetools create -t ${BASE}:beta \
${BASE}:${TAG}-amd64 \
${BASE}:${TAG}-arm64
fi
echo "Published ${BASE}:${TAG} (amd64 + arm64)"
# ─────────────────────────────────────────────────────────────────────────────
# Integration Test
#
# Runs after at least one service build (or merge for multi-arch) succeeds.
# Key fixes vs the previous version:
# 1. sirius-api is ALWAYS started when sirius-engine is tested, because
# start-enhanced.sh validates the API key contract at startup.
# 2. sirius-engine depends_on sirius-api in the test compose config.
# 3. The health-wait loop covers sirius-api even when only engine changed.
# ─────────────────────────────────────────────────────────────────────────────
test:
name: Integration Test
needs:
- detect-changes
- build-ui
- build-api
- build-engine
runs-on: ubuntu-latest
if: >
always() &&
(
needs.build-ui.result == 'success' ||
needs.build-api.result == 'success' ||
needs.build-engine.result == 'success'
)
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.GHCR_PUSH_USER }}
password: ${{ secrets.GHCR_PUSH_TOKEN }}
- name: Determine image tag
id: tag
run: |
# Compute the tag the same way build jobs do (from event context).
# build-* are matrix jobs with no job-level outputs, so we cannot
# use needs.build-ui.outputs.image_tag — it is always empty.
if [ "${{ github.event_name }}" == "pull_request" ]; then
TAG="pr-${{ github.event.number }}"
elif [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == "refs/heads/main" ]; then
TAG="latest"
elif [ "${{ github.event_name }}" == "push" ] && [ "${{ github.ref }}" == "refs/heads/sirius-demo" ]; then
TAG="dev"
elif [ "${{ github.event_name }}" == "repository_dispatch" ]; then
TAG="latest"
else
TAG="latest"
fi
# Integration tests always use the amd64 arch-specific image tag
echo "image_tag=${TAG}-amd64" >> $GITHUB_OUTPUT
echo "Using image tag: ${TAG}-amd64"
- name: Create test environment
run: |
cat > docker-compose.test.yml << 'EOF'
name: sirius-test
services:
sirius-postgres:
image: postgres:15-alpine
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: sirius_test
tmpfs:
- /var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 10
sirius-rabbitmq:
image: rabbitmq:3-management
environment:
RABBITMQ_DEFAULT_USER: guest
RABBITMQ_DEFAULT_PASS: guest
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "ping"]
interval: 5s
timeout: 5s
retries: 10
sirius-valkey:
image: valkey/valkey:latest
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 5s
retries: 10
sirius-ui:
image: ${SIRIUS_UI_IMAGE}
environment:
- NODE_ENV=production
- SKIP_ENV_VALIDATION=1
- DATABASE_URL=postgresql://postgres:postgres@sirius-postgres:5432/sirius_test
- NEXTAUTH_SECRET=test-secret
- INITIAL_ADMIN_PASSWORD=test-admin-password
- NEXTAUTH_URL=http://localhost:3000
- SIRIUS_API_URL=http://sirius-api:9001
- NEXT_PUBLIC_SIRIUS_API_URL=http://localhost:9001
- SIRIUS_API_KEY_FILE=/run/secrets/sirius_api_key
secrets:
- sirius_api_key
depends_on:
sirius-postgres:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -q --tries=1 -O /dev/null http://127.0.0.1:3000/"]
interval: 10s
timeout: 5s
retries: 12
start_period: 15s
sirius-api:
image: ${SIRIUS_API_IMAGE}
environment:
- GO_ENV=production
- API_PORT=9001
- POSTGRES_HOST=sirius-postgres
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=sirius_test
- POSTGRES_PORT=5432
- VALKEY_HOST=sirius-valkey
- VALKEY_PORT=6379
- RABBITMQ_URL=amqp://guest:guest@sirius-rabbitmq:5672/
- LOG_LEVEL=info
- SIRIUS_API_KEY_FILE=/run/secrets/sirius_api_key
secrets:
- sirius_api_key
depends_on:
sirius-postgres:
condition: service_healthy
sirius-rabbitmq:
condition: service_healthy
sirius-valkey:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "KEY=$(tr -d '\\r\\n' < /run/secrets/sirius_api_key); STATUS=$(curl -s -o /dev/null -w '%{http_code}' -H \"X-API-Key: $$KEY\" http://127.0.0.1:9001/host/); echo \"API health probe: $$STATUS\"; [ -n \"$$STATUS\" ] && [ \"$$STATUS\" != '000' ] && echo \"$$STATUS\" | grep -qvE '^5'"]
interval: 10s
timeout: 10s
retries: 24
start_period: 30s
sirius-engine:
image: ${SIRIUS_ENGINE_IMAGE}
environment:
- GO_ENV=production
- ENGINE_MAIN_PORT=5174
- GRPC_AGENT_PORT=50051
- POSTGRES_HOST=sirius-postgres
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=sirius_test
- POSTGRES_PORT=5432
- VALKEY_HOST=sirius-valkey
- VALKEY_PORT=6379
- RABBITMQ_URL=amqp://guest:guest@sirius-rabbitmq:5672/
- LOG_LEVEL=info
- SIRIUS_API_KEY_FILE=/run/secrets/sirius_api_key
- SIRIUS_API_URL=http://sirius-api:9001
- API_BASE_URL=http://sirius-api:9001
secrets:
- sirius_api_key
depends_on:
sirius-postgres:
condition: service_healthy
sirius-rabbitmq:
condition: service_healthy
sirius-valkey:
condition: service_healthy
sirius-api:
condition: service_healthy
healthcheck:
test: ["CMD", "bash", "-c", "echo > /dev/tcp/127.0.0.1/50051"]
interval: 10s
timeout: 5s
retries: 12
start_period: 30s
secrets:
sirius_api_key:
file: ./secrets/sirius_api_key.txt
EOF
mkdir -p secrets
printf '%s\n' 'ci-placeholder-api-key' > secrets/sirius_api_key.txt
chmod 644 secrets/sirius_api_key.txt
IMAGE_TAG="${{ steps.tag.outputs.image_tag }}"
export SIRIUS_UI_IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/sirius-ui:${IMAGE_TAG}"
export SIRIUS_API_IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/sirius-api:${IMAGE_TAG}"
export SIRIUS_ENGINE_IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAMESPACE }}/sirius-engine:${IMAGE_TAG}"
echo "SIRIUS_UI_IMAGE=$SIRIUS_UI_IMAGE" >> $GITHUB_ENV
echo "SIRIUS_API_IMAGE=$SIRIUS_API_IMAGE" >> $GITHUB_ENV
echo "SIRIUS_ENGINE_IMAGE=$SIRIUS_ENGINE_IMAGE" >> $GITHUB_ENV
- name: Run integration test
run: |
echo "Starting integration test..."
# 1. Infrastructure services
docker compose -f docker-compose.test.yml up -d sirius-postgres sirius-rabbitmq sirius-valkey
echo "Waiting for infrastructure services to become healthy..."
timeout 90 bash -c '
until docker compose -f docker-compose.test.yml ps --format json | \
python3 -c "
import sys, json
svcs = [json.loads(l) for l in sys.stdin if l.strip()]
infra = [s for s in svcs if s[\"Service\"] in (\"sirius-postgres\",\"sirius-rabbitmq\",\"sirius-valkey\")]
healthy = [s for s in infra if s.get(\"Health\",\"\") == \"healthy\"]
sys.exit(0 if len(healthy) == 3 else 1)
"; do sleep 3; done
'
echo "Infrastructure healthy"
# 2. Determine which application services to test.
# IMPORTANT: sirius-api must always be started alongside sirius-engine
# because start-enhanced.sh validates the API key contract at startup.
SERVICES_TO_TEST=""
API_REQUIRED=false
if [ "${{ needs.detect-changes.outputs.sirius_api_changes }}" == "true" ]; then
SERVICES_TO_TEST="$SERVICES_TO_TEST sirius-api"
API_REQUIRED=true
fi
if [ "${{ needs.detect-changes.outputs.sirius_engine_changes }}" == "true" ]; then
SERVICES_TO_TEST="$SERVICES_TO_TEST sirius-engine"
# Engine requires the API to be running for its startup preflight check
if [ "$API_REQUIRED" != "true" ]; then
SERVICES_TO_TEST="sirius-api$SERVICES_TO_TEST"
API_REQUIRED=true
echo "Adding sirius-api as required dependency for sirius-engine"
fi
fi
if [ "${{ needs.detect-changes.outputs.sirius_ui_changes }}" == "true" ]; then
SERVICES_TO_TEST="$SERVICES_TO_TEST sirius-ui"
fi
if [ -z "$SERVICES_TO_TEST" ]; then
echo "No application services to test"
exit 0
fi
echo "Starting application services:$SERVICES_TO_TEST"
docker compose -f docker-compose.test.yml up -d $SERVICES_TO_TEST
# 3. Wait for each service to become healthy using Docker's built-in health status.
# This avoids exec-into-container quoting issues (e.g. the engine uses
# /dev/tcp which requires bash and breaks sh -c quoting).
for SVC in $SERVICES_TO_TEST; do
CONTAINER="sirius-test-${SVC}-1"
echo "Waiting for $SVC..."
timeout 180 bash -c "until [ \"\$(docker inspect --format '{{.State.Health.Status}}' ${CONTAINER} 2>/dev/null)\" = 'healthy' ]; do sleep 5; done" \
|| { echo "::error::Timeout waiting for ${SVC} to become healthy"; exit 1; }
echo "$SVC is healthy"
done
# 4. Final health assertions
echo "Service status:"
docker compose -f docker-compose.test.yml ps
echo "Running final health assertions..."
FAILED=0
for SVC in $SERVICES_TO_TEST; do
CONTAINER="sirius-test-${SVC}-1"
HEALTH=$(docker inspect --format '{{.State.Health.Status}}' "$CONTAINER" 2>/dev/null)
if [ "$HEALTH" = "healthy" ]; then
echo " $SVC passed (healthy)"
else
echo " $SVC FAILED (status: ${HEALTH:-unknown})"
FAILED=1
fi
done
if [ "$FAILED" -eq 1 ]; then
echo "::error::One or more services failed their health check"
exit 1
fi
echo "All health checks passed"
- name: Capture diagnostics on failure
if: failure()
run: |
mkdir -p ci-diagnostics
echo "=== docker compose ps ===" > ci-diagnostics/compose-status.txt
docker compose -f docker-compose.test.yml ps --format "table {{.Name}}\t{{.Service}}\t{{.Status}}\t{{.Health}}" \
>> ci-diagnostics/compose-status.txt 2>&1 || true
echo "=== sirius-engine inspect ===" > ci-diagnostics/engine-inspect.json
docker inspect sirius-test-sirius-engine-1 >> ci-diagnostics/engine-inspect.json 2>&1 || true
for SVC in sirius-engine sirius-api sirius-ui sirius-postgres sirius-rabbitmq sirius-valkey; do
echo "--- $SVC logs (last 200 lines) ---" > "ci-diagnostics/${SVC}.log"
docker compose -f docker-compose.test.yml logs --tail=200 "$SVC" \
>> "ci-diagnostics/${SVC}.log" 2>&1 || true
done
echo "::group::Compose Status"
cat ci-diagnostics/compose-status.txt
echo "::endgroup::"
echo "::group::sirius-engine logs"
cat ci-diagnostics/sirius-engine.log
echo "::endgroup::"
echo "::group::sirius-api logs"
cat ci-diagnostics/sirius-api.log
echo "::endgroup::"
- name: Upload diagnostic artifacts
if: failure()
uses: actions/upload-artifact@v4
with:
name: integration-test-diagnostics
path: ci-diagnostics/
retention-days: 7
- name: Cleanup test environment
if: always()
run: |
docker compose -f docker-compose.test.yml down --volumes --remove-orphans 2>/dev/null || true
public-stack-contract:
name: Public Stack Contract
needs:
- build-ui
- build-api
- build-engine
- build-infra
- merge-ui
- merge-api
- merge-engine
- merge-infra
- test
runs-on: ubuntu-latest
if: >
always() &&
(
github.event_name == 'repository_dispatch' ||
(github.event_name == 'push' && github.ref == 'refs/heads/main')
) &&
(needs.build-ui.result == 'success' || needs.build-ui.result == 'skipped') &&
(needs.build-api.result == 'success' || needs.build-api.result == 'skipped') &&
(needs.build-engine.result == 'success' || needs.build-engine.result == 'skipped') &&
(needs.build-infra.result == 'success' || needs.build-infra.result == 'skipped') &&
(needs.merge-ui.result == 'success' || needs.merge-ui.result == 'skipped') &&
(needs.merge-api.result == 'success' || needs.merge-api.result == 'skipped') &&
(needs.merge-engine.result == 'success' || needs.merge-engine.result == 'skipped') &&
(needs.merge-infra.result == 'success' || needs.merge-infra.result == 'skipped') &&
(needs.test.result == 'success' || needs.test.result == 'skipped')
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Validate public latest compose path
run: |
bash scripts/validate-public-compose-path.sh latest
# ─────────────────────────────────────────────────────────────────────────────
# Dispatch to sirius-demo when sirius-demo branch is pushed
# ─────────────────────────────────────────────────────────────────────────────
dispatch-demo-deployment:
name: Dispatch Demo Deployment (sirius-demo branch)
runs-on: ubuntu-latest
needs: [detect-changes, build-ui, build-api, build-engine, test]
if: >
github.ref == 'refs/heads/sirius-demo' &&
github.event_name == 'push' &&
always() &&
(needs.detect-changes.result == 'success' || needs.detect-changes.result == 'skipped') &&
(needs.build-ui.result == 'success' || needs.build-ui.result == 'skipped') &&
(needs.build-api.result == 'success' || needs.build-api.result == 'skipped') &&
(needs.build-engine.result == 'success' || needs.build-engine.result == 'skipped') &&
(needs.test.result == 'success' || needs.test.result == 'skipped')
steps:
- name: Dispatch to sirius-demo repository
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ secrets.DEMO_DISPATCH_TOKEN }}
repository: SiriusScan/sirius-demo
event-type: sirius-demo-updated
client-payload: |
{
"source_repo": "${{ github.repository }}",
"source_branch": "${{ github.ref_name }}",
"source_sha": "${{ github.sha }}",
"triggered_by": "${{ github.actor }}"
}
# ─────────────────────────────────────────────────────────────────────────────
# Canary dispatch to sirius-demo on every main branch push.
# Fixed: commit messages with newlines are sanitized before injection into JSON
# by using `gh api -f` flags which handle escaping correctly.
# ─────────────────────────────────────────────────────────────────────────────
dispatch-demo-canary:
name: Dispatch Demo Canary (main branch)
runs-on: ubuntu-latest
needs: [detect-changes, build-ui, build-api, build-engine, test]
if: >
github.ref == 'refs/heads/main' &&
github.event_name == 'push' &&
always() &&
(needs.detect-changes.result == 'success' || needs.detect-changes.result == 'skipped') &&
(needs.build-ui.result == 'success' || needs.build-ui.result == 'skipped') &&
(needs.build-api.result == 'success' || needs.build-api.result == 'skipped') &&
(needs.build-engine.result == 'success' || needs.build-engine.result == 'skipped') &&
(needs.test.result == 'success' || needs.test.result == 'skipped')
steps:
- name: Dispatch to sirius-demo repository (canary trigger)
env:
GH_TOKEN: ${{ secrets.DEMO_DISPATCH_TOKEN }}
COMMIT_MSG: ${{ github.event.head_commit.message }}
run: |
# Sanitize: take first line only and strip characters that break JSON
SAFE_MSG=$(printf '%s' "$COMMIT_MSG" | head -1 | tr -d '"\\' | cut -c1-200)
gh api repos/SiriusScan/sirius-demo/dispatches \
--method POST \
-f event_type=sirius-main-updated \
-f "client_payload[source_repo]=${{ github.repository }}" \
-f "client_payload[source_branch]=${{ github.ref_name }}" \
-f "client_payload[source_sha]=${{ github.sha }}" \
-f "client_payload[triggered_by]=${{ github.actor }}" \
-f "client_payload[commit_message]=$SAFE_MSG" \
-f "client_payload[canary]=true"