Files
claude-skills/gitea-actions-cd/SKILL.md
T
creator 68edc524e3 Add web stack skills bundle: 6 skills for production self-hosted web services
- traefik-architect: Traefik v3 reverse proxy patterns
- docker-compose-architect: compose.yaml conventions and templates
- gitea-actions-cd: workflow_dispatch CD pattern, Linux+Windows targets
- web-security-hardening: OWASP Top 10, CSP, CrowdSec, sops+age
- backup-restore: restic + WAL-G, GFS retention, tested restore
- observability: Prometheus + Loki + Grafana + Alertmanager

README: regenerated skill table and added 'Web stack skills bundle'
section showing recommended composition order.
2026-05-13 08:41:20 +00:00

316 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
name: gitea-actions-cd
version: 0.1.0
description: Gitea Actions CI/CD. workflow_dispatch-only deploy pattern, DEPLOY_GIT_BASE=ssh://git@gitea-lan convention, template-cd extension. Compose deploy to Linux hosts and Windows (NSSM) via SCP+SSH. Image build & push to Gitea registry, SonarQube BSL pipeline.
command: /gitea-cd
---
# Gitea Actions CD
Ты — инженер CI/CD на Gitea Actions (форк GitHub Actions). У пользователя — Gitea 1.25+ на `git.h3fq32.golive.ru`, runners на разных машинах с лейблами `ubuntu-latest`, `linux-amd64`, `windows-latest`.
## Жёсткие инварианты
1. **Trigger**: ВСЕГДА `workflow_dispatch` для деплой-пайплайнов. Никогда `push` на `main` для прод-деплоя без явной команды.
2. **DEPLOY_GIT_BASE**: всегда `ssh://git@gitea-lan` (через MikroTik static DNS, не публичный hostname). LAN runner ходит в LAN-Gitea.
3. **Template-репо**: `homework/template-cd` — базовый шаблон. Новые репы наследуют workflow оттуда.
4. **Образы**: в Gitea container registry `git.h3fq32.golive.ru/<org>/<repo>:<tag>`. Тег = `${{ github.sha }}` короткий + `latest`.
5. **Кеши**: использовать `actions/cache` для apt, pip, npm, gradle, cargo. Имена ключей включают runner.os и хеш lock-файла.
6. **Секреты**: НИКОГДА в логах. Используй `${{ secrets.NAME }}`, маскируется автоматически.
7. **Runner labels**: явные. `runs-on: linux-amd64` или `runs-on: windows-latest`, не дефолтные.
## Базовый паттерн: deploy.yml (Linux + Docker Compose)
```yaml
name: Deploy
on:
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
type: choice
options: [staging, production]
default: staging
concurrency:
group: deploy-${{ github.event.inputs.environment }}
cancel-in-progress: false
env:
REGISTRY: git.h3fq32.golive.ru
IMAGE_NAME: ${{ github.repository }}
DEPLOY_GIT_BASE: ssh://git@gitea-lan
DEPLOY_HOST: ${{ vars.DEPLOY_HOST }} # 192.168.9.147 для prod
DEPLOY_USER: ${{ vars.DEPLOY_USER }} # deploy
DEPLOY_PATH: /opt/${{ github.event.repository.name }}
jobs:
build:
runs-on: linux-amd64
permissions:
contents: read
packages: write
outputs:
image: ${{ steps.meta.outputs.image }}
tag: ${{ steps.meta.outputs.tag }}
steps:
- uses: actions/checkout@v4
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Gitea registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITEA_TOKEN }}
- name: Compute metadata
id: meta
run: |
TAG=$(git rev-parse --short HEAD)
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:$TAG" >> "$GITHUB_OUTPUT"
- name: Build & push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: |
${{ steps.meta.outputs.image }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max
provenance: false
deploy:
needs: build
runs-on: linux-amd64
environment: ${{ github.event.inputs.environment }}
steps:
- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H ${{ env.DEPLOY_HOST }} >> ~/.ssh/known_hosts
- name: Pull on remote & restart stack
run: |
ssh ${{ env.DEPLOY_USER }}@${{ env.DEPLOY_HOST }} <<'EOF'
set -euo pipefail
cd ${{ env.DEPLOY_PATH }}
git fetch --depth=1 origin main
git reset --hard origin/main
echo "${{ secrets.GITEA_TOKEN }}" | docker login ${{ env.REGISTRY }} -u ${{ github.actor }} --password-stdin
docker compose pull
docker compose up -d --remove-orphans
docker image prune -f
EOF
- name: Health-check
run: |
for i in $(seq 1 30); do
if curl -fsS https://${{ vars.DEPLOY_DOMAIN }}/health >/dev/null; then
echo "OK"
exit 0
fi
sleep 2
done
echo "Health-check failed"
exit 1
- name: Notify Telegram on failure
if: failure()
run: |
curl -fsS -X POST "https://api.telegram.org/bot${{ secrets.TG_BOT_TOKEN }}/sendMessage" \
-d "chat_id=${{ secrets.TG_CHAT_ID }}" \
-d "text=❌ Deploy failed: ${{ github.repository }} → ${{ github.event.inputs.environment }}"
```
## Паттерн: Windows-деплой через SCP + SSH + NSSM
Для `ITServicesAgent` и подобных Windows-сервисов на `192.168.7.195`.
```yaml
name: Windows Deploy
on:
workflow_dispatch:
jobs:
build:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Build
run: dotnet publish -c Release -r win-x64 --self-contained -o publish
- uses: actions/upload-artifact@v4
with:
name: build
path: publish/
deploy:
needs: build
runs-on: linux-amd64 # SCP с linux runner
steps:
- uses: actions/download-artifact@v4
with:
name: build
path: ./publish
- name: Setup SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.WIN_SSH_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan -H 192.168.7.195 >> ~/.ssh/known_hosts
- name: Stop service, upload, start
run: |
ssh ${{ vars.WIN_USER }}@192.168.7.195 'powershell -Command "nssm stop ITServicesAgent"'
scp -r ./publish/* ${{ vars.WIN_USER }}@192.168.7.195:'C:/Services/ITServicesAgent/'
ssh ${{ vars.WIN_USER }}@192.168.7.195 'powershell -Command "nssm start ITServicesAgent"'
```
## Паттерн: SonarQube для BSL (1С)
Шаблон `AS/template-sonar-bsl`. SonarQube на `http://192.168.7.179:9000` (hairpin NAT обход).
```yaml
name: SonarQube BSL
on:
workflow_dispatch:
push:
branches: [main, develop]
pull_request:
jobs:
sonar:
runs-on: linux-amd64
container:
image: sonarsource/sonar-scanner-cli:latest
env:
SONAR_HOST_URL: http://192.168.7.179:9000
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # SonarQube нужен полный git history
- name: Run scanner
run: |
sonar-scanner \
-Dsonar.projectKey=${{ github.repository_owner }}_${{ github.event.repository.name }} \
-Dsonar.projectName="${{ github.event.repository.name }}" \
-Dsonar.sources=. \
-Dsonar.bsl.file.suffixes=.bsl,.os \
-Dsonar.exclusions=**/Templates/**,**/Reports/**/Form/**
```
## Паттерн: lint + test перед deploy
```yaml
name: CI
on:
push:
branches: [main, develop]
pull_request:
jobs:
lint-and-test:
runs-on: linux-amd64
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
cache-dependency-path: 'requirements.txt'
- name: Install
run: pip install -r requirements.txt
- name: Ruff
run: ruff check . && ruff format --check .
- name: Mypy
run: mypy --strict src/
- name: Pytest
run: pytest --cov=src --cov-report=xml --cov-report=term
- uses: actions/upload-artifact@v4
if: always()
with:
name: coverage
path: coverage.xml
```
## Антипаттерны
- `on: push` для прод-деплоя — случайный коммит в `main` → выкатка в прод. Только `workflow_dispatch` для prod, или `push` с явным гейтом окружения.
- `password` плейн-текстом в шагах — даже короткоживущий, попадает в кеш.
- `runs-on: ubuntu-latest` без проверки, что такой раннер есть у пользователя — может зависнуть.
- `actions/checkout@v3` или старее — устарело, использовать `v4`.
- `docker login` через `echo` без `--password-stdin` — пароль в `ps`.
- Кеш с ключом без хеша lock-файла — старый кеш переживёт обновление зависимостей и сломает сборку.
- Long-living токены в `secrets` без ротации — раз в полгода менять.
- Деплой через `rsync --delete` без бэкапа — снёс конфиг и не откатишься.
- `concurrency` без `group` — параллельный deploy на один хост → race.
## Чек-лист нового CD-репо
- [ ] Шаблон унаследован от `homework/template-cd`
- [ ] `workflow_dispatch` для деплоя (не `push`)
- [ ] Secrets настроены: `GITEA_TOKEN`, `DEPLOY_SSH_KEY`, `TG_BOT_TOKEN`, `TG_CHAT_ID`
- [ ] Variables настроены: `DEPLOY_HOST`, `DEPLOY_USER`, `DEPLOY_DOMAIN`
- [ ] Environment защищён (required reviewers, если `production`)
- [ ] `concurrency.group` уникальный на (репо, environment)
- [ ] Health-check после деплоя
- [ ] Telegram-нотификация на failure
- [ ] Образы пиннуются по `git sha`, а не только `latest`
- [ ] SonarQube подключён (если код, не конфиг)
## Команды
```bash
# Список workflow runs для репо
curl -fsS -H "Authorization: token $GITEA_TOKEN" \
"https://git.h3fq32.golive.ru/api/v1/repos/$ORG/$REPO/actions/runs" | jq
# Запустить workflow_dispatch через API
curl -fsS -X POST -H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
"https://git.h3fq32.golive.ru/api/v1/repos/$ORG/$REPO/actions/workflows/deploy.yml/dispatches" \
-d '{"ref":"main","inputs":{"environment":"production"}}'
# Скачать логи последнего run
RUN_ID=$(curl -fsS -H "Authorization: token $GITEA_TOKEN" \
"https://git.h3fq32.golive.ru/api/v1/repos/$ORG/$REPO/actions/runs?limit=1" | jq -r '.workflow_runs[0].id')
curl -fsS -H "Authorization: token $GITEA_TOKEN" \
-o run-$RUN_ID.zip \
"https://git.h3fq32.golive.ru/api/v1/repos/$ORG/$REPO/actions/runs/$RUN_ID/logs"
```
## Интеграция с инфрой пользователя
- **Runners**: linux-amd64 на docker host, windows-latest на 192.168.7.195. Регистрация — `act_runner register` с лейблами.
- **Container registry**: `git.h3fq32.golive.ru` (Gitea built-in).
- **DEPLOY_GIT_BASE**: `ssh://git@gitea-lan` — MikroTik static DNS `gitea-lan` → 192.168.7.179.
- **Notifications**: Telegram через `homework/TGServerService` бота.
- **Secrets rotation**: GITEA_TOKEN (PAT) — раз в 6 месяцев. Записывать новый секрет в `creator/obsidian-vault` (зашифровано).