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
|
||||
.gitea
|
||||
.github
|
||||
.cursor
|
||||
data
|
||||
**/target
|
||||
.claude
|
||||
.codegraph
|
||||
.agents
|
||||
.opencode
|
||||
memory-bank
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
**/.DS_Store
|
||||
**/Thumbs.db
|
||||
**/.env
|
||||
**/.env.*
|
||||
!**/.env.example
|
||||
!**/.env.*.example
|
||||
|
||||
node_modules
|
||||
**/node_modules
|
||||
**/.turbo
|
||||
**/dist
|
||||
**/*.md
|
||||
**/.git*
|
||||
frontend
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
.env
|
||||
.env.*
|
||||
**/.turbo
|
||||
apps/web/src/routeTree.gen.ts
|
||||
apps/web/playwright-report
|
||||
apps/web/test-results
|
||||
|
||||
*.md
|
||||
AGENTS.md
|
||||
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
|
||||
version.txt
|
||||
.release-version
|
||||
CHANGELOG.md
|
||||
.codegraph/daemon.pid
|
||||
|
||||
# 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/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
|
||||
|
||||
- Target `develop` for features; `main` for release/hotfix
|
||||
- CI `test` job must pass
|
||||
- CI quality jobs (`web`, `api`) must pass
|
||||
- At least one approval
|
||||
- Rebase/merge when up to date with target branch
|
||||
- Delete branch after merge
|
||||
|
||||
## Versioning
|
||||
|
||||
- SemVer in [`VERSION`](VERSION)
|
||||
- Bump version in `release/*` or `hotfix/*` branches
|
||||
- Tag `vX.Y.Z` on `main` after release merge
|
||||
- SemVer через [semantic-release](https://semantic-release.gitbook.io/) на push в `main`
|
||||
- Не править версию вручную; тег `vX.Y.Z` ставит CI
|
||||
- Подробнее: [`docs/releasing.md`](docs/releasing.md)
|
||||
|
||||
## Branch protection (Gitea)
|
||||
|
||||
Configure for **main** and **develop**:
|
||||
|
||||
- Require pull request reviews
|
||||
- Require status checks (`test`)
|
||||
- Require status checks (`CI` / quality jobs `web`, `api`)
|
||||
- Require branches to be up to date
|
||||
- No force push, no deletion
|
||||
|
||||
@@ -54,7 +54,4 @@ pnpm --filter web dev
|
||||
|
||||
## Release process
|
||||
|
||||
1. `git checkout -b release/v0.2.0 develop`
|
||||
2. Bump `VERSION`, fix release issues
|
||||
3. PR to `main` → merge → tag `v0.2.0`
|
||||
4. Merge release branch back to `develop`
|
||||
Релизы делает workflow **CD** на push в `main` (semantic-release + bake). Не создавать ветки `release/*` и не bump'ать файлы версии вручную. См. [`docs/releasing.md`](docs/releasing.md).
|
||||
|
||||
-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`)
|
||||
- **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
|
||||
- **CI:** Gitea Actions (Gitflow)
|
||||
- **CI:** Gitea Actions ([docs/releasing.md](docs/releasing.md)) — quality gates + semantic-release + bake (`cfdm` / `cloudflare-domain-manager`)
|
||||
|
||||
## Development
|
||||
|
||||
@@ -53,7 +53,7 @@ shadcn CLI: `cd apps/web && pnpm dlx shadcn@latest add <component>`
|
||||
|
||||
## 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
|
||||
|
||||
|
||||
@@ -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:
|
||||
app:
|
||||
build: .
|
||||
image: git.shx.one/denozord/cfdm:latest
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
|
||||
+8
-1
@@ -45,10 +45,17 @@ health-check работают на двух уровнях:
|
||||
|
||||
## Docker
|
||||
|
||||
Один alpine-контейнер (API + SPA + SQLite). Образы `cfdm` и `cloudflare-domain-manager` — один манифест.
|
||||
|
||||
```bash
|
||||
docker pull git.shx.one/denozord/cfdm:latest
|
||||
# drop-in для прежнего тега:
|
||||
docker pull git.shx.one/denozord/cloudflare-domain-manager:latest
|
||||
|
||||
docker run -d -p 8080:8080 -v cfdm-data:/data \
|
||||
-e CLOUDFLARE_API_TOKEN=... \
|
||||
-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"
|
||||
},
|
||||
"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",
|
||||
"semantic-release": "^25.0.2",
|
||||
"turbo": "^2.5.4"
|
||||
},
|
||||
"packageManager": "pnpm@10.12.1",
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": ["better-sqlite3", "esbuild"]
|
||||
"onlyBuiltDependencies": ["better-sqlite3", "esbuild", "@node-rs/argon2"]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"development": "./src/index.ts",
|
||||
"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