name: Docker images on: push: branches: - main workflow_dispatch: env: REGISTRY: git.shx.one GITEA_SERVER_URL: https://git.shx.one jobs: prepare-release: runs-on: ubuntu-latest outputs: should_release: ${{ steps.release.outputs.should_release }} version: ${{ steps.release.outputs.version }} tag: ${{ steps.release.outputs.tag }} bump: ${{ steps.release.outputs.bump }} release_url: ${{ steps.release.outputs.release_url }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Test release version calculation run: node --test .ci/scripts/compute-release.test.mjs - name: Compute release metadata id: release shell: bash env: GITEA_REPOSITORY: ${{ gitea.repository }} GITEA_SERVER_URL: ${{ env.GITEA_SERVER_URL }} BASE_VERSION: "1.0.0" RELEASE_OUTPUT_DIR: .ci/release run: | set -euo pipefail node .ci/scripts/compute-release.mjs test -f .ci/release/release-manifest.json backend-test: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Node 22 uses: actions/setup-node@v4 with: node-version: 22 - name: Start PostgreSQL 18 shell: bash run: | set -euo pipefail NAME="mm-pg-${{ gitea.run_id }}-${{ gitea.run_attempt }}" docker rm -f "$NAME" 2>/dev/null || true # Docker-backed Gitea runners execute this step in a separate container. # Join its network; a published host loopback port is not reachable here. NETWORKS=$(docker inspect --format '{{range $name, $network := .NetworkSettings.Networks}}{{println $name}}{{end}}' "$(cat /etc/hostname)" 2>/dev/null || true) if [ -n "$NETWORKS" ]; then test "$(printf '%s\n' "$NETWORKS" | wc -l)" -eq 1 NETWORK_ARGS=(--network "$NETWORKS" --network-alias "$NAME") PG_HOST="$NAME" PG_PORT=5432 else # Host-based runners can use a Docker-assigned ephemeral host port. NETWORK_ARGS=(-p 127.0.0.1::5432) PG_HOST=127.0.0.1 fi docker run -d --name "$NAME" --rm \ -e POSTGRES_USER=mmapp \ -e POSTGRES_PASSWORD=mmapp \ -e POSTGRES_DB=mmapp \ "${NETWORK_ARGS[@]}" \ postgres:18-alpine echo "PG_CONTAINER=$NAME" >> "${GITHUB_ENV}" if [ "$PG_HOST" = 127.0.0.1 ]; then PG_PORT=$(docker port "$NAME" 5432/tcp | awk -F: '{print $NF}') fi echo "DATABASE_URL=postgres://mmapp:mmapp@${PG_HOST}:${PG_PORT}/mmapp" >> "${GITHUB_ENV}" for i in $(seq 1 40); do if docker exec "$NAME" pg_isready -U mmapp -d mmapp; then exit 0 fi sleep 1 done echo "PostgreSQL не поднялся" docker logs "$NAME" || true exit 1 - name: Install and test backend shell: bash env: REQUIRE_TEST_POSTGRES: "1" run: | set -euo pipefail npm ci node --input-type=module <<'NODE' import pg from 'pg'; const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL, connectionTimeoutMillis: 8000 }); try { await pool.query('SELECT 1'); console.log('PostgreSQL reachable from the test process'); } finally { await pool.end(); } NODE npm run test --prefix backend - name: Stop PostgreSQL if: always() shell: bash run: docker rm -f "${PG_CONTAINER:-}" 2>/dev/null || true backend-image: needs: - prepare-release - backend-test runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Configure backend image name shell: bash run: | set -euo pipefail owner=$(echo "${{ gitea.repository }}" | cut -d/ -f1 | tr '[:upper:]' '[:lower:]') name=$(echo "${{ gitea.repository }}" | cut -d/ -f2 | tr '[:upper:]' '[:lower:]') stem=$(printf '%s' "$name" | sed -E 's/-[0-9]+$//') echo "IMAGE_NAME=${{ env.REGISTRY }}/${owner}/${stem}-backend" >> "${GITHUB_ENV}" - name: Prepare backend build context shell: bash run: | set -euo pipefail STAGING=".ci/docker/backend" rm -rf "$STAGING" mkdir -p "$STAGING/packages/contracts" "$STAGING/backend" cp package-lock.json "$STAGING/" node <<'NODE' const fs = require("node:fs") const pkg = JSON.parse(fs.readFileSync("package.json", "utf8")) pkg.workspaces = ["packages/*", "backend"] pkg.dependencies = {} pkg.devDependencies = {} delete pkg.scripts fs.writeFileSync(".ci/docker/backend/package.json", `${JSON.stringify(pkg, null, 2)}\n`) NODE cp packages/contracts/package.json packages/contracts/tsconfig.json "$STAGING/packages/contracts/" cp -R packages/contracts/src "$STAGING/packages/contracts/" cp backend/package.json backend/tsconfig.json "$STAGING/backend/" cp -R backend/src "$STAGING/backend/" if [ -d backend/drizzle ]; then cp -R backend/drizzle "$STAGING/backend/" fi - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to Gitea Registry uses: docker/login-action@v3 with: registry: git.shx.one username: ${{ gitea.actor }} password: ${{ secrets.ACTIONS_PAT }} - name: Build and push backend image uses: docker/build-push-action@v5 with: context: .ci/docker/backend file: backend/Dockerfile platforms: linux/amd64 push: true build-args: | APP_VERSION=${{ needs.prepare-release.outputs.version }} tags: | ${{ env.IMAGE_NAME }}:latest ${{ env.IMAGE_NAME }}:${{ gitea.sha }} ${{ env.IMAGE_NAME }}:${{ needs.prepare-release.outputs.version }} cache-from: type=gha,scope=backend cache-to: type=gha,mode=max,scope=backend labels: | org.opencontainers.image.title=mmapp-backend org.opencontainers.image.description=MikroTik Manager backend (Fastify) org.opencontainers.image.version=${{ needs.prepare-release.outputs.version }} org.opencontainers.image.revision=${{ gitea.sha }} org.opencontainers.image.created=${{ gitea.event.head_commit.timestamp }} frontend-image: needs: prepare-release runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Compute release metadata shell: bash env: GITEA_REPOSITORY: ${{ gitea.repository }} GITEA_SERVER_URL: ${{ env.GITEA_SERVER_URL }} BASE_VERSION: "1.0.0" RELEASE_OUTPUT_DIR: .ci/release run: | set -euo pipefail node .ci/scripts/compute-release.mjs test -f .ci/release/release-manifest.json - name: Configure frontend image name shell: bash run: | set -euo pipefail owner=$(echo "${{ gitea.repository }}" | cut -d/ -f1 | tr '[:upper:]' '[:lower:]') name=$(echo "${{ gitea.repository }}" | cut -d/ -f2 | tr '[:upper:]' '[:lower:]') stem=$(printf '%s' "$name" | sed -E 's/-[0-9]+$//') echo "IMAGE_NAME=${{ env.REGISTRY }}/${owner}/${stem}-frontend" >> "${GITHUB_ENV}" - name: Prepare frontend build context shell: bash run: | set -euo pipefail STAGING=".ci/docker/frontend" rm -rf "$STAGING" mkdir -p "$STAGING/packages/contracts" "$STAGING/public" cp package-lock.json next.config.ts tsconfig.json postcss.config.mjs eslint.config.mjs components.json "$STAGING/" node <<'NODE' const fs = require("node:fs") const pkg = JSON.parse(fs.readFileSync("package.json", "utf8")) pkg.workspaces = ["packages/*"] fs.writeFileSync(".ci/docker/frontend/package.json", `${JSON.stringify(pkg, null, 2)}\n`) NODE cp packages/contracts/package.json packages/contracts/tsconfig.json "$STAGING/packages/contracts/" cp -R packages/contracts/src "$STAGING/packages/contracts/" for dir in app components lib shared entities public hooks; do if [ -d "$dir" ]; then cp -R "$dir" "$STAGING/" fi done cp .ci/release/release-manifest.json "$STAGING/public/release-manifest.json" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to Gitea Registry uses: docker/login-action@v3 with: registry: git.shx.one username: ${{ gitea.actor }} password: ${{ secrets.ACTIONS_PAT }} - name: Build and push frontend image uses: docker/build-push-action@v5 with: context: .ci/docker/frontend file: Dockerfile.frontend platforms: linux/amd64 push: true build-args: | BACKEND_INTERNAL_URL=http://backend:8000 NEXT_PUBLIC_BACKEND_URL=same-origin NEXT_PUBLIC_DEFAULT_DATA_SOURCE=live NEXT_PUBLIC_ALLOW_MOCK_DATA=false NEXT_PUBLIC_AUTH_PORTAL_URL=https://auth.shnt.top NEXT_PUBLIC_APP_VERSION=${{ needs.prepare-release.outputs.version }} NEXT_PUBLIC_RELEASE_URL=${{ needs.prepare-release.outputs.release_url }} tags: | ${{ env.IMAGE_NAME }}:latest ${{ env.IMAGE_NAME }}:${{ gitea.sha }} ${{ env.IMAGE_NAME }}:${{ needs.prepare-release.outputs.version }} cache-from: type=gha,scope=frontend cache-to: type=gha,mode=max,scope=frontend labels: | org.opencontainers.image.title=mmapp-frontend org.opencontainers.image.description=MikroTik Manager frontend (Next.js) org.opencontainers.image.version=${{ needs.prepare-release.outputs.version }} org.opencontainers.image.revision=${{ gitea.sha }} org.opencontainers.image.created=${{ gitea.event.head_commit.timestamp }} updater-image: needs: prepare-release runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Configure updater image name shell: bash run: | set -euo pipefail owner=$(echo "${{ gitea.repository }}" | cut -d/ -f1 | tr '[:upper:]' '[:lower:]') name=$(echo "${{ gitea.repository }}" | cut -d/ -f2 | tr '[:upper:]' '[:lower:]') stem=$(printf '%s' "$name" | sed -E 's/-[0-9]+$//') echo "IMAGE_NAME=${{ env.REGISTRY }}/${owner}/${stem}-updater" >> "${GITHUB_ENV}" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to Gitea Registry uses: docker/login-action@v3 with: registry: git.shx.one username: ${{ gitea.actor }} password: ${{ secrets.ACTIONS_PAT }} - name: Build and push updater image uses: docker/build-push-action@v5 with: context: deploy/updater file: deploy/updater/Dockerfile platforms: linux/amd64 push: true tags: | ${{ env.IMAGE_NAME }}:latest ${{ env.IMAGE_NAME }}:${{ gitea.sha }} ${{ env.IMAGE_NAME }}:${{ needs.prepare-release.outputs.version }} cache-from: type=gha,scope=updater cache-to: type=gha,mode=max,scope=updater labels: | org.opencontainers.image.title=mmapp-updater org.opencontainers.image.description=MikroTik Manager autonomous Docker updater org.opencontainers.image.version=${{ needs.prepare-release.outputs.version }} org.opencontainers.image.revision=${{ gitea.sha }} org.opencontainers.image.created=${{ gitea.event.head_commit.timestamp }} publish-release: needs: - prepare-release - backend-image - frontend-image - updater-image if: needs.prepare-release.outputs.should_release == 'true' runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Compute release metadata shell: bash env: GITEA_REPOSITORY: ${{ gitea.repository }} GITEA_SERVER_URL: ${{ env.GITEA_SERVER_URL }} BASE_VERSION: "1.0.0" RELEASE_OUTPUT_DIR: .ci/release run: | set -euo pipefail node .ci/scripts/compute-release.mjs test -f .ci/release/release_notes.md - name: Configure git author shell: bash run: | set -euo pipefail git config user.name "gitea-actions[bot]" git config user.email "gitea-actions[bot]@users.noreply.gitea" - name: Create and push release tag shell: bash env: TAG: ${{ needs.prepare-release.outputs.tag }} run: | set -euo pipefail if git rev-parse "$TAG" >/dev/null 2>&1; then echo "Tag $TAG already exists, skipping tag push" exit 0 fi git tag -a "$TAG" -m "Release $TAG" git push origin "$TAG" - name: Publish Gitea release shell: bash env: TAG: ${{ needs.prepare-release.outputs.tag }} VERSION: ${{ needs.prepare-release.outputs.version }} REPOSITORY: ${{ gitea.repository }} TOKEN: ${{ secrets.ACTIONS_PAT }} run: | set -euo pipefail owner="${REPOSITORY%%/*}" repo="${REPOSITORY#*/}" payload=$(node <<'NODE' const fs = require("node:fs") const body = fs.readFileSync(".ci/release/release_notes.md", "utf8") const payload = { tag_name: process.env.TAG, target_commitish: "main", name: process.env.VERSION, body, draft: false, prerelease: false, } process.stdout.write(JSON.stringify(payload)) NODE ) status=$(curl --silent --show-error --output /tmp/gitea-release.json --write-out "%{http_code}" \ --request POST \ --header "Authorization: token ${TOKEN}" \ --header "Content-Type: application/json" \ --data "$payload" \ "${{ env.GITEA_SERVER_URL }}/api/v1/repos/${owner}/${repo}/releases") if [ "$status" = "409" ] || [ "$status" = "422" ]; then echo "Release for ${TAG} already exists (${status}), skipping" exit 0 fi if [ "$status" -lt 200 ] || [ "$status" -ge 300 ]; then cat /tmp/gitea-release.json exit 1 fi notify-webhook: needs: - prepare-release - backend-image - frontend-image runs-on: ubuntu-latest env: WEBHOOK_URL: ${{ secrets.DEPLOY_WEBHOOK_URL }} steps: - name: Notify deploy webhook if: env.WEBHOOK_URL != '' shell: bash run: | set -euo pipefail curl --fail --silent --show-error --request POST \ --header "Content-Type: application/json" \ --data "{\"repository\":\"${{ gitea.repository }}\",\"sha\":\"${{ gitea.sha }}\",\"ref\":\"${{ gitea.ref }}\",\"version\":\"${{ needs.prepare-release.outputs.version }}\",\"tag\":\"${{ needs.prepare-release.outputs.tag }}\",\"releaseUrl\":\"${{ needs.prepare-release.outputs.release_url }}\"}" \ "$WEBHOOK_URL"