feat(ci): add Docker-only local CI parity (scripts/ci, ci image, compose, hooks, workflows, commitlint, Makefile)
Some checks failed
CI / checks (push) Has been cancelled

This commit is contained in:
2025-09-10 16:15:29 -05:00
parent 6ca0f417ee
commit b0d0c3e3d8
15 changed files with 428 additions and 2 deletions

24
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,24 @@
name: CI
on:
pull_request:
branches: ["**"]
push:
branches: ["integration", "bootstrap", "bootstrap-cicd"]
jobs:
checks:
runs-on: docker
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build CI image
run: docker build -f ci.Dockerfile -t local/ci:latest .
- name: Lint
run: docker run --rm -v ${{ github.workspace }}:/workspace local/ci:latest bash -lc "cd /workspace && IN_CI_CONTAINER=1 scripts/ci lint"
- name: Build validation
run: docker run --rm -v ${{ github.workspace }}:/workspace local/ci:latest bash -lc "cd /workspace && IN_CI_CONTAINER=1 scripts/ci build"

View File

@@ -0,0 +1,19 @@
name: Nightly
on:
schedule:
- cron: '0 3 * * *'
jobs:
report:
runs-on: docker
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build CI image
run: docker build -f ci.Dockerfile -t local/ci:latest .
- name: Lint (nightly)
run: docker run --rm -v ${{ github.workspace }}:/workspace local/ci:latest bash -lc "cd /workspace && IN_CI_CONTAINER=1 scripts/ci lint"

View File

@@ -0,0 +1,29 @@
name: Release
on:
push:
branches: ["main"]
jobs:
tag-and-notes:
runs-on: docker
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Build CI image
run: docker build -f ci.Dockerfile -t local/ci:latest .
- name: Compute tag
id: tag
run: |
TZ=UTC date +"v%Y.%m.%d-%H%M" > tag.txt
echo "tag=$(cat tag.txt)" >> $GITHUB_OUTPUT
- name: Create annotated tag
run: |
git config user.name "ci"
git config user.email "ci@local"
git tag -a ${{ steps.tag.outputs.tag }} -m "Release ${{ steps.tag.outputs.tag }}"
git push origin ${{ steps.tag.outputs.tag }}

5
.githooks/commit-msg Normal file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
set -euo pipefail
scripts/commitlint-hook "$1"

11
.githooks/pre-commit Normal file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
echo "> pre-commit: format + lint + commit message check"
# Run format and lint inside the CI container
scripts/ci format
scripts/ci lint
echo "pre-commit completed."

11
.githooks/pre-push Normal file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
set -euo pipefail
echo "> pre-push: build validation + placeholders for tests/security"
scripts/ci build
scripts/ci test
scripts/ci security
echo "pre-push completed."

33
Makefile Normal file
View File

@@ -0,0 +1,33 @@
SHELL := /usr/bin/env bash
.PHONY: all check quick format lint build test security ci-image hooks-setup
all: check
check:
./scripts/ci all
quick:
./scripts/ci format && ./scripts/ci lint
format:
./scripts/ci format
lint:
./scripts/ci lint
build:
./scripts/ci build
test:
./scripts/ci test
security:
./scripts/ci security
ci-image:
docker build -f ci.Dockerfile -t local/ci:latest .
hooks-setup:
./scripts/setup-hooks

41
ci.Dockerfile Normal file
View File

