refactor: enhance CI workflow for granular change detection by module and improve idempotency key generation in API client. Update job flags and conditions for better handling of file changes across different components.
CI / changes (push) Successful in 5s
CI / openapi (push) Has been skipped
CI / go (push) Successful in 19s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, , evobgp-web) (push) Successful in 1m6s
CI / docker-web (deploy/docker/evobgp-web/Dockerfile, evobgp-all, evobgp-web-all) (push) Successful in 1m1s
CI / docker-bird (push) Successful in 38s
CI / bird2 (push) Successful in 16s
CI / docker-go (deploy/docker/evobgp-agent/Dockerfile, , evobgp-agent) (push) Successful in 1m1s
CI / docker-go (evobgp-all, 1, deploy/docker/gobinary/Dockerfile, , evobgp-all) (push) Successful in 1m32s
CI / docker-go (evobgp-deploy, 0, deploy/docker/gobinary/Dockerfile, , evobgp-deploy) (push) Has been cancelled
CI / docker-go (evobgp-ingest, 0, deploy/docker/gobinary/Dockerfile, , evobgp-ingest) (push) Has been cancelled
CI / docker-go (evobgp-node, 0, deploy/docker/gobinary/Dockerfile, , evobgp-node) (push) Has been cancelled
CI / docker-go (evobgp-render, 0, deploy/docker/gobinary/Dockerfile, , evobgp-render) (push) Has been cancelled
CI / docker-go (evobgp-api, 1, deploy/docker/gobinary/Dockerfile, , evobgp-api) (push) Has been cancelled
CI / docker-go (evobgp-scheduler, 0, deploy/docker/gobinary/Dockerfile, , evobgp-scheduler) (push) Has been cancelled

