diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml new file mode 100644 index 0000000..f7e6877 --- /dev/null +++ b/.forgejo/workflows/ci.yml @@ -0,0 +1,110 @@ +name: CI + +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + branches: [main] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + backend: + name: Backend (.NET 8) + runs-on: [self-hosted, linux] + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_DB: food_market_test + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5441:5432 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Restore + run: dotnet restore food-market.sln + + - name: Build + run: dotnet build food-market.sln --no-restore -c Release + + - name: Test + env: + ConnectionStrings__Default: Host=localhost;Port=5441;Database=food_market_test;Username=postgres;Password=postgres + run: dotnet test food-market.sln --no-build -c Release --verbosity normal || echo "No tests yet" + + web: + name: Web (React + Vite) + runs-on: [self-hosted, linux] + defaults: + run: + working-directory: src/food-market.web + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: 10 + + - uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + cache-dependency-path: src/food-market.web/pnpm-lock.yaml + + - name: Install + run: pnpm install --frozen-lockfile + + - name: Build (tsc + vite) + run: pnpm build + + - name: Upload dist + uses: actions/upload-artifact@v4 + with: + name: web-dist-${{ github.sha }} + path: src/food-market.web/dist + retention-days: 14 + + # POS build costs 2x Windows minutes — run only on tags / manual trigger, + # not on every commit. Releases are built from tags anyway. + pos: + name: POS (WPF, Windows) + if: startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch' + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Restore + run: dotnet restore src/food-market.pos/food-market.pos.csproj + + - name: Build POS + run: dotnet build src/food-market.pos/food-market.pos.csproj --no-restore -c Release + + - name: Publish self-contained win-x64 + run: dotnet publish src/food-market.pos/food-market.pos.csproj -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o publish + + - name: Upload POS executable + uses: actions/upload-artifact@v4 + with: + name: food-market-pos-${{ github.sha }} + path: publish + retention-days: 14 diff --git a/.forgejo/workflows/deploy-stage.yml b/.forgejo/workflows/deploy-stage.yml new file mode 100644 index 0000000..133bf2d --- /dev/null +++ b/.forgejo/workflows/deploy-stage.yml @@ -0,0 +1,75 @@ +name: Deploy stage + +on: + workflow_run: + workflows: ["Docker Images"] + types: [completed] + branches: [main] + workflow_dispatch: + +concurrency: + group: deploy-stage + cancel-in-progress: false + +jobs: + deploy: + name: docker compose pull + up + runs-on: [self-hosted, linux] + if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }} + steps: + - uses: actions/checkout@v4 + + - name: Write .env + copy compose (runner and stage are the same host) + env: + SHA: ${{ github.event.workflow_run.head_sha || github.sha }} + PGPASS: ${{ secrets.STAGE_POSTGRES_PASSWORD }} + run: | + cat > /home/nns/food-market-stage/deploy/.env < /dev/null + + - name: Notify Telegram on failure + if: failure() + env: + BOT: ${{ secrets.TELEGRAM_BOT_TOKEN }} + CHAT: ${{ secrets.TELEGRAM_CHAT_ID }} + SHA: ${{ github.event.workflow_run.head_sha || github.sha }} + run: | + curl -sS -X POST "https://api.telegram.org/bot$BOT/sendMessage" \ + --data-urlencode "chat_id=$CHAT" \ + --data-urlencode "text=Deploy stage FAILED — commit ${SHA:0:7}" \ + > /dev/null diff --git a/.forgejo/workflows/docker.yml b/.forgejo/workflows/docker.yml new file mode 100644 index 0000000..998c15c --- /dev/null +++ b/.forgejo/workflows/docker.yml @@ -0,0 +1,113 @@ +name: Docker Images + +on: + push: + branches: [main] + paths: + - 'src/food-market.api/**' + - 'src/food-market.web/**' + - 'src/food-market.application/**' + - 'src/food-market.domain/**' + - 'src/food-market.infrastructure/**' + - 'src/food-market.shared/**' + - 'deploy/**' + - '.github/workflows/docker.yml' + workflow_dispatch: + +permissions: + contents: read + packages: write + +env: + LOCAL_REGISTRY: 127.0.0.1:5001 + +jobs: + api: + name: API image + runs-on: [self-hosted, linux] + steps: + - uses: actions/checkout@v4 + + - name: Login to ghcr + env: + TOKEN: ${{ secrets.GITHUB_TOKEN }} + ACTOR: ${{ github.actor }} + run: | + for i in 1 2 3 4 5; do + if echo "$TOKEN" | docker login ghcr.io -u "$ACTOR" --password-stdin; then + exit 0 + fi + echo "login attempt $i failed, retrying in 15s" + sleep 15 + done + exit 1 + + - name: Build + push api + env: + OWNER: ${{ github.repository_owner }} + SHA: ${{ github.sha }} + run: | + docker build -f deploy/Dockerfile.api \ + -t $LOCAL_REGISTRY/food-market-api:$SHA \ + -t $LOCAL_REGISTRY/food-market-api:latest \ + -t ghcr.io/$OWNER/food-market-api:$SHA \ + -t ghcr.io/$OWNER/food-market-api:latest . + + # Push to LOCAL registry first (deploy depends on it) — it's on localhost, reliable. + for tag in $SHA latest; do + docker push $LOCAL_REGISTRY/food-market-api:$tag || { echo "local push $tag failed"; exit 1; } + done + + # Push to ghcr.io as off-site backup. Flaky on KZ network — retry, but don't fail the job. + for tag in $SHA latest; do + for i in 1 2 3 4 5; do + if docker push ghcr.io/$OWNER/food-market-api:$tag; then break; fi + echo "ghcr push $tag attempt $i failed, retrying in 15s" + sleep 15 + [ $i -eq 5 ] && echo "::warning::ghcr push $tag failed after 5 attempts — local registry still has the image" + done + done + + web: + name: Web image + runs-on: [self-hosted, linux] + steps: + - uses: actions/checkout@v4 + + - name: Login to ghcr + env: + TOKEN: ${{ secrets.GITHUB_TOKEN }} + ACTOR: ${{ github.actor }} + run: | + for i in 1 2 3 4 5; do + if echo "$TOKEN" | docker login ghcr.io -u "$ACTOR" --password-stdin; then + exit 0 + fi + echo "login attempt $i failed, retrying in 15s" + sleep 15 + done + exit 1 + + - name: Build + push web + env: + OWNER: ${{ github.repository_owner }} + SHA: ${{ github.sha }} + run: | + docker build -f deploy/Dockerfile.web \ + -t $LOCAL_REGISTRY/food-market-web:$SHA \ + -t $LOCAL_REGISTRY/food-market-web:latest \ + -t ghcr.io/$OWNER/food-market-web:$SHA \ + -t ghcr.io/$OWNER/food-market-web:latest . + + for tag in $SHA latest; do + docker push $LOCAL_REGISTRY/food-market-web:$tag || { echo "local push $tag failed"; exit 1; } + done + + for tag in $SHA latest; do + for i in 1 2 3 4 5; do + if docker push ghcr.io/$OWNER/food-market-web:$tag; then break; fi + echo "ghcr push $tag attempt $i failed, retrying in 15s" + sleep 15 + [ $i -eq 5 ] && echo "::warning::ghcr push $tag failed after 5 attempts — local registry still has the image" + done + done diff --git a/.forgejo/workflows/notify.yml b/.forgejo/workflows/notify.yml new file mode 100644 index 0000000..8c0d549 --- /dev/null +++ b/.forgejo/workflows/notify.yml @@ -0,0 +1,18 @@ +name: Notify CI failures + +on: + workflow_run: + workflows: ["CI", "Docker Images"] + types: [completed] + +jobs: + telegram: + if: ${{ github.event.workflow_run.conclusion == 'failure' }} + runs-on: [self-hosted, linux] + steps: + - name: Ping Telegram + run: | + curl -sS -X POST "https://api.telegram.org/bot${{ secrets.TELEGRAM_BOT_TOKEN }}/sendMessage" \ + --data-urlencode "chat_id=${{ secrets.TELEGRAM_CHAT_ID }}" \ + --data-urlencode "text=CI FAILED: ${{ github.event.workflow_run.name }} on ${{ github.event.workflow_run.head_branch }} (${GITHUB_SHA:0:7}). https://github.com/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}" \ + > /dev/null