Refactor CI pipeline with matrix stages (#737)

This commit is contained in:
cloudneutral 2025-12-04 10:40:18 +08:00 committed by GitHub
parent e7847fa690
commit 8ee35d1765
12 changed files with 424 additions and 356 deletions

81
.github/actions/build/action.yml vendored Normal file
View File

@ -0,0 +1,81 @@
name: Build
description: Build artifacts for each service and platform.
inputs:
service:
description: Target service name
required: true
platform:
description: Target platform (e.g., linux/amd64)
required: true
environment:
description: Deployment environment (dev or prod)
required: true
runs:
using: composite
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Prepare Go toolchain
if: inputs.service != 'dashboard'
uses: actions/setup-go@v4
with:
go-version: '1.22'
- name: Build Go binaries
if: inputs.service != 'dashboard'
shell: bash
run: |
set -euo pipefail
platform="${{ inputs.platform }}"
goos="${platform%%/*}"
goarch="${platform##*/}"
mkdir -p build/${{ inputs.service }}/"${goos}-${goarch}"
declare -a targets
if [[ "${{ inputs.service }}" == "rag-server" ]]; then
targets=("rag-server/cmd/xcontrol-server" "rag-server/cmd/rag-server-cli")
elif [[ "${{ inputs.service }}" == "account" ]]; then
targets=("account/cmd/accountsvc")
else
targets=("./...")
fi
for target in "${targets[@]}"; do
binary_name=$(basename "$target")
GOOS="$goos" GOARCH="$goarch" go build -o build/${{ inputs.service }}/"${goos}-${goarch}"/"${binary_name}" "$target"
done
- name: Upload Go artifacts
if: inputs.service != 'dashboard'
uses: actions/upload-artifact@v4
with:
name: ${{ inputs.service }}-${{ inputs.platform }}-${{ inputs.environment }}
path: build/${{ inputs.service }}/
- name: Set up Node.js
if: inputs.service == 'dashboard'
uses: actions/setup-node@v4
with:
node-version: 20
cache: yarn
cache-dependency-path: dashboard/yarn.lock
- name: Install dashboard dependencies
if: inputs.service == 'dashboard'
working-directory: dashboard
shell: bash
run: yarn install --frozen-lockfile
- name: Build dashboard
if: inputs.service == 'dashboard'
working-directory: dashboard
shell: bash
env:
NEXT_PUBLIC_ENV: ${{ inputs.environment }}
run: yarn build
- name: Upload dashboard build output
if: inputs.service == 'dashboard'
uses: actions/upload-artifact@v4
with:
name: dashboard-${{ inputs.platform }}-${{ inputs.environment }}
path: dashboard/.next

62
.github/actions/code-quality/action.yml vendored Normal file
View File

@ -0,0 +1,62 @@
name: Code Quality
description: Run linting and basic quality checks per service/platform/environment matrix entry.
inputs:
service:
description: Target service name
required: true
platform:
description: Target platform (e.g., linux/amd64)
required: true
environment:
description: Deployment environment (dev or prod)
required: true
runs:
using: composite
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install git-secrets
shell: bash
run: |
set -euo pipefail
git clone https://github.com/awslabs/git-secrets.git
sudo make install -C git-secrets
git secrets --install
git secrets --scan
- name: Set up Go
if: inputs.service != 'dashboard'
uses: actions/setup-go@v4
with:
go-version: '1.22'
- name: Go vet
if: inputs.service != 'dashboard'
shell: bash
run: go vet ./...
- name: Go unit tests (quality gate)
if: inputs.service != 'dashboard'
shell: bash
run: go test ./...
- name: Set up Node.js
if: inputs.service == 'dashboard'
uses: actions/setup-node@v4
with:
node-version: 20
cache: yarn
cache-dependency-path: dashboard/yarn.lock
- name: Install dashboard dependencies
if: inputs.service == 'dashboard'
working-directory: dashboard
shell: bash
run: yarn install --frozen-lockfile
- name: Dashboard lint
if: inputs.service == 'dashboard'
working-directory: dashboard
shell: bash
run: yarn lint

41
.github/actions/deploy/action.yml vendored Normal file
View File

@ -0,0 +1,41 @@
name: Deploy
description: Coordinate deployments per service/environment.
inputs:
service:
description: Target service name
required: true
platform:
description: Target platform (e.g., linux/amd64)
required: true
environment:
description: Deployment environment (dev or prod)
required: true
runs:
using: composite
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Prepare rollout context
id: context
shell: bash
run: |
set -euo pipefail
echo "service=${{ inputs.service }}" >> "$GITHUB_OUTPUT"
echo "environment=${{ inputs.environment }}" >> "$GITHUB_OUTPUT"
echo "platform=${{ inputs.platform }}" >> "$GITHUB_OUTPUT"
- name: Deploy placeholder
shell: bash
env:
TARGET_ENV: ${{ steps.context.outputs.environment }}
TARGET_SERVICE: ${{ steps.context.outputs.service }}
TARGET_PLATFORM: ${{ steps.context.outputs.platform }}
run: |
echo "Deploying ${TARGET_SERVICE} (${TARGET_PLATFORM}) to ${TARGET_ENV} namespace"
echo "Hook in Helm/kubectl/ArgoCD rollouts here"
- name: Rollback plan
shell: bash
run: |
echo "Rollback can be re-run per matrix entry by dispatching with allow_deploy=true"

85
.github/actions/security/action.yml vendored Normal file
View File

@ -0,0 +1,85 @@
name: Security
description: Security scanning per service/platform/environment.
inputs:
service:
description: Target service name
required: true
platform:
description: Target platform (e.g., linux/amd64)
required: true
environment:
description: Deployment environment (dev or prod)
required: true
runs:
using: composite
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
if: inputs.service != 'dashboard'
uses: actions/setup-go@v4
with:
go-version: '1.22'
- name: Run golangci-lint
if: inputs.service != 'dashboard'
uses: golangci/golangci-lint-action@v6
with:
version: latest
args: ./...
- name: Install gosec
if: inputs.service != 'dashboard'
shell: bash
run: go install github.com/securego/gosec/v2/cmd/gosec@latest
- name: Run gosec
if: inputs.service != 'dashboard'
shell: bash
run: gosec ./...
- name: Trivy filesystem scan
if: inputs.service != 'dashboard'
uses: aquasecurity/trivy-action@0.24.0
with:
scan-type: fs
scan-ref: .
severity: HIGH,CRITICAL
ignore-unfixed: true
format: table
exit-code: "0"
- name: Set up Node.js
if: inputs.service == 'dashboard'
uses: actions/setup-node@v4
with:
node-version: 20
cache: yarn
cache-dependency-path: dashboard/yarn.lock
- name: Install dashboard dependencies
if: inputs.service == 'dashboard'
working-directory: dashboard
shell: bash
run: yarn install --frozen-lockfile
- name: Run ESLint
if: inputs.service == 'dashboard'
working-directory: dashboard
shell: bash
run: yarn lint
- name: Semgrep security rules
if: inputs.service == 'dashboard'
uses: returntocorp/semgrep-action@v1
with:
config: p/ci
paths: dashboard
- name: npm audit (production)
if: inputs.service == 'dashboard'
working-directory: dashboard
shell: bash
run: npm audit --production
continue-on-error: true

61
.github/actions/test/action.yml vendored Normal file
View File

@ -0,0 +1,61 @@
name: Test
description: Run service-specific tests.
inputs:
service:
description: Target service name
required: true
platform:
description: Target platform (e.g., linux/amd64)
required: true
environment:
description: Deployment environment (dev or prod)
required: true
runs:
using: composite
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
if: inputs.service != 'dashboard'
uses: actions/setup-go@v4
with:
go-version: '1.22'
- name: Run Go integration tests
if: inputs.service != 'dashboard'
shell: bash
run: |
set -euo pipefail
go test ./... -run Integration -count=1
- name: Set up Node.js
if: inputs.service == 'dashboard'
uses: actions/setup-node@v4
with:
node-version: 20
cache: yarn
cache-dependency-path: dashboard/yarn.lock
- name: Install dashboard dependencies
if: inputs.service == 'dashboard'
working-directory: dashboard
shell: bash
run: yarn install --frozen-lockfile
- name: Run dashboard unit tests
if: inputs.service == 'dashboard'
working-directory: dashboard
shell: bash
env:
NODE_ENV: ${{ inputs.environment }}
run: yarn test:unit
- name: Run dashboard e2e tests
if: inputs.service == 'dashboard'
working-directory: dashboard
shell: bash
env:
PORT: 3100
NODE_ENV: ${{ inputs.environment }}
run: yarn test:e2e

View File

@ -1,114 +0,0 @@
name: Build and Release
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
deploy_environment:
description: "Deployment target environment"
type: choice
options: [staging, production]
default: staging
allow_deploy:
description: "Trigger deployment stage when dispatching manually"
type: boolean
default: true
permissions:
contents: read
packages: write
id-token: write
jobs:
static-security:
name: 🛡️ Static checks (lint, SAST)
uses: ./.github/workflows/security-check.yml
secrets: inherit
code-quality:
name: 🧰 Unit tests & code vetting
uses: ./.github/workflows/code-analysis.yml
secrets: inherit
build-go-matrix:
name: 🎧 Build Go artifacts (multi-arch)
needs:
- static-security
- code-quality
runs-on: ubuntu-latest
strategy:
matrix:
include:
- { goos: linux, goarch: amd64 }
- { goos: linux, goarch: arm64 }
- { goos: darwin, goarch: amd64 }
- { goos: windows, goarch: amd64 }
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.22"
- name: Run Go tests (single representative target)
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
run: go test ./...
- name: Build server and CLI
run: |
set -euo pipefail
mkdir -p build
GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -o build/xcontrol-server-${{ matrix.goos }}-${{ matrix.goarch }} ./cmd/xcontrol-server
GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} go build -o build/xcontrol-cli-${{ matrix.goos }}-${{ matrix.goarch }} ./client
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: xcontrol-${{ matrix.goos }}-${{ matrix.goarch }}
path: |
build/xcontrol-server-${{ matrix.goos }}-${{ matrix.goarch }}
build/xcontrol-cli-${{ matrix.goos }}-${{ matrix.goarch }}
base-images:
name: 🛢️ Build base images (multi-arch)
needs: build-go-matrix
uses: ./.github/actions/build-images/base.yml
with:
push_images: ${{ github.event_name != 'pull_request' }}
secrets: inherit
service-images:
name: 🛢️ Build service images (multi-arch + SBOM)
needs:
- build-go-matrix
- base-images
uses: ./.github/actions/build-images/serive.yml
with:
push_images: ${{ github.event_name != 'pull_request' }}
secrets: inherit
deploy-and-rollback:
name: 🚀 Deploy & rollback hooks
needs:
- base-images
- service-images
if: (github.event_name == 'workflow_dispatch' && inputs.allow_deploy == true) || github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Prepare rollout context
id: context
run: |
set -euo pipefail
env_name="${{ github.event.inputs.deploy_environment || 'staging' }}"
echo "environment=${env_name}" >> "$GITHUB_OUTPUT"
- name: Deploy (placeholder)
env:
TARGET_ENV: ${{ steps.context.outputs.environment }}
run: |
echo "Deploying images to ${TARGET_ENV} via existing GitOps/Ansible pipelines"
echo "Add environment-specific rollout commands here"
- name: Rollback plan ready
run: |
echo "Rollback can be triggered by re-running this workflow with allow_deploy=true"
echo "and by pointing deploy_environment to the target to restore"