This commit is contained in:
Denozordec
2026-04-05 22:17:29 +07:00
parent 7d85fef602
commit 54eaad3f78
2 changed files with 218 additions and 79 deletions
+199 -78
View File
@@ -7,22 +7,38 @@ on:
branches: [main, master] branches: [main, master]
jobs: jobs:
# ---------------------------------------------------------------------------
# Гранулярная детекция изменений по модулям.
# Каждый флаг соответствует группе файлов; downstream-джобы запускаются
# только когда их группа затронута. Изменение CI-конфигурации (.gitea/workflows/*)
# поднимает все флаги, чтобы гарантировать полный прогон.
# ---------------------------------------------------------------------------
changes: changes:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
openapi: ${{ steps.detect.outputs.openapi }} openapi: ${{ steps.detect.outputs.openapi }}
code: ${{ steps.detect.outputs.code }} go: ${{ steps.detect.outputs.go }}
web: ${{ steps.detect.outputs.web }}
bird_conf: ${{ steps.detect.outputs.bird_conf }}
docker_go: ${{ steps.detect.outputs.docker_go }}
docker_web: ${{ steps.detect.outputs.docker_web }}
docker_bird: ${{ steps.detect.outputs.docker_bird }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with: with:
fetch-depth: 0 fetch-depth: 0
- id: detect - id: detect
name: Detect changed paths (OpenAPI vs doc-only vs code) name: Detect changed paths per module
run: | run: |
set -euo pipefail set -euo pipefail
openapi=false openapi=false
code=true go=false
web=false
bird_conf=false
docker_go=false
docker_web=false
docker_bird=false
if [ "${{ github.event_name }}" = "pull_request" ]; then if [ "${{ github.event_name }}" = "pull_request" ]; then
base="${{ github.event.pull_request.base.sha }}" base="${{ github.event.pull_request.base.sha }}"
@@ -36,52 +52,61 @@ jobs:
elif git rev-parse --verify HEAD~1 >/dev/null 2>&1; then elif git rev-parse --verify HEAD~1 >/dev/null 2>&1; then
FILES="$(git diff --name-only HEAD~1 HEAD)" FILES="$(git diff --name-only HEAD~1 HEAD)"
else else
# Нет родителя (первый коммит / тонкий clone) — полный пайплайн + OpenAPI openapi=true; go=true; web=true; bird_conf=true
openapi=true docker_go=true; docker_web=true; docker_bird=true
code=true for v in openapi go web bird_conf docker_go docker_web docker_bird; do
echo "openapi=$openapi" >> "$GITHUB_OUTPUT" echo "$v=true" >> "$GITHUB_OUTPUT"
echo "code=$code" >> "$GITHUB_OUTPUT" done
echo "No parent commit: openapi=true code=true" echo "No parent commit — full pipeline"
exit 0 exit 0
fi fi
fi fi
# Пустой diff (редко) — не отключаем сборку из осторожности
if [ -z "$(printf '%s' "$FILES" | tr -d '[:space:]')" ]; then if [ -z "$(printf '%s' "$FILES" | tr -d '[:space:]')" ]; then
openapi=false go=true; web=true
code=true for v in openapi go web bird_conf docker_go docker_web docker_bird; do
echo "openapi=$openapi" >> "$GITHUB_OUTPUT" eval "echo \"\$v=\$$v\"" >> "$GITHUB_OUTPUT"
echo "code=$code" >> "$GITHUB_OUTPUT" done
echo "Empty diff: openapi=false code=true" echo "Empty diff — safe fallback: go=true web=true"
exit 0 exit 0
fi fi
if printf '%s\n' "$FILES" | grep -qE '^docs/openapi\.yaml$|^redocly\.yaml$'; then ci_changed=false
openapi=true
fi
doc_only_all=true
while IFS= read -r f || [ -n "${f:-}" ]; do while IFS= read -r f || [ -n "${f:-}" ]; do
[ -z "${f:-}" ] && continue [ -z "${f:-}" ] && continue
if [[ "$f" == "README.md" || "$f" == ".gitea/README.md" || "$f" == "web/README.md" || "$f" == "redocly.yaml" || "$f" == docs/* ]]; then case "$f" in
continue .gitea/workflows/*) ci_changed=true ;;
fi docs/openapi.yaml|redocly.yaml) openapi=true ;;
doc_only_all=false web/README.md) ;; # doc-only
break web/*) web=true ;;
deploy/bird/*) bird_conf=true ;;
deploy/docker/gobinary/*) docker_go=true ;;
deploy/docker/evobgp-agent/*) docker_go=true ;;
deploy/docker/evobgp-web/*) docker_web=true ;;
deploy/docker/bird2/*) docker_bird=true ;;
go.mod|go.sum|go.work) go=true ;;
*.go) go=true ;;
cmd/*|internal/*) go=true ;;
esac
done <<< "$FILES" done <<< "$FILES"
if $doc_only_all; then if $ci_changed; then
code=false go=true; web=true; bird_conf=true
else docker_go=true; docker_web=true; docker_bird=true
code=true
fi fi
echo "openapi=$openapi" >> "$GITHUB_OUTPUT" for v in openapi go web bird_conf docker_go docker_web docker_bird; do
echo "code=$code" >> "$GITHUB_OUTPUT" eval "echo \"\$v=\$$v\"" >> "$GITHUB_OUTPUT"
echo "Changed files (first 20):" done
printf '%s\n' "$FILES" | head -n 20
echo "openapi=$openapi code=$code"
echo "Changed files (first 30):"
printf '%s\n' "$FILES" | head -n 30
echo "--- flags ---"
echo "openapi=$openapi go=$go web=$web bird_conf=$bird_conf"
echo "docker_go=$docker_go docker_web=$docker_web docker_bird=$docker_bird ci=$ci_changed"
# ---------------------------------------------------------------------------
openapi: openapi:
needs: [changes] needs: [changes]
if: needs.changes.outputs.openapi == 'true' if: needs.changes.outputs.openapi == 'true'
@@ -94,9 +119,10 @@ jobs:
- name: Lint OpenAPI (Redocly) - name: Lint OpenAPI (Redocly)
run: npx --yes @redocly/cli@1 lint docs/openapi.yaml run: npx --yes @redocly/cli@1 lint docs/openapi.yaml
# ---------------------------------------------------------------------------
go: go:
needs: [changes] needs: [changes]
if: needs.changes.outputs.code == 'true' if: needs.changes.outputs.go == 'true'
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
@@ -117,13 +143,12 @@ jobs:
go build -o "$out/$name" "./$d" go build -o "$out/$name" "./$d"
done done
# ---------------------------------------------------------------------------
bird2: bird2:
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [go] needs: [go]
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# Вложенный docker + bind-mount ломается, если демон Docker на хосте не видит путь
# workspace runner'а (/workspace/...). bird -p на самом job-раннере надёжнее.
- name: Install bird2 - name: Install bird2
run: | run: |
set -euxo pipefail set -euxo pipefail
@@ -148,10 +173,20 @@ jobs:
bird -c "$WS/$conf" -p bird -c "$WS/$conf" -p
done done
# Сборка образов и push в Container Registry Gitea (только push в main/master; см. .gitea/README.md). # ---------------------------------------------------------------------------
docker-images: # Docker: Go-бинарники (api, all, scheduler, ingest, render, deploy, node, agent).
needs: [go] # Запускается если изменился Go-код или Go-Dockerfile.
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') # Если Go-код менялся — требуем успех go-тестов; если только Dockerfile — go skipped, ОК.
# ---------------------------------------------------------------------------
docker-go:
needs: [changes, go]
if: >-
always() &&
needs.changes.result == 'success' &&
needs.go.result != 'failure' &&
github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') &&
(needs.changes.outputs.go == 'true' || needs.changes.outputs.docker_go == 'true')
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
fail-fast: false fail-fast: false
@@ -192,24 +227,13 @@ jobs:
bin: evobgp-node bin: evobgp-node
birdc: "0" birdc: "0"
evobgp_upstream: "" evobgp_upstream: ""
- image: evobgp-web
dockerfile: deploy/docker/evobgp-web/Dockerfile
evobgp_upstream: ""
- image: evobgp-web-all
dockerfile: deploy/docker/evobgp-web/Dockerfile
evobgp_upstream: evobgp-all
- image: evobgp-agent - image: evobgp-agent
dockerfile: deploy/docker/evobgp-agent/Dockerfile dockerfile: deploy/docker/evobgp-agent/Dockerfile
evobgp_upstream: "" evobgp_upstream: ""
- image: evobgp-bird2
dockerfile: deploy/docker/bird2/Dockerfile
evobgp_upstream: ""
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
- name: Prepare image metadata - name: Prepare image metadata
id: meta id: meta
run: | run: |
@@ -218,22 +242,16 @@ jobs:
echo "owner_lc=$owner_lc" >> "$GITHUB_OUTPUT" echo "owner_lc=$owner_lc" >> "$GITHUB_OUTPUT"
short_sha="$(echo '${{ github.sha }}' | cut -c1-7)" short_sha="$(echo '${{ github.sha }}' | cut -c1-7)"
echo "short_sha=$short_sha" >> "$GITHUB_OUTPUT" echo "short_sha=$short_sha" >> "$GITHUB_OUTPUT"
echo "docker_push=true" >> "$GITHUB_OUTPUT"
- name: Log in to Gitea Registry - name: Log in to Gitea Registry
if: steps.meta.outputs.docker_push == 'true'
uses: docker/login-action@v3 uses: docker/login-action@v3
with: with:
registry: git.shts.su registry: git.shts.su
username: ${{ gitea.actor }} username: ${{ gitea.actor }}
# PAT репозитория или встроенный токен job (нужны права на пакеты в настройках Actions).
password: ${{ secrets.ACTIONS_PAT || gitea.token }} password: ${{ secrets.ACTIONS_PAT || gitea.token }}
- name: Build and push ${{ matrix.image }} - name: Build and push ${{ matrix.image }}
env: env:
OWNER_LC: ${{ steps.meta.outputs.owner_lc }} OWNER_LC: ${{ steps.meta.outputs.owner_lc }}
SHORT_SHA: ${{ steps.meta.outputs.short_sha }} SHORT_SHA: ${{ steps.meta.outputs.short_sha }}
DO_PUSH: ${{ steps.meta.outputs.docker_push }}
DF: ${{ matrix.dockerfile }} DF: ${{ matrix.dockerfile }}
IMAGE: ${{ matrix.image }} IMAGE: ${{ matrix.image }}
BIN: ${{ matrix.bin }} BIN: ${{ matrix.bin }}
@@ -244,7 +262,7 @@ jobs:
WS="${{ github.workspace }}" WS="${{ github.workspace }}"
cd "$WS" cd "$WS"
TAG_LOCAL="evobgp:${IMAGE}-${SHORT_SHA}" IMG="git.shts.su/${OWNER_LC}/${IMAGE}"
BUILD_ARGS=() BUILD_ARGS=()
if [ -n "${BIN:-}" ]; then if [ -n "${BIN:-}" ]; then
BUILD_ARGS+=(--build-arg "BIN=${BIN}") BUILD_ARGS+=(--build-arg "BIN=${BIN}")
@@ -254,23 +272,126 @@ jobs:
BUILD_ARGS+=(--build-arg "EVOBGP_UPSTREAM=${EVOBGP_UPSTREAM}") BUILD_ARGS+=(--build-arg "EVOBGP_UPSTREAM=${EVOBGP_UPSTREAM}")
fi fi
if [ "$DO_PUSH" = "true" ]; then docker buildx build \
IMG="git.shts.su/${OWNER_LC}/${IMAGE}" --platform linux/amd64 \
docker buildx build \ --file "$DF" \
--platform linux/amd64 \ "${BUILD_ARGS[@]}" \
--file "$DF" \ --tag "${IMG}:latest" \
"${BUILD_ARGS[@]}" \ --tag "${IMG}:${SHORT_SHA}" \
--tag "${IMG}:latest" \ --tag "${IMG}:sha-${{ github.sha }}" \
--tag "${IMG}:${SHORT_SHA}" \ --push \
--tag "${IMG}:sha-${{ github.sha }}" \ "$WS"
--push \
"$WS" # ---------------------------------------------------------------------------
else # Docker: Web-фронтенд (evobgp-web, evobgp-web-all).
docker buildx build \ # Не зависит от Go-тестов — SvelteKit собирается отдельно.
--platform linux/amd64 \ # ---------------------------------------------------------------------------
--file "$DF" \ docker-web:
"${BUILD_ARGS[@]}" \ needs: [changes]
--tag "$TAG_LOCAL" \ if: >-
--load \ github.event_name == 'push' &&
"$WS" (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') &&
(needs.changes.outputs.web == 'true' || needs.changes.outputs.docker_web == 'true')
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- image: evobgp-web
dockerfile: deploy/docker/evobgp-web/Dockerfile
evobgp_upstream: ""
- image: evobgp-web-all
dockerfile: deploy/docker/evobgp-web/Dockerfile
evobgp_upstream: evobgp-all
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Prepare image metadata
id: meta
run: |
set -euo pipefail
owner_lc="$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')"
echo "owner_lc=$owner_lc" >> "$GITHUB_OUTPUT"
short_sha="$(echo '${{ github.sha }}' | cut -c1-7)"
echo "short_sha=$short_sha" >> "$GITHUB_OUTPUT"
- name: Log in to Gitea Registry
uses: docker/login-action@v3
with:
registry: git.shts.su
username: ${{ gitea.actor }}
password: ${{ secrets.ACTIONS_PAT || gitea.token }}
- name: Build and push ${{ matrix.image }}
env:
OWNER_LC: ${{ steps.meta.outputs.owner_lc }}
SHORT_SHA: ${{ steps.meta.outputs.short_sha }}
DF: ${{ matrix.dockerfile }}
IMAGE: ${{ matrix.image }}
EVOBGP_UPSTREAM: ${{ matrix.evobgp_upstream }}
run: |
set -euxo pipefail
WS="${{ github.workspace }}"
cd "$WS"
IMG="git.shts.su/${OWNER_LC}/${IMAGE}"
BUILD_ARGS=()
if [ -n "${EVOBGP_UPSTREAM:-}" ]; then
BUILD_ARGS+=(--build-arg "EVOBGP_UPSTREAM=${EVOBGP_UPSTREAM}")
fi fi
docker buildx build \
--platform linux/amd64 \
--file "$DF" \
"${BUILD_ARGS[@]}" \
--tag "${IMG}:latest" \
--tag "${IMG}:${SHORT_SHA}" \
--tag "${IMG}:sha-${{ github.sha }}" \
--push \
"$WS"
# ---------------------------------------------------------------------------
# Docker: BIRD2 (evobgp-bird2).
# Собирается только при изменении deploy/bird/ или Dockerfile bird2.
# ---------------------------------------------------------------------------
docker-bird:
needs: [changes]
if: >-
github.event_name == 'push' &&
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') &&
(needs.changes.outputs.bird_conf == 'true' || needs.changes.outputs.docker_bird == 'true')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Prepare image metadata
id: meta
run: |
set -euo pipefail
owner_lc="$(echo '${{ github.repository_owner }}' | tr '[:upper:]' '[:lower:]')"
echo "owner_lc=$owner_lc" >> "$GITHUB_OUTPUT"
short_sha="$(echo '${{ github.sha }}' | cut -c1-7)"
echo "short_sha=$short_sha" >> "$GITHUB_OUTPUT"
- name: Log in to Gitea Registry
uses: docker/login-action@v3
with:
registry: git.shts.su
username: ${{ gitea.actor }}
password: ${{ secrets.ACTIONS_PAT || gitea.token }}
- name: Build and push evobgp-bird2
env:
OWNER_LC: ${{ steps.meta.outputs.owner_lc }}
SHORT_SHA: ${{ steps.meta.outputs.short_sha }}
run: |
set -euxo pipefail
WS="${{ github.workspace }}"
IMG="git.shts.su/${OWNER_LC}/evobgp-bird2"
docker buildx build \
--platform linux/amd64 \
--file deploy/docker/bird2/Dockerfile \
--tag "${IMG}:latest" \
--tag "${IMG}:${SHORT_SHA}" \
--tag "${IMG}:sha-${{ github.sha }}" \
--push \
"$WS"
+19 -1
View File
@@ -27,6 +27,24 @@ function mergeHeaders(init?: RequestInit, extraHeaders?: Record<string, string>)
return h; return h;
} }
/**
* Idempotency keys: `crypto.randomUUID()` exists only in secure contexts (HTTPS / localhost).
* Over plain HTTP to a LAN IP it is often undefined — use getRandomValues or a fallback.
*/
function newIdempotencyKey(): string {
const c = typeof globalThis !== 'undefined' ? globalThis.crypto : undefined;
if (c?.randomUUID) return c.randomUUID();
if (c?.getRandomValues) {
const buf = new Uint8Array(16);
c.getRandomValues(buf);
buf[6] = (buf[6]! & 0x0f) | 0x40;
buf[8] = (buf[8]! & 0x3f) | 0x80;
const hex = [...buf].map((b) => b.toString(16).padStart(2, '0')).join('');
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
}
return `idem-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 14)}`;
}
export class ApiError extends Error { export class ApiError extends Error {
constructor( constructor(
public readonly status: number, public readonly status: number,
@@ -58,7 +76,7 @@ export async function apiMutate<T = void>(
const headers: Record<string, string> = {}; const headers: Record<string, string> = {};
if (body !== undefined) headers['Content-Type'] = 'application/json'; if (body !== undefined) headers['Content-Type'] = 'application/json';
if (opts?.idempotent !== false) { if (opts?.idempotent !== false) {
headers['Idempotency-Key'] = crypto.randomUUID(); headers['Idempotency-Key'] = newIdempotencyKey();
} }
const res = await fetch(path, { const res = await fetch(path, {
method, method,