@@ -0,0 +1,41 @@
FROM debian:12-slim
ARG DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates curl git bash coreutils findutils file python3 python3-pip \
&& rm -rf /var/lib/apt/lists/*
# Install shfmt, hadolint, actionlint (static), shellcheck, yamllint, node tools
RUN set -eux; \
# shellcheck
apt-get update && apt-get install -y --no-install-recommends shellcheck && rm -rf /var/lib/apt/lists/*; \
# shfmt
SHFMT_VER=3.7.0; curl -fsSL -o /usr/local/bin/shfmt https://github.com/mvdan/sh/releases/download/v${SHFMT_VER}/shfmt_v${SHFMT_VER}_linux_amd64 && chmod +x /usr/local/bin/shfmt; \
# hadolint
HADOLINT_VER=2.12.0; curl -fsSL -o /usr/local/bin/hadolint https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VER}/hadolint-Linux-x86_64 && chmod +x /usr/local/bin/hadolint;
# actionlint
RUN set -eux; \
AL_VER=1.7.1; \
curl -fsSL -o /usr/local/bin/actionlint https://github.com/rhysd/actionlint/releases/download/v${AL_VER}/actionlint_${AL_VER}_linux_amd64.tar.gz; \
tar -C /usr/local/bin -xzf /usr/local/bin/actionlint; \
rm -f /usr/local/bin/actionlint
# yamllint via pip
RUN pip3 install --no-cache-dir yamllint==1.35.1
# Node + npm for prettier, markdownlint, commitlint
RUN set -eux; \
curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get update && apt-get install -y --no-install-recommends nodejs && \
rm -rf /var/lib/apt/lists/*
RUN npm --location=global install \
prettier@3.3.3 \
markdownlint-cli@0.39.0 \
@commitlint/cli@19.5.0 @commitlint/config-conventional@19.5.0
WORKDIR /workspace
ENTRYPOINT ["bash","-lc"]
CMD ["bash"]

4
commitlint.config.cjs Normal file
View File

@@ -0,0 +1,4 @@
module.exports = {
extends: ['@commitlint/config-conventional'],
};

13
docker/ci.compose.yml Normal file
View File

@@ -0,0 +1,13 @@
services:
ci:
build:
context: ..
dockerfile: ci.Dockerfile
working_dir: /workspace
volumes:
- "../:/workspace:Z"
environment:
- IN_CI_CONTAINER=1
entrypoint: ["bash","-lc"]
command: ["bash"]

View File

@@ -0,0 +1,69 @@
**Bootstrap CI/CD Proposal (Phase 1)**
- Scope: Local developer parity via Docker-first tooling and hooks, minimal CI placeholders (no runners required yet). Applies to this repo (docs/scripts/docker-compose), with an easy path to template for others.
**Checks To Implement Now (Local via Docker)**
- Stacks: shell, Dockerfiles/Compose, Markdown/Docs, YAML; Python/Node optional later.
- Formatters/Linters:
- shell: shfmt + shellcheck
- docker: hadolint
- markdown: markdownlint + prettier
- yaml: yamllint + actionlint (for workflows)
- Tests: none for now (lint-only baseline).
- Security: skip for this repo now.
**Execution Model**
- Docker-only: all checks run inside a pinned `ci` image. Host only orchestrates Docker/Compose.
- Single entrypoint: `scripts/ci` with phases: `format`, `lint`, `build` (compose validate), `test` (no-op for now), `security` (no-op), `all`.
- Compose file: `docker/ci.compose.yml` defines `ci` service that mounts repo and executes `scripts/ci <phase>`.
**Hooks Parity**
- Provide Git hooks via pre-commit framework and native Git hooks:
- pre-commit: run `format`, `lint`, and commit message check (Conventional Commits).
- pre-push: run `build` (compose config validation) and keep `test`/`security` as no-ops for now.
- Commit message style: Conventional Commits via `commitlint` rule-set; enforce in CI later and locally via `commit-msg` hook.
**Minimal CI (Deferred Enablement)**
- Workflows will be prepared but can stay disabled until runners are available:
- `.gitea/workflows/ci.yml`: mirrors local `lint` + `build` using the same `ci` image; triggered on PRs when enabled.
- `.gitea/workflows/release.yml`: on `main` merges, tags with `vYYYY.MM.DD-HHMM` and (optionally) creates release notes; can be enabled later.
- `.gitea/workflows/nightly.yml`: scheduled dependency/lint refresh; optional for later.
- All jobs execute inside the `ci` container image; no host package installs.
**Caching & Matrix**
- Matrix: single Linux image for now.
- Caching: enable Docker layer cache when CI runners are available; no special local caching required.
**Concurrency & Timeouts (defaults for later)**
- Cancel in-progress on same ref: enabled for PRs.
- Job timeout: 30 minutes.
**Protected Check Names (for later enforcement)**
- `ci / lint`, `ci / build`, `ci / commitlint`. Tests/Security can be added when introduced.
**Files To Add (upon approval)**
- `scripts/ci` (bash) — phases and Docker/host detection (host executes Docker only).
- `ci.Dockerfile` — pinned versions: shfmt, shellcheck, hadolint, yamllint, markdownlint-cli, prettier, actionlint, commitlint.
- `docker/ci.compose.yml``ci` service to run checks.
- `.pre-commit-config.yaml` — wire to `scripts/ci` phases; enable `commit-msg` hook for commitlint.
- `commitlint.config.cjs` — Conventional Commits rules.
- `.gitea/workflows/ci.yml`, `release.yml`, `nightly.yml` — prepared but can be disabled until runners are ready.
- `Makefile``check`, `quick`, `lint`, `format`, `build` targets mapping to scripts.
**Rollout Plan**
1) Implement local tooling and hooks on `bootstrap-cicd`.
2) Document quickstart in `docs/engineering/ci-cd.md`.
3) Later: enable Gitea workflows when runners are ready; add protected checks.
4) Optionally expand with tests/security scanners and language stacks per repo.
If this matches your intent, I will scaffold the above on `bootstrap-cicd` and then capture the finalized process in `instructions/bootstrap-cicd.md`.

View File

@@ -13,6 +13,8 @@ Answer style: short codes + notes, e.g. `1:a,c 2:b 3:docker`.
- f) Node/JS - f) Node/JS
- g) Other (specify) - g) Other (specify)
a,b,c,d potentially e.
2) Formatters/linters per stack: 2) Formatters/linters per stack:
- shell: a) shfmt b) shellcheck c) both - shell: a) shfmt b) shellcheck c) both
- docker: a) hadolint - docker: a) hadolint
@@ -21,11 +23,20 @@ Answer style: short codes + notes, e.g. `1:a,c 2:b 3:docker`.
- python (if used): a) black b) ruff c) pytest (tests) - python (if used): a) black b) ruff c) pytest (tests)
- node (if used): a) eslint b) prettier c) jest (tests) - node (if used): a) eslint b) prettier c) jest (tests)
shell: c
docker: a
markdown: c
yaml: c
I will leave python/node testing up to you. It isn't needed for this repo unless you create python scripts at some point.
3) Testing scope now: 3) Testing scope now:
- a) none (docs/scripts only) - a) none (docs/scripts only)
- b) smoke tests for scripts (bats/pytest-sh) - b) smoke tests for scripts (bats/pytest-sh)
- c) unit tests for scripts (specify framework) - c) unit tests for scripts (specify framework)
A (other then linting)
4) Security scanning: 4) Security scanning:
- a) trivy fs - a) trivy fs
- b) grype - b) grype
@@ -33,21 +44,29 @@ Answer style: short codes + notes, e.g. `1:a,c 2:b 3:docker`.
- d) npm audit (node) - d) npm audit (node)
- e) skip for this repo - e) skip for this repo
e
5) Execution environment for CI: 5) Execution environment for CI:
- a) run inside repos `ci.Dockerfile` - a) run inside repos `ci.Dockerfile`
- b) run on runner host with packages - b) run on runner host with packages
- c) mix (specify) - c) mix (specify)
All execution MUST be done in docker containers. Absolutely no work must be done on the host beyond git operations and docker orchestration.
6) Matrix needs (now): 6) Matrix needs (now):
- a) none (single Linux image) - a) none (single Linux image)
- b) multiple language versions (specify) - b) multiple language versions (specify)
- c) OS matrix (Linux only for now?) - c) OS matrix (Linux only for now?)
Um. I don't know. I think just a simle Linux environment can be assumed?
7) Caching: 7) Caching:
- a) enable tool caches (pip/npm) in CI - a) enable tool caches (pip/npm) in CI
- b) enable Docker layer cache - b) enable Docker layer cache
- c) none - c) none
I guess docker layer cache? It will be two weeks before I'm working on software (and therefore setup gitea CI runners etc).
8) Check names to protect on branches (final labels): 8) Check names to protect on branches (final labels):
- a) ci / lint - a) ci / lint
- b) ci / test - b) ci / test
@@ -55,9 +74,11 @@ Answer style: short codes + notes, e.g. `1:a,c 2:b 3:docker`.
- d) ci / security - d) ci / security
- e) ci / commitlint - e) ci / commitlint
I don't know, leave it up to you
9) Hooks parity: 9) Hooks parity:
- pre-commit: run format+lint+commitlint? (y/n) - pre-commit: run format+lint+commitlint? (y/n) y
- pre-push: run test+build+security (fast profile)? (y/n) - pre-push: run test+build+security (fast profile)? (y/n) y
10) Concurrency & timeouts: 10) Concurrency & timeouts:
- cancel in-progress on new commits to same PR? (y/n) - cancel in-progress on new commits to same PR? (y/n)
@@ -85,3 +106,5 @@ Answer style: short codes + notes, e.g. `1:a,c 2:b 3:docker`.
Notes: add any constraints about runners, container registry, or build tools. Notes: add any constraints about runners, container registry, or build tools.
Lets just ignore all things CI for now? I'm brand new to CI. Use your best judgement/adopt best practices and/or ignore CI as needed. Do track that it's an outstanding item to go in depth on though. I don't want it to block moving forward with the dozen or so docs repos I need to use this LLM workflow with though.

115
scripts/ci Normal file
View File

@@ -0,0 +1,115 @@
#!/usr/bin/env bash
set -euo pipefail
PHASE="${1:-}"
usage() {
echo "Usage: scripts/ci <format|lint|build|test|security|all>" >&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

10
scripts/commitlint-hook Normal file
View File

@@ -0,0 +1,10 @@
#!/usr/bin/env bash
set -euo pipefail
MSG_FILE="${1:-.git/COMMIT_EDITMSG}"
# Run commitlint inside the CI container against the commit message file
docker compose -f docker/ci.compose.yml run --rm \
-e IN_CI_CONTAINER=1 \
ci bash -lc "commitlint --edit ${MSG_FILE}"

19
scripts/setup-hooks Normal file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
root_dir="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
mkdir -p "$root_dir/.git/hooks"
for hook in pre-commit pre-push commit-msg; do
src="$root_dir/.githooks/$hook"
dest="$root_dir/.git/hooks/$hook"
if [[ -f "$src" ]]; then
cp "$src" "$dest"
chmod +x "$dest"
echo "Installed hook: $hook"
fi
done
echo "Git hooks installed."