View File

@ -1,29 +0,0 @@
name: Validate CMS configuration
on:
pull_request:
paths:
- 'config/cms.json'
- 'config/cms.schema.json'
- 'scripts/validate_cms_config.py'
- '.github/workflows/cms-config.yml'
push:
branches: [main]
paths:
- 'config/cms.json'
- 'config/cms.schema.json'
- 'scripts/validate_cms_config.py'
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install dependencies
run: python -m pip install --upgrade pip jsonschema
- name: Validate cms.json
run: python scripts/validate_cms_config.py

View File

@ -1,52 +0,0 @@
name: Code Analysis
on:
workflow_call:
pull_request:
branches: [main]
workflow_dispatch:
jobs:
analyze:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install git-secrets
run: |
git clone https://github.com/awslabs/git-secrets.git
sudo make install -C git-secrets
git secrets --install
git secrets --scan
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: 1.21
- name: Vet
run: go vet ./...
- name: Run tests
run: go test ./...
dashboard-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: yarn
cache-dependency-path: dashboard/yarn.lock
- name: Install dashboard dependencies
working-directory: dashboard
run: yarn install --frozen-lockfile
- name: Install Playwright browsers
working-directory: dashboard
run: npx playwright install --with-deps chromium
- name: Run unit tests
working-directory: dashboard
run: yarn test:unit
- name: Run end-to-end tests
working-directory: dashboard
env:
PORT: 3100
run: yarn test:e2e

