diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..f2f133c --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -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" + diff --git a/.gitea/workflows/nightly.yml b/.gitea/workflows/nightly.yml new file mode 100644 index 0000000..4d12627 --- /dev/null +++ b/.gitea/workflows/nightly.yml @@ -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" + diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..ecc296c --- /dev/null +++ b/.gitea/workflows/release.yml @@ -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 }} + diff --git a/.githooks/commit-msg b/.githooks/commit-msg new file mode 100644 index 0000000..69abee3 --- /dev/null +++ b/.githooks/commit-msg @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +scripts/commitlint-hook "$1" + diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100644 index 0000000..9b3ecda --- /dev/null +++ b/.githooks/pre-commit @@ -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." + diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100644 index 0000000..17a2682 --- /dev/null +++ b/.githooks/pre-push @@ -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." + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ff8b74f --- /dev/null +++ b/Makefile @@ -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 + diff --git a/ci.Dockerfile b/ci.Dockerfile new file mode 100644 index 0000000..9e7c097 --- /dev/null +++ b/ci.Dockerfile @@ -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"] + diff --git a/commitlint.config.cjs b/commitlint.config.cjs new file mode 100644 index 0000000..3acb487 --- /dev/null +++ b/commitlint.config.cjs @@ -0,0 +1,4 @@ +module.exports = { + extends: ['@commitlint/config-conventional'], +}; + diff --git a/docker/ci.compose.yml b/docker/ci.compose.yml new file mode 100644 index 0000000..6f370e7 --- /dev/null +++ b/docker/ci.compose.yml @@ -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"] + diff --git a/proposals/bootstrap-cicd.md b/proposals/bootstrap-cicd.md new file mode 100644 index 0000000..1c5b44b --- /dev/null +++ b/proposals/bootstrap-cicd.md @@ -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 `. + +**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`. + diff --git a/questions/bootstrap-cicd.md b/questions/bootstrap-cicd.md index d2599e9..26b6fd5 100644 --- a/questions/bootstrap-cicd.md +++ b/questions/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 @@ -20,12 +22,21 @@ Answer style: short codes + notes, e.g. `1:a,c 2:b 3:docker`. - yaml: a) yamllint b) actionlint (for workflows) c) both - 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. \ No newline at end of file diff --git a/scripts/ci b/scripts/ci new file mode 100644 index 0000000..5c55b36 --- /dev/null +++ b/scripts/ci @@ -0,0 +1,115 @@ +#!/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 + diff --git a/scripts/commitlint-hook b/scripts/commitlint-hook new file mode 100644 index 0000000..021f21c --- /dev/null +++ b/scripts/commitlint-hook @@ -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}" + diff --git a/scripts/setup-hooks b/scripts/setup-hooks new file mode 100644 index 0000000..e62f375 --- /dev/null +++ b/scripts/setup-hooks @@ -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." +