#!/usr/bin/env bash set -euo pipefail PHASE="${1:-}" usage() { echo "Usage: scripts/ci " >&2 exit 2 } if [[ -z "${PHASE}" ]]; then usage fi repo_root() { git rev-parse --show-toplevel 2>/dev/null || pwd } run_outside_container() { local phase="$1" local root root="$(repo_root)" if ! command -v docker >/dev/null 2>&1; then echo "Docker is required to run CI tasks locally." >&2 exit 1 fi if ! command -v docker-compose >/dev/null 2>&1 && ! docker compose version >/dev/null 2>&1; then echo "Docker Compose v2+ is required (docker compose)." >&2 exit 1 fi # Build ci image if needed and run the requested phase inside the container (cd "$root" && docker compose -f docker/ci.compose.yml run --rm \ -e IN_CI_CONTAINER=1 \ ci bash -lc "cd /workspace && scripts/ci --inside ${phase}") } run_format() { echo ">> Formatting" # shell: format in-place shfmt -bn -ci -i 2 -w . # prettier for markdown/yaml/json/etc prettier --log-level warn --write \ "**/*.md" "**/*.yaml" "**/*.yml" "**/*.json" \ "**/*.css" "**/*.html" 2>/dev/null || true } run_lint() { echo ">> Linting" # shellcheck mapfile -t sh_files < <(git ls-files -z | xargs -0 file --mime-type | awk -F: '/(x-shellscript|text\/x-shellscript)/{print $1}'; git ls-files "*.sh") if [[ ${#sh_files[@]} -gt 0 ]]; then shellcheck -x "${sh_files[@]}" || (echo "Shellcheck failed" && exit 1) shfmt -d . fi # hadolint on Dockerfiles if ls Dockerfile* docker/*Dockerfile* 1>/dev/null 2>&1; then hadolint Dockerfile* docker/*Dockerfile* 2>/dev/null || true fi # yamllint if git ls-files "*.yml" "*.yaml" | grep -q .; then yamllint -s $(git ls-files "*.yml" "*.yaml") fi # markdownlint if git ls-files "*.md" | grep -q .; then markdownlint $(git ls-files "*.md") fi # actionlint for workflow files if present if [ -d .gitea/workflows ]; then actionlint -color fi } run_build() { echo ">> Build checks" # Validate docker compose configs if present if [ -f docker-compose.yml ] || [ -f docker/compose.yml ]; then docker compose config -q fi } run_test() { echo ">> Tests (none defined)" } run_security() { echo ">> Security checks (skipped for this repo)" } run_inside_container() { local phase="$1" case "$phase" in format) run_format ;; lint) run_lint ;; build) run_build ;; test) run_test ;; security) run_security ;; all) run_format; run_lint; run_build; run_test; run_security ;; *) usage ;; esac } if [[ "${1:-}" == "--inside" ]]; then shift PHASE="${1:-}" [[ -z "$PHASE" ]] && usage run_inside_container "$PHASE" exit 0 fi if [[ "${IN_CI_CONTAINER:-}" != "1" ]]; then run_outside_container "$PHASE" else run_inside_container "$PHASE" fi