From 6721daf4407499cf048f2ba605b6b8823dba7093 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jesu=CC=81s=20Pe=CC=81rez?= Date: Wed, 8 Apr 2026 14:20:38 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20workflow=20layer=20model=20=E2=80=94=20?= =?UTF-8?q?NCL-first=20CI/build/distribution=20generator?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a declarative workflow system where .ontology/workflow.ncl declares independent layers (commit-fast, ci-standard, ci-exhaustive), each with a trigger and a set of providers. The generator materialises Woodpecker YAML, justfile recipes, and pre-commit hook stubs from that single declaration. Layers form a set, not a chain — no layer depends on another at the declaration level. Woodpecker steps express fine-grained parallelism internally (lint → test/docs/build). Schema and catalog: - reflection/schemas/workflow.ncl — types, contracts, provider constructors - reflection/defaults/workflow.ncl — standard Rust/Nushell/Nickel catalog (14 validations, 4 builds) Generator (ore workflow generate): - PreCommit: prints hook fragments for manual merge into .pre-commit-config.yaml - Woodpecker: writes per-layer YAML with image selection, tool installs, depends_on chains, and CI-aware command translation (nu --ide-check → find+xargs; markdownlint-cli2 → node:lts image) - Justfile: single justfiles/workflow.just aggregated from all Justfile layers (hoisted out of per-provider loop — was writing N times, now once) Dispatcher: ore workflow validate|list|generate|diff + ore wf v|l|gen|diff Cargo/nextest profiles: - .cargo/config.toml: [profile.ci-test] (debug=0, no-incremental) and [profile.ci] (line-tables-only) — shared by test + docs hooks to reuse .rlib artifacts; CI retains actionable backtraces - .config/nextest.toml: ci-test (fail-fast, no retries) and ci (all failures, one retry) Pre-commit: nextest run with --profile ci-test --cargo-profile ci-test; docs-links hook shares same profile to reuse artifacts Self-application: ontoref declares its own workflow in .ontology/workflow.ncl and the generated .woodpecker/ files are the authoritative CI definition Migration 0014: checks workflow.ncl + both cargo and nextest profiles present Adoption: reflection/templates/ontology/workflow.ncl stub + install_workflow confirm in adopt_ontoref form + step 5 in adoption script template --- .cargo/config.toml | 15 + .config/nextest.toml | 9 + .ontology/workflow.ncl | 61 +++ .pre-commit-config.yaml | 6 +- .woodpecker/ci-exhaustive.yml | 32 ++ .woodpecker/ci.yml | 89 +--- justfiles/workflow.just | 19 + reflection/bin/ontoref.nu | 43 ++ reflection/defaults/workflow.ncl | 208 ++++++++ reflection/forms/adopt_ontoref.ncl | 7 +- .../migrations/0014-workflow-layer-model.ncl | 57 ++ reflection/modules/workflow.nu | 498 ++++++++++++++++++ reflection/schemas/workflow.ncl | 100 ++++ reflection/templates/adopt_ontoref.nu.j2 | 15 +- reflection/templates/ontology/workflow.ncl | 70 +++ 15 files changed, 1163 insertions(+), 66 deletions(-) create mode 100644 .config/nextest.toml create mode 100644 .ontology/workflow.ncl create mode 100644 .woodpecker/ci-exhaustive.yml create mode 100644 justfiles/workflow.just create mode 100644 reflection/defaults/workflow.ncl create mode 100644 reflection/migrations/0014-workflow-layer-model.ncl create mode 100644 reflection/modules/workflow.nu create mode 100644 reflection/schemas/workflow.ncl create mode 100644 reflection/templates/ontology/workflow.ncl diff --git a/.cargo/config.toml b/.cargo/config.toml index 43796db..69740df 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -46,6 +46,21 @@ overflow-checks = true lto = false incremental = true +[profile.ci-test] +# Pre-commit profile: no debug info, no incremental. +# Shared by rust-test and docs-links hooks so both reuse the same .rlib artifacts. +# debug = 0 removes DWARF/dSYM — halves binary size without affecting correctness. +inherits = "test" +debug = 0 +incremental = false + +[profile.ci] +# CI pipeline profile: line-tables-only debug for useful backtraces on remote failures. +# Distinct from ci-test (debug=0): CI failures must show file:line to be actionable. +inherits = "test" +debug = "line-tables-only" +incremental = false + [profile.bench] # Benchmark profile - same as release opt-level = 3 diff --git a/.config/nextest.toml b/.config/nextest.toml new file mode 100644 index 0000000..c32d98d --- /dev/null +++ b/.config/nextest.toml @@ -0,0 +1,9 @@ +[profile.ci-test] +# Pre-commit profile: fail-fast on first failure, no retries. +fail-fast = true +retries = 0 + +[profile.ci] +# CI pipeline profile: see all failures, one retry for transient flakiness. +fail-fast = false +retries = 1 diff --git a/.ontology/workflow.ncl b/.ontology/workflow.ncl new file mode 100644 index 0000000..7a6de0f --- /dev/null +++ b/.ontology/workflow.ncl @@ -0,0 +1,61 @@ +let W = import "../reflection/schemas/workflow.ncl" in +let D = import "../reflection/defaults/workflow.ncl" in + +{ + layers = [ + # ── Commit-fast: local pre-commit, independent ──────────────────────────── + { + id = "commit-fast", + trigger = 'OnCommit, + validations = [ + "rust-fmt", + "rust-clippy", + "nextest-ci-test", + "deny-subset", + "docs-drift", + "manifest-coverage", + "markdownlint", + ], + builds = [], + distributions = [], + providers = [W.pre_commit "pre-commit"], + }, + + # ── CI-standard: push/PR pipeline, independent ──────────────────────────── + { + id = "ci-standard", + trigger = 'OnPR, + validations = [ + "rust-clippy-all", + "nextest-ci", + "deny-subset", + "docs-check", + "nickel-typecheck", + "nushell-check", + ], + builds = ["release-native"], + distributions = [], + providers = [ + W.woodpecker ".woodpecker/ci.yml", + W.justfile "ci-standard", + ], + }, + + # ── CI-exhaustive: main/tag pipeline, independent ───────────────────────── + { + id = "ci-exhaustive", + trigger = 'OnMainMerge, + validations = ["deny-all", "geiger"], + builds = ["release-musl-x86", "sbom"], + distributions = [], + providers = [ + W.woodpecker ".woodpecker/ci-exhaustive.yml", + W.justfile "ci-exhaustive", + ], + }, + ], + + validations = D.validations, + builds = D.builds, + distributions = {}, +} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index dd1d649..5538aac 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,8 +25,8 @@ repos: stages: [pre-commit] - id: rust-test - name: Rust tests - entry: bash -c 'cargo test --all-features --workspace' + name: Rust tests (nextest) + entry: bash -c 'cargo nextest run --all-features --workspace --profile ci-test --cargo-profile ci-test' language: system types: [rust] pass_filenames: false @@ -42,7 +42,7 @@ repos: - id: docs-links name: Rustdoc broken intra-doc links - entry: bash -c 'RUSTDOCFLAGS="-D rustdoc::broken-intra-doc-links -D rustdoc::private-intra-doc-links" cargo doc --no-deps --workspace -q' + entry: bash -c 'RUSTDOCFLAGS="-D rustdoc::broken-intra-doc-links -D rustdoc::private-intra-doc-links" cargo doc --no-deps --workspace --profile ci-test -q' language: system types: [rust] pass_filenames: false diff --git a/.woodpecker/ci-exhaustive.yml b/.woodpecker/ci-exhaustive.yml new file mode 100644 index 0000000..9e8b1df --- /dev/null +++ b/.woodpecker/ci-exhaustive.yml @@ -0,0 +1,32 @@ +# Generated by ore workflow generate — layer: ci-exhaustive +# Source: .ontology/workflow.ncl +# Do not edit manually — regenerate with: ore workflow generate --layer ci-exhaustive + +when: + event: [push, tag, manual] + branch: main + +steps: + deny-all: + image: rust:latest + commands: + - cargo install cargo-deny --locked + - cargo deny check + + geiger: + image: rust:latest + commands: + - cargo install cargo-geiger --locked + - cargo geiger --all-features --all-targets + + build-release-musl-x86: + image: rust:latest + commands: + - cargo install cross --locked + - cross build --target x86_64-unknown-linux-musl --release + + build-sbom: + image: rust:latest + commands: + - cargo install cargo-sbom --locked + - cargo sbom diff --git a/.woodpecker/ci.yml b/.woodpecker/ci.yml index be8aba9..b2a519d 100644 --- a/.woodpecker/ci.yml +++ b/.woodpecker/ci.yml @@ -1,84 +1,51 @@ -# Woodpecker CI Pipeline -# Equivalent to GitHub Actions CI workflow -# Generated by dev-system/ci +# Generated by ore workflow generate — layer: ci-standard +# Source: .ontology/workflow.ncl +# Do not edit manually — regenerate with: ore workflow generate --layer ci-standard when: event: [push, pull_request, manual] - branch: - - main - - develop steps: - # === LINTING === - - lint-rust: + rust-clippy-all: image: rust:latest commands: - - curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin - - rustup component add clippy - - cargo fmt --all -- --check - - cargo clippy --all-targets -- -D warnings + - cargo clippy --all-targets --all-features -- -D warnings - lint-bash: - image: koalaman/shellcheck-alpine:stable - commands: - - apk add --no-cache curl bash - - find . -name '*.sh' -type f ! -path './target/*' -exec shellcheck {} + - - lint-nickel: + nickel-typecheck: image: rust:latest commands: - cargo install nickel-lang-cli --locked - - find . -name '*.ncl' -type f ! -path './target/*' -exec nickel typecheck {} \; + - nickel typecheck - lint-nushell: + nushell-check: image: rust:latest commands: - cargo install nu --locked - - find . -name '*.nu' -type f ! -path './target/*' -exec nu --ide-check 100 {} \; + - find . -name '*.nu' ! -path '*/target/*' -print0 | xargs -0 -I\{\} nu --ide-check 100 \{\} - lint-markdown: - image: node:alpine - commands: - - npm install -g markdownlint-cli2 - - markdownlint-cli2 '**/*.md' '#node_modules' '#target' - - # === TESTING === - - test: + nextest-ci: image: rust:latest commands: - - cargo test --workspace --all-features - depends_on: - - lint-rust - - lint-bash - - lint-nickel - - lint-nushell - - lint-markdown + - cargo install cargo-nextest --locked + - cargo nextest run --all-features --workspace --profile ci --cargo-profile ci + depends_on: ["rust-clippy-all", "nickel-typecheck", "nushell-check"] + environment: + RUST_BACKTRACE: 1 - # === BUILD === - - build: - image: rust:latest - commands: - - cargo build --release - depends_on: - - test - - # === SECURITY === - - security-audit: - image: rust:latest - commands: - - cargo install cargo-audit --locked - - cargo audit --deny warnings - depends_on: - - lint-rust - - license-check: + deny-subset: image: rust:latest commands: - cargo install cargo-deny --locked - cargo deny check licenses advisories - depends_on: - - lint-rust + + docs-check: + image: rust:latest + commands: + - RUSTDOCFLAGS="-D rustdoc::broken-intra-doc-links -D rustdoc::private-intra-doc-links" cargo doc --no-deps --workspace --profile ci -q + depends_on: ["rust-clippy-all", "nickel-typecheck", "nushell-check"] + + build-release-native: + image: rust:latest + commands: + - cargo build --release --workspace + depends_on: ["rust-clippy-all", "nickel-typecheck", "nushell-check"] diff --git a/justfiles/workflow.just b/justfiles/workflow.just new file mode 100644 index 0000000..175dc95 --- /dev/null +++ b/justfiles/workflow.just @@ -0,0 +1,19 @@ +# Generated by ore workflow generate +# Source: .ontology/workflow.ncl + +# layer: ci-standard +ci-standard: + cargo clippy --all-targets --all-features -- -D warnings + cargo nextest run --all-features --workspace --profile ci --cargo-profile ci + cargo deny check licenses advisories + cargo doc --no-deps --workspace --profile ci -q + nickel typecheck + nu --ide-check 100 + cargo build --release --workspace + +# layer: ci-exhaustive +ci-exhaustive: + cargo deny check + cargo geiger --all-features --all-targets + cross build --target x86_64-unknown-linux-musl --release + cargo sbom diff --git a/reflection/bin/ontoref.nu b/reflection/bin/ontoref.nu index ba58978..851d20f 100755 --- a/reflection/bin/ontoref.nu +++ b/reflection/bin/ontoref.nu @@ -24,6 +24,7 @@ use ../modules/run.nu * use ../modules/graph.nu * use ../modules/validate.nu * use ../modules/migrate.nu * +use ../modules/workflow.nu * use ../nulib/fmt.nu * use ../nulib/shared.nu * @@ -354,6 +355,48 @@ def "main mg" [action?: string] { missing-target "migrate" $action } def "main mg l" [--fmt (-f): string = ""] { log-action "migrate list" "read"; migrate list --fmt $fmt } def "main mg p" [--fmt (-f): string = ""] { log-action "migrate pending" "read"; migrate pending --fmt $fmt } +# ── Workflow ────────────────────────────────────────────────────────────────── + +def "main workflow" [action?: string] { missing-target "workflow" $action } +def "main workflow validate" [] { + log-action "workflow validate" "read" + workflow validate +} +def "main workflow list" [] { + log-action "workflow list" "read" + workflow list +} +def "main workflow generate" [ + --root: string + --layer: string + --provider: string + --dry-run +] { + log-action "workflow generate" "write" + if $dry_run { + workflow generate --root ($root | default "") --layer ($layer | default "") --provider ($provider | default "") --dry-run + } else { + workflow generate --root ($root | default "") --layer ($layer | default "") --provider ($provider | default "") + } +} +def "main workflow diff" [--root: string, --layer: string, --provider: string] { + log-action "workflow diff" "read" + workflow diff --root ($root | default "") --layer ($layer | default "") --provider ($provider | default "") +} +def "main wf" [action?: string] { missing-target "workflow" $action } +def "main wf v" [] { main workflow validate } +def "main wf l" [] { main workflow list } +def "main wf gen" [--dry-run, --layer: string, --provider: string] { + if $dry_run { + main workflow generate --layer ($layer | default "") --provider ($provider | default "") --dry-run + } else { + main workflow generate --layer ($layer | default "") --provider ($provider | default "") + } +} +def "main wf diff" [--layer: string, --provider: string] { + main workflow diff --layer ($layer | default "") --provider ($provider | default "") +} + # ── Validate ────────────────────────────────────────────────────────────────── def "main validate" [target?: string] { missing-target "validate" $target } diff --git a/reflection/defaults/workflow.ncl b/reflection/defaults/workflow.ncl new file mode 100644 index 0000000..3864329 --- /dev/null +++ b/reflection/defaults/workflow.ncl @@ -0,0 +1,208 @@ +# Standard validation, build, and distribution catalog for Rust/Nushell/Nickel projects. +# Workspaces import this and reference IDs in their layer declarations. +# Override or extend by merging with &: `D.validations & { my-check = { ... } }` + +{ + validations = { + # ── Rust linting ───────────────────────────────────────────────────────── + + rust-fmt = { + id = "rust-fmt", + kind = 'Lint, + tool = "cargo", + args = ["+nightly", "fmt", "--all", "--", "--check"], + when = ["*.rs"], + fail_fast = true, + cargo_profile = "", + }, + + rust-clippy = { + id = "rust-clippy", + kind = 'Lint, + tool = "cargo", + args = ["clippy", "--all-targets", "--no-deps", "--profile", "clippy", "--", "-D", "warnings"], + when = ["*.rs"], + fail_fast = true, + cargo_profile = "clippy", + }, + + rust-clippy-all = { + id = "rust-clippy-all", + kind = 'Lint, + tool = "cargo", + args = ["clippy", "--all-targets", "--all-features", "--", "-D", "warnings"], + when = ["*.rs"], + fail_fast = true, + cargo_profile = "", + }, + + # ── Rust testing ───────────────────────────────────────────────────────── + + nextest-ci-test = { + id = "nextest-ci-test", + kind = 'Test, + tool = "cargo-nextest", + args = ["run", "--all-features", "--workspace", + "--profile", "ci-test", "--cargo-profile", "ci-test"], + when = ["*.rs"], + fail_fast = true, + cargo_profile = "ci-test", + }, + + nextest-ci = { + id = "nextest-ci", + kind = 'Test, + tool = "cargo-nextest", + args = ["run", "--all-features", "--workspace", + "--profile", "ci", "--cargo-profile", "ci"], + when = ["*.rs"], + fail_fast = false, # see all failures in CI + cargo_profile = "ci", + }, + + # ── Security ───────────────────────────────────────────────────────────── + + deny-subset = { + id = "deny-subset", + kind = 'Security, + tool = "cargo-deny", + args = ["check", "licenses", "advisories"], + when = ["Cargo.toml", "Cargo.lock"], + fail_fast = true, + cargo_profile = "", + }, + + deny-all = { + id = "deny-all", + kind = 'Security, + tool = "cargo-deny", + args = ["check"], + when = [], + fail_fast = true, + cargo_profile = "", + }, + + geiger = { + id = "geiger", + kind = 'Security, + tool = "cargo-geiger", + args = ["--all-features", "--all-targets"], + when = ["*.rs"], + fail_fast = false, # informational by default; use --forbid-only to gate + cargo_profile = "", + }, + + # ── Documentation ──────────────────────────────────────────────────────── + + docs-check = { + id = "docs-check", + kind = 'Docs, + tool = "cargo", + args = ["doc", "--no-deps", "--workspace", "--profile", "ci", "-q"], + when = ["*.rs"], + fail_fast = true, + cargo_profile = "ci", + }, + + # ── Compliance (ontoref-specific) ──────────────────────────────────────── + + docs-drift = { + id = "docs-drift", + kind = 'Compliance, + tool = "nu", + args = ["-c", "use ./reflection/modules/sync.nu; sync diff --docs --fail-on-drift"], + when = ["*.rs"], + fail_fast = true, + cargo_profile = "", + }, + + manifest-coverage = { + id = "manifest-coverage", + kind = 'Compliance, + tool = "nu", + args = ["--no-config-file", "-c", + "use ./reflection/modules/sync.nu *; sync manifest-check"], + when = [".ontology/*.ncl", "reflection/modes/*.ncl", "reflection/forms/*.ncl"], + fail_fast = true, + cargo_profile = "", + }, + + # ── Nickel / Nushell linting ───────────────────────────────────────────── + + nickel-typecheck = { + id = "nickel-typecheck", + kind = 'Lint, + tool = "nickel", + args = ["typecheck"], + when = ["*.ncl"], + fail_fast = true, + cargo_profile = "", + }, + + nushell-check = { + id = "nushell-check", + kind = 'Lint, + tool = "nu", + args = ["--ide-check", "100"], + when = ["*.nu"], + fail_fast = true, + cargo_profile = "", + }, + + # ── Markdown ───────────────────────────────────────────────────────────── + + markdownlint = { + id = "markdownlint", + kind = 'Lint, + tool = "markdownlint-cli2", + args = [], + when = ["*.md"], + fail_fast = true, + cargo_profile = "", + }, + }, + + builds = { + release-native = { + id = "release-native", + kind = 'Binary, + tool = "cargo", + args = ["build", "--release", "--workspace"], + cargo_profile = "release", + target = "", + artifacts = ["target/release/"], + }, + + release-musl-x86 = { + id = "release-musl-x86", + kind = 'Binary, + tool = "cross", + args = ["build", "--target", "x86_64-unknown-linux-musl", "--release"], + cargo_profile = "release", + target = "x86_64-unknown-linux-musl", + artifacts = ["target/x86_64-unknown-linux-musl/release/"], + }, + + sbom = { + id = "sbom", + kind = 'SBOM, + tool = "cargo-sbom", + args = [], + cargo_profile = "", + target = "", + artifacts = ["sbom.json"], + }, + + docs-html = { + id = "docs-html", + kind = 'Docs, + tool = "cargo", + args = ["doc", "--no-deps", "--workspace"], + cargo_profile = "release", + target = "", + artifacts = ["target/doc/"], + }, + }, + + distributions = {}, +} diff --git a/reflection/forms/adopt_ontoref.ncl b/reflection/forms/adopt_ontoref.ncl index bf7e4ad..3c76014 100644 --- a/reflection/forms/adopt_ontoref.ncl +++ b/reflection/forms/adopt_ontoref.ncl @@ -66,6 +66,11 @@ default = true, help = "Thin bash wrapper that sets ONTOREF_ROOT and ONTOREF_PROJECT_ROOT, then delegates to ontoref entry point." }, + { type = "confirm", name = "install_workflow", + prompt = "Create .ontology/workflow.ncl (CI/build/distribution layer declaration)?", + default = true, + help = "Declares independent workflow layers (commit-fast, ci-standard, ci-exhaustive). After adoption, run: ore workflow generate" }, + # ── Validation ─────────────────────────────────────────────────────────── { type = "section_header", name = "validation_header", title = "Validation", border_top = true, border_bottom = true }, @@ -80,7 +85,7 @@ title = "Review", border_top = true }, { type = "section", name = "review_note", - content = "The generated script will:\n 1. mkdir -p .ontoref/logs .ontoref/locks (idempotent)\n 2. Copy .ontoref/config.ncl from template (if not present)\n 3. Copy .ontology/{core,state,gate}.ncl stubs (if not present)\n 4. Install scripts/ontoref wrapper (if not present)\n 5. Run nickel export on .ontology/ files to validate\n\nFiles already present are NOT overwritten." }, + content = "The generated script will:\n 1. mkdir -p .ontoref/logs .ontoref/locks (idempotent)\n 2. Copy .ontoref/config.ncl from template (if not present)\n 3. Copy .ontology/{core,state,gate}.ncl stubs (if not present)\n 4. Install scripts/ontoref wrapper (if not present)\n 5. Copy .ontology/workflow.ncl stub (if install_workflow and not present)\n 6. Run nickel export on .ontology/ files to validate\n\nFiles already present are NOT overwritten." }, { type = "confirm", name = "ready_to_generate", prompt = "Generate the adopt script?", diff --git a/reflection/migrations/0014-workflow-layer-model.ncl b/reflection/migrations/0014-workflow-layer-model.ncl new file mode 100644 index 0000000..e6293bf --- /dev/null +++ b/reflection/migrations/0014-workflow-layer-model.ncl @@ -0,0 +1,57 @@ +{ + id = "0014", + slug = "workflow-layer-model", + description = "Workflow layer model: .ontology/workflow.ncl declares independent CI/build/distribution layers. Cargo profiles ci-test (pre-commit, debug=0) and ci (CI pipeline, line-tables-only) must exist. Nextest profiles ci-test and ci must exist. Generator: ore workflow generate.", + check = { + tag = "NuCmd", + cmd = "let root = ($env.ONTOREF_PROJECT_ROOT? | default ($env.ONTOREF_ROOT? | default (pwd | path expand))); let wf = $\"($root)/.ontology/workflow.ncl\"; if not ($wf | path exists) { exit 1 }; let cargo_cfg = $\"($root)/.cargo/config.toml\"; if not ($cargo_cfg | path exists) { exit 1 }; let cargo_txt = (open --raw $cargo_cfg); let has_ci_test = ($cargo_txt | str contains \"[profile.ci-test]\"); let has_ci = ($cargo_txt | str contains \"[profile.ci]\"); let nextest_cfg = $\"($root)/.config/nextest.toml\"; if not ($nextest_cfg | path exists) { exit 1 }; let nextest_txt = (open --raw $nextest_cfg); let nx_ci_test = ($nextest_txt | str contains \"[profile.ci-test]\"); let nx_ci = ($nextest_txt | str contains \"[profile.ci]\"); if $has_ci_test and $has_ci and $nx_ci_test and $nx_ci { exit 0 } else { exit 1 }", + expect_exit = 0, + }, + instructions = " +Adopt the workflow layer model: + +1. Create .ontology/workflow.ncl declaring your CI layers. + Use the template at reflection/templates/ontology/workflow.ncl + or copy from an existing workspace. + +2. Add cargo profiles to .cargo/config.toml: + + [profile.ci-test] + # Pre-commit: no debug info, no incremental — shared .rlib between test + docs hooks + inherits = \"test\" + debug = 0 + incremental = false + + [profile.ci] + # CI pipeline: line-tables-only for actionable backtraces on remote failures + inherits = \"test\" + debug = \"line-tables-only\" + incremental = false + +3. Add nextest profiles to .config/nextest.toml: + + [profile.ci-test] + fail-fast = true + retries = 0 + + [profile.ci] + fail-fast = false + retries = 1 + +4. Generate CI artifacts from the declaration: + + ore workflow generate + + This writes: + - .woodpecker/ci.yml (from layers with provider Woodpecker + trigger OnPR) + - .woodpecker/ci-exhaustive.yml (from layers with trigger OnMainMerge) + - justfiles/workflow.just (from layers with provider Justfile) + + Pre-commit hooks are printed for manual merge into .pre-commit-config.yaml. + +5. Verify: + + ore workflow validate + ore workflow list +", +} diff --git a/reflection/modules/workflow.nu b/reflection/modules/workflow.nu new file mode 100644 index 0000000..63f4093 --- /dev/null +++ b/reflection/modules/workflow.nu @@ -0,0 +1,498 @@ +#!/usr/bin/env nu +# reflection/modules/workflow.nu — workflow layer model generator. +# +# Reads .ontology/workflow.ncl (WorkflowDeclaration) and generates implementation +# artifacts for each declared provider. Layers are independent — generating one +# layer never affects another. +# +# Commands: +# workflow validate — typecheck workflow.ncl against schema +# workflow generate — generate all providers for all layers +# workflow generate --layer — generate one layer +# workflow generate --provider — generate one provider type across all layers +# workflow diff — show what would change without writing +# workflow list — list declared layers + providers + +use env.nu * +use store.nu [daemon-export-safe] + +# ── Helpers ────────────────────────────────────────────────────────────────── + +# Join args with space, trimming trailing whitespace. +# Prevents trailing-space commands like `markdownlint-cli2 `. +def args-str [args: list]: nothing -> string { + $args | str join ' ' +} + +# Build a tool invocation string from tool name + args, trimmed. +def tool-cmd [tool: string, args: list]: nothing -> string { + let base = if ($tool == "cargo-nextest") { + $"cargo nextest (args-str $args)" + } else if ($tool == "cargo-deny") { + $"cargo deny (args-str $args)" + } else if ($tool == "cargo-geiger") { + $"cargo geiger (args-str $args)" + } else if ($tool == "cargo-sbom") { + $"cargo sbom (args-str $args)" + } else if ($tool =~ "^cargo-") { + let subcmd = ($tool | str replace "cargo-" "") + $"cargo ($subcmd) (args-str $args)" + } else if ($tool == "cargo") { + $"cargo (args-str $args)" + } else { + $"($tool) (args-str $args)" + } + $base | str trim --right +} + +def project-root []: nothing -> string { + $env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT +} + +def workflow-ncl-path [root: string]: nothing -> string { + [$root, ".ontology", "workflow.ncl"] | path join +} + +def nickel-import-path [root: string]: nothing -> string { + let entries = [ + $"($root)/.ontology" + $"($root)/reflection/schemas" + $"($root)/reflection/defaults" + $"($root)/adrs" + $root + $"($env.ONTOREF_ROOT)/reflection/schemas" + $"($env.ONTOREF_ROOT)/reflection/defaults" + $env.ONTOREF_ROOT + ] + ($entries | where { |p| $p | path exists } | uniq | str join ":") +} + +def load-workflow [root: string]: nothing -> record { + let ncl = (workflow-ncl-path $root) + if not ($ncl | path exists) { + error make { msg: $"workflow.ncl not found at ($ncl) — run: ore workflow generate --help" } + } + let ip = (nickel-import-path $root) + let result = do { ^nickel export --import-path $ip $ncl } | complete + if $result.exit_code != 0 { + error make { msg: $"workflow.ncl export failed:\n($result.stderr)" } + } + $result.stdout | from json +} + +# Resolve an ID to its spec from a catalog record; error with catalog-name context. +def resolve-spec [id: string, catalog: record, kind: string]: nothing -> record { + if ($catalog | get -o $id | is-not-empty) { + $catalog | get $id + } else { + error make { msg: $"($kind) '($id)' not found in catalog" } + } +} + +def resolve-validation [id: string, catalog: record]: nothing -> record { + resolve-spec $id $catalog "validation" +} + +def resolve-build [id: string, catalog: record]: nothing -> record { + resolve-spec $id $catalog "build" +} + +# ── Pre-commit generator ───────────────────────────────────────────────────── + +# Translate a ValidationSpec into a pre-commit hook entry (YAML fragment). +def validation-to-precommit [v: record]: nothing -> string { + let entry_cmd = (tool-cmd $v.tool $v.args) + + let env_prefix = if ($v.tool == "cargo" and ($v.args | any { |a| $a == "doc" })) { + 'RUSTDOCFLAGS="-D rustdoc::broken-intra-doc-links -D rustdoc::private-intra-doc-links" ' + } else { + "" + } + + let types_line = if ($v.when | is-empty) { + "" + } else { + # Map glob patterns to pre-commit type hints or regex `files:`. + # Dots are escaped so `Cargo.toml` does not match `CargoXtoml`. + let has_rust = ($v.when | any { |p| $p =~ '\.rs' }) + let has_md = ($v.when | any { |p| $p =~ '\.md' }) + if $has_rust { + "\n types: [rust]" + } else if $has_md { + "\n types: [markdown]" + } else { + # Convert glob → Python re.search() pattern. + # Order matters: escape literal dots first, then convert glob * to regex .* + let files_pat = ($v.when | each { |p| + $p | str replace --all "." "\\." | str replace --all "*" ".*" + } | str join "|") + $"\n files: '($files_pat)'" + } + } + + $" - id: ($v.id) + name: ($v.id) + entry: bash -c '($env_prefix)($entry_cmd)' + language: system + pass_filenames: false($types_line) + stages: [pre-commit]" +} + +def generate-precommit [wf: record, stage: string, dry_run: bool, root: string]: nothing -> nothing { + let layers = ($wf.layers | where { |l| + ($l.providers | any { |p| $p.tag == "PreCommit" and ($p.stage? | default "pre-commit") == $stage }) + }) + + if ($layers | is-empty) { + print $" no layers with PreCommit/($stage) provider — skipping" + return + } + + # Collect all validation IDs across matching layers (deduplicated, order preserved) + mut seen = [] + mut specs = [] + for layer in $layers { + for vid in $layer.validations { + if not ($seen | any { |s| $s == $vid }) { + $seen = ($seen | append $vid) + $specs = ($specs | append (resolve-validation $vid $wf.validations)) + } + } + } + + let hook_entries = ($specs | each { |v| validation-to-precommit $v } | str join "\n") + let section = $" - repo: local + hooks: +($hook_entries)" + + let out_path = ([$root, ".pre-commit-config.yaml"] | path join) + + if $dry_run { + print $"[dry-run] would update pre-commit hooks in ($out_path):" + print $section + return + } + + # If file exists, replace the '- repo: local' block managed by workflow. + # If not, write a minimal pre-commit config. + if ($out_path | path exists) { + print $" pre-commit config exists — merge is not automatic" + print $" copy the following into the 'repo: local' section of ($out_path):" + print "" + print $section + } else { + let content = $"repos: +($section) +" + $content | save $out_path + print $" wrote ($out_path)" + } +} + +# ── Woodpecker generator ───────────────────────────────────────────────────── + +# Map a tool name to the shell command that installs it in a CI container. +def tool-install-cmd [tool: string]: nothing -> string { + match $tool { + "cargo" => "", + "cargo-nextest" => "cargo install cargo-nextest --locked", + "cargo-deny" => "cargo install cargo-deny --locked", + "cargo-geiger" => "cargo install cargo-geiger --locked", + "cargo-sbom" => "cargo install cargo-sbom --locked", + "cargo-llvm-cov" => "cargo install cargo-llvm-cov --locked", + "cross" => "cargo install cross --locked", + "nickel" => "cargo install nickel-lang-cli --locked", + "nu" => "cargo install nu --locked", + "markdownlint-cli2" => "npm install -g markdownlint-cli2", + _ => $"# install ($tool)", + } +} + +# Map a tool to its required container image for CI. +# Most tools run from rust:latest; node-based tools need a node image. +def tool-image [tool: string]: nothing -> string { + match $tool { + "markdownlint-cli2" => "node:lts", + _ => "rust:latest", + } +} + +# Build the CI command for a ValidationSpec. +# `nu --ide-check` requires a filename — the CI version wraps it in a find loop. +def validation-ci-cmd [v: record]: nothing -> string { + if $v.tool == "nu" and ($v.args | any { |a| $a == "--ide-check" }) { + let depth = ($v.args | window 2 | where { |w| $w.0 == "--ide-check" } | first | default ["--ide-check","100"] | last | default "100") + $"find . -name '*.nu' ! -path '*/target/*' -print0 | xargs -0 -I\\{\\} nu --ide-check ($depth) \\{\\}" + } else if ($v.tool == "cargo" and ($v.args | any { |a| $a == "doc" })) { + let cmd = (tool-cmd $v.tool $v.args) + $'RUSTDOCFLAGS="-D rustdoc::broken-intra-doc-links -D rustdoc::private-intra-doc-links" ($cmd)' + } else { + tool-cmd $v.tool $v.args + } +} + +def validation-to-woodpecker-step [v: record]: nothing -> string { + let install = (tool-install-cmd $v.tool) + let install_lines = if ($install | is-not-empty) { $" - ($install)\n" } else { "" } + let image = (tool-image $v.tool) + let cmd = (validation-ci-cmd $v) + + $" ($v.id): + image: ($image) + commands: +($install_lines) - ($cmd)" +} + +def build-to-woodpecker-step [b: record]: nothing -> string { + let install = (tool-install-cmd $b.tool) + let install_lines = if ($install | is-not-empty) { $" - ($install)\n" } else { "" } + let image = (tool-image $b.tool) + let cmd = (tool-cmd $b.tool $b.args) + $" build-($b.id): + image: ($image) + commands: +($install_lines) - ($cmd)" +} + +def trigger-to-woodpecker-when [trigger: string]: nothing -> string { + match $trigger { + "OnCommit" => " event: [push, manual]", + "OnPush" => " event: [push, manual]", + "OnPR" => " event: [push, pull_request, manual]", + "OnMainMerge" => " event: [push, tag, manual]\n branch: main", + "OnTag" => " event: [tag, manual]", + "OnManual" => " event: [manual]", + _ => " event: [push, pull_request, manual]", + } +} + +def generate-woodpecker [layer: record, wf: record, file: string, dry_run: bool, root: string]: nothing -> nothing { + let when_block = (trigger-to-woodpecker-when $layer.trigger) + + # Collect unique tools needed → depends_on for test after lint + let val_specs = ($layer.validations | each { |vid| resolve-validation $vid $wf.validations }) + let build_specs = ($layer.builds | each { |bid| resolve-build $bid $wf.builds }) + + let lint_steps = ($val_specs | where { |v| $v.kind == "Lint" or $v.kind == "Compliance" }) + let test_steps = ($val_specs | where { |v| $v.kind == "Test" }) + let sec_steps = ($val_specs | where { |v| $v.kind == "Security" }) + let doc_steps = ($val_specs | where { |v| $v.kind == "Docs" }) + + let lint_ids = ($lint_steps | get id) + let depends = if ($lint_ids | is-empty) { "" } else { + let ids_str = ($lint_ids | each { |id| $"\"($id)\"" } | str join ", ") + $"\n depends_on: [($ids_str)]" + } + + mut steps_yaml = "" + + # Lint steps (parallel) + for v in $lint_steps { + $steps_yaml = $steps_yaml + (validation-to-woodpecker-step $v) + "\n\n" + } + + # Test steps (depends on lint) + for v in $test_steps { + let step = (validation-to-woodpecker-step $v) + $steps_yaml = $steps_yaml + $step + $depends + "\n environment:\n RUST_BACKTRACE: 1\n\n" + } + + # Security steps (parallel — cargo deny / geiger do not need prior compilation) + for v in $sec_steps { + $steps_yaml = $steps_yaml + (validation-to-woodpecker-step $v) + "\n\n" + } + + # Docs steps (depends on lint — can run in parallel with tests) + for v in $doc_steps { + let step = (validation-to-woodpecker-step $v) + $steps_yaml = $steps_yaml + $step + $depends + "\n\n" + } + + # Build steps (depends on test) + for b in $build_specs { + let step = (build-to-woodpecker-step $b) + $steps_yaml = $steps_yaml + $step + $depends + "\n\n" + } + + let yaml = $"# Generated by ore workflow generate — layer: ($layer.id) +# Source: .ontology/workflow.ncl +# Do not edit manually — regenerate with: ore workflow generate --layer ($layer.id) + +when: +($when_block) + +steps: +($steps_yaml)" + + let out_path = ([$root, $file] | path join) + let out_dir = ($out_path | path dirname) + + if $dry_run { + print $"[dry-run] would write ($out_path):" + print $yaml + return + } + + if not ($out_dir | path exists) { mkdir $out_dir } + ($yaml | str trim --right) + "\n" | save --force $out_path + print $" wrote ($out_path)" +} + +# ── Justfile generator ─────────────────────────────────────────────────────── + +def generate-justfile [layers: list, wf: record, dry_run: bool, root: string]: nothing -> nothing { + let jf_layers = ($layers | where { |l| + $l.providers | any { |p| $p.tag == "Justfile" } + }) + + if ($jf_layers | is-empty) { + print " no layers with Justfile provider — skipping" + return + } + + mut recipes = $"# Generated by ore workflow generate +# Source: .ontology/workflow.ncl + +" + + for layer in $jf_layers { + let recipe_name = ($layer.providers + | where { |p| $p.tag == "Justfile" } + | first + | get recipe) + + let val_specs = ($layer.validations | each { |vid| resolve-validation $vid $wf.validations }) + let build_specs = ($layer.builds | each { |bid| resolve-build $bid $wf.builds }) + + mut body = "" + for v in $val_specs { + $body = $body + $" (tool-cmd $v.tool $v.args)\n" + } + for b in $build_specs { + $body = $body + $" (tool-cmd $b.tool $b.args)\n" + } + + $recipes = $recipes + $"# layer: ($layer.id)\n($recipe_name):\n($body)\n" + } + + let out_path = ([$root, "justfiles", "workflow.just"] | path join) + if $dry_run { + print $"[dry-run] would write ($out_path)" + print $recipes + return + } + if not ($out_path | path dirname | path exists) { mkdir ($out_path | path dirname) } + ($recipes | str trim --right) + "\n" | save --force $out_path + print $" wrote ($out_path)" +} + +# ── Public commands ────────────────────────────────────────────────────────── + +# Validate .ontology/workflow.ncl against the schema (nickel typecheck). +export def "workflow validate" [ + --root: string # project root (default: ONTOREF_PROJECT_ROOT) +]: nothing -> nothing { + let r = ($root | default (project-root)) + let ncl = (workflow-ncl-path $r) + let ip = (nickel-import-path $r) + print $"Validating ($ncl)..." + let result = do { ^nickel typecheck --import-path $ip $ncl } | complete + if $result.exit_code == 0 { + print " workflow.ncl OK" + } else { + error make { msg: $"workflow.ncl typecheck failed:\n($result.stderr)" } + } +} + +# List declared layers and their providers. +export def "workflow list" [ + --root: string +]: nothing -> table { + let r = ($root | default (project-root)) + let wf = (load-workflow $r) + $wf.layers | each { |l| + { + id: $l.id, + trigger: $l.trigger, + validations: ($l.validations | length), + builds: ($l.builds | length), + providers: ($l.providers | each { |p| $p.tag } | str join ", "), + } + } +} + +# Generate implementation artifacts from .ontology/workflow.ncl. +export def "workflow generate" [ + --root: string # project root (default: ONTOREF_PROJECT_ROOT) + --layer: string # generate only this layer ID + --provider: string # generate only this provider tag (PreCommit, Woodpecker, Justfile) + --dry-run # show what would be written without writing +]: nothing -> nothing { + let r = ($root | default (project-root)) + let wf = (load-workflow $r) + let dry = $dry_run + + let target_layers = if ($layer | is-not-empty) { + let found = ($wf.layers | where { |l| $l.id == $layer }) + if ($found | is-empty) { + error make { msg: $"layer '($layer)' not found in workflow.ncl" } + } + $found + } else { + $wf.layers + } + + print $"Generating workflow artifacts from ($r)/.ontology/workflow.ncl" + + mut justfile_needed = false + + for layer in $target_layers { + print $"\n layer: ($layer.id) [($layer.trigger)]" + + for prov in $layer.providers { + let tag = $prov.tag + if ($provider | is-not-empty) and $tag != $provider { continue } + + match $tag { + "PreCommit" => { + let stage = ($prov | get -o stage | default "pre-commit") + print $" provider: PreCommit/($stage)" + generate-precommit $wf $stage $dry $r + }, + "Woodpecker" => { + let file = $prov.file + print $" provider: Woodpecker → ($file)" + generate-woodpecker $layer $wf $file $dry $r + }, + "Justfile" => { + # Defer: justfiles/workflow.just aggregates ALL Justfile layers. + # Generate once after the loop to avoid clobbering on multi-layer runs. + $justfile_needed = true + print " provider: Justfile → justfiles/workflow.just [deferred to post-loop]" + }, + _ => { + print $" provider: ($tag) — not yet implemented" + }, + } + } + } + + # Justfile generation is hoisted here so it runs once with the full layer set. + if $justfile_needed { + print "\n generating justfiles/workflow.just" + generate-justfile $target_layers $wf $dry $r + } + + print "\nDone." +} + +# Show what would change without writing any files. +export def "workflow diff" [ + --root: string + --layer: string + --provider: string +]: nothing -> nothing { + workflow generate --root ($root | default (project-root)) --dry-run + ...(if ($layer | is-not-empty) { [--layer $layer] } else { [] }) + ...(if ($provider | is-not-empty) { [--provider $provider] } else { [] }) +} diff --git a/reflection/schemas/workflow.ncl b/reflection/schemas/workflow.ncl new file mode 100644 index 0000000..5ba46aa --- /dev/null +++ b/reflection/schemas/workflow.ncl @@ -0,0 +1,100 @@ +let layer_trigger = [| + 'OnCommit, # pre-commit (git) / jjw pre-op check (jj) + 'OnPush, # pre-push (git) / rad patch submit (radicle) + 'OnPR, # pull_request on remote CI + 'OnMainMerge, # push to trunk/main branch + 'OnTag, # semver tag event + 'OnManual, # explicit user/agent trigger +|] in + +let validation_kind = [| 'Lint, 'Test, 'Security, 'Compliance, 'Docs |] in + +let build_kind = [| 'Binary, 'Library, 'Container, 'Docs, 'SBOM |] in + +let distribution_kind = [| 'CargoRegistry, 'ContainerRegistry, 'Package, 'Artifact |] in + +# Validation spec — tool invocation, VCS/provider agnostic. +# The generator translates this into provider-specific syntax. +let validation_spec = { + id | String | default = "", + kind | validation_kind | default = 'Lint, + tool | String | default = "", + args | Array String | default = [], + when | Array String | default = [], # glob patterns; empty = always run + fail_fast | Bool | default = true, + cargo_profile | String | default = "", # "" if not applicable +} in + +# Build spec — artifact production, VCS/provider agnostic. +let build_spec = { + id | String | default = "", + kind | build_kind | default = 'Binary, + tool | String | default = "", + args | Array String | default = [], + cargo_profile | String | default = "", + target | String | default = "", # cargo target triple; "" = native + artifacts | Array String | default = [], # expected output paths +} in + +# Distribution spec — where to send artifacts after build. +let distribution_spec = { + id | String | default = "", + kind | distribution_kind | default = 'Artifact, + tool | String | default = "", + args | Array String | default = [], + destination | String | default = "", +} in + +# A workflow layer is an independent, self-contained set of operations. +# Layers do NOT depend on each other — they form a set, not a chain. +# A workspace may activate any combination of layers independently. +let workflow_layer = { + id | String | default = "", + trigger | layer_trigger | default = 'OnManual, + validations | Array String | default = [], # IDs from the active validations catalog + builds | Array String | default = [], # IDs from the active builds catalog + distributions | Array String | default = [], # IDs from the active distributions catalog + + # Provider implementations enabled for this layer. + # Each provider generates a different artifact from the same layer declaration. + # Use the ProviderImpl helpers below to construct typed provider records. + providers | Array { tag | String, .. } | default = [], +} in + +# Complete workflow declaration for a workspace. +# Workspaces import defaults/workflow.ncl and extend/override as needed. +let workflow_declaration = { + layers | Array workflow_layer | default = [], + validations | { _ | validation_spec } | default = {}, + builds | { _ | build_spec } | default = {}, + distributions | { _ | distribution_spec } | default = {}, +} in + +# ── Provider constructor helpers ────────────────────────────────────────────── +# Use these to build the providers array in WorkflowLayer. + +let mk_pre_commit = fun s => { tag = "PreCommit", stage = s } in +let mk_woodpecker = fun f => { tag = "Woodpecker", file = f } in +let mk_github_actions = fun f => { tag = "GithubActions", file = f } in +let mk_justfile = fun r => { tag = "Justfile", recipe = r } in +let mk_jjw_wrapper = { tag = "JjwWrapper" } in + +{ + # Types / contracts — use as field contracts: `field | W.WorkflowLayer` + LayerTrigger = layer_trigger, + ValidationKind = validation_kind, + BuildKind = build_kind, + DistributionKind = distribution_kind, + ValidationSpec = validation_spec, + BuildSpec = build_spec, + DistributionSpec = distribution_spec, + WorkflowLayer = workflow_layer, + WorkflowDeclaration = workflow_declaration, + + # Provider constructors — use in `providers = [W.pre_commit "pre-commit", ...]` + pre_commit = mk_pre_commit, + woodpecker = mk_woodpecker, + github_actions = mk_github_actions, + justfile = mk_justfile, + jjw_wrapper = mk_jjw_wrapper, +} diff --git a/reflection/templates/adopt_ontoref.nu.j2 b/reflection/templates/adopt_ontoref.nu.j2 index 8e8a360..d70e535 100644 --- a/reflection/templates/adopt_ontoref.nu.j2 +++ b/reflection/templates/adopt_ontoref.nu.j2 @@ -73,7 +73,20 @@ if ($wrapper_dest | path exists) { } {% endif %} -# ── 5. Validate .ontology/ ──────────────────────────────────────────────────── +# ── 5. .ontology/workflow.ncl ──────────────────────────────────────────────── + +{% if install_workflow %} +let wf_dest = $"($project_dir)/.ontology/workflow.ncl" +if ($wf_dest | path exists) { + print_skip ".ontology/workflow.ncl" +} else { + let wf_src = $"($ontoref_dir)/templates/ontology/workflow.ncl" + $wf_src | open --raw | save $wf_dest + print_ok ".ontology/workflow.ncl (stub — edit layers, then: ore workflow generate)" +} +{% endif %} + +# ── 6. Validate .ontology/ ──────────────────────────────────────────────────── {% if validate_after %} print "" diff --git a/reflection/templates/ontology/workflow.ncl b/reflection/templates/ontology/workflow.ncl new file mode 100644 index 0000000..215cdeb --- /dev/null +++ b/reflection/templates/ontology/workflow.ncl @@ -0,0 +1,70 @@ +let W = import "../reflection/schemas/workflow.ncl" in +let D = import "../reflection/defaults/workflow.ncl" in + +# Workflow declaration for {{ project_name }}. +# Each layer is independent — layers form a set, not a chain. +# Activate a layer by adding a provider; comment out providers to disable generation. +# +# Run after editing: ore workflow generate +# Preview changes: ore workflow diff + +{ + layers = [ + # ── Commit-fast: local pre-commit checks ──────────────────────────────────── + # Generates entries for .pre-commit-config.yaml (manual merge required). + { + id = "commit-fast", + trigger = 'OnCommit, + validations = [ + "rust-fmt", + "rust-clippy", + "nextest-ci-test", + "deny-subset", + "docs-drift", + "manifest-coverage", + ], + builds = [], + distributions = [], + providers = [W.pre_commit "pre-commit"], + }, + + # ── CI-standard: push/PR pipeline ─────────────────────────────────────────── + # Generates .woodpecker/ci.yml (or equivalent for other providers). + { + id = "ci-standard", + trigger = 'OnPR, + validations = [ + "rust-clippy-all", + "nextest-ci", + "deny-subset", + "docs-check", + "nickel-typecheck", + "nushell-check", + ], + builds = ["release-native"], + distributions = [], + providers = [ + W.woodpecker ".woodpecker/ci.yml", + W.justfile "ci-standard", + ], + }, + + # ── CI-exhaustive: main/tag pipeline ──────────────────────────────────────── + # Generates .woodpecker/ci-exhaustive.yml. Disable if not needed. + # { + # id = "ci-exhaustive", + # trigger = 'OnMainMerge, + # validations = ["deny-all", "geiger"], + # builds = ["release-musl-x86", "sbom"], + # distributions = [], + # providers = [ + # W.woodpecker ".woodpecker/ci-exhaustive.yml", + # W.justfile "ci-exhaustive", + # ], + # }, + ], + + validations = D.validations, + builds = D.builds, + distributions = {}, +}