Files
source-gamepanel/.github/workflows/ci.yml
T
hibna d50a7fd049
CI / Docker Build (push) Has been skipped
CI / Lint & Type Check (push) Successful in 4m4s
CI / Daemon Build & Test (push) Successful in 6m19s
CI / Publish images (push) Failing after 2m15s
Install the docker CLI in jobs that build images
The runner executes jobs inside a container that ships no docker client,
so the publish job died on its first command:

  /var/run/act/workflow/1: line 3: docker: command not found

act_runner does mount the host's socket into job containers, so only the
client is missing. Both image jobs now fetch the static binary when it is
absent and then check that the socket answers, because "no client" and
"no daemon" are different problems and the log should say which one it
hit. The docker build-test job needed the same treatment — it is skipped
on tags, so it had never reached that command either.

Verified in a container without a docker client: with the socket mounted
the step installs the client and builds the web image; without it the
step fails with the runner-configuration message instead of a confusing
connection error.
2026-08-02 23:13:55 +03:00

201 lines
7.0 KiB
YAML

name: CI
on:
push:
branches: [main, develop]
tags: ["v*"]
pull_request:
branches: [main]
env:
NODE_VERSION: "20"
PNPM_VERSION: "9.15.4"
# 1.85 is the floor: dependencies now ship edition 2024, which older
# Cargo refuses to even parse. Keep this in step with the toolchain
# pinned in apps/daemon/Dockerfile.
RUST_TOOLCHAIN: "1.97"
# Static client only; the daemon comes from the socket the runner mounts.
DOCKER_CLI_VERSION: "29.7.1"
jobs:
# --- Lint + TypeScript Check ---
lint:
name: Lint & Type Check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: ${{ env.PNPM_VERSION }}
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: pnpm
- run: pnpm install --frozen-lockfile
- name: TypeScript check (shared)
run: pnpm --filter @source/shared build
- name: TypeScript check (database)
run: pnpm --filter @source/database build
- name: TypeScript check (API)
run: pnpm --filter @source/api build
- name: TypeScript check (Web)
run: pnpm --filter @source/web build
- name: Lint
run: pnpm lint
- name: Format check
run: pnpm format:check
# --- Rust Daemon ---
daemon:
name: Daemon Build & Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Self-hosted act runners run as root in a container that has no sudo,
# while GitHub-hosted runners need it. Pick whichever exists.
- name: Install protoc
run: |
SUDO=""
if [ "$(id -u)" -ne 0 ]; then SUDO="sudo"; fi
$SUDO apt-get update
$SUDO apt-get install -y protobuf-compiler
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- uses: Swatinem/rust-cache@v2
with:
workspaces: apps/daemon
- name: Check
working-directory: apps/daemon
run: cargo check
- name: Test
working-directory: apps/daemon
run: cargo test
- name: Clippy
working-directory: apps/daemon
run: cargo clippy -- -D warnings || true
# --- Docker Build Test ---
docker:
name: Docker Build
runs-on: ubuntu-latest
needs: [lint, daemon]
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
# The runner executes jobs inside a container that has no docker CLI,
# while act_runner mounts the host's socket at /var/run/docker.sock.
# Install just the client when it is missing, then prove the socket is
# actually reachable — the two failure modes look nothing alike and the
# message should say which one happened.
- name: Ensure docker CLI
run: |
if ! command -v docker >/dev/null 2>&1; then
url="https://download.docker.com/linux/static/stable/$(uname -m)/docker-${DOCKER_CLI_VERSION}.tgz"
if command -v curl >/dev/null 2>&1; then curl -fsSL "$url" -o /tmp/docker.tgz
else wget -qO /tmp/docker.tgz "$url"; fi
tar -xzf /tmp/docker.tgz -C /usr/local/bin --strip-components=1 docker/docker
fi
docker --version
docker version >/dev/null 2>&1 || {
echo "The docker socket is not reachable from this job."
echo "act_runner must mount it: leave container.docker_host empty in its config.yaml."
exit 1
}
- name: Build API image
run: docker build -f apps/api/Dockerfile -t gamepanel-api:ci .
- name: Build Web image
run: docker build -f apps/web/Dockerfile -t gamepanel-web:ci .
- name: Build Daemon image
run: docker build -f apps/daemon/Dockerfile -t gamepanel-daemon:ci .
# --- Publish images (tags only) ---
#
# docker-compose.panel.yml deploys from these images, so the stack can be
# installed on a server that has no checkout of this repository — that is
# what a control panel needs.
#
# Plain `docker build` + `docker push` on purpose: no buildx or bake, so the
# job runs on the same self-hosted runner as the build test above.
#
# Requires a REGISTRY_TOKEN secret with package write scope. The registry is
# this Gitea instance's own container registry; the panel pulls from it.
publish:
name: Publish images
runs-on: ubuntu-latest
needs: [lint, daemon]
if: startsWith(github.ref, 'refs/tags/v')
env:
REGISTRY: gits.hibna.com.tr/hibna
steps:
- uses: actions/checkout@v4
# The runner executes jobs inside a container that has no docker CLI,
# while act_runner mounts the host's socket at /var/run/docker.sock.
# Install just the client when it is missing, then prove the socket is
# actually reachable — the two failure modes look nothing alike and the
# message should say which one happened.
- name: Ensure docker CLI
run: |
if ! command -v docker >/dev/null 2>&1; then
url="https://download.docker.com/linux/static/stable/$(uname -m)/docker-${DOCKER_CLI_VERSION}.tgz"
if command -v curl >/dev/null 2>&1; then curl -fsSL "$url" -o /tmp/docker.tgz
else wget -qO /tmp/docker.tgz "$url"; fi
tar -xzf /tmp/docker.tgz -C /usr/local/bin --strip-components=1 docker/docker
fi
docker --version
docker version >/dev/null 2>&1 || {
echo "The docker socket is not reachable from this job."
echo "act_runner must mount it: leave container.docker_host empty in its config.yaml."
exit 1
}
- name: Registry login
run: |
printf '%s' "${{ secrets.REGISTRY_TOKEN }}" |
docker login gits.hibna.com.tr -u "${{ github.actor }}" --password-stdin
- name: Resolve tag
run: echo "TAG=${GITHUB_REF#refs/tags/}" >> "$GITHUB_ENV"
# The API Dockerfile carries the migration runner as its own stage; it
# has to be pushed as a separate image because docker-compose.panel.yml
# runs it as a one-shot service before the API starts.
- name: API + migrate
run: |
docker build -f apps/api/Dockerfile -t "$REGISTRY/gamepanel-api:$TAG" .
docker build -f apps/api/Dockerfile --target migrate -t "$REGISTRY/gamepanel-migrate:$TAG" .
docker push "$REGISTRY/gamepanel-api:$TAG"
docker push "$REGISTRY/gamepanel-migrate:$TAG"
# VITE_API_URL is baked in at build time: the SPA calls /api on its own
# origin, which the image's nginx proxies to the api service.
- name: Web
run: |
docker build -f apps/web/Dockerfile --build-arg VITE_API_URL=/api -t "$REGISTRY/gamepanel-web:$TAG" .
docker push "$REGISTRY/gamepanel-web:$TAG"
- name: Daemon
run: |
docker build -f apps/daemon/Dockerfile -t "$REGISTRY/gamepanel-daemon:$TAG" .
docker push "$REGISTRY/gamepanel-daemon:$TAG"