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
Some checks failed
CI / checks (push) Has been cancelled
This commit is contained in:
24
.gitea/workflows/ci.yml
Normal file
24
.gitea/workflows/ci.yml
Normal 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"
|
||||
|
19
.gitea/workflows/nightly.yml
Normal file
19
.gitea/workflows/nightly.yml
Normal 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"
|
||||
|
29
.gitea/workflows/release.yml
Normal file
29
.gitea/workflows/release.yml
Normal 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
5
.githooks/commit-msg
Normal file
@@ -0,0 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
scripts/commitlint-hook "$1"
|
||||
|
11
.githooks/pre-commit
Normal file
11
.githooks/pre-commit
Normal 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
11
.githooks/pre-push
Normal 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
33
Makefile
Normal 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
41
ci.Dockerfile
Normal 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
4
commitlint.config.cjs
Normal file
@@ -0,0 +1,4 @@
|
||||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
};
|
||||
|
13
docker/ci.compose.yml
Normal file
13
docker/ci.compose.yml
Normal 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"]
|
||||
|
69
proposals/bootstrap-cicd.md
Normal file
69
proposals/bootstrap-cicd.md
Normal 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`.
|
||||
|
@@ -13,6 +13,8 @@ Answer style: short codes + notes, e.g. `1:a,c 2:b 3:docker`.
|
||||
- f) Node/JS
|
||||
- g) Other (specify)
|
||||
|
||||
a,b,c,d potentially e.
|
||||
|
||||
2) Formatters/linters per stack:
|
||||
- shell: a) shfmt b) shellcheck c) both
|
||||
- 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)
|
||||
- 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:
|
||||
- a) none (docs/scripts only)
|
||||
- b) smoke tests for scripts (bats/pytest-sh)
|
||||
- c) unit tests for scripts (specify framework)
|
||||
|
||||
A (other then linting)
|
||||
|
||||
4) Security scanning:
|
||||
- a) trivy fs
|
||||
- b) grype
|
||||
@@ -33,21 +44,29 @@ Answer style: short codes + notes, e.g. `1:a,c 2:b 3:docker`.
|
||||
- d) npm audit (node)
|
||||
- e) skip for this repo
|
||||
|
||||
e
|
||||
|
||||
5) Execution environment for CI:
|
||||
- a) run inside repo’s `ci.Dockerfile`
|
||||
- b) run on runner host with packages
|
||||
- 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):
|
||||
- a) none (single Linux image)
|
||||
- b) multiple language versions (specify)
|
||||
- c) OS matrix (Linux only for now?)
|
||||
|
||||
Um. I don't know. I think just a simle Linux environment can be assumed?
|
||||
|
||||
7) Caching:
|
||||
- a) enable tool caches (pip/npm) in CI
|
||||
- b) enable Docker layer cache
|
||||
- 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):
|
||||
- a) ci / lint
|
||||
- b) ci / test
|
||||
@@ -55,9 +74,11 @@ Answer style: short codes + notes, e.g. `1:a,c 2:b 3:docker`.
|
||||
- d) ci / security
|
||||
- e) ci / commitlint
|
||||
|
||||
I don't know, leave it up to you
|
||||
|
||||
9) Hooks parity:
|
||||
- pre-commit: run format+lint+commitlint? (y/n)
|
||||
- pre-push: run test+build+security (fast profile)? (y/n)
|
||||
- pre-commit: run format+lint+commitlint? (y/n) y
|
||||
- pre-push: run test+build+security (fast profile)? (y/n) y
|
||||
|
||||
10) Concurrency & timeouts:
|
||||
- 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.
|
||||
|
||||
|
||||
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
115
scripts/ci
Normal 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
10
scripts/commitlint-hook
Normal 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
19
scripts/setup-hooks
Normal 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."
|
||||
|
Reference in New Issue
Block a user