chore: update Docker configuration and CI/CD setup
quality / commitlint (push) Skipped
CD / update-wiki (push) Failing after 8s
quality / changes (push) Successful in 7s
quality / docker-check (push) Skipped
quality / web (push) Successful in 1m8s
quality / api (push) Successful in 41s
CD / quality (push) Successful in 2m0s
CD / publish (push) Failing after 11m24s
quality / commitlint (push) Skipped
CD / update-wiki (push) Failing after 8s
quality / changes (push) Successful in 7s
quality / docker-check (push) Skipped
quality / web (push) Successful in 1m8s
quality / api (push) Successful in 41s
CD / quality (push) Successful in 2m0s
CD / publish (push) Failing after 11m24s
- Removed Dockerfile and Dockerfile.test as part of the transition to a pre-built image. - Updated .dockerignore and .gitignore to reflect new build context and ignored files. - Modified docker-compose.yml to use the new image for the application. - Enhanced documentation in AGENTS.md and README.md to include CI/CD details and release process. - Added new dependencies for commit linting and semantic release in package.json and pnpm-lock.yaml. This commit streamlines the Docker setup and improves the CI/CD workflow with Gitea Actions.
This commit is contained in:
+47
-11
@@ -1,15 +1,51 @@
|
|||||||
|
# Build context = repository root (deploy/docker/docker-bake.hcl).
|
||||||
.git
|
.git
|
||||||
|
.gitea
|
||||||
|
.github
|
||||||
.cursor
|
.cursor
|
||||||
data
|
.claude
|
||||||
**/target
|
.codegraph
|
||||||
|
.agents
|
||||||
|
.opencode
|
||||||
|
memory-bank
|
||||||
|
.vscode
|
||||||
|
.idea
|
||||||
|
|
||||||
|
**/.DS_Store
|
||||||
|
**/Thumbs.db
|
||||||
|
**/.env
|
||||||
|
**/.env.*
|
||||||
|
!**/.env.example
|
||||||
|
!**/.env.*.example
|
||||||
|
|
||||||
|
node_modules
|
||||||
**/node_modules
|
**/node_modules
|
||||||
**/.turbo
|
|
||||||
**/dist
|
**/dist
|
||||||
**/*.md
|
**/.turbo
|
||||||
**/.git*
|
apps/web/src/routeTree.gen.ts
|
||||||
frontend
|
apps/web/playwright-report
|
||||||
*.db
|
apps/web/test-results
|
||||||
*.db-shm
|
|
||||||
*.db-wal
|
*.md
|
||||||
.env
|
AGENTS.md
|
||||||
.env.*
|
CONTRIBUTING.md
|
||||||
|
LICENSE
|
||||||
|
docs
|
||||||
|
|
||||||
|
.pre-commit-config.yaml
|
||||||
|
.releaserc.json
|
||||||
|
.commitlintrc.*
|
||||||
|
commitlint.config.cjs
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
|
data
|
||||||
|
*.exe
|
||||||
|
**/*.test.ts
|
||||||
|
**/*.spec.ts
|
||||||
|
coverage
|
||||||
|
.coverage
|
||||||
|
*.out
|
||||||
|
.release-version
|
||||||
|
CHANGELOG.md
|
||||||
|
deploy/docker/docker-bake.override.hcl
|
||||||
|
deploy/compose/runtime-logs
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
# Gitea Actions
|
||||||
|
|
||||||
|
| Workflow | Когда | Что |
|
||||||
|
|----------|--------|-----|
|
||||||
|
| [workflows/ci.yaml](workflows/ci.yaml) | pull request в main/master | quality gates + commitlint |
|
||||||
|
| [workflows/cd.yaml](workflows/cd.yaml) | push в main/master | quality gates + semantic-release + docker push; wiki параллельно |
|
||||||
|
| [workflows/quality.yaml](workflows/quality.yaml) | reusable (`workflow_call`) | changes, web, api, commitlint, docker-check |
|
||||||
|
|
||||||
|
Подробнее: [docs/releasing.md](../docs/releasing.md).
|
||||||
|
|
||||||
|
## CI (quality gates)
|
||||||
|
|
||||||
|
Job **changes** вычисляет флаги по путям в diff. Полный прогон: `.gitea/workflows/*`, `scripts/*`, корневой `package.json` / `pnpm-lock.yaml` / `.releaserc.json`. Правки `.cursor/`, `.claude/`, `*.md` quality jobs не запускают (wiki — отдельный job в CD).
|
||||||
|
|
||||||
|
На **pull request** — **commitlint**. При изменении `deploy/docker/**` / `.dockerignore` — job **docker-check** (`bake --print`, bake без `--push` если есть доступ к registry).
|
||||||
|
|
||||||
|
Кэш зависимостей — нативный `actions/cache` (cache server act_runner), ключ `sha256sum` lockfile (не `hashFiles`). Пути **абсолютные** (`$HOME/.pnpm-store`): тильда `~` на Gitea часто не раскрывается и даёт вечный miss.
|
||||||
|
|
||||||
|
Кэшируется целиком: pnpm store + `node_modules` + corepack. При hit: `pnpm install --offline`. `HUSKY=0` в `scripts/ci/pnpm-ci.sh` (у корня есть `prepare: husky`).
|
||||||
|
|
||||||
|
Если restore пишет `connect ECONNREFUSED` / `cache server not configured` — на runner включите cache server (см. ниже). Иначе каждый job снова качает пакеты (~минуты).
|
||||||
|
|
||||||
|
Runner: `ubuntu-latest`, Docker для **docker-check** (PR) и **publish** (CD).
|
||||||
|
|
||||||
|
## CD (job publish)
|
||||||
|
|
||||||
|
После успешных quality gates на **push в main** job **publish**:
|
||||||
|
|
||||||
|
1. `pnpm exec semantic-release` — тег `vX.Y.Z` на **текущий commit** (без дополнительного commit в main).
|
||||||
|
2. Gitea Release + `CHANGELOG.md` как attachment (не в git).
|
||||||
|
3. Зеркало base-образов в `cfdm-buildcache:base-*` (`deploy/docker/mirror-base-images.sh`; skip существующих тегов, `linux/amd64`, retry при 429).
|
||||||
|
4. `docker buildx bake default --push` с `VERSION=X.Y.Z`, `pull=false`, named builder `cfdm` (`cleanup: false`).
|
||||||
|
|
||||||
|
Если releasable-коммитов нет — semantic-release no-op, образы не публикуются.
|
||||||
|
|
||||||
|
Повтор упавшего **publish** (тег уже есть, bake нет): detect берёт `v*` на `HEAD` и всё равно пушит образы. Подробнее: [docs/releasing.md](../docs/releasing.md#перезапуск-упавшего-job-publish).
|
||||||
|
|
||||||
|
Job **update-wiki** идёт **параллельно** publish (не блокирует образы): при diff `docs/Home.md` копирует файл в wiki-репозиторий (`GITEA_TOKEN`).
|
||||||
|
|
||||||
|
### Секреты
|
||||||
|
|
||||||
|
**`ACTIONS_PAT`**: push tags, releases, Container Registry. Для git tag fallback: `gitea.token`. Push OCI — **только PAT** (у job token Gitea нет права packages).
|
||||||
|
|
||||||
|
**`GITEA_TOKEN`**: clone/push wiki.
|
||||||
|
|
||||||
|
### Теги образов
|
||||||
|
|
||||||
|
```text
|
||||||
|
git.shx.one/<owner>/cfdm:latest
|
||||||
|
git.shx.one/<owner>/cfdm:v1.2.3
|
||||||
|
git.shx.one/<owner>/cfdm:1.2.3
|
||||||
|
git.shx.one/<owner>/cfdm:<short-sha>
|
||||||
|
git.shx.one/<owner>/cfdm:sha-<full-sha>
|
||||||
|
```
|
||||||
|
|
||||||
|
Тот же манифест публикуется как `cloudflare-domain-manager` (drop-in для текущего `docker pull`).
|
||||||
|
|
||||||
|
Кэш сборки: `cfdm-buildcache:node-buildcache` и `cfdm-buildcache:base-*`.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker pull git.shx.one/denozord/cfdm:latest
|
||||||
|
docker pull git.shx.one/denozord/cloudflare-domain-manager:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
См. [deploy/docker/README.md](../deploy/docker/README.md).
|
||||||
|
|
||||||
|
## act_runner: cache server
|
||||||
|
|
||||||
|
`actions/cache` ходит в **встроенный cache server** runner (не GitHub `type=gha`). Кэш локален для этого runner.
|
||||||
|
|
||||||
|
В `config.yaml` runner:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
cache:
|
||||||
|
enabled: true
|
||||||
|
dir: "" # по умолчанию $HOME/.cache/actcache
|
||||||
|
host: "" # IP, доступный из job-контейнера (не 0.0.0.0)
|
||||||
|
port: 8088
|
||||||
|
```
|
||||||
|
|
||||||
|
Если runner в Docker, а jobs — отдельные контейнеры: пробросьте порт и задайте `host` (LAN IP хоста) или `external_server: "http://<host>:8088/"`. Иначе restore — timeout/ECONNREFUSED и пакеты качаются снова.
|
||||||
|
|
||||||
|
Не делайте `docker system prune -a` по cron: сотрётся и Docker-кэш FROM, и пользы от `cleanup: false` у buildx не будет.
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
name: CD
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, master]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
quality:
|
||||||
|
uses: ./.gitea/workflows/quality.yaml
|
||||||
|
with:
|
||||||
|
is_pull_request: false
|
||||||
|
before_sha: ${{ github.event.before }}
|
||||||
|
head_sha: ${{ github.sha }}
|
||||||
|
allow_registry_login: false
|
||||||
|
secrets:
|
||||||
|
ACTIONS_PAT: ${{ secrets.ACTIONS_PAT }}
|
||||||
|
|
||||||
|
update-wiki:
|
||||||
|
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout main repository
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 2
|
||||||
|
- name: Check for changes in documentation
|
||||||
|
id: check_changes
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
if ! git rev-parse --verify HEAD~1 >/dev/null 2>&1; then
|
||||||
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
elif git diff HEAD~1 HEAD --quiet -- docs/Home.md; then
|
||||||
|
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
- name: Clone Wiki repository
|
||||||
|
if: steps.check_changes.outputs.changed == 'true'
|
||||||
|
run: |
|
||||||
|
WIKI_URL=$(echo "${{ github.server_url }}/${{ github.repository }}.wiki.git" | sed -e "s|://|://gitea-actions:${{ secrets.GITEA_TOKEN }}@|")
|
||||||
|
git clone "${WIKI_URL}" cfdm.wiki
|
||||||
|
- name: Update and push Wiki content
|
||||||
|
if: steps.check_changes.outputs.changed == 'true'
|
||||||
|
run: |
|
||||||
|
cp docs/Home.md cfdm.wiki/Home.md
|
||||||
|
cd cfdm.wiki
|
||||||
|
git config user.name "Gitea Actions"
|
||||||
|
git config user.email "actions@gitea"
|
||||||
|
git add Home.md
|
||||||
|
git diff --staged --quiet || git commit -m "docs: Update Wiki from main repository"
|
||||||
|
git push
|
||||||
|
|
||||||
|
publish:
|
||||||
|
needs: [quality]
|
||||||
|
if: >-
|
||||||
|
always() &&
|
||||||
|
(github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master') &&
|
||||||
|
needs.quality.result == 'success'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
packages: write
|
||||||
|
releases: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
fetch-tags: true
|
||||||
|
token: ${{ secrets.ACTIONS_PAT || gitea.token }}
|
||||||
|
persist-credentials: true
|
||||||
|
- 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: Install release tooling
|
||||||
|
env:
|
||||||
|
PNPM_CACHE_HIT: ${{ steps.pnpm-cache.outputs.cache-hit }}
|
||||||
|
run: sh scripts/ci/pnpm-ci.sh
|
||||||
|
- name: Verify releasable commit messages
|
||||||
|
run: pnpm exec node scripts/commit/verify-release-commits.mjs
|
||||||
|
- name: Semantic release
|
||||||
|
run: pnpm exec semantic-release
|
||||||
|
env:
|
||||||
|
GITEA_URL: https://git.shx.one
|
||||||
|
GITEA_TOKEN: ${{ secrets.ACTIONS_PAT || gitea.token }}
|
||||||
|
- name: Detect new release
|
||||||
|
id: rel
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
version=""
|
||||||
|
if [ -f .release-version ]; then
|
||||||
|
version="$(tr -d '[:space:]' < .release-version)"
|
||||||
|
echo "New release from semantic-release: $version"
|
||||||
|
else
|
||||||
|
git fetch --tags --force origin || true
|
||||||
|
tag="$(git tag --points-at HEAD --list 'v*.*.*' | sort -V | tail -n1 || true)"
|
||||||
|
if [ -n "${tag:-}" ]; then
|
||||||
|
version="${tag#v}"
|
||||||
|
echo "Reuse existing tag $tag on HEAD (release retry)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if [ -n "${version:-}" ]; then
|
||||||
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "released=true" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "released=false" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "No releasable commits — skipping image publish"
|
||||||
|
fi
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
if: steps.rel.outputs.released == 'true'
|
||||||
|
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
|
with:
|
||||||
|
name: cfdm
|
||||||
|
driver: docker-container
|
||||||
|
cleanup: false
|
||||||
|
- name: Prepare image metadata
|
||||||
|
if: steps.rel.outputs.released == 'true'
|
||||||
|
id: meta
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
echo "version=${{ steps.rel.outputs.version }}" >> "$GITHUB_OUTPUT"
|
||||||
|
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"
|
||||||
|
echo "build_time=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT"
|
||||||
|
- name: Log in to Gitea Registry
|
||||||
|
if: steps.rel.outputs.released == 'true'
|
||||||
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
||||||
|
with:
|
||||||
|
registry: git.shx.one
|
||||||
|
username: ${{ gitea.actor }}
|
||||||
|
password: ${{ secrets.ACTIONS_PAT }}
|
||||||
|
- name: Mirror base images into buildcache
|
||||||
|
if: steps.rel.outputs.released == 'true'
|
||||||
|
env:
|
||||||
|
REGISTRY: git.shx.one/${{ steps.meta.outputs.owner_lc }}
|
||||||
|
MIRROR_ENV_FILE: ${{ runner.temp }}/mirror-base.env
|
||||||
|
run: sh deploy/docker/mirror-base-images.sh
|
||||||
|
- name: Build and push images (bake)
|
||||||
|
if: steps.rel.outputs.released == 'true'
|
||||||
|
env:
|
||||||
|
REGISTRY: git.shx.one/${{ steps.meta.outputs.owner_lc }}
|
||||||
|
IMAGE_TAG: latest
|
||||||
|
VERSION: ${{ steps.meta.outputs.version }}
|
||||||
|
SHORT_SHA: ${{ steps.meta.outputs.short_sha }}
|
||||||
|
SHA_FULL: ${{ github.sha }}
|
||||||
|
BUILD_TIME: ${{ steps.meta.outputs.build_time }}
|
||||||
|
CACHE_REF_NODE: git.shx.one/${{ steps.meta.outputs.owner_lc }}/cfdm-buildcache:node-buildcache
|
||||||
|
BUILDX_BAKE_ENTITLEMENTS_FS: "0"
|
||||||
|
BUILDX_BAKE_FILE_RELATIVE_PATHS: "1"
|
||||||
|
MIRROR_ENV_FILE: ${{ runner.temp }}/mirror-base.env
|
||||||
|
working-directory: deploy/docker
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
if [ -f "${MIRROR_ENV_FILE}" ]; then
|
||||||
|
set -a
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
. "${MIRROR_ENV_FILE}"
|
||||||
|
set +a
|
||||||
|
fi
|
||||||
|
docker buildx bake --allow=fs.read="${{ github.workspace }}" \
|
||||||
|
-f docker-bake.hcl default --push
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main, master]
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: ci-${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
quality:
|
||||||
|
uses: ./.gitea/workflows/quality.yaml
|
||||||
|
with:
|
||||||
|
is_pull_request: true
|
||||||
|
base_sha: ${{ github.event.pull_request.base.sha }}
|
||||||
|
head_sha: ${{ github.event.pull_request.head.sha }}
|
||||||
|
allow_registry_login: ${{ github.event.pull_request.head.repo.full_name == github.repository }}
|
||||||
|
secrets:
|
||||||
|
ACTIONS_PAT: ${{ secrets.ACTIONS_PAT }}
|
||||||
@@ -1,149 +0,0 @@
|
|||||||
name: Build and Push CFDM Docker Image
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main, develop, 'feature/**', 'release/**', 'hotfix/**']
|
|
||||||
tags: ['v*']
|
|
||||||
paths: ['**']
|
|
||||||
pull_request:
|
|
||||||
branches: [main, develop]
|
|
||||||
paths: ['**']
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push:
|
|
||||||
if: startsWith(gitea.ref, 'refs/tags/v') || (gitea.ref_name == 'main' && gitea.event_name == 'push')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Login to Gitea Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: git.shx.one
|
|
||||||
username: ${{ gitea.actor }}
|
|
||||||
password: ${{ secrets.ACTIONS_PAT }}
|
|
||||||
|
|
||||||
- name: Create version file
|
|
||||||
run: |
|
|
||||||
VERSION=$(cat VERSION)
|
|
||||||
BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ')
|
|
||||||
BRANCH="${{ gitea.ref_name }}"
|
|
||||||
echo "APP_VERSION=${VERSION}" > ./version.txt
|
|
||||||
echo "BUILD_DATE=${BUILD_DATE}" >> ./version.txt
|
|
||||||
echo "GIT_BRANCH=${BRANCH}" >> ./version.txt
|
|
||||||
echo "GIT_COMMIT=${{ gitea.sha }}" >> ./version.txt
|
|
||||||
echo "GIT_COMMIT_SHORT=$(echo ${{ gitea.sha }} | cut -c1-7)" >> ./version.txt
|
|
||||||
echo "BUILD_TIMESTAMP=$(date -u +%s)" >> ./version.txt
|
|
||||||
|
|
||||||
- name: Docker meta
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: git.shx.one/${{ gitea.repository }}
|
|
||||||
tags: |
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
type=raw,value=latest,enable=${{ gitea.ref_name == 'main' }}
|
|
||||||
type=sha,prefix={{date 'YYYYMMDD'}}-,enable=${{ gitea.ref_name == 'main' }}
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ./Dockerfile
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
build-args: |
|
|
||||||
NODE_VERSION=22.23.0-bookworm-slim
|
|
||||||
cache-from: type=registry,ref=git.shx.one/${{ gitea.repository }}:buildcache
|
|
||||||
cache-to: type=registry,ref=git.shx.one/${{ gitea.repository }}:buildcache,mode=max
|
|
||||||
provenance: true
|
|
||||||
|
|
||||||
create-release:
|
|
||||||
needs: build-and-push
|
|
||||||
if: startsWith(gitea.ref, 'refs/tags/v')
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Generate changelog
|
|
||||||
id: changelog
|
|
||||||
run: |
|
|
||||||
LAST_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
|
|
||||||
if [[ -n "$LAST_TAG" ]]; then
|
|
||||||
CHANGELOG=$(git log --pretty=format:"- **%h** %s (%an, %ar)" --no-merges ${LAST_TAG}..HEAD 2>/dev/null || echo "")
|
|
||||||
else
|
|
||||||
CHANGELOG=$(git log --pretty=format:"- **%h** %s (%an, %ar)" --no-merges -10 2>/dev/null || echo "")
|
|
||||||
fi
|
|
||||||
if [[ -z "$CHANGELOG" ]]; then CHANGELOG="- No changes detected"; fi
|
|
||||||
echo "CHANGELOG<<EOF" >> $GITEA_OUTPUT
|
|
||||||
echo "$CHANGELOG" >> $GITEA_OUTPUT
|
|
||||||
echo "EOF" >> $GITEA_OUTPUT
|
|
||||||
|
|
||||||
- name: Create Release
|
|
||||||
run: |
|
|
||||||
VERSION="${{ gitea.ref_name }}"
|
|
||||||
RELEASE_DATA=$(cat <<EOF
|
|
||||||
{
|
|
||||||
"tag_name": "${VERSION}",
|
|
||||||
"target_commitish": "${{ gitea.sha }}",
|
|
||||||
"name": "Cloudflare Domain Manager ${VERSION}",
|
|
||||||
"body": "## Cloudflare Domain Manager ${VERSION}\n\n### Docker\n\n\`\`\`bash\ndocker pull git.shx.one/${{ gitea.repository }}:${VERSION}\ndocker run -d -p 8080:8080 -v ./data:/data --name cfdm git.shx.one/${{ gitea.repository }}:${VERSION}\n\`\`\`\n\n### Changelog\n\n${{ steps.changelog.outputs.CHANGELOG }}",
|
|
||||||
"draft": false,
|
|
||||||
"prerelease": false
|
|
||||||
}
|
|
||||||
EOF
|
|
||||||
)
|
|
||||||
HTTP_CODE=$(curl -s -w "%{http_code}" -o /tmp/release.json -X POST \
|
|
||||||
-H "Authorization: token ${{ secrets.GITEA_TOKEN }}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "$RELEASE_DATA" \
|
|
||||||
"${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}/releases")
|
|
||||||
if [ "$HTTP_CODE" -eq 201 ] || [ "$HTTP_CODE" -eq 409 ]; then
|
|
||||||
echo "Release OK (HTTP $HTTP_CODE)"
|
|
||||||
else
|
|
||||||
cat /tmp/release.json
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
update-wiki:
|
|
||||||
if: gitea.ref_name == 'main' && gitea.event_name == 'push'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout main repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 2
|
|
||||||
|
|
||||||
- name: Check for changes in documentation
|
|
||||||
id: check_changes
|
|
||||||
run: |
|
|
||||||
if git diff HEAD~1 HEAD --quiet -- docs/Home.md; then
|
|
||||||
echo "changed=false" >> $GITEA_OUTPUT
|
|
||||||
else
|
|
||||||
echo "changed=true" >> $GITEA_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Clone Wiki repository
|
|
||||||
if: steps.check_changes.outputs.changed == 'true'
|
|
||||||
run: |
|
|
||||||
WIKI_URL=$(echo "${{ gitea.server_url }}/${{ gitea.repository }}.wiki.git" | sed -e "s|://|://gitea-actions:${{ secrets.GITEA_TOKEN }}@|")
|
|
||||||
git clone "${WIKI_URL}" cfdm.wiki
|
|
||||||
|
|
||||||
- name: Update and push Wiki content
|
|
||||||
if: steps.check_changes.outputs.changed == 'true'
|
|
||||||
run: |
|
|
||||||
cp docs/Home.md cfdm.wiki/Home.md
|
|
||||||
cd cfdm.wiki
|
|
||||||
git config user.name "Gitea Actions"
|
|
||||||
git config user.email "actions@gitea"
|
|
||||||
git add Home.md
|
|
||||||
git diff --staged --quiet || git commit -m "docs: Update Wiki from main repository"
|
|
||||||
git push
|
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
# 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:
|
||||||
|
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:
|
||||||
|
fetch-depth: 2
|
||||||
|
- 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
|
||||||
|
|
||||||
|
web=false
|
||||||
|
api=false
|
||||||
|
docker=false
|
||||||
|
|
||||||
|
set_all_flags_true() {
|
||||||
|
web=true
|
||||||
|
api=true
|
||||||
|
docker=true
|
||||||
|
}
|
||||||
|
|
||||||
|
write_outputs() {
|
||||||
|
for v in 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"
|
||||||
|
if [ -n "$before" ] && [ "$before" != "0000000000000000000000000000000000000000" ]; 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
|
||||||
|
;;
|
||||||
|
.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-compose.yml)
|
||||||
|
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 "web=$web api=$api docker=$docker full_pipeline=$full_pipeline"
|
||||||
|
|
||||||
|
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, lint, test, build
|
||||||
|
env:
|
||||||
|
PNPM_CACHE_HIT: ${{ steps.pnpm-cache.outputs.cache-hit }}
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
sh scripts/ci/pnpm-ci.sh
|
||||||
|
pnpm --filter web lint
|
||||||
|
pnpm exec turbo run test --filter=web
|
||||||
|
pnpm exec turbo run build --filter=web
|
||||||
|
|
||||||
|
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, test, build
|
||||||
|
env:
|
||||||
|
PNPM_CACHE_HIT: ${{ steps.pnpm-cache.outputs.cache-hit }}
|
||||||
|
run: |
|
||||||
|
set -euxo pipefail
|
||||||
|
sh scripts/ci/pnpm-ci.sh
|
||||||
|
pnpm exec turbo run test --filter=@cfdm/api
|
||||||
|
pnpm exec turbo run build --filter=@cfdm/api
|
||||||
|
|
||||||
|
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"
|
||||||
|
|
||||||
|
docker-check:
|
||||||
|
needs: [changes]
|
||||||
|
if: inputs.is_pull_request && needs.changes.outputs.docker == 'true'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||||
|
- uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
|
||||||
|
with:
|
||||||
|
name: cfdm
|
||||||
|
driver: docker-container
|
||||||
|
cleanup: false
|
||||||
|
- 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: bake (no push)
|
||||||
|
if: inputs.allow_registry_login
|
||||||
|
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}/cfdm-buildcache:node-buildcache"
|
||||||
|
docker buildx bake --allow=fs.read="${{ github.workspace }}" -f docker-bake.hcl default
|
||||||
@@ -32,6 +32,8 @@ Thumbs.db
|
|||||||
|
|
||||||
# Build
|
# Build
|
||||||
version.txt
|
version.txt
|
||||||
|
.release-version
|
||||||
|
CHANGELOG.md
|
||||||
.codegraph/daemon.pid
|
.codegraph/daemon.pid
|
||||||
|
|
||||||
# Local MCP configs (may contain REUI license Bearer)
|
# Local MCP configs (may contain REUI license Bearer)
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"branches": ["main", "master"],
|
||||||
|
"tagFormat": "v${version}",
|
||||||
|
"plugins": [
|
||||||
|
[
|
||||||
|
"@semantic-release/commit-analyzer",
|
||||||
|
{
|
||||||
|
"preset": "conventionalcommits",
|
||||||
|
"releaseRules": [
|
||||||
|
{ "type": "feat", "release": "minor" },
|
||||||
|
{ "type": "fix", "release": "patch" },
|
||||||
|
{ "type": "perf", "release": "patch" },
|
||||||
|
{ "type": "ci", "release": "patch" },
|
||||||
|
{ "type": "refactor", "release": "patch" },
|
||||||
|
{ "breaking": true, "release": "major" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/release-notes-generator",
|
||||||
|
{
|
||||||
|
"preset": "conventionalcommits"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/changelog",
|
||||||
|
{
|
||||||
|
"changelogFile": "CHANGELOG.md"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@semantic-release/exec",
|
||||||
|
{
|
||||||
|
"successCmd": "echo ${nextRelease.version} > .release-version"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"@markwylde/semantic-release-gitea",
|
||||||
|
{
|
||||||
|
"giteaUrl": "https://git.shx.one",
|
||||||
|
"assets": [
|
||||||
|
{
|
||||||
|
"path": "CHANGELOG.md",
|
||||||
|
"label": "Changelog"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -33,4 +33,8 @@ Skill: `.claude/skills/reui` (`668fb463eb`).
|
|||||||
|
|
||||||
## Docs
|
## Docs
|
||||||
|
|
||||||
[`docs/Home.md`](docs/Home.md), [`CONTRIBUTING.md`](CONTRIBUTING.md), [`docs/ui-design-contract.md`](docs/ui-design-contract.md).
|
[`docs/Home.md`](docs/Home.md), [`CONTRIBUTING.md`](CONTRIBUTING.md), [`docs/ui-design-contract.md`](docs/ui-design-contract.md), [`docs/releasing.md`](docs/releasing.md).
|
||||||
|
|
||||||
|
## CI/CD
|
||||||
|
|
||||||
|
Gitea Actions: [`.gitea/workflows`](.gitea/workflows) (`ci.yaml` / `cd.yaml` / `quality.yaml`). Образы — `docker buildx bake` из `deploy/docker` (runtime `node:22-alpine`, теги `cfdm` + `cloudflare-domain-manager`). Подробнее: [`.gitea/README.md`](.gitea/README.md).
|
||||||
|
|||||||
+6
-9
@@ -17,23 +17,23 @@ Types: `feat`, `fix`, `docs`, `style`, `refactor`, `test`, `chore`
|
|||||||
## Pull requests
|
## Pull requests
|
||||||
|
|
||||||
- Target `develop` for features; `main` for release/hotfix
|
- Target `develop` for features; `main` for release/hotfix
|
||||||
- CI `test` job must pass
|
- CI quality jobs (`web`, `api`) must pass
|
||||||
- At least one approval
|
- At least one approval
|
||||||
- Rebase/merge when up to date with target branch
|
- Rebase/merge when up to date with target branch
|
||||||
- Delete branch after merge
|
- Delete branch after merge
|
||||||
|
|
||||||
## Versioning
|
## Versioning
|
||||||
|
|
||||||
- SemVer in [`VERSION`](VERSION)
|
- SemVer через [semantic-release](https://semantic-release.gitbook.io/) на push в `main`
|
||||||
- Bump version in `release/*` or `hotfix/*` branches
|
- Не править версию вручную; тег `vX.Y.Z` ставит CI
|
||||||
- Tag `vX.Y.Z` on `main` after release merge
|
- Подробнее: [`docs/releasing.md`](docs/releasing.md)
|
||||||
|
|
||||||
## Branch protection (Gitea)
|
## Branch protection (Gitea)
|
||||||
|
|
||||||
Configure for **main** and **develop**:
|
Configure for **main** and **develop**:
|
||||||
|
|
||||||
- Require pull request reviews
|
- Require pull request reviews
|
||||||
- Require status checks (`test`)
|
- Require status checks (`CI` / quality jobs `web`, `api`)
|
||||||
- Require branches to be up to date
|
- Require branches to be up to date
|
||||||
- No force push, no deletion
|
- No force push, no deletion
|
||||||
|
|
||||||
@@ -54,7 +54,4 @@ pnpm --filter web dev
|
|||||||
|
|
||||||
## Release process
|
## Release process
|
||||||
|
|
||||||
1. `git checkout -b release/v0.2.0 develop`
|
Релизы делает workflow **CD** на push в `main` (semantic-release + bake). Не создавать ветки `release/*` и не bump'ать файлы версии вручную. См. [`docs/releasing.md`](docs/releasing.md).
|
||||||
2. Bump `VERSION`, fix release issues
|
|
||||||
3. PR to `main` → merge → tag `v0.2.0`
|
|
||||||
4. Merge release branch back to `develop`
|
|
||||||
|
|||||||
-52
@@ -1,52 +0,0 @@
|
|||||||
# syntax=docker/dockerfile:1
|
|
||||||
|
|
||||||
ARG NODE_VERSION=22.23.0-bookworm-slim
|
|
||||||
|
|
||||||
FROM node:${NODE_VERSION} AS build
|
|
||||||
WORKDIR /app
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends python3 make g++ \
|
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
|
||||||
&& corepack enable
|
|
||||||
|
|
||||||
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml turbo.json ./
|
|
||||||
COPY apps/web/package.json apps/web/
|
|
||||||
COPY apps/api/package.json apps/api/
|
|
||||||
COPY packages/ui/package.json packages/ui/
|
|
||||||
COPY packages/shared/package.json packages/shared/
|
|
||||||
COPY packages/db/package.json packages/db/
|
|
||||||
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
|
|
||||||
pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
COPY apps/web apps/web
|
|
||||||
COPY apps/api apps/api
|
|
||||||
COPY packages/ui packages/ui
|
|
||||||
COPY packages/shared packages/shared
|
|
||||||
COPY packages/db packages/db
|
|
||||||
|
|
||||||
RUN pnpm turbo build --filter=web --filter=@cfdm/api \
|
|
||||||
&& pnpm --filter @cfdm/api deploy --prod /out \
|
|
||||||
&& cp -r apps/web/dist /out/static \
|
|
||||||
&& rm -rf /out/src /out/test /out/.turbo \
|
|
||||||
&& rm -rf /out/node_modules/@cfdm/db/src /out/node_modules/@cfdm/db/scripts /out/node_modules/@cfdm/db/.turbo \
|
|
||||||
&& rm -rf /out/node_modules/@cfdm/shared/src /out/node_modules/@cfdm/shared/.turbo \
|
|
||||||
&& find /out/dist /out/node_modules/@cfdm -type f \( -name '*.d.ts' -o -name '*.map' -o -name 'tsconfig*.json' -o -name 'vitest.config.ts' -o -name 'drizzle.config.ts' \) -delete
|
|
||||||
|
|
||||||
FROM node:${NODE_VERSION}
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends ca-certificates \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
ENV NODE_ENV=production \
|
|
||||||
STATIC_DIR=/app/static \
|
|
||||||
DATABASE_URL=sqlite:/data/app.db \
|
|
||||||
SERVER_PORT=8080
|
|
||||||
|
|
||||||
COPY --from=build /out ./
|
|
||||||
|
|
||||||
EXPOSE 8080
|
|
||||||
VOLUME ["/data"]
|
|
||||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
|
||||||
CMD ["node", "-e", "fetch('http://127.0.0.1:8080/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
|
||||||
CMD ["node", "dist/server.js"]
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
# syntax=docker/dockerfile:1
|
|
||||||
|
|
||||||
ARG NODE_VERSION=22.23.0-bookworm-slim
|
|
||||||
|
|
||||||
FROM node:${NODE_VERSION} AS test
|
|
||||||
WORKDIR /app
|
|
||||||
RUN apt-get update \
|
|
||||||
&& apt-get install -y --no-install-recommends python3 make g++ \
|
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
|
||||||
&& corepack enable
|
|
||||||
|
|
||||||
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml turbo.json ./
|
|
||||||
COPY apps/web/package.json apps/web/
|
|
||||||
COPY apps/api/package.json apps/api/
|
|
||||||
COPY packages/ui/package.json packages/ui/
|
|
||||||
COPY packages/shared/package.json packages/shared/
|
|
||||||
COPY packages/db/package.json packages/db/
|
|
||||||
RUN --mount=type=cache,id=pnpm-store,target=/root/.local/share/pnpm/store \
|
|
||||||
pnpm install --frozen-lockfile
|
|
||||||
|
|
||||||
COPY apps/web apps/web
|
|
||||||
COPY apps/api apps/api
|
|
||||||
COPY packages/ui packages/ui
|
|
||||||
COPY packages/shared packages/shared
|
|
||||||
COPY packages/db packages/db
|
|
||||||
|
|
||||||
RUN pnpm turbo build --filter=@cfdm/shared --filter=@cfdm/db --filter=@cfdm/api
|
|
||||||
CMD ["sh", "-c", "pnpm --filter @cfdm/api test && pnpm --filter web test"]
|
|
||||||
@@ -28,7 +28,7 @@ Open http://localhost:8080 — default login `admin` / `admin` (dev only).
|
|||||||
- **Backend:** Node.js 22, Fastify 5, Drizzle ORM, SQLite (`apps/api`)
|
- **Backend:** Node.js 22, Fastify 5, Drizzle ORM, SQLite (`apps/api`)
|
||||||
- **Shared:** Zod schemas (`packages/shared`), DB layer (`packages/db`)
|
- **Shared:** Zod schemas (`packages/shared`), DB layer (`packages/db`)
|
||||||
- **Frontend:** pnpm monorepo (`apps/web` + `packages/ui`), React, Vite, TanStack Router/Query/Table, shadcn/ui (base-nova), Recharts
|
- **Frontend:** pnpm monorepo (`apps/web` + `packages/ui`), React, Vite, TanStack Router/Query/Table, shadcn/ui (base-nova), Recharts
|
||||||
- **CI:** Gitea Actions (Gitflow)
|
- **CI:** Gitea Actions ([docs/releasing.md](docs/releasing.md)) — quality gates + semantic-release + bake (`cfdm` / `cloudflare-domain-manager`)
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ shadcn CLI: `cd apps/web && pnpm dlx shadcn@latest add <component>`
|
|||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
See [docs/Home.md](docs/Home.md) and [CONTRIBUTING.md](CONTRIBUTING.md).
|
See [docs/Home.md](docs/Home.md), [docs/releasing.md](docs/releasing.md) and [CONTRIBUTING.md](CONTRIBUTING.md).
|
||||||
|
|
||||||
## Load balancing & health checks
|
## Load balancing & health checks
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
/** @type {import('@commitlint/types').UserConfig} */
|
||||||
|
module.exports = {
|
||||||
|
extends: ['@commitlint/config-conventional'],
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'scope-no-commas': ({ scope }) => {
|
||||||
|
if (scope && scope.includes(',')) {
|
||||||
|
return [
|
||||||
|
false,
|
||||||
|
'scope must not contain commas (semantic-release will not parse the commit type)'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return [true];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
// Subject после «:» на русском — не английский sentence-case.
|
||||||
|
'subject-case': [0],
|
||||||
|
'scope-no-commas': [2, 'always']
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
services:
|
||||||
|
app:
|
||||||
|
# cfdm и cloudflare-domain-manager — один образ (алиас для drop-in).
|
||||||
|
image: git.shx.one/denozord/cfdm:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "8080:8080"
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: sqlite:/data/app.db
|
||||||
|
CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN:-}
|
||||||
|
JWT_SECRET: ${JWT_SECRET:-change-me-in-production}
|
||||||
|
AUTH_REQUIRED: ${AUTH_REQUIRED:-false}
|
||||||
|
AUTH_JWT_SECRET: ${AUTH_JWT_SECRET:-${JWT_SECRET:-change-me-in-production}}
|
||||||
|
AUTH_ISSUER: ${AUTH_ISSUER:-https://auth.shnt.top}
|
||||||
|
AUTH_PORTAL_URL: ${AUTH_PORTAL_URL:-https://auth.shnt.top}
|
||||||
|
ADMIN_USERNAME: ${ADMIN_USERNAME:-admin}
|
||||||
|
ADMIN_PASSWORD_HASH: ${ADMIN_PASSWORD_HASH:-}
|
||||||
|
LOG_LEVEL: info
|
||||||
|
CERT_CHECK_CRON: "0 */6 * * *"
|
||||||
|
STATIC_DIR: /app/static
|
||||||
|
volumes:
|
||||||
|
- cfdm-data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8080/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 3
|
||||||
|
start_period: 15s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
cfdm-data:
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Docker-образы Cloudflare Domain Manager
|
||||||
|
|
||||||
|
## CI (Gitea Actions)
|
||||||
|
|
||||||
|
Сборка образов — **`docker buildx bake`** ([docker-bake.hcl](docker-bake.hcl)), не отдельные `docker build`.
|
||||||
|
|
||||||
|
**Publish:** push в `main` после quality gates — workflow [CD](../../.gitea/workflows/cd.yaml) job **publish**: semantic-release + зеркало base-образов + bake с `VERSION` из релиза.
|
||||||
|
|
||||||
|
Bake читает переменные из **окружения** (`REGISTRY`, `IMAGE_TAG`, `CACHE_REF_NODE`, `BASE_NODE`).
|
||||||
|
|
||||||
|
BuildKit кэширует pnpm store через `RUN --mount=type=cache`. На CI mounts живут, пока named builder `cfdm` не удаляют (`cleanup: false`).
|
||||||
|
|
||||||
|
### Слои и runtime
|
||||||
|
|
||||||
|
| Образ | Runtime base | Заметка |
|
||||||
|
|-------|----------------|---------|
|
||||||
|
| `cfdm`, `cloudflare-domain-manager` | `node:22-alpine` | один процесс: API + SPA + SQLite; `better-sqlite3` и `@node-rs/argon2` собираются в alpine на stage `deps`/`build` |
|
||||||
|
|
||||||
|
`python3` / `make` / `g++` только в `deps`/`build`. Context режется корневым [.dockerignore](../../.dockerignore).
|
||||||
|
|
||||||
|
Теги `cfdm` и `cloudflare-domain-manager` — один и тот же образ (`cloudflare-domain-manager` — drop-in для текущего `docker pull`).
|
||||||
|
|
||||||
|
### Кэш registry
|
||||||
|
|
||||||
|
- `git.shx.one/<owner>/cfdm-buildcache:node-buildcache` — **запись** только из `node-build`
|
||||||
|
- `git.shx.one/<owner>/cfdm-buildcache:base-node-22-alpine` — зеркало FROM с Docker Hub. Только `linux/amd64`; skip если тег уже в Gitea. Неуспешный copy не валит CD — bake берёт Docker Hub FROM.
|
||||||
|
|
||||||
|
`pull = false` в bake: не перекачивать FROM, если слой уже в builder. Не запускайте `docker system prune -a` на runner.
|
||||||
|
|
||||||
|
Параллельный `cache-to` в один ref ломает manifest (`content descriptor … not found`).
|
||||||
|
|
||||||
|
Если CI падает на «not found» после смены схемы кэша — один раз удалите теги `cfdm-buildcache:*` в registry и пересоберите.
|
||||||
|
|
||||||
|
## Локальная сборка
|
||||||
|
|
||||||
|
Из корня репозитория:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd deploy/docker
|
||||||
|
export REGISTRY=git.shx.one/<owner>
|
||||||
|
export IMAGE_TAG=latest
|
||||||
|
export SHORT_SHA=$(git rev-parse --short HEAD)
|
||||||
|
export VERSION=dev
|
||||||
|
export BUILD_TIME=
|
||||||
|
docker buildx bake --allow=fs.read=../.. -f docker-bake.hcl default
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверка манифеста без сборки:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker buildx bake --allow=fs.read=../.. -f docker-bake.hcl --print default
|
||||||
|
```
|
||||||
|
|
||||||
|
В `docker-bake.hcl`: `context = "../.."` резолвится **от cwd** (каталог `deploy/docker/`). `dockerfile` — путь от этого context (корень репо). Из корня репо не вызывать bake с `-f deploy/docker/docker-bake.hcl`: получится `lstat ../../deploy`. Можно задать `BUILDX_BAKE_FILE_RELATIVE_PATHS=1`.
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
|
# All-in-one CFDM (Fastify + SPA + SQLite). Analog evofw / evobgp-all.
|
||||||
|
ARG BASE_NODE=docker.io/library/node:22-alpine
|
||||||
|
|
||||||
|
FROM ${BASE_NODE} AS deps
|
||||||
|
WORKDIR /repo
|
||||||
|
RUN apk add --no-cache python3 make g++ \
|
||||||
|
&& corepack enable && corepack prepare pnpm@10.12.1 --activate
|
||||||
|
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml turbo.json ./
|
||||||
|
COPY apps/web/package.json apps/web/
|
||||||
|
COPY apps/api/package.json apps/api/
|
||||||
|
COPY packages/ui/package.json packages/ui/
|
||||||
|
COPY packages/shared/package.json packages/shared/
|
||||||
|
COPY packages/db/package.json packages/db/
|
||||||
|
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
|
||||||
|
pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
FROM deps AS build
|
||||||
|
COPY apps/web apps/web
|
||||||
|
COPY apps/api apps/api
|
||||||
|
COPY packages/ui packages/ui
|
||||||
|
COPY packages/shared packages/shared
|
||||||
|
COPY packages/db packages/db
|
||||||
|
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
|
||||||
|
pnpm turbo build --filter=web --filter=@cfdm/api \
|
||||||
|
&& pnpm --filter @cfdm/api deploy --prod /out \
|
||||||
|
&& cp -r apps/web/dist /out/static \
|
||||||
|
&& rm -rf /out/src /out/test /out/.turbo \
|
||||||
|
&& rm -rf /out/node_modules/@cfdm/db/src /out/node_modules/@cfdm/db/scripts /out/node_modules/@cfdm/db/.turbo \
|
||||||
|
&& rm -rf /out/node_modules/@cfdm/shared/src /out/node_modules/@cfdm/shared/.turbo \
|
||||||
|
&& find /out/dist /out/node_modules/@cfdm -type f \( -name '*.d.ts' -o -name '*.map' -o -name 'tsconfig*.json' -o -name 'vitest.config.ts' -o -name 'drizzle.config.ts' \) -delete
|
||||||
|
|
||||||
|
FROM ${BASE_NODE} AS runtime
|
||||||
|
WORKDIR /app
|
||||||
|
RUN apk add --no-cache ca-certificates
|
||||||
|
ARG VERSION=dev
|
||||||
|
ARG GIT_SHA=unknown
|
||||||
|
ARG BUILD_TIME=
|
||||||
|
ENV NODE_ENV=production \
|
||||||
|
STATIC_DIR=/app/static \
|
||||||
|
DATABASE_URL=sqlite:/data/app.db \
|
||||||
|
SERVER_PORT=8080 \
|
||||||
|
APP_VERSION=${VERSION} \
|
||||||
|
GIT_SHA=${GIT_SHA} \
|
||||||
|
BUILD_TIME=${BUILD_TIME}
|
||||||
|
COPY --from=build /out ./
|
||||||
|
EXPOSE 8080
|
||||||
|
VOLUME ["/data"]
|
||||||
|
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||||
|
CMD ["node", "-e", "fetch('http://127.0.0.1:8080/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"]
|
||||||
|
CMD ["node", "dist/server.js"]
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
# Единая сборка образа CFDM (buildx bake: -f deploy/docker/docker-bake.hcl).
|
||||||
|
# context = "../.." — корень репо. Bake резолвит context от cwd (не от HCL),
|
||||||
|
# поэтому вызов из deploy/docker/ либо BUILDX_BAKE_FILE_RELATIVE_PATHS=1.
|
||||||
|
|
||||||
|
variable "REGISTRY" {
|
||||||
|
default = "git.shx.one/denozord"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "IMAGE_TAG" {
|
||||||
|
default = "latest"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "SHORT_SHA" {
|
||||||
|
default = "dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "SHA_FULL" {
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "VERSION" {
|
||||||
|
default = "dev"
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "BUILD_TIME" {
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "CACHE_REF_NODE" {
|
||||||
|
default = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
variable "BASE_NODE" {
|
||||||
|
default = "docker.io/library/node:22-alpine"
|
||||||
|
}
|
||||||
|
|
||||||
|
function "node-cache-from" {
|
||||||
|
params = []
|
||||||
|
result = notequal("", CACHE_REF_NODE) ? ["type=registry,ref=${CACHE_REF_NODE}"] : []
|
||||||
|
}
|
||||||
|
|
||||||
|
# Экспорт кэша — один writer на ref (параллельный cache-to в один ref ломает manifest).
|
||||||
|
function "node-cache-to-export" {
|
||||||
|
params = []
|
||||||
|
result = notequal("", CACHE_REF_NODE) ? ["type=registry,ref=${CACHE_REF_NODE},mode=max"] : []
|
||||||
|
}
|
||||||
|
|
||||||
|
function "image-tags" {
|
||||||
|
params = [name]
|
||||||
|
result = concat(
|
||||||
|
notequal("", SHA_FULL) ? [
|
||||||
|
"${REGISTRY}/${name}:${IMAGE_TAG}",
|
||||||
|
"${REGISTRY}/${name}:${SHORT_SHA}",
|
||||||
|
"${REGISTRY}/${name}:sha-${SHA_FULL}",
|
||||||
|
] : [
|
||||||
|
"${REGISTRY}/${name}:${IMAGE_TAG}",
|
||||||
|
"${REGISTRY}/${name}:${SHORT_SHA}",
|
||||||
|
],
|
||||||
|
notequal(VERSION, "dev") && notequal(VERSION, "") ? [
|
||||||
|
"${REGISTRY}/${name}:v${VERSION}",
|
||||||
|
"${REGISTRY}/${name}:${VERSION}",
|
||||||
|
] : []
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
group "default" {
|
||||||
|
targets = ["cfdm"]
|
||||||
|
}
|
||||||
|
|
||||||
|
target "_common" {
|
||||||
|
platforms = ["linux/amd64"]
|
||||||
|
pull = false
|
||||||
|
}
|
||||||
|
|
||||||
|
target "node-deps" {
|
||||||
|
inherits = ["_common"]
|
||||||
|
context = "../.."
|
||||||
|
dockerfile = "deploy/docker/cfdm/Dockerfile"
|
||||||
|
target = "deps"
|
||||||
|
args = {
|
||||||
|
BASE_NODE = BASE_NODE
|
||||||
|
}
|
||||||
|
cache-from = node-cache-from()
|
||||||
|
}
|
||||||
|
|
||||||
|
target "node-build" {
|
||||||
|
inherits = ["_common"]
|
||||||
|
context = "../.."
|
||||||
|
dockerfile = "deploy/docker/cfdm/Dockerfile"
|
||||||
|
target = "build"
|
||||||
|
args = {
|
||||||
|
BASE_NODE = BASE_NODE
|
||||||
|
}
|
||||||
|
contexts = {
|
||||||
|
deps = "target:node-deps"
|
||||||
|
}
|
||||||
|
cache-from = node-cache-from()
|
||||||
|
cache-to = node-cache-to-export()
|
||||||
|
}
|
||||||
|
|
||||||
|
target "cfdm" {
|
||||||
|
inherits = ["_common"]
|
||||||
|
context = "../.."
|
||||||
|
dockerfile = "deploy/docker/cfdm/Dockerfile"
|
||||||
|
target = "runtime"
|
||||||
|
args = {
|
||||||
|
BASE_NODE = BASE_NODE
|
||||||
|
VERSION = VERSION
|
||||||
|
GIT_SHA = notequal("", SHA_FULL) ? SHA_FULL : SHORT_SHA
|
||||||
|
BUILD_TIME = BUILD_TIME
|
||||||
|
}
|
||||||
|
contexts = {
|
||||||
|
build = "target:node-build"
|
||||||
|
}
|
||||||
|
cache-from = node-cache-from()
|
||||||
|
tags = concat(image-tags("cfdm"), image-tags("cloudflare-domain-manager"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
# Copy-by-digest зеркало базовых образов в Gitea Container Registry (без rebuild).
|
||||||
|
# Требует: docker login в REGISTRY host, docker buildx.
|
||||||
|
# REGISTRY=git.shx.one/<owner> (без суффикса /cfdm-buildcache)
|
||||||
|
#
|
||||||
|
# Источник library-образов — Docker Hub (docker.io).
|
||||||
|
# Если тег уже есть в Gitea — skip (без повторного pull).
|
||||||
|
# Копируем только linux/amd64 — bake platforms совпадает.
|
||||||
|
# Шаг CD не должен падать: неуспешный copy оставляет bake на Docker Hub FROM.
|
||||||
|
set -eu
|
||||||
|
REGISTRY="${REGISTRY:?REGISTRY required (git.shx.one/<owner>)}"
|
||||||
|
CACHE_REPO="${REGISTRY}/cfdm-buildcache"
|
||||||
|
ENV_FILE="${MIRROR_ENV_FILE:-}"
|
||||||
|
PLATFORM="${MIRROR_PLATFORM:-linux/amd64}"
|
||||||
|
RETRIES="${MIRROR_RETRIES:-4}"
|
||||||
|
|
||||||
|
tmp="$(mktemp)"
|
||||||
|
cleanup() { rm -f "$tmp"; }
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
dest_exists() {
|
||||||
|
docker buildx imagetools inspect "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
copy_retry() {
|
||||||
|
src="$1"
|
||||||
|
dest="$2"
|
||||||
|
n=0
|
||||||
|
while [ "$n" -lt "$RETRIES" ]; do
|
||||||
|
n=$((n + 1))
|
||||||
|
if docker buildx imagetools create --platform "$PLATFORM" --tag "$dest" "$src"; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
delay=$((n * 25))
|
||||||
|
echo "mirror retry ${n}/${RETRIES}, sleep ${delay}s: ${dest}"
|
||||||
|
sleep "$delay"
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
mirror() {
|
||||||
|
src="$1"
|
||||||
|
tag="$2"
|
||||||
|
var="$3"
|
||||||
|
dest="${CACHE_REPO}:${tag}"
|
||||||
|
if dest_exists "$dest"; then
|
||||||
|
echo "skip (already in registry): ${dest}"
|
||||||
|
printf '%s=%s\n' "$var" "$dest" >>"$tmp"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "mirror ${src} -> ${dest} (${PLATFORM})"
|
||||||
|
if copy_retry "$src" "$dest"; then
|
||||||
|
printf '%s=%s\n' "$var" "$dest" >>"$tmp"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "warn: ${dest} not mirrored — bake uses Docker Hub FROM for ${var}"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
mirror "docker.io/library/node:22-alpine" "base-node-22-alpine" BASE_NODE
|
||||||
|
|
||||||
|
echo "----- mirrored BASE_* -----"
|
||||||
|
cat "$tmp"
|
||||||
|
if [ -n "$ENV_FILE" ]; then
|
||||||
|
env_dir="$(dirname "$ENV_FILE")"
|
||||||
|
if [ -d "$env_dir" ]; then
|
||||||
|
cp "$tmp" "$ENV_FILE"
|
||||||
|
echo "wrote ${ENV_FILE}"
|
||||||
|
else
|
||||||
|
echo "warn: MIRROR_ENV_FILE dir missing (${env_dir}) — skip write"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
app:
|
app:
|
||||||
build: .
|
image: git.shx.one/denozord/cfdm:latest
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
+8
-1
@@ -45,10 +45,17 @@ health-check работают на двух уровнях:
|
|||||||
|
|
||||||
## Docker
|
## Docker
|
||||||
|
|
||||||
|
Один alpine-контейнер (API + SPA + SQLite). Образы `cfdm` и `cloudflare-domain-manager` — один манифест.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
docker pull git.shx.one/denozord/cfdm:latest
|
||||||
|
# drop-in для прежнего тега:
|
||||||
docker pull git.shx.one/denozord/cloudflare-domain-manager:latest
|
docker pull git.shx.one/denozord/cloudflare-domain-manager:latest
|
||||||
|
|
||||||
docker run -d -p 8080:8080 -v cfdm-data:/data \
|
docker run -d -p 8080:8080 -v cfdm-data:/data \
|
||||||
-e CLOUDFLARE_API_TOKEN=... \
|
-e CLOUDFLARE_API_TOKEN=... \
|
||||||
-e JWT_SECRET=... \
|
-e JWT_SECRET=... \
|
||||||
git.shx.one/denozord/cloudflare-domain-manager:latest
|
git.shx.one/denozord/cfdm:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Compose: корневой `docker-compose.yml` или `deploy/compose/docker-compose.example.yaml`. Сборка: `deploy/docker` (bake). Релизы: [`docs/releasing.md`](releasing.md).
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
# Релизы и версионирование Cloudflare Domain Manager
|
||||||
|
|
||||||
|
CFDM использует [Conventional Commits](https://www.conventionalcommits.org/) и [semantic-release](https://semantic-release.gitbook.io/) для полностью автоматических релизов на Gitea (`git.shx.one`). Ручное повышение версии в коде не требуется.
|
||||||
|
|
||||||
|
Заголовок после `тип(scope):` — **на русском** (см. `.cursor/rules/commit-messages-ru.mdc`). Analyzer смотрит только префикс `feat` / `fix` / …
|
||||||
|
|
||||||
|
## Как определяется версия
|
||||||
|
|
||||||
|
| Тип коммита | Bump |
|
||||||
|
|-------------|------|
|
||||||
|
| `feat` | minor (1.0.0 → 1.1.0) |
|
||||||
|
| `fix`, `perf`, `ci`, `refactor` | patch (1.5.1 → 1.5.2) |
|
||||||
|
| `feat!`, `fix!` или `BREAKING CHANGE:` в теле | major (1.0.0 → 2.0.0) |
|
||||||
|
| `docs`, `chore`, `test` | без релиза |
|
||||||
|
|
||||||
|
**Scope:** один идентификатор **без запятых** (`web`, `api`, `ci`). Заголовок `refactor(a, b): …` **не парсится** semantic-release → релиз не создаётся (commitlint на PR это тоже отклонит).
|
||||||
|
|
||||||
|
`refactor` — patch без новых функций: перестройка кода/UI при том же поведении для пользователя.
|
||||||
|
|
||||||
|
Первый релиз при отсутствии git-тегов — **1.0.0**, если есть releasable-коммиты.
|
||||||
|
|
||||||
|
## CI-пайплайн (push в main)
|
||||||
|
|
||||||
|
```text
|
||||||
|
push/merge в main
|
||||||
|
→ workflow CD: quality (web, api)
|
||||||
|
→ job publish:
|
||||||
|
→ semantic-release: git tag vX.Y.Z на текущий commit (без доп. commit)
|
||||||
|
→ Gitea Release + CHANGELOG.md как attachment
|
||||||
|
→ зеркало base-образов в cfdm-buildcache:base-*
|
||||||
|
→ docker buildx bake с VERSION=X.Y.Z (pull=false, named builder cfdm)
|
||||||
|
→ образы: latest, vX.Y.Z, X.Y.Z, sha-*, короткий SHA
|
||||||
|
→ job update-wiki (параллельно publish): docs/Home.md → wiki
|
||||||
|
```
|
||||||
|
|
||||||
|
Pull request: workflow **CI** — quality gates + commitlint; релиз и образы **не** публикуются.
|
||||||
|
|
||||||
|
Workflows: [.gitea/workflows/ci.yaml](../.gitea/workflows/ci.yaml), [.gitea/workflows/cd.yaml](../.gitea/workflows/cd.yaml), reusable [.gitea/workflows/quality.yaml](../.gitea/workflows/quality.yaml).
|
||||||
|
|
||||||
|
Конфиг semantic-release: [.releaserc.json](../.releaserc.json) — без `@semantic-release/git` (CHANGELOG не коммитится в репозиторий).
|
||||||
|
|
||||||
|
## Секреты Gitea
|
||||||
|
|
||||||
|
Один PAT — **`ACTIONS_PAT`** (Settings → Actions → Secrets).
|
||||||
|
|
||||||
|
| Право PAT | Зачем |
|
||||||
|
|-----------|--------|
|
||||||
|
| push tags | git tag `vX.Y.Z` на commit merge |
|
||||||
|
| releases | Gitea Release + notes |
|
||||||
|
| packages (Container Registry) | push образов |
|
||||||
|
|
||||||
|
Fallback для **git tag**: `gitea.token`, если PAT недоступен. Push образов в Container Registry — **только `ACTIONS_PAT`** (у job token Gitea нет права packages).
|
||||||
|
|
||||||
|
Wiki: секрет **`GITEA_TOKEN`** для clone/push `*.wiki.git`.
|
||||||
|
|
||||||
|
## Источник правды для версии
|
||||||
|
|
||||||
|
Semver из git-тега пробрасывается в образ как `APP_VERSION` / `GIT_SHA` / `BUILD_TIME` при bake (`deploy/docker/cfdm/Dockerfile`).
|
||||||
|
|
||||||
|
## Scope и semantic-release
|
||||||
|
|
||||||
|
Парсер conventional-commits **не понимает запятые в scope**. Правило: **один scope** (`web`, `api`, `ci`, `docker`).
|
||||||
|
|
||||||
|
На push в `main` job **publish** запускает `scripts/commit/verify-release-commits.mjs` — в логе будут предупреждения о непарсящихся коммитах.
|
||||||
|
|
||||||
|
Если релиз «не создался», а CI зелёный: смотрите лог release — часто `No releasable commits`. Исправление: новый коммит с корректным заголовком (например `fix(web): …`).
|
||||||
|
|
||||||
|
## Перезапуск упавшего job publish
|
||||||
|
|
||||||
|
semantic-release пишет `.release-version` только в `successCmd` при **новом** релизе. Если тег `vX.Y.Z` уже создан, а `docker buildx bake` упал, повторный run того же SHA делает semantic-release no-op (файла нет). Job **publish** тогда берёт версию из git-тега на `HEAD` и публикует образы.
|
||||||
|
|
||||||
|
Перезапускать нужно **весь job publish**, не отдельный шаг bake: checkout + semantic-release + detect + bake идут подряд.
|
||||||
|
|
||||||
|
## CHANGELOG
|
||||||
|
|
||||||
|
Release notes — в Gitea Release; файл `CHANGELOG.md` генерируется в CI и прикрепляется как asset, **не** попадает в git history.
|
||||||
|
|
||||||
|
## Проверка после релиза
|
||||||
|
|
||||||
|
1. Один run workflow **CD** на push в main: job **publish** зелёный.
|
||||||
|
2. Gitea: тег `vX.Y.Z` на том же commit, что и merge; Release с notes.
|
||||||
|
3. Container Registry: `cfdm:vX.Y.Z`, `cfdm:X.Y.Z`, `cfdm:latest` и алиас `cloudflare-domain-manager` с теми же тегами.
|
||||||
|
4. `curl http://localhost:8080/health` → `"status":"ok"`.
|
||||||
|
|
||||||
|
## Первый релиз (bootstrap)
|
||||||
|
|
||||||
|
Merge в `main` с `feat(release): …` → **v1.0.0** в том же CI run.
|
||||||
+10
-1
@@ -10,11 +10,20 @@
|
|||||||
"prepare": "husky"
|
"prepare": "husky"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@commitlint/cli": "^19.8.1",
|
||||||
|
"@commitlint/config-conventional": "^19.8.1",
|
||||||
|
"@markwylde/semantic-release-gitea": "^2.2.0",
|
||||||
|
"@semantic-release/changelog": "^6.0.3",
|
||||||
|
"@semantic-release/commit-analyzer": "^13.0.1",
|
||||||
|
"@semantic-release/exec": "^7.0.0",
|
||||||
|
"@semantic-release/release-notes-generator": "^14.0.3",
|
||||||
|
"conventional-commits-parser": "^6.4.0",
|
||||||
"husky": "^9.1.7",
|
"husky": "^9.1.7",
|
||||||
|
"semantic-release": "^25.0.2",
|
||||||
"turbo": "^2.5.4"
|
"turbo": "^2.5.4"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.12.1",
|
"packageManager": "pnpm@10.12.1",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": ["better-sqlite3", "esbuild"]
|
"onlyBuiltDependencies": ["better-sqlite3", "esbuild", "@node-rs/argon2"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./dist/index.d.ts",
|
"types": "./dist/index.d.ts",
|
||||||
|
"development": "./src/index.ts",
|
||||||
"import": "./dist/index.js"
|
"import": "./dist/index.js"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+2962
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
# Абсолютные пути для actions/cache: на act_runner «~» часто не раскрывается → cache miss.
|
||||||
|
set -eu
|
||||||
|
: "${GITHUB_ENV:?GITHUB_ENV required (Gitea/GitHub Actions)}"
|
||||||
|
|
||||||
|
HOME_DIR="${HOME:-}"
|
||||||
|
if [ -z "$HOME_DIR" ]; then
|
||||||
|
HOME_DIR="$(getent passwd "$(id -u)" 2>/dev/null | cut -d: -f6 || true)"
|
||||||
|
fi
|
||||||
|
HOME_DIR="${HOME_DIR:-/root}"
|
||||||
|
|
||||||
|
PNPM_STORE_DIR="${HOME_DIR}/.pnpm-store"
|
||||||
|
COREPACK_HOME="${HOME_DIR}/.cache/node/corepack"
|
||||||
|
|
||||||
|
mkdir -p "$PNPM_STORE_DIR" "$COREPACK_HOME"
|
||||||
|
|
||||||
|
{
|
||||||
|
echo "HOME_DIR=${HOME_DIR}"
|
||||||
|
echo "PNPM_STORE_DIR=${PNPM_STORE_DIR}"
|
||||||
|
echo "COREPACK_HOME=${COREPACK_HOME}"
|
||||||
|
} >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
echo "cache-env HOME_DIR=${HOME_DIR}"
|
||||||
|
echo "cache-env PNPM_STORE_DIR=${PNPM_STORE_DIR}"
|
||||||
|
echo "cache-env COREPACK_HOME=${COREPACK_HOME}"
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
# pnpm install из кэша Gitea (store + node_modules). Без повторного download при hit.
|
||||||
|
set -eu
|
||||||
|
: "${PNPM_STORE_DIR:?PNPM_STORE_DIR required — run scripts/ci/export-cache-env.sh first}"
|
||||||
|
|
||||||
|
export HUSKY=0
|
||||||
|
export COREPACK_HOME="${COREPACK_HOME:-${HOME:-/root}/.cache/node/corepack}"
|
||||||
|
mkdir -p "$PNPM_STORE_DIR" "$COREPACK_HOME"
|
||||||
|
corepack enable
|
||||||
|
pnpm config set store-dir "$PNPM_STORE_DIR"
|
||||||
|
|
||||||
|
echo "pnpm store: $(pnpm store path)"
|
||||||
|
echo "PNPM_CACHE_HIT=${PNPM_CACHE_HIT:-}"
|
||||||
|
|
||||||
|
if [ "${PNPM_CACHE_HIT:-}" = "true" ]; then
|
||||||
|
if pnpm install --frozen-lockfile --offline; then
|
||||||
|
echo "pnpm install --offline (cache hit)"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "offline install failed — prefer-offline"
|
||||||
|
fi
|
||||||
|
pnpm install --frozen-lockfile --prefer-offline
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* Warns about commits since the last tag that semantic-release cannot parse.
|
||||||
|
* Exit 0 always — semantic-release still decides release/no-op.
|
||||||
|
*/
|
||||||
|
import { execSync } from 'node:child_process';
|
||||||
|
import { CommitParser } from 'conventional-commits-parser';
|
||||||
|
|
||||||
|
const parser = new CommitParser();
|
||||||
|
|
||||||
|
const RELEASABLE = new Set(['feat', 'fix', 'perf', 'ci', 'refactor']);
|
||||||
|
|
||||||
|
function lastTag() {
|
||||||
|
try {
|
||||||
|
return execSync('git describe --tags --abbrev=0', { encoding: 'utf8' }).trim();
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function commitsSince(ref) {
|
||||||
|
const range = ref ? `${ref}..HEAD` : 'HEAD';
|
||||||
|
const out = execSync(`git log ${range} --format=%H%x09%s`, { encoding: 'utf8' }).trim();
|
||||||
|
if (!out) return [];
|
||||||
|
return out.split('\n').map((line) => {
|
||||||
|
const [hash, subject] = line.split('\t');
|
||||||
|
return { hash: hash.trim(), subject: subject.trim() };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const tag = lastTag();
|
||||||
|
const commits = commitsSince(tag);
|
||||||
|
const unparseable = [];
|
||||||
|
const releasable = [];
|
||||||
|
|
||||||
|
for (const { hash, subject } of commits) {
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = parser.parse(subject);
|
||||||
|
} catch {
|
||||||
|
unparseable.push({ hash: hash.slice(0, 7), subject });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!parsed?.type) {
|
||||||
|
unparseable.push({ hash: hash.slice(0, 7), subject });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (RELEASABLE.has(parsed.type)) {
|
||||||
|
releasable.push({ hash: hash.slice(0, 7), subject, type: parsed.type });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (commits.length === 0) {
|
||||||
|
console.log(`No new commits since ${tag || 'initial'}.`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (unparseable.length > 0) {
|
||||||
|
console.warn('::warning:: Commits not parseable by semantic-release (no version bump):');
|
||||||
|
for (const b of unparseable) {
|
||||||
|
console.warn(` ${b.hash} ${b.subject}`);
|
||||||
|
}
|
||||||
|
console.warn('Fix: single scope without commas, e.g. refactor(web): summary');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (releasable.length > 0) {
|
||||||
|
console.log(`Releasable since ${tag}: ${releasable.length} commit(s).`);
|
||||||
|
} else {
|
||||||
|
console.warn('::warning:: No releasable commits since last tag — release job will no-op.');
|
||||||
|
}
|
||||||
|
|
||||||
|
process.exit(0);
|
||||||
Reference in New Issue
Block a user