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
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:
+199
-78
@@ -7,22 +7,38 @@ on:
|
||||
branches: [main, master]
|
||||
|
||||
jobs:
|
||||
# ---------------------------------------------------------------------------
|
||||
# Гранулярная детекция изменений по модулям.
|
||||
# Каждый флаг соответствует группе файлов; downstream-джобы запускаются
|
||||
# только когда их группа затронута. Изменение CI-конфигурации (.gitea/workflows/*)
|
||||
# поднимает все флаги, чтобы гарантировать полный прогон.
|
||||
# ---------------------------------------------------------------------------
|
||||
changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
openapi: ${{ steps.detect.outputs.openapi }}
|
||||
code: ${{ steps.detect.outputs.code }}
|
||||
openapi: ${{ steps.detect.outputs.openapi }}
|
||||
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:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- id: detect
|
||||
name: Detect changed paths (OpenAPI vs doc-only vs code)
|
||||
name: Detect changed paths per module
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
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
|
||||
base="${{ github.event.pull_request.base.sha }}"
|
||||
@@ -36,52 +52,61 @@ jobs:
|
||||
elif git rev-parse --verify HEAD~1 >/dev/null 2>&1; then
|
||||
FILES="$(git diff --name-only HEAD~1 HEAD)"
|
||||
else
|
||||
# Нет родителя (первый коммит / тонкий clone) — полный пайплайн + OpenAPI
|
||||
openapi=true
|
||||
code=true
|
||||
echo "openapi=$openapi" >> "$GITHUB_OUTPUT"
|
||||
echo "code=$code" >> "$GITHUB_OUTPUT"
|
||||
echo "No parent commit: openapi=true code=true"
|
||||
openapi=true; go=true; web=true; bird_conf=true
|
||||
docker_go=true; docker_web=true; docker_bird=true
|
||||
for v in openapi go web bird_conf docker_go docker_web docker_bird; do
|
||||
echo "$v=true" >> "$GITHUB_OUTPUT"
|
||||
done
|
||||
echo "No parent commit — full pipeline"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Пустой diff (редко) — не отключаем сборку из осторожности
|
||||
if [ -z "$(printf '%s' "$FILES" | tr -d '[:space:]')" ]; then
|
||||
openapi=false
|
||||
code=true
|
||||
echo "openapi=$openapi" >> "$GITHUB_OUTPUT"
|
||||
echo "code=$code" >> "$GITHUB_OUTPUT"
|
||||
echo "Empty diff: openapi=false code=true"
|
||||
go=true; web=true
|
||||
for v in openapi go web bird_conf docker_go docker_web docker_bird; do
|
||||
eval "echo \"\$v=\$$v\"" >> "$GITHUB_OUTPUT"
|
||||
done
|
||||
echo "Empty diff — safe fallback: go=true web=true"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if printf '%s\n' "$FILES" | grep -qE '^docs/openapi\.yaml$|^redocly\.yaml$'; then
|
||||
openapi=true
|
||||
fi
|
||||
ci_changed=false
|
||||
|
||||
doc_only_all=true
|
||||
while IFS= read -r f || [ -n "${f:-}" ]; do
|
||||
[ -z "${f:-}" ] && continue
|
||||
if [[ "$f" == "README.md" || "$f" == ".gitea/README.md" || "$f" == "web/README.md" || "$f" == "redocly.yaml" || "$f" == docs/* ]]; then
|
||||
continue
|
||||
fi
|
||||
doc_only_all=false
|
||||
break
|
||||
case "$f" in
|
||||
.gitea/workflows/*) ci_changed=true ;;
|
||||
docs/openapi.yaml|redocly.yaml) openapi=true ;;
|
||||
web/README.md) ;; # doc-only
|
||||
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"
|
||||
|
||||
if $doc_only_all; then
|
||||
code=false
|
||||
else
|
||||
code=true
|
||||
if $ci_changed; then
|
||||
go=true; web=true; bird_conf=true
|
||||
docker_go=true; docker_web=true; docker_bird=true
|
||||
fi
|
||||
|
||||
echo "openapi=$openapi" >> "$GITHUB_OUTPUT"
|
||||
echo "code=$code" >> "$GITHUB_OUTPUT"
|
||||
echo "Changed files (first 20):"
|
||||
printf '%s\n' "$FILES" | head -n 20
|
||||
echo "openapi=$openapi code=$code"
|
||||
for v in openapi go web bird_conf docker_go docker_web docker_bird; do
|
||||
eval "echo \"\$v=\$$v\"" >> "$GITHUB_OUTPUT"
|
||||
done
|
||||
|
||||
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:
|
||||
needs: [changes]
|
||||
if: needs.changes.outputs.openapi == 'true'
|
||||
@@ -94,9 +119,10 @@ jobs:
|
||||
- name: Lint OpenAPI (Redocly)
|
||||
run: npx --yes @redocly/cli@1 lint docs/openapi.yaml
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
go:
|
||||
needs: [changes]
|
||||
if: needs.changes.outputs.code == 'true'
|
||||
if: needs.changes.outputs.go == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
@@ -117,13 +143,12 @@ jobs:
|
||||
go build -o "$out/$name" "./$d"
|
||||
done
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
bird2:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [go]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
# Вложенный docker + bind-mount ломается, если демон Docker на хосте не видит путь
|
||||
# workspace runner'а (/workspace/...). bird -p на самом job-раннере надёжнее.
|
||||
- name: Install bird2
|
||||
run: |
|
||||
set -euxo pipefail
|
||||
@@ -148,10 +173,20 @@ jobs:
|
||||
bird -c "$WS/$conf" -p
|
||||
done
|
||||
|
||||
# Сборка образов и push в Container Registry Gitea (только push в main/master; см. .gitea/README.md).
|
||||
docker-images:
|
||||
needs: [go]
|
||||
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master')
|
||||
# ---------------------------------------------------------------------------
|
||||
# Docker: Go-бинарники (api, all, scheduler, ingest, render, deploy, node, agent).
|
||||
# Запускается если изменился Go-код или Go-Dockerfile.
|
||||
# Если 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
|
||||
strategy:
|
||||
fail-fast: false
|
||||
@@ -192,24 +227,13 @@ jobs:
|
||||
bin: evobgp-node
|
||||
birdc: "0"
|
||||
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
|
||||
dockerfile: deploy/docker/evobgp-agent/Dockerfile
|
||||
evobgp_upstream: ""
|
||||
- image: evobgp-bird2
|
||||
dockerfile: deploy/docker/bird2/Dockerfile
|
||||
evobgp_upstream: ""
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Prepare image metadata
|
||||
id: meta
|
||||
run: |
|
||||
@@ -218,22 +242,16 @@ jobs:
|
||||
echo "owner_lc=$owner_lc" >> "$GITHUB_OUTPUT"
|
||||
short_sha="$(echo '${{ github.sha }}' | cut -c1-7)"
|
||||
echo "short_sha=$short_sha" >> "$GITHUB_OUTPUT"
|
||||
echo "docker_push=true" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Log in to Gitea Registry
|
||||
if: steps.meta.outputs.docker_push == 'true'
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.shts.su
|
||||
username: ${{ gitea.actor }}
|
||||
# PAT репозитория или встроенный токен job (нужны права на пакеты в настройках Actions).
|
||||
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 }}
|
||||
DO_PUSH: ${{ steps.meta.outputs.docker_push }}
|
||||
DF: ${{ matrix.dockerfile }}
|
||||
IMAGE: ${{ matrix.image }}
|
||||
BIN: ${{ matrix.bin }}
|
||||
@@ -244,7 +262,7 @@ jobs:
|
||||
WS="${{ github.workspace }}"
|
||||
cd "$WS"
|
||||
|
||||
TAG_LOCAL="evobgp:${IMAGE}-${SHORT_SHA}"
|
||||
IMG="git.shts.su/${OWNER_LC}/${IMAGE}"
|
||||
BUILD_ARGS=()
|
||||
if [ -n "${BIN:-}" ]; then
|
||||
BUILD_ARGS+=(--build-arg "BIN=${BIN}")
|
||||
@@ -254,23 +272,126 @@ jobs:
|
||||
BUILD_ARGS+=(--build-arg "EVOBGP_UPSTREAM=${EVOBGP_UPSTREAM}")
|
||||
fi
|
||||
|
||||
if [ "$DO_PUSH" = "true" ]; then
|
||||
IMG="git.shts.su/${OWNER_LC}/${IMAGE}"
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--file "$DF" \
|
||||
"${BUILD_ARGS[@]}" \
|
||||
--tag "${IMG}:latest" \
|
||||
--tag "${IMG}:${SHORT_SHA}" \
|
||||
--tag "${IMG}:sha-${{ github.sha }}" \
|
||||
--push \
|
||||
"$WS"
|
||||
else
|
||||
docker buildx build \
|
||||
--platform linux/amd64 \
|
||||
--file "$DF" \
|
||||
"${BUILD_ARGS[@]}" \
|
||||
--tag "$TAG_LOCAL" \
|
||||
--load \
|
||||
"$WS"
|
||||
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: Web-фронтенд (evobgp-web, evobgp-web-all).
|
||||
# Не зависит от Go-тестов — SvelteKit собирается отдельно.
|
||||
# ---------------------------------------------------------------------------
|
||||
docker-web:
|
||||
needs: [changes]
|
||||
if: >-
|
||||
github.event_name == 'push' &&
|
||||
(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
|
||||
|
||||
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"
|
||||
|
||||
@@ -27,6 +27,24 @@ function mergeHeaders(init?: RequestInit, extraHeaders?: Record<string, string>)
|
||||
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 {
|
||||
constructor(
|
||||
public readonly status: number,
|
||||
@@ -58,7 +76,7 @@ export async function apiMutate<T = void>(
|
||||
const headers: Record<string, string> = {};
|
||||
if (body !== undefined) headers['Content-Type'] = 'application/json';
|
||||
if (opts?.idempotent !== false) {
|
||||
headers['Idempotency-Key'] = crypto.randomUUID();
|
||||
headers['Idempotency-Key'] = newIdempotencyKey();
|
||||
}
|
||||
const res = await fetch(path, {
|
||||
method,
|
||||
|
||||
Reference in New Issue
Block a user