ci: replace composite actions with reusable workflows and extract scripts

This commit is contained in:
Haitao Pan 2025-12-04 13:20:52 +08:00
parent 629e5baf17
commit 3ac729e651
8 changed files with 256 additions and 297 deletions

View File

@ -1,197 +0,0 @@
name: Build Base Images
on:
workflow_call:
outputs:
node_builder_image:
description: "Node builder image with the preferred tag for downstream workflows"
value: ${{ jobs.collect-base-images.outputs.node_builder_image }}
node_runtime_image:
description: "Node runtime image with the preferred tag for downstream workflows"
value: ${{ jobs.collect-base-images.outputs.node_runtime_image }}
openresty_geoip_image:
description: "OpenResty GeoIP image with the preferred tag for downstream workflows"
value: ${{ jobs.collect-base-images.outputs.openresty_geoip_image }}
postgres_runtime_image:
description: "Postgres runtime image with the preferred tag for downstream workflows"
value: ${{ jobs.collect-base-images.outputs.postgres_runtime_image }}
node_builder_digest:
description: "Node builder image digest for downstream workflows"
value: ${{ jobs.collect-base-images.outputs.node_builder_digest }}
node_runtime_digest:
description: "Node runtime image digest for downstream workflows"
value: ${{ jobs.collect-base-images.outputs.node_runtime_digest }}
openresty_geoip_digest:
description: "OpenResty GeoIP image digest for downstream workflows"
value: ${{ jobs.collect-base-images.outputs.openresty_geoip_digest }}
postgres_runtime_digest:
description: "Postgres runtime image digest for downstream workflows"
value: ${{ jobs.collect-base-images.outputs.postgres_runtime_digest }}
inputs:
push_images:
description: "Push images instead of building locally"
type: boolean
default: true
workflow_dispatch:
inputs:
push_images:
description: "Push images instead of building locally"
type: boolean
default: true
push:
paths:
- "deploy/base-images/**"
permissions:
contents: read
packages: write
id-token: write
env:
REGISTRY: ghcr.io
ORG: cloud-neutral-toolkit
jobs:
build-base:
strategy:
matrix:
image:
- { name: node-builder, file: deploy/base-images/node-builder.Dockerfile }
- { name: node-runtime, file: deploy/base-images/node-runtime.Dockerfile }
- { name: openresty-geoip, file: deploy/base-images/openresty-geoip.Dockerfile }
- { name: postgres-runtime, file: deploy/base-images/postgres-runtime-wth-extensions.Dockerfile }
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
#- uses: docker/metadata-action@v5
# id: meta
# with:
# images: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.image.name }}
# tags: |
# type=sha
# type=raw,value=latest
- name: Generate Auto Tags
id: meta
uses: ./.github/actions/auto-tag
with:
image: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.image.name }}
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
id: build
with:
context: .
file: ${{ matrix.image.file }}
platforms: linux/amd64,linux/arm64
push: ${{ (github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch') && inputs.push_images || github.event_name == 'push' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- uses: anchore/sbom-action@v0
with:
image: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.image.name }}@${{ steps.build.outputs.digest }}
output-file: sbom.spdx.json
- name: Capture image metadata
env:
IMAGE_NAME: ${{ matrix.image.name }}
DIGEST: ${{ steps.build.outputs.digest }}
META_TAGS: ${{ steps.meta.outputs.tags }}
run: |
python - <<'PY'
import json
import os
tags = os.environ.get("META_TAGS", "").splitlines()
preferred = next((tag for tag in tags if tag.endswith(":latest")), tags[0] if tags else "")
metadata = {
"name": os.environ.get("IMAGE_NAME", ""),
"image": f"{os.environ.get('REGISTRY')}/{os.environ.get('ORG')}/{os.environ.get('IMAGE_NAME')}",
"digest": os.environ.get("DIGEST", ""),
"tags": tags,
"preferred_tag": preferred,
"image_with_digest": f"{os.environ.get('REGISTRY')}/{os.environ.get('ORG')}/{os.environ.get('IMAGE_NAME')}@{os.environ.get('DIGEST', '')}",
}
with open("image-metadata.json", "w", encoding="utf-8") as f:
json.dump(metadata, f)
PY
- uses: actions/upload-artifact@v4
with:
name: base-image-metadata-${{ matrix.image.name }}
path: image-metadata.json
- uses: actions/upload-artifact@v4
with:
name: sbom-${{ matrix.image.name }}
path: sbom.spdx.json
- uses: sigstore/cosign-installer@v3
with:
cosign-release: 'v2.4.1'
- name: Sign
env:
COSIGN_EXPERIMENTAL: "true"
run: |
COSIGN_IMAGE=${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.image.name }}@${{ steps.build.outputs.digest }}
cosign sign --yes "$COSIGN_IMAGE"
collect-base-images:
runs-on: ubuntu-latest
needs: build-base
outputs:
node_builder_image: ${{ steps.expose.outputs.node_builder_image }}
node_runtime_image: ${{ steps.expose.outputs.node_runtime_image }}
openresty_geoip_image: ${{ steps.expose.outputs.openresty_geoip_image }}
postgres_runtime_image: ${{ steps.expose.outputs.postgres_runtime_image }}
node_builder_digest: ${{ steps.expose.outputs.node_builder_digest }}
node_runtime_digest: ${{ steps.expose.outputs.node_runtime_digest }}
openresty_geoip_digest: ${{ steps.expose.outputs.openresty_geoip_digest }}
postgres_runtime_digest: ${{ steps.expose.outputs.postgres_runtime_digest }}
steps:
- uses: actions/download-artifact@v4
with:
pattern: base-image-metadata-*
merge-multiple: true
path: metadata
- name: Extract preferred base image tags
id: expose
run: |
declare -A refs
declare -A digests
for file in metadata/*.json; do
name=$(jq -r '.name' "$file")
preferred=$(jq -r '.preferred_tag' "$file")
digest=$(jq -r '.digest' "$file")
refs[$name]=$preferred
digests[$name]=$digest
done
{
echo "node_builder_image=${refs[node-builder]}"
echo "node_runtime_image=${refs[node-runtime]}"
echo "openresty_geoip_image=${refs[openresty-geoip]}"
echo "postgres_runtime_image=${refs[postgres-runtime]}"
echo "node_builder_digest=${digests[node-builder]}"
echo "node_runtime_digest=${digests[node-runtime]}"
echo "openresty_geoip_digest=${digests[openresty-geoip]}"
echo "postgres_runtime_digest=${digests[postgres-runtime]}"
} >> "$GITHUB_OUTPUT"

9
.github/scripts/cosign/sign.sh vendored Executable file
View File

@ -0,0 +1,9 @@
#!/usr/bin/env bash
set -e
REG="ghcr.io/cloud-neutral-toolkit"
cosign sign --yes "$REG/node-builder@$NODE_BUILDER_DIGEST"
cosign sign --yes "$REG/node-runtime@$NODE_RUNTIME_DIGEST"
cosign sign --yes "$REG/openresty-geoip@$OPENRESTY_GEOIP_DIGEST"
cosign sign --yes "$REG/postgres-runtime@$POSTGRES_RUNTIME_DIGEST"

28
.github/scripts/metadata/gen.py vendored Executable file
View File

@ -0,0 +1,28 @@
#!/usr/bin/env python3
import json, sys
if len(sys.argv) < 4:
print("Usage: gen.py <image-name> <digest> <tags>")
sys.exit(1)
name = sys.argv[1]
digest = sys.argv[2]
raw_tags = sys.argv[3]
tags = raw_tags.splitlines()
preferred = next((t for t in tags if t.endswith(":latest")), tags[0] if tags else "")
metadata = {
"name": name,
"digest": digest,
"tags": tags,
"preferred_tag": preferred,
"image": f"ghcr.io/cloud-neutral-toolkit/{name}",
"image_with_digest": f"ghcr.io/cloud-neutral-toolkit/{name}@{digest}",
}
outfile = f"image-metadata-{name}.json"
with open(outfile, "w", encoding="utf-8") as f:
json.dump(metadata, f, indent=2)
print(f"[metadata] Wrote: {outfile}")

7
.github/scripts/sbom/generate.sh vendored Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/env bash
set -e
IMAGE="$1"
OUT="$2"
anchore-cli sbom generate "$IMAGE" -o "$OUT"

15
.github/scripts/utils/preferred-tag.sh vendored Executable file
View File

@ -0,0 +1,15 @@
#!/usr/bin/env bash
set -e
tags="$1"
preferred=""
while IFS= read -r line; do
[[ "$line" == *":latest" ]] && preferred="$line" && break
done <<< "$tags"
if [[ -z "$preferred" ]]; then
preferred="$(echo "$tags" | head -n 1)"
fi
echo "$preferred"

109
.github/workflows/build-base-images.yml vendored Normal file
View File

@ -0,0 +1,109 @@
name: Build Base Images
on:
workflow_call:
inputs:
registry:
description: "Target registry"
type: string
required: true
org:
description: "Target organization"
type: string
required: true
push_images:
description: "Push images instead of building locally"
type: boolean
default: true
workflow_dispatch:
inputs:
registry:
description: "Target registry"
type: string
default: "ghcr.io"
org:
description: "Target organization"
type: string
default: "cloud-neutral-toolkit"
push_images:
description: "Push images instead of building locally"
type: boolean
default: true
push:
paths:
- "deploy/base-images/**"
permissions:
contents: read
packages: write
id-token: write
env:
REGISTRY: ${{ inputs.registry }}
ORG: ${{ inputs.org }}
jobs:
build-base:
strategy:
matrix:
image:
- { name: node-builder, file: deploy/base-images/node-builder.Dockerfile }
- { name: node-runtime, file: deploy/base-images/node-runtime.Dockerfile }
- { name: openresty-geoip, file: deploy/base-images/openresty-geoip.Dockerfile }
- { name: postgres-runtime, file: deploy/base-images/postgres-runtime-wth-extensions.Dockerfile }
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate Auto Tags
id: meta
uses: ./.github/actions/auto-tag
with:
image: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.image.name }}
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
id: build
with:
context: .
file: ${{ matrix.image.file }}
platforms: linux/amd64,linux/arm64
push: ${{ (github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch') && inputs.push_images || github.event_name == 'push' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- uses: anchore/sbom-action@v0
with:
image: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.image.name }}@${{ steps.build.outputs.digest }}
output-file: sbom.spdx.json
- uses: actions/upload-artifact@v4
with:
name: sbom-${{ matrix.image.name }}
path: sbom.spdx.json
- uses: sigstore/cosign-installer@v3
with:
cosign-release: 'v2.4.1'
- name: Sign Image
env:
COSIGN_EXPERIMENTAL: "true"
run: |
COSIGN_IMAGE=${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.image.name }}@${{ steps.build.outputs.digest }}
cosign sign --yes "$COSIGN_IMAGE"

View File

@ -4,36 +4,38 @@ on:
workflow_call:
inputs:
push_images:
description: "Push images instead of local builds"
description: "Push service images instead of local builds"
type: boolean
default: true
# Optional overrides from parent workflow
postgres_runtime_digest:
description: "Digest for the Postgres runtime base image"
type: string
required: false
default: ''
default: ""
node_runtime_digest:
description: "Digest for the Node runtime base image"
type: string
required: false
default: ''
default: ""
node_builder_digest:
description: "Digest for the Node builder base image"
type: string
required: false
default: ''
default: ""
openresty_geoip_digest:
description: "Digest for the OpenResty GeoIP base image"
type: string
required: false
default: ''
default: ""
secrets: inherit
workflow_dispatch: {}
push:
branches: [ main ]
paths:
- "dashboard/**"
- "rag-server/**"
- "account/**"
workflow_dispatch: {}
permissions:
contents: read
@ -46,27 +48,22 @@ env:
BASE_ORG: cloud-neutral-toolkit
jobs:
base-images:
uses: ./.github/workflows/build-base-images.yml
secrets: inherit
with:
push_images: ${{ inputs.push_images }}
build-service:
needs: base-images
strategy:
matrix:
service:
- { name: dashboard, context: dashboard, file: dashboard/Dockerfile, lint: "pnpm lint" }
- { name: rag-server, context: rag-server, file: rag-server/Dockerfile, lint: "go vet ./..." }
- { name: account, context: account, file: account/Dockerfile, lint: "go vet ./..." }
- { name: dashboard, context: dashboard, file: dashboard/Dockerfile }
- { name: rag-server, context: rag-server, file: rag-server/Dockerfile }
- { name: account, context: account, file: account/Dockerfile }
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Lint
# -----------------------------
# Lint per-language
# -----------------------------
- name: Lint
working-directory: ${{ matrix.service.context }}
run: |
@ -78,14 +75,18 @@ jobs:
go vet ./...
fi
# -----------------------------
# Login
# -----------------------------
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# -----------------------------
# Metadata
# -----------------------------
- uses: docker/metadata-action@v5
id: meta
with:
@ -99,6 +100,9 @@ jobs:
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
# -----------------------------
# Build Service Images
# -----------------------------
- uses: docker/build-push-action@v6
id: build
with:
@ -109,25 +113,41 @@ jobs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
NODE_BUILDER_IMAGE=${{ env.REGISTRY }}/${{ env.BASE_ORG }}/node-builder@${{ inputs.node_builder_digest || needs.base-images.outputs.node_builder_digest }}
NODE_RUNTIME_IMAGE=${{ env.REGISTRY }}/${{ env.BASE_ORG }}/node-runtime@${{ inputs.node_runtime_digest || needs.base-images.outputs.node_runtime_digest }}
NODE_BUILDER_IMAGE=${{ env.REGISTRY }}/${{ env.BASE_ORG }}/node-builder@${{ inputs.node_builder_digest }}
NODE_RUNTIME_IMAGE=${{ env.REGISTRY }}/${{ env.BASE_ORG }}/node-runtime@${{ inputs.node_runtime_digest }}
POSTGRES_RUNTIME_IMAGE=${{ env.REGISTRY }}/${{ env.BASE_ORG }}/postgres-runtime@${{ inputs.postgres_runtime_digest }}
OPENRESTY_GEOIP_IMAGE=${{ env.REGISTRY }}/${{ env.BASE_ORG }}/openresty-geoip@${{ inputs.openresty_geoip_digest }}
# -----------------------------
# SBOM
# -----------------------------
- uses: anchore/sbom-action@v0
with:
image: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.service.name }}@${{ steps.build.outputs.digest }}
output-file: sbom.spdx.json
- uses: actions/upload-artifact@v4
with:
name: sbom-${{ matrix.service.name }}
path: sbom.spdx.json
# -----------------------------
# Trivy Scan
# -----------------------------
- uses: aquasecurity/trivy-action@0.28.0
with:
image-ref: ${{ env.REGISTRY }}/${{ env.ORG }}/${{ matrix.service.name }}@${{ steps.build.outputs.digest }}
severity: HIGH,CRITICAL
exit-code: '1'
# -----------------------------
# Cosign
# -----------------------------
- uses: sigstore/cosign-installer@v3
with:
cosign-release: 'v2.4.1'
- name: Cosign
- name: Cosign Sign
env:
COSIGN_EXPERIMENTAL: "true"
run: |

View File

@ -21,111 +21,79 @@ permissions:
jobs:
# -------------------------------------------------------------
# CI STAGE 1 — Code Quality (environment-independent)
# CI — Code Quality → Build → Test → Security
# -------------------------------------------------------------
code-quality:
name: "Code quality • ${{ matrix.service }} @ ${{ matrix.platform }}"
ci:
name: "CI • ${{ matrix.service }} @ ${{ matrix.platform }}"
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
platform: ["linux/amd64", "linux/arm64"]
service: ["dashboard", "rag-server", "account"]
steps:
- uses: ./.github/actions/code-quality
with:
service: ${{ matrix.service }}
platform: ${{ matrix.platform }}
steps:
- name: Code Quality
uses: ./.github/actions/code-quality
with:
service: ${{ matrix.service }}
platform: ${{ matrix.platform }}
# -------------------------------------------------------------
# CI STAGE 2 — Build
# -------------------------------------------------------------
build:
name: "Build • ${{ matrix.service }} @ ${{ matrix.platform }}"
runs-on: ubuntu-latest
needs: code-quality
strategy:
fail-fast: false
matrix:
platform: ["linux/amd64", "linux/arm64"]
service: ["dashboard", "rag-server", "account"]
steps:
- uses: ./.github/actions/build
with:
service: ${{ matrix.service }}
platform: ${{ matrix.platform }}
- name: Build
uses: ./.github/actions/build
with:
service: ${{ matrix.service }}
platform: ${{ matrix.platform }}
- name: Build Base Images
id: base
uses: ./.github/actions/build-base-images/
secrets: inherit
with:
push_images: false # CI 阶段不 push只 build 并生成 digest
- name: Build Base Images
uses: ./.github/workflows/build-base-images.yml
secrets: inherit
with:
registry: ghcr.io
org: cloud-neutral-toolkit
push_images: true
- name: Build Service Images
uses: ./.github/actions/build-service-images/
uses: ./.github/workflows/build-service-images.yml
secrets: inherit
with:
push_images: false
registry: ghcr.io
org: cloud-neutral-toolkit
push_images: true
node_builder_digest: ${{ steps.base.outputs.node_builder_digest }}
node_runtime_digest: ${{ steps.base.outputs.node_runtime_digest }}
postgres_runtime_digest: ${{ steps.base.outputs.postgres_runtime_digest }}
openresty_geoip_digest: ${{ steps.base.outputs.openresty_geoip_digest }}
# -------------------------------------------------------------
# CI STAGE 3 — Test
# -------------------------------------------------------------
test:
name: "Test • ${{ matrix.service }} @ ${{ matrix.platform }}"
runs-on: ubuntu-latest
needs: build
strategy:
fail-fast: false
matrix:
platform: ["linux/amd64", "linux/arm64"]
service: ["dashboard", "rag-server", "account"]
steps:
- uses: ./.github/actions/test
- name: "Test • ${{ matrix.service }} @ ${{ matrix.platform }}"
steps:
uses: ./.github/actions/test
with:
service: ${{ matrix.service }}
platform: ${{ matrix.platform }}
- name: - name: Security Check
steps:
uses: ./.github/actions/security
with:
service: ${{ matrix.service }}
platform: ${{ matrix.platform }}
# -------------------------------------------------------------
# CI STAGE 4 — Security
# CD — Deploy只在 workflow_dispatch 时跑)
# -------------------------------------------------------------
security:
name: "Security • ${{ matrix.service }} @ ${{ matrix.platform }}"
runs-on: ubuntu-latest
needs: test
strategy:
fail-fast: false
matrix:
platform: ["linux/amd64", "linux/arm64"]
service: ["dashboard", "rag-server", "account"]
steps:
- uses: ./.github/actions/security
with:
service: ${{ matrix.service }}
platform: ${{ matrix.platform }}
# -------------------------------------------------------------
# CD — Deploy (only with workflow_dispatch)
# -------------------------------------------------------------
deploy:
cd:
name: "Deploy • ${{ matrix.service }} (${{ github.event.inputs.environment }})"
runs-on: ubuntu-latest
needs: security
needs: ci
if: github.event_name == 'workflow_dispatch'
env:
ENVIRONMENT: ${{ github.event.inputs.environment }}
strategy:
fail-fast: false
matrix:
platform: ["linux/amd64", "linux/arm64"]
service: ["dashboard", "rag-server", "account"]
steps:
- uses: ./.github/actions/deploy
- name: Deploy Services
uses: ./.github/actions/deploy
secrets: inherit
with:
service: ${{ matrix.service }}
platform: ${{ matrix.platform }}
environment: ${{ env.ENVIRONMENT }}