94
.github/workflows/pipeline.yml vendored Normal file
View File

@ -0,0 +1,94 @@
name: CloudNativeSuite Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
allow_deploy:
description: "Allow deployment stage to run"
type: boolean
default: true
permissions:
contents: read
packages: write
id-token: write
x-matrix: &matrix
platform: [linux/amd64, linux/arm64]
service: [dashboard, rag-server, account]
environment: [dev, prod]
jobs:
code-quality:
name: "Code quality • ${{ matrix.service }} @ ${{ matrix.platform }} (${{ matrix.environment }})"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix: *matrix
steps:
- uses: ./.github/actions/code-quality
with:
service: ${{ matrix.service }}
platform: ${{ matrix.platform }}
environment: ${{ matrix.environment }}
build:
name: "Build • ${{ matrix.service }} @ ${{ matrix.platform }} (${{ matrix.environment }})"
runs-on: ubuntu-latest
needs: code-quality
strategy:
fail-fast: false
matrix: *matrix
steps:
- uses: ./.github/actions/build
with:
service: ${{ matrix.service }}
platform: ${{ matrix.platform }}
environment: ${{ matrix.environment }}
test:
name: "Test • ${{ matrix.service }} @ ${{ matrix.platform }} (${{ matrix.environment }})"
runs-on: ubuntu-latest
needs: build
strategy:
fail-fast: false
matrix: *matrix
steps:
- uses: ./.github/actions/test
with:
service: ${{ matrix.service }}
platform: ${{ matrix.platform }}
environment: ${{ matrix.environment }}
security:
name: "Security • ${{ matrix.service }} @ ${{ matrix.platform }} (${{ matrix.environment }})"
runs-on: ubuntu-latest
needs: test
strategy:
fail-fast: false
matrix: *matrix
steps:
- uses: ./.github/actions/security
with:
service: ${{ matrix.service }}
platform: ${{ matrix.platform }}
environment: ${{ matrix.environment }}
deploy:
name: "Deploy • ${{ matrix.service }} (${{ matrix.environment }})"
runs-on: ubuntu-latest
needs: security
if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.event.inputs.allow_deploy == 'true')
strategy:
fail-fast: false
matrix: *matrix
steps:
- uses: ./.github/actions/deploy
with:
service: ${{ matrix.service }}
platform: ${{ matrix.platform }}
environment: ${{ matrix.environment }}

