diff --git a/.github/RUNNER_SETUP.md b/.github/RUNNER_SETUP.md index 400e3f4..2ae5040 100644 --- a/.github/RUNNER_SETUP.md +++ b/.github/RUNNER_SETUP.md @@ -1,19 +1,33 @@ -# Self-hosted runner setup (macOS arm64) +# Self-hosted runner setup (Debian container) This repo is public, so only run the runner for trusted workflows. `ci.yml` has no `pull_request` trigger on purpose: a fork PR can edit the workflow and run on this machine. -1. Repo → Settings → Actions → Runners → **New self-hosted runner** → macOS / ARM64. - Follow the download and `./config.sh --url ... --token ...` steps in `~/actions-runner`. - Keep the default labels (`self-hosted`, `macOS`, `ARM64`). -2. Install as a launchd service: `./svc.sh install && ./svc.sh start`. -3. The service does not load shell profiles. Add the tool directories to `~/actions-runner/.path` - (one `PATH` line, then `./svc.sh stop && ./svc.sh start`): +The runner is the `myoung34/github-runner:debian-bookworm` image. Jobs use `runs-on: [self-hosted, linux]`, which matches the image's default `self-hosted`, `Linux` and `X64`/`ARM64` labels. +The image has no ruff, uv, gitleaks or actionlint, so each job installs what it needs with `.github/scripts/setup-tools.sh`. +The script downloads pinned releases, verifies their SHA-256 checksums, caches them under `$RUNNER_TOOL_CACHE`, and adds them to `PATH`. +To bump a tool, change the version at the top of the script. +The script needs `curl`, `tar`, `sha256sum` and `install`, which the image provides. - ``` - /Users//.local/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin - ``` +## docker-compose -4. Install the tools the workflow calls directly: `brew install gitleaks actionlint jq` - (`uv` provides `uvx ruff` and `uvx pip-audit`). -5. Repo → Settings → Actions → General → Fork pull request workflows → - **Require approval for all outside collaborators**. +```yaml +services: + github-runner: + image: myoung34/github-runner:debian-bookworm + restart: always + environment: + REPO_URL: "https://github.com/nprasad2077/MNIST" + RUNNER_TOKEN: "" # Settings → Actions → Runners → New self-hosted runner + RUNNER_NAME: "coolify-debian-runner" + RUNNER_WORKDIR: "/tmp/runner/work" + # No docker.sock mount: the jobs don't use Docker, and the socket gives every job root on the host. +``` + +- The registration token expires after about an hour. Keep it out of git; set it in Coolify's environment variables, not in this file. +- The container filesystem is disposable. When the container is recreated, the next run downloads the tools again. + +## Repo setting + +Settings → Actions → General → Fork pull request workflows → **Require approval for all outside collaborators**. + +Check the container with `docker exec bash -c 'cd "$(mktemp -d)" && curl -sSfL https://raw.githubusercontent.com/nprasad2077/MNIST/dev/.github/scripts/setup-tools.sh | bash -s uv gitleaks actionlint'`. diff --git a/.github/scripts/setup-tools.sh b/.github/scripts/setup-tools.sh new file mode 100644 index 0000000..4bef1fe --- /dev/null +++ b/.github/scripts/setup-tools.sh @@ -0,0 +1,77 @@ +#!/usr/bin/env bash +# Install pinned CI tools (Linux x64/arm64) on the self-hosted runner and put them on PATH. +# Usage: setup-tools.sh [uv] [gitleaks] [actionlint] +# Needs only curl, tar and sha256sum. Downloads are verified against the published checksums. +set -euo pipefail + +UV_VERSION=0.10.7 +GITLEAKS_VERSION=8.30.1 +ACTIONLINT_VERSION=1.7.12 + +# Versions are part of the path, so bumping a pin installs into a fresh directory. +BIN="${RUNNER_TOOL_CACHE:-$HOME/.cache}/mnist-ci/uv${UV_VERSION}-gl${GITLEAKS_VERSION}-al${ACTIONLINT_VERSION}/bin" + +case "$(uname -m)" in + x86_64) UV_ARCH=x86_64 GL_ARCH=x64 AL_ARCH=amd64 ;; + aarch64 | arm64) UV_ARCH=aarch64 GL_ARCH=arm64 AL_ARCH=arm64 ;; + *) echo "::error::unsupported architecture $(uname -m)"; exit 1 ;; +esac + +# Download /, verify it against /, extract into $BIN. +install_tarball() { + local base=$1 archive=$2 sums=$3 strip=$4 + shift 4 + local tmp + tmp=$(mktemp -d) + curl -sSfL --retry 3 -o "$tmp/$archive" "$base/$archive" + curl -sSfL --retry 3 -o "$tmp/$sums" "$base/$sums" + (cd "$tmp" && grep -F " $archive" "$sums" | sha256sum -c -) + tar -xzf "$tmp/$archive" -C "$tmp" + mkdir -p "$BIN" + for member in "$@"; do + install -m 0755 "$tmp/${strip}${member}" "$BIN/$member" + done + rm -rf "$tmp" +} + +for tool in "$@"; do + if [ -x "$BIN/$tool" ]; then + continue + fi + case "$tool" in + uv) + triple="uv-${UV_ARCH}-unknown-linux-gnu" + # uv publishes one .sha256 per archive; reuse the same verification path. + base="https://github.com/astral-sh/uv/releases/download/${UV_VERSION}" + tmp=$(mktemp -d) + curl -sSfL --retry 3 -o "$tmp/$triple.tar.gz" "$base/$triple.tar.gz" + curl -sSfL --retry 3 -o "$tmp/$triple.tar.gz.sha256" "$base/$triple.tar.gz.sha256" + (cd "$tmp" && sha256sum -c "$triple.tar.gz.sha256") + tar -xzf "$tmp/$triple.tar.gz" -C "$tmp" + mkdir -p "$BIN" + install -m 0755 "$tmp/$triple/uv" "$tmp/$triple/uvx" "$BIN/" + rm -rf "$tmp" + ;; + gitleaks) + install_tarball "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}" \ + "gitleaks_${GITLEAKS_VERSION}_linux_${GL_ARCH}.tar.gz" "gitleaks_${GITLEAKS_VERSION}_checksums.txt" "" gitleaks + ;; + actionlint) + install_tarball "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}" \ + "actionlint_${ACTIONLINT_VERSION}_linux_${AL_ARCH}.tar.gz" "actionlint_${ACTIONLINT_VERSION}_checksums.txt" "" actionlint + ;; + *) echo "::error::unknown tool: $tool"; exit 1 ;; + esac +done + +if [ -n "${GITHUB_PATH:-}" ]; then + echo "$BIN" >> "$GITHUB_PATH" +fi +export PATH="$BIN:$PATH" +for tool in "$@"; do + case "$tool" in + gitleaks) "$tool" version ;; + actionlint) "$tool" -version | head -1 ;; + *) "$tool" --version ;; + esac +done diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d303430..706bf05 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,65 +19,76 @@ env: jobs: lint-notebooks: - runs-on: self-hosted + runs-on: [self-hosted, linux] steps: - uses: actions/checkout@v4 with: persist-credentials: false + - run: bash .github/scripts/setup-tools.sh uv - run: uvx "ruff@${RUFF_VERSION}" check --output-format=github notebooks/ format-notebooks: - runs-on: self-hosted + runs-on: [self-hosted, linux] continue-on-error: true # advisory: notebooks were never formatted steps: - uses: actions/checkout@v4 with: persist-credentials: false + - run: bash .github/scripts/setup-tools.sh uv - run: uvx "ruff@${RUFF_VERSION}" format --check notebooks/ secret-scan: - runs-on: self-hosted + runs-on: [self-hosted, linux] steps: - uses: actions/checkout@v4 with: fetch-depth: 0 persist-credentials: false + - run: bash .github/scripts/setup-tools.sh gitleaks - run: gitleaks detect --source . --redact --verbose dep-audit: - runs-on: self-hosted + runs-on: [self-hosted, linux] continue-on-error: true # advisory: pins are stale; Dependabot tracks bumps steps: - uses: actions/checkout@v4 with: persist-credentials: false + - run: bash .github/scripts/setup-tools.sh uv # Audit the pins only; installing requirements.txt fails on pywinpty. - run: uvx pip-audit -r requirements.txt --no-deps --disable-pip workflow-lint: - runs-on: self-hosted + runs-on: [self-hosted, linux] steps: - uses: actions/checkout@v4 with: persist-credentials: false + - run: bash .github/scripts/setup-tools.sh actionlint - run: actionlint notebook-hygiene: - runs-on: self-hosted + runs-on: [self-hosted, linux] steps: - uses: actions/checkout@v4 with: persist-credentials: false + - run: bash .github/scripts/setup-tools.sh uv - run: uv run --python 3.11 --no-project python .github/scripts/notebook_hygiene.py summary: if: always() needs: [lint-notebooks, format-notebooks, secret-scan, dep-audit, workflow-lint, notebook-hygiene] - runs-on: self-hosted + runs-on: [self-hosted, linux] steps: - name: Write job summary env: - RESULTS: ${{ toJSON(needs) }} + LINT: ${{ needs.lint-notebooks.result }} + FORMAT: ${{ needs.format-notebooks.result }} + SECRETS: ${{ needs.secret-scan.result }} + AUDIT: ${{ needs.dep-audit.result }} + WORKFLOW: ${{ needs.workflow-lint.result }} + HYGIENE: ${{ needs.notebook-hygiene.result }} EVENT: ${{ github.event_name }} REF_NAME: ${{ github.ref_name }} ACTOR: ${{ github.actor }} @@ -88,7 +99,12 @@ jobs: echo echo "| Job | Result |" echo "|---|---|" - echo "$RESULTS" | jq -r 'to_entries[] | "| \(.key) | \(.value.result) |"' + echo "| lint-notebooks | $LINT |" + echo "| format-notebooks | $FORMAT |" + echo "| secret-scan | $SECRETS |" + echo "| dep-audit | $AUDIT |" + echo "| workflow-lint | $WORKFLOW |" + echo "| notebook-hygiene | $HYGIENE |" echo echo "- **Trigger:** $EVENT" echo "- **Ref:** $REF_NAME @ ${GITHUB_SHA::7}"