Fix CI #1661
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build master | |
| on: | |
| push: | |
| branches: | |
| - master | |
| workflow_dispatch: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| changes: | |
| if: github.repository == 'hexlet-codebattle/codebattle' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| codebattle_image: ${{ steps.filter.outputs.codebattle_image }} | |
| runner_image: ${{ steps.filter.outputs.runner_image }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| - name: Detect image-related changes | |
| id: filter | |
| uses: dorny/paths-filter@v3 | |
| with: | |
| filters: | | |
| codebattle_image: | |
| - 'Containerfile.codebattle' | |
| - 'nginx.conf' | |
| - 'nginx-assets-entrypoint.sh' | |
| - 'mix.exs' | |
| - 'mix.lock' | |
| - 'apps/codebattle/**' | |
| - 'apps/runner/**' | |
| - 'apps/phoenix_gon/**' | |
| - 'config/**' | |
| runner_image: | |
| - 'Containerfile.runner' | |
| - 'mix.exs' | |
| - 'mix.lock' | |
| - 'apps/runner/**' | |
| backend_tests: | |
| if: github.repository == 'hexlet-codebattle/codebattle' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| env: | |
| MIX_ENV: test | |
| POSTGRES_PASSWORD: postgres | |
| OTP_VERSION: "28.2" | |
| ELIXIR_VERSION: "1.19.4" | |
| services: | |
| db: | |
| image: postgres:16-alpine | |
| ports: ["5432:5432"] | |
| env: | |
| POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }} | |
| POSTGRES_HOST_AUTH_METHOD: trust | |
| options: >- | |
| --health-cmd pg_isready | |
| --health-interval 10s | |
| --health-timeout 5s | |
| --health-retries 5 | |
| --name=pg_ci | |
| --mount type=tmpfs,destination=/var/lib/postgresql/data | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| - name: Setup Elixir | |
| uses: erlef/setup-beam@v1 | |
| with: | |
| otp-version: ${{ env.OTP_VERSION }} | |
| elixir-version: ${{ env.ELIXIR_VERSION }} | |
| - name: Cache deps | |
| uses: actions/cache@v4 | |
| with: | |
| path: ./deps | |
| key: ${{ runner.os }}-deps-${{ env.OTP_VERSION }}-${{ env.ELIXIR_VERSION }}-${{ hashFiles('mix.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-deps-${{ env.OTP_VERSION }}-${{ env.ELIXIR_VERSION }}- | |
| - name: Cache build artifacts | |
| uses: actions/cache@v4 | |
| with: | |
| path: ./_build | |
| key: ${{ runner.os }}-build-${{ env.MIX_ENV }}-${{ env.OTP_VERSION }}-${{ env.ELIXIR_VERSION }}-${{ hashFiles('mix.lock', 'mix.exs', 'apps/**/mix.exs', 'config/**/*.exs') }} | |
| restore-keys: | | |
| ${{ runner.os }}-build-${{ env.MIX_ENV }}-${{ env.OTP_VERSION }}-${{ env.ELIXIR_VERSION }}- | |
| - name: Get deps | |
| run: | | |
| mix local.hex --force | |
| mix local.rebar --force | |
| mix deps.get | |
| working-directory: . | |
| - name: Mix deps.compile | |
| run: mix compile --warnings-as-errors | |
| working-directory: . | |
| - name: Setup db | |
| run: mix ecto.create && mix ecto.migrate | |
| working-directory: . | |
| - name: Mix tests | |
| run: make test | |
| - name: Upload coverage to Codecov | |
| uses: codecov/codecov-action@v3 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| file: ./apps/codebattle/cover/excoveralls.json | |
| fail_ci_if_error: false | |
| elixir_quality: | |
| if: github.repository == 'hexlet-codebattle/codebattle' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| env: | |
| MIX_ENV: test | |
| OTP_VERSION: "28.2" | |
| ELIXIR_VERSION: "1.19.4" | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| - name: Setup Elixir | |
| uses: erlef/setup-beam@v1 | |
| with: | |
| otp-version: ${{ env.OTP_VERSION }} | |
| elixir-version: ${{ env.ELIXIR_VERSION }} | |
| - name: Cache deps | |
| uses: actions/cache@v4 | |
| with: | |
| path: ./deps | |
| key: ${{ runner.os }}-deps-${{ env.OTP_VERSION }}-${{ env.ELIXIR_VERSION }}-${{ hashFiles('mix.lock') }} | |
| restore-keys: | | |
| ${{ runner.os }}-deps-${{ env.OTP_VERSION }}-${{ env.ELIXIR_VERSION }}- | |
| - name: Cache build artifacts | |
| uses: actions/cache@v4 | |
| with: | |
| path: ./_build | |
| key: ${{ runner.os }}-build-${{ env.MIX_ENV }}-${{ env.OTP_VERSION }}-${{ env.ELIXIR_VERSION }}-${{ hashFiles('mix.lock', 'mix.exs', 'apps/**/mix.exs', 'config/**/*.exs') }} | |
| restore-keys: | | |
| ${{ runner.os }}-build-${{ env.MIX_ENV }}-${{ env.OTP_VERSION }}-${{ env.ELIXIR_VERSION }}- | |
| - name: Ensure Dialyzer PLT directory | |
| run: mkdir -p priv/plts | |
| - name: Cache Dialyzer PLT | |
| uses: actions/cache@v4 | |
| with: | |
| path: priv/plts | |
| key: ${{ runner.os }}-dialyzer-plt-${{ env.OTP_VERSION }}-${{ env.ELIXIR_VERSION }}-${{ hashFiles('mix.lock', 'mix.exs', 'apps/**/mix.exs') }} | |
| restore-keys: | | |
| ${{ runner.os }}-dialyzer-plt-${{ env.OTP_VERSION }}-${{ env.ELIXIR_VERSION }}- | |
| - name: Get deps | |
| run: | | |
| mix local.hex --force | |
| mix local.rebar --force | |
| mix deps.get | |
| working-directory: . | |
| - name: Mix deps.compile | |
| run: mix compile --warnings-as-errors | |
| working-directory: . | |
| - name: Mix format | |
| run: mix format --check-formatted | |
| working-directory: . | |
| - name: Mix credo | |
| run: mix credo | |
| working-directory: . | |
| - name: Mix audit | |
| run: mix hex.audit | |
| working-directory: . | |
| - name: Mix dialyzer | |
| run: make dialyzer | |
| frontend_quality: | |
| if: github.repository == 'hexlet-codebattle/codebattle' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 9 | |
| run_install: false | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: "24" | |
| cache: "pnpm" | |
| cache-dependency-path: ./apps/codebattle/pnpm-lock.yaml | |
| - name: Install pnpm dependencies | |
| run: pnpm install --frozen-lockfile | |
| working-directory: ./apps/codebattle | |
| - name: Frontend lint (JS + SCSS) | |
| run: pnpm run lint | |
| working-directory: ./apps/codebattle | |
| - name: Frontend format check (JS + SCSS) | |
| run: pnpm run format:check | |
| working-directory: ./apps/codebattle | |
| - name: Run jest | |
| run: pnpm run test | |
| working-directory: ./apps/codebattle | |
| build_images: | |
| needs: [changes] | |
| if: | | |
| github.repository == 'hexlet-codebattle/codebattle' && | |
| (needs.changes.outputs.codebattle_image == 'true' || needs.changes.outputs.runner_image == 'true') | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 70 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 1 | |
| - name: Login to Github Container Registry | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: vtm9 | |
| password: ${{ secrets.GH_REGISTRY_TOKEN }} | |
| - name: Setup Docker Buildx | |
| uses: docker/setup-buildx-action@v3 | |
| - name: Build codebattle image layers | |
| if: needs.changes.outputs.codebattle_image == 'true' | |
| run: | | |
| make BUILDX_OUTPUT=--load GIT_HASH=${{ github.sha }} build-codebattle | |
| - name: Build runner image layers | |
| if: needs.changes.outputs.runner_image == 'true' | |
| run: | | |
| make BUILDX_OUTPUT=--load GIT_HASH=${{ github.sha }} build-runner | |
| - name: Wait for required checks | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const required = ["backend_tests", "elixir_quality", "frontend_quality"]; | |
| const sleep = ms => new Promise(resolve => setTimeout(resolve, ms)); | |
| const deadline = Date.now() + 45 * 60 * 1000; | |
| while (true) { | |
| const jobs = await github.paginate( | |
| github.rest.actions.listJobsForWorkflowRun, | |
| { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| run_id: context.runId, | |
| per_page: 100, | |
| }, | |
| response => response.data.jobs, | |
| ); | |
| const safeRuns = (jobs || []).filter(run => run && typeof run.name === "string"); | |
| const byName = new Map(); | |
| for (const run of safeRuns) { | |
| const existing = byName.get(run.name); | |
| if (!existing || (run.id ?? 0) > (existing.id ?? 0)) { | |
| byName.set(run.name, run); | |
| } | |
| } | |
| const pending = []; | |
| const failed = []; | |
| for (const name of required) { | |
| const run = byName.get(name); | |
| if (!run || run.status !== "completed") { | |
| pending.push(name); | |
| continue; | |
| } | |
| if (run.conclusion !== "success") { | |
| failed.push(`${name}:${run.conclusion}`); | |
| } | |
| } | |
| core.info( | |
| `jobs_total=${(jobs || []).length}, jobs_named=${safeRuns.length}, pending=[${pending.join(", ")}], failed=[${failed.join(", ")}]`, | |
| ); | |
| if (failed.length > 0) { | |
| core.setFailed(`Required checks failed: ${failed.join(", ")}`); | |
| return; | |
| } | |
| if (pending.length === 0) { | |
| core.info("All required checks passed. Continue to push."); | |
| return; | |
| } | |
| if (Date.now() > deadline) { | |
| core.setFailed(`Timed out waiting for required checks: ${pending.join(", ")}`); | |
| return; | |
| } | |
| await sleep(20_000); | |
| } | |
| - name: Login to Github Container Registry (before push) | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: vtm9 | |
| password: ${{ secrets.GH_REGISTRY_TOKEN }} | |
| - name: Push codebattle image set | |
| if: needs.changes.outputs.codebattle_image == 'true' | |
| run: | | |
| make push-codebattle | |
| - name: Push runner image set | |
| if: needs.changes.outputs.runner_image == 'true' | |
| run: | | |
| make push-runner |