View File

@ -1,36 +0,0 @@
name: RAG Benchmark
on:
pull_request:
push:
branches: [ main, 'release/**' ]
jobs:
bench:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with: { go-version: '1.22' }
- name: Run ragbench
env:
API_BASE: ${{ secrets.RAG_API_BASE }}
run: |
cd ragbench
if [ -n "${API_BASE}" ]; then API_FLAG="-api ${API_BASE}"; fi
go run ./cmd/ragbench ${API_FLAG} -in queries.yaml -out report.md
- name: Upload report artifact
uses: actions/upload-artifact@v4
with:
name: rag-benchmark-report
path: ragbench/report.md
if-no-files-found: error
- name: Comment report to PR
if: github.event_name == 'pull_request'
uses: marocchino/sticky-pull-request-comment@v2
with:
path: ragbench/report.md

View File

@ -1,49 +0,0 @@
name: Require Cherry-pick to release/*
on:
pull_request:
branches: ['release/**']
types: [opened, synchronize, reopened]
jobs:
verify:
runs-on: ubuntu-latest
steps:
- name: Checkout PR HEAD with history
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fetch main
run: git fetch origin main --quiet
- name: Get PR commit SHAs (base..head)
id: list
run: |
echo "COMMITS=$(git log --format=%H origin/${{ github.base_ref }}..HEAD | tr '\n' ' ')" >> $GITHUB_OUTPUT
- name: Verify each commit is a cherry-pick from main
run: |
set -euo pipefail
for C in ${{ steps.list.outputs.COMMITS }}; do
MSG="$(git log -1 --pretty=%B "$C")"
echo "Checking $C"
# 1) 必须含有 -x 生成的 trailer
if ! echo "$MSG" | grep -qiE 'cherry picked from commit [0-9a-f]{7,40}'; then
echo "::error::Commit $C lacks 'cherry picked from commit <SHA>' (use 'git cherry-pick -x')."
exit 1
fi
# 2) 取出来源 SHA
ORIG=$(printf "%s" "$MSG" | sed -nE 's/.*cherry picked from commit ([0-9a-f]{7,40}).*/\1/ip' | head -n1 | tr -d '[:space:]')
if [ -z "$ORIG" ]; then
echo "::error::Cannot parse original commit SHA from $C message."
exit 1
fi
# 3) 来源必须在 main 上backport/forward-port 均可换成目标分支)
if ! git merge-base --is-ancestor "$ORIG" origin/main; then
echo "::error::Original commit $ORIG not found on origin/main."
exit 1
fi
done

View File

@ -1,76 +0,0 @@
name: Security Checks
on:
workflow_call:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
nextjs-security:
name: nextjs-security
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: yarn
cache-dependency-path: ui/dashboard/yarn.lock
- name: Install dependencies
working-directory: ui/dashboard
run: yarn install --frozen-lockfile
- name: Run ESLint
working-directory: ui/dashboard
run: yarn lint
- name: Run npm audit (production)
working-directory: ui/dashboard
run: npm audit --production
continue-on-error: true
- name: Run Semgrep security rules
uses: returntocorp/semgrep-action@v1
with:
config: p/ci
paths: ui/dashboard
go-security:
name: go-security
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.22"
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v6
with:
version: latest
args: ./...
- name: Install gosec
run: go install github.com/securego/gosec/v2/cmd/gosec@latest
- name: Run gosec
run: gosec ./...
- name: Run Trivy filesystem scan
uses: aquasecurity/trivy-action@0.24.0
with:
scan-type: fs
scan-ref: .
severity: HIGH,CRITICAL
ignore-unfixed: true
format: table
exit-code: "0"