diff --git a/.github/file-size-defaults.yml b/.github/file-size-defaults.yml new file mode 100644 index 0000000..6a96ca5 --- /dev/null +++ b/.github/file-size-defaults.yml @@ -0,0 +1,24 @@ +# Org-wide file-size defaults. Consumed by .github/workflows/file-size.yml, +# which is referenced as a Required Workflow by terraform-github's +# github_organization_ruleset.org_file_size_check. +# +# Per-repo overrides land in the consuming repo at .github/file-size.yml +# (preferred) or .file-size.yml at the root (legacy, deprecation warning). + +defaults: + # Bytes. Files larger than `warn` emit ::warning::; files larger than + # `error` emit ::error:: and fail the check. + warn: 6144 + error: 12288 + +# File extensions scanned by the check. Per-repo overrides REPLACE this list +# (not additive) — repos that need a different scan set declare their own. +scan: + - .md + - .nix + - .tf + +# Always-exempt files (base name, no extension). Per-repo `exempt` lists are +# additive to this org default. +exempt: + - CHANGELOG diff --git a/.github/scripts/file-size-check.sh b/.github/scripts/file-size-check.sh new file mode 100755 index 0000000..963f869 --- /dev/null +++ b/.github/scripts/file-size-check.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +# File-size check executed by .github/workflows/file-size.yml. +# +# Reads org defaults from a trusted file (passed as the only argument) and +# per-repo overrides from the consuming repo's .github/file-size.yml or +# legacy .file-size.yml at root. Override values are attacker-controllable +# on PR runs and are validated against strict regexes before use; the +# defaults file is in the org-controlled checkout and therefore trusted. +# +# Emits GitHub Actions ::warning:: and ::error:: annotations. Exits non-zero +# only when at least one file exceeds the error threshold. + +set -euo pipefail + +if [ $# -ne 1 ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +DEFAULTS_FILE=$1 + +if [ ! -f "$DEFAULTS_FILE" ]; then + echo "::error::defaults file not found: $DEFAULTS_FILE" + exit 2 +fi + +# Validators for attacker-controllable override values. yq output is treated +# as untrusted string data; nothing flows into shell eval, but explicit +# format checks block surprise inputs. +is_positive_int() { [[ "$1" =~ ^[0-9]+$ ]] && [ "$1" -gt 0 ]; } +is_extension() { [[ "$1" =~ ^\.[a-zA-Z0-9]+$ ]]; } +is_basename() { [[ "$1" =~ ^[a-zA-Z0-9_.-]+$ ]]; } + +# Org defaults — trusted source, no validation needed. +WARN=$(yq '.defaults.warn' "$DEFAULTS_FILE") +ERR=$(yq '.defaults.error' "$DEFAULTS_FILE") +DEFAULT_SCAN=$(yq '.scan | .[]' "$DEFAULTS_FILE" | tr '\n' ' ') +EXEMPT=" $(yq '.exempt | .[]' "$DEFAULTS_FILE" | tr '\n' ' ')" + +# Per-repo override: prefer .github/file-size.yml, fall back to legacy +# .file-size.yml at root (deprecation warning emitted). +OVERRIDE="" +if [ -f ".github/file-size.yml" ]; then + OVERRIDE=".github/file-size.yml" +elif [ -f ".file-size.yml" ]; then + OVERRIDE=".file-size.yml" + echo "::warning::.file-size.yml at repo root is deprecated; move to .github/file-size.yml" +fi + +EXT_LIMIT=0 +EXTENDED=" " + +if [ -n "$OVERRIDE" ]; then + cand=$(yq ".defaults.warn // $WARN" "$OVERRIDE") + if is_positive_int "$cand"; then + WARN=$cand + else + echo "::warning file=$OVERRIDE::defaults.warn must be a positive integer; ignoring '$cand'" + fi + + cand=$(yq ".defaults.error // $ERR" "$OVERRIDE") + if is_positive_int "$cand"; then + ERR=$cand + else + echo "::warning file=$OVERRIDE::defaults.error must be a positive integer; ignoring '$cand'" + fi + + cand=$(yq '.extended.limit // 0' "$OVERRIDE") + if [[ "$cand" =~ ^[0-9]+$ ]]; then + EXT_LIMIT=$cand + else + echo "::warning file=$OVERRIDE::extended.limit must be a non-negative integer; ignoring '$cand'" + fi + + cfg_scan="" + while IFS= read -r ext; do + [ -z "$ext" ] && continue + if is_extension "$ext"; then + cfg_scan="$cfg_scan$ext " + else + echo "::warning file=$OVERRIDE::scan entry '$ext' must match ^\\.[a-zA-Z0-9]+\$; skipped" + fi + done < <(yq '.scan // [] | .[]' "$OVERRIDE") + [ -n "$cfg_scan" ] && DEFAULT_SCAN="$cfg_scan" + + while IFS= read -r base; do + [ -z "$base" ] && continue + if is_basename "$base"; then + EXTENDED="$EXTENDED$base " + else + echo "::warning file=$OVERRIDE::extended.files entry '$base' must match ^[a-zA-Z0-9_.-]+\$; skipped" + fi + done < <(yq '.extended.files // [] | .[]' "$OVERRIDE") + + while IFS= read -r base; do + [ -z "$base" ] && continue + if is_basename "$base"; then + EXEMPT="$EXEMPT$base " + else + echo "::warning file=$OVERRIDE::exempt entry '$base' must match ^[a-zA-Z0-9_.-]+\$; skipped" + fi + done < <(yq '.exempt // [] | .[]' "$OVERRIDE") +fi + +# Build find name arguments from scan extensions. +name_args=() +first=true +for ext in $DEFAULT_SCAN; do + if $first; then + first=false + else + name_args+=(-o) + fi + name_args+=(-name "*${ext}") +done + +errors=0 +warnings=0 + +while IFS= read -r -d '' f; do + base="${f##*/}" + base="${base%.*}" + size=$(stat -c%s "$f") + + if [[ "$EXEMPT" == *" $base "* ]]; then + continue + fi + + if [[ "$EXT_LIMIT" -gt 0 ]] && [[ "$EXTENDED" == *" $base "* ]]; then + limit=$EXT_LIMIT + warn_threshold=$limit + else + limit=$ERR + warn_threshold=$WARN + fi + + if [ "$size" -gt "$limit" ]; then + echo "::error file=$f::$f is $((size / 1024))KB (exceeds $((limit / 1024))KB limit)" + errors=$((errors + 1)) + elif [ "$size" -gt "$warn_threshold" ]; then + echo "::warning file=$f::$f is $((size / 1024))KB (exceeds $((warn_threshold / 1024))KB recommended)" + warnings=$((warnings + 1)) + fi +done < <(find . -type f \( "${name_args[@]}" \) \ + -not -path "./.git/*" \ + -not -path "./.org-github/*" \ + -not -path "./node_modules/*" \ + -not -path "./result/*" \ + -not -name "*.lock" \ + -not -name "package-lock.json" \ + -not -name "pnpm-lock.yaml" \ + -print0) + +echo "File size check: ${errors} error(s), ${warnings} warning(s)" +if [ "$errors" -gt 0 ]; then + exit 1 +fi +exit 0 diff --git a/.github/workflows/file-size.yml b/.github/workflows/file-size.yml new file mode 100644 index 0000000..edf243e --- /dev/null +++ b/.github/workflows/file-size.yml @@ -0,0 +1,49 @@ +# Standalone file-size check for org-wide enforcement via Required Workflows. +# +# Referenced by dryvist/terraform-github's github_organization_ruleset +# (org_file_size_check), which injects this workflow into the default-branch +# PRs of EVERY repo in the org. All logic lives in +# .github/scripts/file-size-check.sh (org-controlled). Defaults — thresholds, +# scan extensions, exempt list — are sourced from +# .github/file-size-defaults.yml. +# +# Per-repo overrides land in the consuming repo at: +# .github/file-size.yml — preferred (consolidated workflow-consumed +# configs live under .github/) +# .file-size.yml — legacy root path; supported with a deprecation +# warning, removed in a future iteration +# +# Override fields (all optional, all additive unless noted): +# defaults: { warn: , error: } # overrides org defaults +# scan: [<.ext>, …] # REPLACES default scan list +# extended: { limit: , files: [] } # additive higher-limit set +# exempt: [] # additive to org exempt list +name: File Size + +on: + pull_request: + +permissions: + contents: read + +concurrency: + group: org-file-size-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + check: + name: Check + runs-on: ubuntu-latest + steps: + - name: Checkout target repo + uses: actions/checkout@v6 + + - name: Checkout org config (single source of truth) + uses: actions/checkout@v6 + with: + repository: dryvist/.github + ref: main + path: .org-github + + - name: Check file sizes + run: .org-github/.github/scripts/file-size-check.sh .org-github/.github/file-size-defaults.yml diff --git a/CLAUDE.md b/CLAUDE.md index 628e152..105364a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -41,20 +41,46 @@ The canonical `biome.jsonc` and `.markdownlint-cli2.yaml` live in this repo at the root. Repos copy them at scaffold time; periodic sync is handled by Renovate's custom manager (or manual update for now — see `renovate.json`). -## Inheritance from `JacobPEvans/.github` - -We reuse JacobPEvans's reusable workflows directly. Don't fork or wrap them -unless we need behavior they don't provide. - -| Need | Inherited from | Caller pattern | -| --- | --- | --- | -| Release-please (org-wide major-bump block) | `JacobPEvans/.github/.github/workflows/_release-please.yml@main` | `release-please.yml` in any dryvist repo | -| Renovate presets | `github>JacobPEvans/.github:renovate-presets` | `extends` in `renovate.json` | -| Security policy structure | `JacobPEvans/.github/SECURITY.md` | Adapted/scoped to dryvist (this repo) | - -**Inheritance chain:** `JacobPEvans/.github` → `dryvist/.github` → individual -dryvist repos. Re-inheritance works through the same mechanisms (workflow -`uses:` + Renovate `extends:`). +## Workflow library ownership (migration in progress) + +`dryvist/.github` is becoming the **source of truth** for the shared workflow +library. `JacobPEvans-personal/.github` is being reduced to a consumer — +inheritance flows dryvist → JacobPEvans-personal, never the reverse. Each +workflow migrates atomically: it lands here, all consumers flip their +`uses:` to point at this repo, then the source in +`JacobPEvans-personal/.github` is deleted. + +When adding a new shared workflow (one that more than one repo will call), +write it here. Don't add it to `JacobPEvans-personal/.github`. Existing +`uses: JacobPEvans-personal/.github/.github/workflows/_*.yml@main` references +should be flipped to `uses: dryvist/.github/.github/workflows/.yml@main` +the next time they're touched, even if their workflow isn't formally +migrated yet. + +Sourced from this repo (`dryvist/.github`) — Required Workflows attached +via `terraform-github`; no per-repo caller needed: + +- `file-size` — workflow at `.github/workflows/file-size.yml`, logic in + `.github/scripts/file-size-check.sh`, defaults in + `.github/file-size-defaults.yml`. +- `markdownlint` — workflow at `.github/workflows/markdownlint.yml`, + config in `.markdownlint-cli2.yaml` at the repo root. + +Still inherited from `JacobPEvans-personal/.github` (pending migration +into this repo): + +- Release-please — `_release-please.yml@main`. Per-repo caller + `release-please.yml` forwards `GH_APP_ID` / `GH_APP_PRIVATE_KEY` + secrets (see Prereq below). +- Renovate presets — extends + `github>JacobPEvans-personal/.github:renovate-presets` in + `renovate.json`. +- Security policy structure — `SECURITY.md` template, scoped and + adapted in this repo. + +Older docs and PR templates may still use the redirect-friendly +`JacobPEvans/` form. Don't mass-rewrite those — see `~/CLAUDE.local.md` +for the redirect rules. **Prereq for release-please:** the inherited workflow needs a GitHub App token at runtime. dryvist exposes two generic org-level secrets — caller @@ -78,7 +104,15 @@ steps.) - AI assistant policy (this file) - Org-wide tooling configs (`biome.jsonc`, `renovate.json`) - Community health files GitHub auto-applies (`SECURITY.md`, `profile/README.md`) -- Caller workflow templates that wire up inherited reusable workflows +- The shared workflow library (`.github/workflows/*.yml`) — Required + Workflows referenced by org rulesets in `terraform-github`, plus + reusables that any dryvist repo can opt into with `uses:` +- Bash/POSIX implementations of workflow steps (`.github/scripts/*.sh`) — + extracted from workflow YAML per the no-scripts rule; ship as + committed artifacts with `+x` in the index +- Workflow defaults (`.github/-defaults.yml`) — no magic numbers in + workflow YAML or scripts; thresholds and lists live in these dedicated + files, consumed via `yq` It does **NOT** contain anything vendor- or product-specific. Cribl pack infrastructure lives in [`dryvist/cc-edge-pack-template`](https://github.com/dryvist/cc-edge-pack-template). @@ -98,10 +132,12 @@ For every change in dryvist: ## When in doubt -- Read [`JacobPEvans/.github`](https://github.com/JacobPEvans/.github) for the - upstream patterns we inherit. +- Read [`JacobPEvans-personal/.github`](https://github.com/JacobPEvans-personal/.github) + for patterns still inherited from there (mostly release-please and Renovate + presets, pending migration into this repo). - Read this repo's `biome.jsonc` for current lint/format rules. - Read [`dryvist/cc-edge-pack-template`](https://github.com/dryvist/cc-edge-pack-template) for Cribl-specific test/build scaffolding. -- For release-please specifics, the inherited workflow's docstring at - `JacobPEvans/.github/.github/workflows/_release-please.yml` is authoritative. +- For release-please specifics, the (still-)inherited workflow's docstring at + `JacobPEvans-personal/.github/.github/workflows/_release-please.yml` is + authoritative until that workflow migrates here.