@keelcore/standards 0.9.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,496 @@
1
+ # CI Standards
2
+
3
+ These rules govern all CI workflow and script work. They are non-negotiable.
4
+
5
+ **Maturity:** Required
6
+ **Version:** 1.0.0
7
+ **Last Reviewed:** 2026-03-09
8
+
9
+ ## Core Principle
10
+
11
+ CI workflow YAML is thin orchestration only. No build, test, lint, packaging, signing, or release logic lives
12
+ in YAML — it lives in `scripts/` entrypoints.
13
+
14
+ ## Workflow File Responsibilities
15
+
16
+ YAML may only contain:
17
+
18
+ - Trigger selection
19
+ - Permissions
20
+ - Matrix definition
21
+ - Caching
22
+ - Environment setup (toolchain version via language version file, e.g. `go-version-file: go.mod`)
23
+ - Artifact upload / download
24
+ - Invocation of stable `scripts/` entrypoints
25
+
26
+ ## Trigger Requirements
27
+
28
+ - Every workflow and every job within a workflow must be manually triggerable via `workflow_dispatch:`.
29
+ - `workflow_dispatch:` is a required field on every workflow file without exception.
30
+ - No workflow is merged without `workflow_dispatch:` in the trigger block.
31
+ - Manual triggers must accept the same inputs as automated triggers where relevant (e.g. ref, artifact version).
32
+
33
+ ## Script Entrypoints
34
+
35
+ One script per concern. Scripts are the canonical entry point for all CI logic. Workflow YAML invokes them;
36
+ it does not duplicate their logic.
37
+
38
+ ## Makefile Entrypoints
39
+
40
+ - Every `run:` step in workflow YAML MUST be `make <target>` and nothing else.
41
+ - No `run:` may call bash, sh, bats, go, chmod, or any tool directly.
42
+ - Each CI concern maps to exactly one Makefile target.
43
+ - Multi-command steps must be aggregated into one target; YAML calls the target.
44
+ - Setup steps (tooling, fixtures) are also Makefile targets (e.g. `make setup-bats`).
45
+ - The Makefile is the single dev-facing interface: what CI calls, a developer can call locally.
46
+ - Every `scripts/**/*.sh` file (except sourced library files under `scripts/lib/`) MUST have a
47
+ Makefile target that invokes it — the script path must appear in a Makefile recipe.
48
+
49
+ ## Universal Canonical Makefile Targets
50
+
51
+ Every project MUST define these language-independent targets. They form a stable, tool-agnostic
52
+ interface for humans and CI alike. Names are fixed; projects may not rename or skip them.
53
+ Language-specific subtargets (e.g. `make lint-go`, `make test-unit`) are additive — they do not
54
+ replace the universal targets.
55
+
56
+ ### Always required
57
+
58
+ | Target | Makefile target | Script | Responsibility |
59
+ |---|---|---|---|
60
+ | Build default artifact | `make build` | project-specific | Produce the primary deliverable (binary, image, package). |
61
+ | Run all linters | `make lint` | project-specific | Format check + static analysis. Must invoke `make lint-newlines`. |
62
+ | Run full test suite | `make test` | project-specific | Unit + integration tests in sequence. |
63
+ | Run unit tests | `make unit-test` | `scripts/test/ci.sh` | Unit tests only; produces JUnit XML and coverage profile. |
64
+ | Run integration tests | `make integration-test` | project-specific | BATS / end-to-end tests against a built artifact. |
65
+ | Remove artifacts | `make clean` | — | Remove `dist/` and generated files. |
66
+ | Standards audit | `make audit` | `scripts/ci/audit-make-targets.sh` | CI/Makefile compliance auditor. |
67
+ | Code coverage report | `make coverage` | `scripts/test/coverage.sh` | Print total %, uncovered statements, total lines. |
68
+ | PR coverage gate | `make ci-coverage-delta` | `scripts/test/coverage-delta.sh` | Fail PR if coverage drops > threshold vs. base branch. |
69
+ | PR policy gate | `make ci-pr-policy` | `scripts/ci/pr-policy.sh` | Title, body, branch naming, linked issue enforcement. |
70
+ | Secret scan | `make ci-secret-scan` | `scripts/ci/secret-scan.sh` | gitleaks scan; required on every PR and default-branch push. |
71
+ | DCO check | `make ci-dco` | `scripts/ci/dco-check.sh` | Signed-off-by trailer on every commit in the PR. |
72
+ | Trailing newlines | `make lint-newlines` | `scripts/lint/newlines.sh` | Every `.md`, `.sh`, `.go` ends with `\n`. |
73
+ | Legal drift check | `make check-legal-drift` | `scripts/check-legal-drift.sh` | Copied legal files (LICENSE, TRADEMARK) match the source of truth. |
74
+
75
+ ### Required for projects with releases
76
+
77
+ | Target | Makefile target | Script | Responsibility |
78
+ |---|---|---|---|
79
+ | Generate checksums | `make release-checksums` | `scripts/release/checksums.sh` | Write `dist/SHA256SUMS` for all release artifacts. |
80
+ | Verify checksums | `make release-checksums-verify` | `scripts/release/checksums.sh --verify` | Confirm downloaded artifacts match `SHA256SUMS`. |
81
+ | Generate SBOM | `make release-sbom` | `scripts/release/sbom.sh` | SPDX JSON SBOM via syft; attached to every release. |
82
+ | Sign artifacts | `make release-sign` | `scripts/release/sign.sh` | cosign keyless signing; bundles attached alongside binaries. |
83
+ | Upload to release | `make release-upload` | `scripts/release/upload.sh` | Push artifacts + checksums + SBOM to GitHub Release. |
84
+ | Version tagging | `make create-release` | `scripts/release/create-release.sh` | Schema-diff semver bump + annotated tag; developer action only. |
85
+
86
+ ### Required for projects with container images
87
+
88
+ | Target | Makefile target | Script | Responsibility |
89
+ |---|---|---|---|
90
+ | Build and push image | `make release-docker` | `scripts/release/docker.sh` | Build, tag, push, and sign container images from `dist/`. |
91
+
92
+ ### Required for projects with Helm charts
93
+
94
+ | Target | Makefile target | Script | Responsibility |
95
+ |---|---|---|---|
96
+ | Helm lint | `make lint-helm` | `scripts/lint/helm.sh` | `helm lint` + template render; no cluster required. |
97
+ | Helm schema validation | `make lint-helm-validate` | `scripts/lint/helm-validate.sh` | kubeconform validation of rendered templates. |
98
+ | Helm chart publish | `make release-helm-push` | `scripts/release/helm-push.sh` | OCI push to GHCR + cosign signing of the chart digest. |
99
+
100
+ ## CI Auditor
101
+
102
+ - Every project MUST include `scripts/ci/audit-make-targets.sh`.
103
+ - The auditor enforces the three invariants:
104
+ 1. All workflow `run:` steps are `make <target>`.
105
+ 2. All `scripts/**/*.sh` files (except `scripts/lib/`) have a Makefile target.
106
+ 3. All universal canonical targets exist in the Makefile.
107
+ - `make audit` invokes the auditor.
108
+ - The auditor MUST be a required CI step (add to the workflow as `run: make audit`).
109
+ - The auditor MUST be wired into the pre-commit hook, gated on changes to
110
+ `.github/workflows/`, `scripts/`, or `Makefile`.
111
+
112
+ ### Canonical Script Source of Truth
113
+
114
+ The source-of-truth copies of all canonical scripts live in the `keelcore/standards` repository
115
+ (`scripts/` directory). Downstream repos MUST keep their copies byte-identical to the standards
116
+ versions. Any change to a canonical script must be made first in `keelcore/standards` and then
117
+ propagated to all downstream repos.
118
+
119
+ To verify that a downstream repo's canonical scripts match the standards source of truth, run:
120
+
121
+ ```bash
122
+ make verify-canonical REPO=<path-to-downstream-repo-root>
123
+ ```
124
+
125
+ from the `keelcore/standards` repository root. This compares `sha256sum` of each mandatory script
126
+ and reports PASS/FAIL per file, exiting 1 if any mismatch is found.
127
+
128
+ ## Canonical CI Scripts
129
+
130
+ Scripts whose names and responsibilities are fixed across all projects. Projects must not rename
131
+ these scripts or move their canonical logic elsewhere.
132
+
133
+ The source of truth for all canonical scripts is the `keelcore/standards` repository. Downstream
134
+ repo copies must be byte-identical to the standards versions for the scripts marked **canonical**.
135
+ Use `make verify-canonical REPO=<path>` from the standards repo to verify compliance.
136
+
137
+ ### Always required
138
+
139
+ | Script | Makefile target | Responsibility | Source of truth |
140
+ |---|---|---|---|
141
+ | `scripts/ci/audit-make-targets.sh` | `make audit` | CI/Makefile standards compliance auditor. | keelcore/standards |
142
+ | `scripts/ci/pr-policy.sh` | `make ci-pr-policy` | PR policy gate: title, body, branch naming, linked issue. | project-specific |
143
+ | `scripts/ci/secret-scan.sh` | `make ci-secret-scan` | Secret scanning on every PR and default-branch push. | keelcore/standards |
144
+ | `scripts/ci/dco-check.sh` | `make ci-dco` | DCO Signed-off-by trailer verification. | keelcore/standards |
145
+ | `scripts/lint/newlines.sh` | `make lint-newlines` | Trailing newline enforcement for `.md`, `.sh`, `.go` files. | keelcore/standards |
146
+ | `scripts/test/coverage.sh` | `make coverage` | Coverage report: total %, uncovered statements, total lines. | keelcore/standards |
147
+ | `scripts/test/coverage-delta.sh` | `make ci-coverage-delta` | PR coverage delta gate (fails if drop exceeds threshold). | keelcore/standards |
148
+ | `scripts/check-legal-drift.sh` | `make check-legal-drift` | Verify copied legal files match the source of truth. | project-specific |
149
+ | `scripts/lib/paths.sh` | _(sourced; no target)_ | Shared path-filter helpers; sourced by other scripts. | keelcore/standards |
150
+
151
+ ### Required for projects with releases
152
+
153
+ | Script | Makefile target | Responsibility |
154
+ |---|---|---|
155
+ | `scripts/release/checksums.sh` | `make release-checksums` / `make release-checksums-verify` | Generate or verify `dist/SHA256SUMS`. |
156
+ | `scripts/release/sbom.sh` | `make release-sbom` | Generate SPDX JSON SBOM via syft. |
157
+ | `scripts/release/sign.sh` | `make release-sign` | cosign keyless signing of all release artifacts. |
158
+ | `scripts/release/upload.sh` | `make release-upload` | Upload artifacts to GitHub Release via gh CLI. |
159
+ | `scripts/release/create-release.sh` | `make create-release` | Schema-diff semver version computation + tag; developer action. |
160
+
161
+ ### Required for projects with container images
162
+
163
+ | Script | Makefile target | Responsibility |
164
+ |---|---|---|
165
+ | `scripts/release/docker.sh` | `make release-docker` | Build, tag, push, and sign container images. |
166
+
167
+ ### Required for projects with Helm charts
168
+
169
+ | Script | Makefile target | Responsibility |
170
+ |---|---|---|
171
+ | `scripts/lint/helm.sh` | `make lint-helm` | `helm lint` + template render; no cluster required. |
172
+ | `scripts/lint/helm-validate.sh` | `make lint-helm-validate` | kubeconform schema validation of rendered Helm templates. |
173
+ | `scripts/release/helm-push.sh` | `make release-helm-push` | OCI Helm chart push to GHCR + cosign signing. |
174
+
175
+ ## Source File Formatting Invariants
176
+
177
+ Every text source file MUST end with a single trailing newline (`\n`). Files without one fail the
178
+ pre-commit hook and CI lint. Covered extensions:
179
+
180
+ - Source: `.go`, `.sh`
181
+ - Markup / documentation: `.md`
182
+ - Config / data: `.yml`, `.yaml`, `.toml`, `.json`
183
+ - Git control files: `.gitignore`, `.gitattributes`
184
+
185
+ Rules:
186
+
187
+ - Enforce via `scripts/lint/newlines.sh`; auto-fix via `scripts/lint/newlines.sh --fix`.
188
+ - The pre-commit hook checks staged files; `make lint-newlines` checks the full tracked tree.
189
+ - Adding a new text-format type to the repo? Add its glob to `scripts/lint/newlines.sh` and the
190
+ pre-commit `staged_text_files` filter in the same commit.
191
+
192
+ ## Build Platform vs. Target Platform
193
+
194
+ - Prefer build-once, deploy/validate-everywhere.
195
+ - Use cross-compilation to produce artifacts from a single runner.
196
+ - Use runner matrices for testing/smoke validation on target platforms, not for rebuilding.
197
+ - Per-platform builds only when required by native signing, platform-bound linkers, OS-specific packaging,
198
+ or target-specific verification unavailable elsewhere. State the reason explicitly when doing so.
199
+ - Artifact promotion over artifact regeneration: the CI-tested artifact is the release artifact.
200
+
201
+ ## Platform-Specific Finishing Steps
202
+
203
+ - Signing, notarization, and native packaging are acceptable reasons for platform matrices.
204
+ - Build the artifact in a canonical build job; use a platform matrix only for the platform-bound finishing step.
205
+ - Keep signing, notarization, and packaging steps in repository scripts; workflow YAML provides only
206
+ orchestration, credentials, and artifact movement.
207
+
208
+ ## Permissions
209
+
210
+ - `permissions: contents: read` by default on all workflows and jobs.
211
+ - `contents: write` only on the narrowly scoped release/publish job that requires it.
212
+ - `id-token: write` only on jobs that use OIDC federation or provenance attestation.
213
+ - Never grant `actions: write` or `packages: write` beyond the job that explicitly requires it.
214
+ - Set permissions at the job level, not just the workflow level, to narrow blast radius.
215
+
216
+ ## Action Version Pinning
217
+
218
+ - Always pin action versions to a full commit SHA, not a tag. Tags are mutable.
219
+ - Correct: `actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2`
220
+ - Incorrect: `actions/checkout@v4`
221
+ - Use Dependabot (`dependabot.yml`) with `package-ecosystem: github-actions` to keep SHA pins current.
222
+
223
+ ## Toolchain Version
224
+
225
+ - Single source of truth: language version file (e.g. `go.mod`, `.nvmrc`, `.python-version`).
226
+ - Never hardcode toolchain version in workflow YAML.
227
+
228
+ ## Caching
229
+
230
+ - Cache only module cache and build cache.
231
+ - Cache keys must derive from the authoritative dependency lock file (e.g. `go.sum`, `package-lock.json`).
232
+ - Do not cache opaque directories with unclear invalidation behavior.
233
+ - Fork PRs must not share cache with the base repository in ways that allow cache poisoning.
234
+
235
+ ## Timeouts
236
+
237
+ - Every job must set `timeout-minutes`.
238
+ - Calibrate per job type; err on the side of generous but finite bounds.
239
+
240
+ ## Supply Chain Security
241
+
242
+ ### SLSA Provenance Attestation
243
+
244
+ - Release workflows must produce SLSA build provenance for all published artifacts.
245
+ - Use `actions/attest-build-provenance` or `slsa-github-generator` for SLSA Level 3 attestations.
246
+ - Attestations must be attached to GitHub Releases alongside the binaries.
247
+
248
+ ### Artifact Signing
249
+
250
+ - All release artifacts must be signed with Sigstore/cosign.
251
+ - Signing lives in `scripts/release/sign.sh`; workflow YAML provides only the OIDC token and artifact path.
252
+ - Signatures and certificates are uploaded alongside binaries.
253
+
254
+ ### SBOM
255
+
256
+ - Every release must include a machine-readable SBOM in SPDX or CycloneDX format.
257
+ - Generated by `scripts/release/sbom.sh`.
258
+ - Attached to GitHub Releases as a named artifact.
259
+
260
+ ### Artifact Integrity Between Jobs
261
+
262
+ - Checksum artifacts before upload; verify the checksum after download in downstream jobs.
263
+ - Release artifacts ship with a `SHA256SUMS` file.
264
+ - The `SHA256SUMS` file is itself signed by the release signing key.
265
+
266
+ ## Credential and Secrets Discipline
267
+
268
+ ### OIDC Federation
269
+
270
+ - Use GitHub OIDC to authenticate to cloud providers instead of long-lived static credentials.
271
+ - Static secrets are acceptable only for services that do not support OIDC.
272
+
273
+ ### Secret Scanning
274
+
275
+ - All PRs trigger a secret scanning job before code review.
276
+ - Secret scanning also runs on the default branch on every push.
277
+ - This job is a required status check.
278
+
279
+ ### Secrets in Logs
280
+
281
+ - Scripts must never echo, print, or log secret values.
282
+ - Workflow YAML must not pass secrets as positional arguments; use environment variables.
283
+ - CI logs are treated as potentially public.
284
+
285
+ ## PR Policy Compliance
286
+
287
+ ### Policy Gate Job
288
+
289
+ - Every PR triggers a `pr-policy` job before human code review begins.
290
+ - This job is a required status check that runs first in the workflow DAG.
291
+
292
+ ### PR Policy Checks
293
+
294
+ - Commit message format: enforce conventional commit format or project-defined conventions.
295
+ - PR title format: must match the project's defined pattern.
296
+ - PR description completeness: reject PRs with empty or template-stub descriptions.
297
+ - Linked issue: require a referenced issue number for non-trivial changes.
298
+ - Branch naming: enforce branch naming conventions.
299
+ - File size gates: reject PRs adding binary blobs or files exceeding a defined threshold.
300
+ - Forbidden file patterns: reject PRs modifying protected files without CODEOWNERS approval.
301
+ - CODEOWNERS coverage: validate that all changed files are covered by a CODEOWNERS rule.
302
+ - Secret scan: a detected secret fails the gate immediately.
303
+
304
+ ### Policy Script Discipline
305
+
306
+ - All policy logic lives in a script, not in workflow YAML.
307
+ - Policy output must be machine-readable (exit code) and human-readable (stderr summary).
308
+ - Policy script must be runnable locally by developers before pushing.
309
+
310
+ ## CODEOWNERS and Workflow File Protection
311
+
312
+ - `.github/CODEOWNERS` must designate a security or infra owner for:
313
+ - All files under `.github/workflows/`
314
+ - All files under `scripts/`
315
+ - All dependency manifest and lock files
316
+ - Workflow file changes require CODEOWNERS review; author self-approval is not permitted.
317
+
318
+ ## Fork PR Security
319
+
320
+ - Fork PRs must use the `pull_request` event trigger, NOT `pull_request_target`.
321
+ - Fork PRs must never receive access to repository secrets or write-scoped tokens.
322
+ - Use GitHub Environment protection rules to prevent fork PRs from accessing release secrets.
323
+
324
+ ## Environment Protection Rules
325
+
326
+ - All release and deployment jobs must run in a named GitHub Environment.
327
+ - Named environments must have required reviewers configured.
328
+ - This creates a mandatory human gate before any artifact is published, signed, or deployed.
329
+
330
+ ## Reproducible Builds
331
+
332
+ - Builds must be deterministic: same inputs produce byte-identical outputs.
333
+ - Strip debug paths and build IDs from release artifacts.
334
+ - Verify reproducibility in CI by building twice and comparing checksums.
335
+
336
+ ## Test Result Reporting
337
+
338
+ - Use a structured test reporter (e.g. `gotestsum`, `pytest-junit`) to produce JUnit XML output.
339
+ - Upload JUnit XML as a workflow artifact for test history and flake detection.
340
+ - Publish a human-readable summary to `$GITHUB_STEP_SUMMARY` on each run.
341
+
342
+ ## Concurrency
343
+
344
+ - Add concurrency control to cancel superseded runs for the same branch/PR.
345
+ - Concurrency group key: `${{ github.workflow }}-${{ github.ref }}`.
346
+ - `cancel-in-progress: true` for PR workflows; `cancel-in-progress: false` for main branch and release.
347
+
348
+ ## Code Coverage Gate
349
+
350
+ ### Requirement
351
+
352
+ - Any PR that reduces code coverage below the current baseline is automatically rejected.
353
+ - This is a required status check; it blocks merge regardless of review approval.
354
+
355
+ ### Baseline Storage
356
+
357
+ - Coverage baseline is stored in the repository itself at `coverage/baseline.json`.
358
+ - The baseline is committed to the default branch after every successful merge.
359
+ - Storing in-repo provides: version history, blame, auditability, and no external service dependency.
360
+ - Format: `{ "total": 87.4, "updated": "2026-03-09T12:00:00Z", "commit": "<sha>" }`
361
+
362
+ ### CI Workflow
363
+
364
+ - PR job: compute current coverage → download `coverage/baseline.json` from the base branch →
365
+ compare → fail if current < baseline.
366
+ - Post-merge job (runs on default branch only): recompute coverage → commit updated
367
+ `coverage/baseline.json` → push directly (no PR required for baseline updates).
368
+ - The post-merge baseline update commit must be signed and attributed to the CI bot identity.
369
+ - Baseline update commits use a conventional commit message: `chore: update coverage baseline to XX.X%`.
370
+
371
+ ### Coverage Script
372
+
373
+ - Coverage computation lives in `scripts/test/coverage.sh`, not in workflow YAML.
374
+ - The script outputs a JSON report and exits non-zero if coverage drops below baseline.
375
+ - The script is runnable locally so developers can check before pushing.
376
+
377
+ ### Exemptions
378
+
379
+ - PRs that only modify documentation, fixtures, or non-code assets may be exempted via PR label.
380
+ - Exemption label: `coverage-exempt` — requires explicit CODEOWNERS approval to apply.
381
+ - Exemptions are logged in CI output for audit purposes.
382
+
383
+ ## Integrity
384
+
385
+ - Integrity checks run against produced artifacts, not hypothetical outputs.
386
+ - Smoke tests validate meaningful runtime behavior, not just file existence.
387
+
388
+ ## Break-Glass Procedure
389
+
390
+ - A documented break-glass procedure must exist for bypassing CI when a critical security patch must ship.
391
+ - Bypass requires two senior engineers approving outside the normal flow and creation of an incident record.
392
+ - The bypass procedure must maintain an audit trail.
393
+ - Document the break-glass procedure in `docs/break-glass.md` or equivalent.
394
+
395
+ ## Failure Attribution
396
+
397
+ - Keep job responsibilities narrow enough that a failure points to a clear domain.
398
+ - Job names in workflow YAML must be descriptive and match the script concern they delegate to.
399
+
400
+ ## Artifact Naming
401
+
402
+ - Encode target OS, architecture, and feature profile in artifact filenames.
403
+ - Names must be explicit, stable, and reviewable.
404
+
405
+ ## Release Tagging
406
+
407
+ ### Versioning Specification
408
+
409
+ All version numbers follow **Semantic Versioning 2.0.0** — [https://semver.org](https://semver.org).
410
+ That document is canonical and must not be contradicted by project-specific rules.
411
+ Version tags take the form `vMAJOR.MINOR.PATCH` with no pre-release or build-metadata
412
+ extensions at this time.
413
+
414
+ Specific semver.org rules in force:
415
+
416
+ - **[Item 2](https://semver.org/#spec-item-2):** Version numbers take the form
417
+ `MAJOR.MINOR.PATCH` — non-negative integers, no leading zeroes.
418
+ - **[Item 4](https://semver.org/#spec-item-4):** MAJOR is incremented for incompatible
419
+ API changes; MINOR and PATCH reset to 0.
420
+ - **[Item 5](https://semver.org/#spec-item-5):** MINOR is incremented for new
421
+ backward-compatible functionality; PATCH resets to 0.
422
+ - **[Item 6](https://semver.org/#spec-item-6):** PATCH is incremented for
423
+ backward-compatible bug fixes only.
424
+ - **[Item 9](https://semver.org/#spec-item-9):** Major version zero (`0.y.z`) is for
425
+ initial development. The public API is not yet stable and anything may change.
426
+ - **[Item 10](https://semver.org/#spec-item-10):** `1.0.0` defines the first stable
427
+ public API. Subsequent increments follow the rules above without exception.
428
+
429
+ ### Tagging is a Developer Action — Never CI
430
+
431
+ - Release version tags (`vX.Y.Z`) are created exclusively by developers running
432
+ `scripts/release/create-release.sh` from a local checkout.
433
+ - CI must never create, push, or move version tags under any circumstance.
434
+ - This is a hard policy: no workflow file may contain `git tag`, `git push --tags`,
435
+ or equivalent tag-creation steps.
436
+ - Rationale: a tag is a public commitment about what version of the software a
437
+ commit represents. That decision requires human judgment and cannot be delegated
438
+ to automation.
439
+
440
+ ### Automatic Tag Triggering Is Permitted
441
+
442
+ - CI workflows _triggered by_ a pushed tag (e.g. `on: push: tags: ['v*']`) are
443
+ permitted and expected — they run the release pipeline once a developer has
444
+ pushed a tag.
445
+ - The distinction is: CI reacts to tags; CI does not create tags.
446
+
447
+ ### Version Computation
448
+
449
+ - Bump level (major / minor / patch) is derived from `pkg/config/schema.yaml` by
450
+ diffing the field set at the previous tag against the field set at HEAD:
451
+ - Removed fields → breaking change → major bump
452
+ ([semver.org item 4](https://semver.org/#spec-item-4)).
453
+ - Added fields (no removals) → new feature → minor bump
454
+ ([semver.org item 5](https://semver.org/#spec-item-5)).
455
+ - No field surface change → patch bump
456
+ ([semver.org item 6](https://semver.org/#spec-item-6)).
457
+ - `scripts/release/create-release.sh` performs this computation, presents
458
+ findings to the developer for approval, then creates and pushes the annotated
459
+ tag on confirmation.
460
+ - `scripts/release/gen-schema.sh` regenerates `pkg/config/schema.yaml` from
461
+ `cmd/config-schema/main.go` after any change to the config struct. The
462
+ consistency suite enforces that `schema.yaml` is never stale.
463
+
464
+ ### --force Override Rules
465
+
466
+ Developers may override the computed version with `--force vX.Y.Z`. Only bare
467
+ `MAJOR.MINOR.PATCH` versions are accepted; pre-release identifiers (`-alpha.1`)
468
+ and build metadata (`+sha`) are not supported by the tooling at this time.
469
+
470
+ - **Pre-1.0 (major = 0):** Only versions in the `0.x.y` range or exactly `v1.0.0`
471
+ are accepted. Forcing `v1.1.0` or `v2.0.0` from a `0.x` base is rejected.
472
+ - **Post-1.0 (major ≥ 1):** `--force` is restricted to exact single-step increments:
473
+ - Patch: `cur_maj.cur_min.(cur_pat+1)` — always allowed.
474
+ - Minor: `cur_maj.(cur_min+1).0` — always allowed (e.g. internal improvements).
475
+ - Major: only accepted when breaking changes are detected **and** the forced
476
+ version exactly matches the auto-computed version. `--force` cannot suppress
477
+ or redirect a breaking-change bump, and cannot skip versions.
478
+
479
+ ## Do Not
480
+
481
+ - Omit `workflow_dispatch:` from any workflow's trigger block.
482
+ - Call bash, tools, or scripts directly from any YAML `run:` step — every `run:` must be `make <target>`.
483
+ - Rebuild artifacts in downstream jobs that could download a previously built artifact.
484
+ - Use per-OS build matrices when cross-compilation achieves the same result.
485
+ - Mix concerns (build + test + publish) in one script or one job.
486
+ - Silently alter triggers, required checks, or artifact semantics when making surgical CI changes.
487
+ - Pin action versions to tags instead of commit SHAs.
488
+ - Use `pull_request_target` without explicit safety guards against fork-contributed code.
489
+ - Grant elevated permissions at the workflow level when job-level scoping is possible.
490
+ - Use long-lived static credentials when OIDC federation is available.
491
+ - Echo or log secret values in scripts or workflow output.
492
+ - Skip the PR policy gate for any PR regardless of urgency (break-glass procedure exists for emergencies).
493
+ - Allow release jobs to run without environment protection and required human reviewers.
494
+ - Ship release artifacts without provenance attestation, signatures, or checksums.
495
+ - Create, push, or move version tags from CI. Tags are a developer action only.
496
+ - Bypass the `create-release.sh` script to tag manually without the schema-diff approval step.
@@ -0,0 +1,156 @@
1
+ # Coding Standards
2
+
3
+ These rules govern all code edits in this project. They are non-negotiable.
4
+
5
+ **Maturity:** Required
6
+ **Version:** 1.0.0
7
+ **Last Reviewed:** 2026-03-09
8
+
9
+ ## Core Preservation
10
+
11
+ 1. No drift — do not remove code, comments, structure, or behavior unless explicitly instructed or strictly required.
12
+ 2. Semantic preservation is the default.
13
+ 3. Existing comments are sticky — do not delete or rewrite unless wrong/obsolete due to the current change.
14
+ 4. Reader trust is paramount — changes must be locally verifiable by a careful human reviewer.
15
+ 5. Preserve error-handling strategy — no silent discards, no wrapping-depth changes.
16
+
17
+ ## Commenting Rules
18
+
19
+ 6. Comments explain enduring intent, invariants, contracts, architecture — not patch notes.
20
+ 7. No "added this", "fixed this", "temporary workaround" style narration.
21
+ 8. Comments are file-local; no broader project lore unless strictly necessary.
22
+ 9. Prefer code over commentary for the obvious.
23
+ 10. When updating code, update any nearby comment that would become misleading.
24
+
25
+ ## Scope Control
26
+
27
+ 11. All edits to existing files must be surgical — minimum necessary lines/regions/symbols.
28
+ 12. No opportunistic widening — no drive-by refactors, renames, formatting sweeps, dead-code cleanup.
29
+ 13. Do not introduce dead code — no unreachable branches, unused variables/imports, orphaned helpers.
30
+ 14. Do not reflow stable text gratuitously.
31
+ 15. Preserve surrounding code shape.
32
+ 16. Minimize diff surface area; correctness wins over minimalism when they conflict.
33
+
34
+ ## Consistency and Reviewability
35
+
36
+ 17. Human reviewability outranks aesthetic optimization.
37
+ 18. Match local conventions: repo/module > file-local > language style guide.
38
+ 19. Avoid sweeping rewrites.
39
+ 20. Maintain interface stability — no silent signature/flag/config-key changes.
40
+ 21. When practical, preserve backward compatibility; do not silently drop old forms.
41
+ 22. Preserve blame usefulness — keep unchanged lines untouched.
42
+
43
+ ## Refactoring Rules
44
+
45
+ 23. Refactoring is opt-in, not ambient.
46
+ 24. Required refactors must be local.
47
+ 25. Do not bundle unrelated improvements.
48
+ 26. Prefer extension over churn.
49
+
50
+ ## File Editing Discipline
51
+
52
+ 27. Preserve untouched regions exactly.
53
+ 28. Do not reorder imports, declarations, functions, keys, or fields unless required.
54
+ 29. Do not rename symbols for taste.
55
+ 30. Do not change comments/docstrings/whitespace outside the affected area.
56
+ 31. Never leave a file in a partially migrated state.
57
+
58
+ ## New File Discipline
59
+
60
+ 32. Follow naming, package structure, header conventions of adjacent files.
61
+ 33. Creating a new file requires explicit justification; prefer extending existing files.
62
+ 34. Test files may use longer functions, explicit repetition, table-driven patterns — do not compress.
63
+
64
+ ## Style
65
+
66
+ 35. Prefer boring, conventional, long-lived patterns over novelty.
67
+ 36. Follow language style guide unless local style clearly overrides.
68
+ 37. Conservative house style: readability, portability, diff minimization, explicitness, reviewer clarity.
69
+ 38. Style must be consistent within the file and stable across future edits.
70
+
71
+ ## Security
72
+
73
+ 39. No hardcoded credentials, tokens, private keys, disabled TLS verification, open CORS, permissive file modes.
74
+ Note existing violations and raise them rather than propagating them.
75
+
76
+ ## Documentation
77
+
78
+ 40. When observable behavior/flags/config/API/errors change, note that external docs may need updating.
79
+
80
+ ## Formatting and Linting Automation
81
+
82
+ 41. Format and lint via project scripts, not ad-hoc commands.
83
+ 42. Propose `scripts/format.sh` and `scripts/lint.sh` if they do not exist.
84
+ 43. Do not embed formatter/linter invocations in YAML or Makefiles when a script can be the entry point.
85
+
86
+ ## Git Hook Integration
87
+
88
+ 44. Pre-commit hook calls a single stable entry point: `scripts/git_precommit.sh`.
89
+ 45. `scripts/git_precommit.sh` invokes format + lint scripts; no logic duplicated in the hook itself.
90
+
91
+ ## Change Justification
92
+
93
+ 46. Every non-trivial change must be justifiable as necessary.
94
+ 47. No speculative fixes.
95
+ 48. No silent cleanup of unconventional-but-correct code.
96
+ 49. Preserve intent over stylistic purity.
97
+
98
+ ## Completeness and Consistency
99
+
100
+ 50. Do not leave referenced functions/variables/imports/code paths broken.
101
+ 51. No partial renames, partial signature migrations, inconsistent call-site updates.
102
+ 52. Changed code must remain internally consistent with the rest of the file.
103
+ 53. Prefer a complete narrow fix over an ambitious broad rewrite.
104
+
105
+ ## Default Operating Principle
106
+
107
+ 54. Treat every edit as a constrained maintenance operation, not a redesign opportunity.
108
+ 55. Ideal change: minimal, correct, legible, locally consistent, unsurprising.
109
+
110
+ ## Concurrency and Shared-State Safety
111
+
112
+ 56. Do not remove synchronization primitives or introduce unprotected shared state.
113
+ 57. Do not introduce goroutines/threads/async into previously synchronous code paths.
114
+ 58. Do not move code across concurrency boundaries without preserving ordering and safety.
115
+
116
+ ## Type and Contract Safety
117
+
118
+ 59. Do not widen specific types to any/interface{}/generic containers.
119
+ 60. No unsafe casts, reflection-driven access, or dynamic fallbacks where typed alternatives exist.
120
+ 61. Do not remove type checks, assertions, validation, or boundary contracts without equal replacement.
121
+
122
+ ## Ambiguity Control
123
+
124
+ 62. When ambiguous, prefer the narrowest conservative interpretation consistent with literal text and intent.
125
+ 63. Seek clarification only when conservative interpretation produces broken, incomplete, or unsafe output.
126
+
127
+ ## Complexity Preservation
128
+
129
+ 64. Do not introduce worse asymptotic complexity where a better one existed.
130
+ 65. Do not replace indexed/cached/streaming/incremental behavior with full scans or redundant allocation.
131
+
132
+ ## Source File Trailing Newlines
133
+
134
+ 66. Every text source file (`.go`, `.sh`, `.md`, `.yml`, `.yaml`, `.toml`, `.json`,
135
+ `.gitignore`, `.gitattributes`) MUST end with a single trailing newline character.
136
+ 67. Verify and fix trailing newlines before handing off any file edits.
137
+ 68. Adding a new text-format type to the repo requires updating `scripts/lint/newlines.sh`
138
+ and the pre-commit hook in the same commit.
139
+
140
+ ## Go Module Version Format
141
+
142
+ 69. The `go` directive in `go.mod` MUST use the two-component form: `go 1.25` — never `go 1.25.0`.
143
+ 70. After every `go mod tidy` or `go get`, verify the `go` directive and strip any trailing `.0`.
144
+
145
+ ## Static Analysis Suppression
146
+
147
+ 71. The standalone `staticcheck` binary does NOT respect `//nolint:staticcheck` directives.
148
+ Use `//lint:ignore SA1019 <reason>` instead.
149
+ 72. Place the `//lint:ignore` directive on its own line immediately above the flagged statement —
150
+ inline (end-of-line) placement may be silently ignored.
151
+
152
+ ## Program Entry Points
153
+
154
+ 73. `main` (and any program entry point) must be KISS: orchestration and wiring only.
155
+ 74. No default behaviors, business logic, or configuration in `main` — those belong in the library.
156
+ 75. A correct `main` is ~15 lines: parse flags, call library, handle exit.