quality / changes (push) Successful in 7s
quality / commitlint (push) Skipped
quality / openapi (push) Successful in 30s
quality / web (push) Successful in 1m3s
quality / api (push) Successful in 1m15s
quality / linux-agent-smoke (push) Failing after 27s
quality / docker-check (push) Failing after 2m28s
CD / quality (push) Failing after 5m52s
CD / publish (push) Skipped
Implement reliable agent apply, telemetry v2, analytics retention, guarded rollouts, enrollment, labels and webhooks. Improve ReUI frontend data safety and bounded list loading, and add Docker smoke, recovery and resource checks. Document local verification and pending native acceptance.
396 lines
15 KiB
YAML
396 lines
15 KiB
YAML
# Quality gates (reusable). Callers: ci.yaml (PR), cd.yaml (push main).
|
|
name: quality
|
|
|
|
on:
|
|
workflow_call:
|
|
inputs:
|
|
is_pull_request:
|
|
type: boolean
|
|
required: true
|
|
base_sha:
|
|
type: string
|
|
required: false
|
|
default: ""
|
|
head_sha:
|
|
type: string
|
|
required: false
|
|
default: ""
|
|
before_sha:
|
|
type: string
|
|
required: false
|
|
default: ""
|
|
allow_registry_login:
|
|
type: boolean
|
|
required: false
|
|
default: false
|
|
secrets:
|
|
ACTIONS_PAT:
|
|
required: false
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
jobs:
|
|
changes:
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
openapi: ${{ steps.detect.outputs.openapi }}
|
|
web: ${{ steps.detect.outputs.web }}
|
|
api: ${{ steps.detect.outputs.api }}
|
|
docker: ${{ steps.detect.outputs.docker }}
|
|
steps:
|
|
- if: ${{ inputs.is_pull_request }}
|
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
with:
|
|
fetch-depth: 0
|
|
- if: ${{ inputs.is_pull_request == false }}
|
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
with:
|
|
# Full history: before_sha may be many commits behind the new tip
|
|
# (multi-commit pushes) and must exist locally for git diff.
|
|
fetch-depth: 0
|
|
- id: detect
|
|
name: Detect changed paths per module
|
|
env:
|
|
IS_PR: ${{ inputs.is_pull_request }}
|
|
BASE_SHA: ${{ inputs.base_sha }}
|
|
HEAD_SHA: ${{ inputs.head_sha }}
|
|
BEFORE_SHA: ${{ inputs.before_sha }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
openapi=false
|
|
web=false
|
|
api=false
|
|
docker=false
|
|
|
|
set_all_flags_true() {
|
|
openapi=true
|
|
web=true
|
|
api=true
|
|
docker=true
|
|
}
|
|
|
|
write_outputs() {
|
|
for v in openapi web api docker; do
|
|
eval "echo \"\$v=\$$v\"" >> "$GITHUB_OUTPUT"
|
|
done
|
|
}
|
|
|
|
if [ "$IS_PR" = "true" ]; then
|
|
FILES="$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")"
|
|
else
|
|
after="${HEAD_SHA:-$(git rev-parse HEAD)}"
|
|
before="$BEFORE_SHA"
|
|
# Guard: a stale/unresolvable before_sha must not fail the pipeline.
|
|
if [ -n "$before" ] && [ "$before" != "0000000000000000000000000000000000000000" ]; then
|
|
if ! git cat-file -e "$before^{commit}" 2>/dev/null; then
|
|
echo "::warning::before_sha $before not resolvable, falling back to HEAD~1"
|
|
before=""
|
|
fi
|
|
fi
|
|
if [ -n "$before" ]; then
|
|
FILES="$(git diff --name-only "$before" "$after")"
|
|
elif git rev-parse --verify HEAD~1 >/dev/null 2>&1; then
|
|
FILES="$(git diff --name-only HEAD~1 HEAD)"
|
|
else
|
|
set_all_flags_true
|
|
write_outputs
|
|
echo "No parent commit — full pipeline (all modules)"
|
|
exit 0
|
|
fi
|
|
fi
|
|
|
|
if [ -z "$(printf '%s' "$FILES" | tr -d '[:space:]')" ]; then
|
|
set_all_flags_true
|
|
write_outputs
|
|
echo "Empty diff — full pipeline fallback"
|
|
exit 0
|
|
fi
|
|
|
|
full_pipeline=false
|
|
|
|
while IFS= read -r f || [ -n "${f:-}" ]; do
|
|
[ -z "${f:-}" ] && continue
|
|
case "$f" in
|
|
.gitea/workflows/*|scripts/*)
|
|
full_pipeline=true
|
|
;;
|
|
docs/openapi.yaml|docs/openapi.schemas.json|redocly.yaml)
|
|
openapi=true
|
|
api=true
|
|
;;
|
|
.cursor/*|.claude/*|.codegraph/*|.agents/*)
|
|
;;
|
|
*.md|AGENTS.md)
|
|
;;
|
|
apps/web/README.md|apps/web/components.json|packages/ui/components.json)
|
|
;;
|
|
apps/web/*|packages/ui/*)
|
|
web=true
|
|
;;
|
|
apps/api/*|packages/db/*)
|
|
api=true
|
|
;;
|
|
packages/shared/*)
|
|
web=true
|
|
api=true
|
|
;;
|
|
deploy/compose/*|deploy/docker/*|.dockerignore)
|
|
docker=true
|
|
;;
|
|
docs/*)
|
|
;;
|
|
package.json|pnpm-lock.yaml|pnpm-workspace.yaml|turbo.json|.releaserc.json|commitlint.config.cjs)
|
|
full_pipeline=true
|
|
;;
|
|
*)
|
|
;;
|
|
esac
|
|
done <<< "$FILES"
|
|
|
|
if $full_pipeline; then
|
|
set_all_flags_true
|
|
fi
|
|
|
|
write_outputs
|
|
|
|
echo "Changed files (first 30):"
|
|
printf '%s\n' "$FILES" | head -n 30
|
|
echo "--- flags ---"
|
|
echo "openapi=$openapi web=$web api=$api docker=$docker full_pipeline=$full_pipeline"
|
|
|
|
openapi:
|
|
needs: [changes]
|
|
if: needs.changes.outputs.openapi == 'true' || needs.changes.outputs.web == 'true'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
with:
|
|
node-version: "22"
|
|
- name: Export cache paths
|
|
run: sh scripts/ci/export-cache-env.sh
|
|
- id: pnpm-hash
|
|
run: echo "key=$(sha256sum pnpm-lock.yaml | awk '{print $1}')" >> "$GITHUB_OUTPUT"
|
|
- id: pnpm-cache
|
|
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
|
with:
|
|
path: |
|
|
${{ env.PNPM_STORE_DIR }}
|
|
${{ env.COREPACK_HOME }}
|
|
node_modules
|
|
apps/web/node_modules
|
|
apps/api/node_modules
|
|
packages/ui/node_modules
|
|
packages/shared/node_modules
|
|
packages/db/node_modules
|
|
key: pnpm-${{ runner.os }}-${{ steps.pnpm-hash.outputs.key }}
|
|
restore-keys: |
|
|
pnpm-${{ runner.os }}-
|
|
- name: pnpm install, Redocly
|
|
env:
|
|
PNPM_CACHE_HIT: ${{ steps.pnpm-cache.outputs.cache-hit }}
|
|
run: |
|
|
set -euxo pipefail
|
|
sh scripts/ci/pnpm-ci.sh
|
|
pnpm exec redocly lint docs/openapi.yaml
|
|
|
|
web:
|
|
needs: [changes]
|
|
if: needs.changes.outputs.web == 'true'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
with:
|
|
node-version: "22"
|
|
- name: Export cache paths
|
|
run: sh scripts/ci/export-cache-env.sh
|
|
- id: pnpm-hash
|
|
run: echo "key=$(sha256sum pnpm-lock.yaml | awk '{print $1}')" >> "$GITHUB_OUTPUT"
|
|
- id: pnpm-cache
|
|
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
|
with:
|
|
path: |
|
|
${{ env.PNPM_STORE_DIR }}
|
|
${{ env.COREPACK_HOME }}
|
|
node_modules
|
|
apps/web/node_modules
|
|
apps/api/node_modules
|
|
packages/ui/node_modules
|
|
packages/shared/node_modules
|
|
packages/db/node_modules
|
|
key: pnpm-${{ runner.os }}-${{ steps.pnpm-hash.outputs.key }}
|
|
restore-keys: |
|
|
pnpm-${{ runner.os }}-
|
|
- name: pnpm install, typecheck, build
|
|
env:
|
|
PNPM_CACHE_HIT: ${{ steps.pnpm-cache.outputs.cache-hit }}
|
|
run: |
|
|
set -euxo pipefail
|
|
sh scripts/ci/pnpm-ci.sh
|
|
pnpm --filter @evofw/web run typecheck
|
|
pnpm --filter @evofw/web run test
|
|
pnpm --filter @evofw/web run build
|
|
|
|
api:
|
|
needs: [changes]
|
|
if: needs.changes.outputs.api == 'true'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
with:
|
|
node-version: "22"
|
|
- name: Export cache paths
|
|
run: sh scripts/ci/export-cache-env.sh
|
|
- id: pnpm-hash
|
|
run: echo "key=$(sha256sum pnpm-lock.yaml | awk '{print $1}')" >> "$GITHUB_OUTPUT"
|
|
- id: pnpm-cache
|
|
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
|
with:
|
|
path: |
|
|
${{ env.PNPM_STORE_DIR }}
|
|
${{ env.COREPACK_HOME }}
|
|
node_modules
|
|
apps/web/node_modules
|
|
apps/api/node_modules
|
|
packages/ui/node_modules
|
|
packages/shared/node_modules
|
|
packages/db/node_modules
|
|
key: pnpm-${{ runner.os }}-${{ steps.pnpm-hash.outputs.key }}
|
|
restore-keys: |
|
|
pnpm-${{ runner.os }}-
|
|
- name: pnpm install, typecheck, test, build
|
|
env:
|
|
PNPM_CACHE_HIT: ${{ steps.pnpm-cache.outputs.cache-hit }}
|
|
run: |
|
|
set -euxo pipefail
|
|
sh scripts/ci/pnpm-ci.sh
|
|
# turbo builds @evofw/{shared,db} first — api typecheck resolves their dist types
|
|
pnpm exec turbo run typecheck --filter=@evofw/api --filter=@evofw/db --filter=@evofw/shared
|
|
pnpm exec turbo run test --filter=@evofw/api --filter=@evofw/shared
|
|
node --test scripts/ci/sqlite-backup.test.mjs
|
|
pnpm exec turbo run build --filter=@evofw/api
|
|
node --test scripts/ci/benchmark-fixture.test.mjs
|
|
node --test scripts/ci/manifest-platform.test.mjs
|
|
node scripts/ci/generate-openapi-schemas.mjs --check
|
|
|
|
commitlint:
|
|
if: inputs.is_pull_request
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
with:
|
|
fetch-depth: 0
|
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
with:
|
|
node-version: "22"
|
|
- name: Export cache paths
|
|
run: sh scripts/ci/export-cache-env.sh
|
|
- id: pnpm-hash
|
|
run: echo "key=$(sha256sum pnpm-lock.yaml | awk '{print $1}')" >> "$GITHUB_OUTPUT"
|
|
- id: pnpm-cache
|
|
uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
|
|
with:
|
|
path: |
|
|
${{ env.PNPM_STORE_DIR }}
|
|
${{ env.COREPACK_HOME }}
|
|
node_modules
|
|
apps/web/node_modules
|
|
apps/api/node_modules
|
|
packages/ui/node_modules
|
|
packages/shared/node_modules
|
|
packages/db/node_modules
|
|
key: pnpm-${{ runner.os }}-${{ steps.pnpm-hash.outputs.key }}
|
|
restore-keys: |
|
|
pnpm-${{ runner.os }}-
|
|
- name: Lint commit messages
|
|
env:
|
|
BASE_SHA: ${{ inputs.base_sha }}
|
|
HEAD_SHA: ${{ inputs.head_sha }}
|
|
PNPM_CACHE_HIT: ${{ steps.pnpm-cache.outputs.cache-hit }}
|
|
run: |
|
|
set -euxo pipefail
|
|
sh scripts/ci/pnpm-ci.sh
|
|
pnpm exec commitlint --from "$BASE_SHA" --to "$HEAD_SHA"
|
|
|
|
linux-agent-smoke:
|
|
needs: [changes]
|
|
if: needs.changes.outputs.api == 'true' || needs.changes.outputs.docker == 'true'
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
- name: Install isolated Linux smoke prerequisites
|
|
run: sudo apt-get update && sudo apt-get install -y iproute2 nftables iptables ipset python3 curl util-linux coreutils
|
|
- name: Run shipped agent in isolated network and mount namespaces
|
|
run: |
|
|
python3 scripts/ci/linux-agent-fixture.test.py
|
|
sudo bash scripts/ci/linux-netns-smoke.sh
|
|
|
|
docker-check:
|
|
needs: [changes]
|
|
# Every CD invocation must pass upgrade validation, including docs-only releases.
|
|
if: "!inputs.is_pull_request || needs.changes.outputs.docker == 'true' || needs.changes.outputs.api == 'true' || needs.changes.outputs.web == 'true'"
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
SMOKE_IMAGE: evofw-smoke:${{ github.run_id }}-${{ github.run_attempt }}
|
|
steps:
|
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
with:
|
|
node-version: "22"
|
|
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
|
with:
|
|
name: evofw-${{ github.run_id }}-${{ github.run_attempt }}-quality
|
|
driver: docker-container
|
|
cleanup: true
|
|
buildkitd-config: deploy/docker/buildkitd.toml
|
|
- name: Verify run-scoped builder effective GC configuration
|
|
run: sh scripts/ci/prepare-builder.sh evofw-${{ github.run_id }}-${{ github.run_attempt }}-quality deploy/docker/buildkitd.toml
|
|
- name: Log in to Gitea Registry
|
|
if: inputs.allow_registry_login
|
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
|
with:
|
|
registry: git.shx.one
|
|
username: ${{ gitea.actor }}
|
|
password: ${{ secrets.ACTIONS_PAT }}
|
|
- name: bake --print
|
|
working-directory: deploy/docker
|
|
env:
|
|
BUILDX_BAKE_ENTITLEMENTS_FS: "0"
|
|
BUILDX_BAKE_FILE_RELATIVE_PATHS: "1"
|
|
run: docker buildx bake --allow=fs.read="${{ github.workspace }}" -f docker-bake.hcl --print default
|
|
- name: Build smoke image (read release cache, no cache publication)
|
|
working-directory: deploy/docker
|
|
env:
|
|
BUILDX_BAKE_ENTITLEMENTS_FS: "0"
|
|
BUILDX_BAKE_FILE_RELATIVE_PATHS: "1"
|
|
run: |
|
|
set -euxo pipefail
|
|
owner_lc="$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')"
|
|
export CACHE_REF_NODE="git.shx.one/${owner_lc}/evofw-buildcache:node-buildcache"
|
|
docker buildx bake --allow=fs.read="${{ github.workspace }}" -f docker-bake.hcl \
|
|
--set evofw.cache-to= --set "evofw.tags=$SMOKE_IMAGE" --load default
|
|
- name: Capture exact built image identity
|
|
id: built-image
|
|
run: echo "id=$(docker image inspect --format '{{.Id}}' "$SMOKE_IMAGE")" >> "$GITHUB_OUTPUT"
|
|
- name: Built image runtime smoke
|
|
env:
|
|
PREVIOUS_IMAGE: ${{ vars.EVOFW_PREVIOUS_IMAGE }}
|
|
REQUIRE_UPGRADE: ${{ !inputs.is_pull_request }}
|
|
FIRST_RELEASE: ${{ vars.EVOFW_FIRST_RELEASE == 'true' }}
|
|
SMOKE_ARTIFACT_DIR: runtime-artifacts
|
|
BUILT_IMAGE: ${{ steps.built-image.outputs.id }}
|
|
run: sh scripts/ci/image-smoke.sh "$BUILT_IMAGE" "${PREVIOUS_IMAGE:-}"
|
|
- name: Built image constrained steady performance and correctness gate
|
|
timeout-minutes: 15
|
|
env:
|
|
BUILT_IMAGE: ${{ steps.built-image.outputs.id }}
|
|
BENCH_P95_LIMIT_MS: "500"
|
|
BENCH_CONCURRENCY: "10"
|
|
BENCH_STEADY_CONCURRENCY: "2"
|
|
BENCH_STEADY_INTERVAL_MS: "1200"
|
|
run: sh scripts/bench/runtime.sh "$BUILT_IMAGE" "runtime-artifacts/benchmark-${{ github.run_id }}-${{ github.run_attempt }}"
|