feat: workflow layer model — NCL-first CI/build/distribution generator
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
This commit is contained in:
parent
a6e0287c6f
commit
6721daf440
15 changed files with 1163 additions and 66 deletions
|
|
@ -46,6 +46,21 @@ overflow-checks = true
|
||||||
lto = false
|
lto = false
|
||||||
incremental = true
|
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]
|
[profile.bench]
|
||||||
# Benchmark profile - same as release
|
# Benchmark profile - same as release
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
|
||||||
9
.config/nextest.toml
Normal file
9
.config/nextest.toml
Normal file
|
|
@ -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
|
||||||
61
.ontology/workflow.ncl
Normal file
61
.ontology/workflow.ncl
Normal file
|
|
@ -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 = {},
|
||||||
|
}
|
||||||
|
|
@ -25,8 +25,8 @@ repos:
|
||||||
stages: [pre-commit]
|
stages: [pre-commit]
|
||||||
|
|
||||||
- id: rust-test
|
- id: rust-test
|
||||||
name: Rust tests
|
name: Rust tests (nextest)
|
||||||
entry: bash -c 'cargo test --all-features --workspace'
|
entry: bash -c 'cargo nextest run --all-features --workspace --profile ci-test --cargo-profile ci-test'
|
||||||
language: system
|
language: system
|
||||||
types: [rust]
|
types: [rust]
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
|
|
@ -42,7 +42,7 @@ repos:
|
||||||
|
|
||||||
- id: docs-links
|
- id: docs-links
|
||||||
name: Rustdoc broken intra-doc 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
|
language: system
|
||||||
types: [rust]
|
types: [rust]
|
||||||
pass_filenames: false
|
pass_filenames: false
|
||||||
|
|
|
||||||
32
.woodpecker/ci-exhaustive.yml
Normal file
32
.woodpecker/ci-exhaustive.yml
Normal file
|
|
@ -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
|
||||||
|
|
@ -1,84 +1,51 @@
|
||||||
# Woodpecker CI Pipeline
|
# Generated by ore workflow generate — layer: ci-standard
|
||||||
# Equivalent to GitHub Actions CI workflow
|
# Source: .ontology/workflow.ncl
|
||||||
# Generated by dev-system/ci
|
# Do not edit manually — regenerate with: ore workflow generate --layer ci-standard
|
||||||
|
|
||||||
when:
|
when:
|
||||||
event: [push, pull_request, manual]
|
event: [push, pull_request, manual]
|
||||||
branch:
|
|
||||||
- main
|
|
||||||
- develop
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# === LINTING ===
|
rust-clippy-all:
|
||||||
|
|
||||||
lint-rust:
|
|
||||||
image: rust:latest
|
image: rust:latest
|
||||||
commands:
|
commands:
|
||||||
- curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh | bash -s -- --to /usr/local/bin
|
- cargo clippy --all-targets --all-features -- -D warnings
|
||||||
- rustup component add clippy
|
|
||||||
- cargo fmt --all -- --check
|
|
||||||
- cargo clippy --all-targets -- -D warnings
|
|
||||||
|
|
||||||
lint-bash:
|
nickel-typecheck:
|
||||||
image: koalaman/shellcheck-alpine:stable
|
|
||||||
commands:
|
|
||||||
- apk add --no-cache curl bash
|
|
||||||
- find . -name '*.sh' -type f ! -path './target/*' -exec shellcheck {} +
|
|
||||||
|
|
||||||
lint-nickel:
|
|
||||||
image: rust:latest
|
image: rust:latest
|
||||||
commands:
|
commands:
|
||||||
- cargo install nickel-lang-cli --locked
|
- 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
|
image: rust:latest
|
||||||
commands:
|
commands:
|
||||||
- cargo install nu --locked
|
- 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:
|
nextest-ci:
|
||||||
image: node:alpine
|
|
||||||
commands:
|
|
||||||
- npm install -g markdownlint-cli2
|
|
||||||
- markdownlint-cli2 '**/*.md' '#node_modules' '#target'
|
|
||||||
|
|
||||||
# === TESTING ===
|
|
||||||
|
|
||||||
test:
|
|
||||||
image: rust:latest
|
image: rust:latest
|
||||||
commands:
|
commands:
|
||||||
- cargo test --workspace --all-features
|
- cargo install cargo-nextest --locked
|
||||||
depends_on:
|
- cargo nextest run --all-features --workspace --profile ci --cargo-profile ci
|
||||||
- lint-rust
|
depends_on: ["rust-clippy-all", "nickel-typecheck", "nushell-check"]
|
||||||
- lint-bash
|
environment:
|
||||||
- lint-nickel
|
RUST_BACKTRACE: 1
|
||||||
- lint-nushell
|
|
||||||
- lint-markdown
|
|
||||||
|
|
||||||
# === BUILD ===
|
deny-subset:
|
||||||
|
|
||||||
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:
|
|
||||||
image: rust:latest
|
image: rust:latest
|
||||||
commands:
|
commands:
|
||||||
- cargo install cargo-deny --locked
|
- cargo install cargo-deny --locked
|
||||||
- cargo deny check licenses advisories
|
- 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"]
|
||||||
|
|
|
||||||
19
justfiles/workflow.just
Normal file
19
justfiles/workflow.just
Normal file
|
|
@ -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
|
||||||
|
|
@ -24,6 +24,7 @@ use ../modules/run.nu *
|
||||||
use ../modules/graph.nu *
|
use ../modules/graph.nu *
|
||||||
use ../modules/validate.nu *
|
use ../modules/validate.nu *
|
||||||
use ../modules/migrate.nu *
|
use ../modules/migrate.nu *
|
||||||
|
use ../modules/workflow.nu *
|
||||||
|
|
||||||
use ../nulib/fmt.nu *
|
use ../nulib/fmt.nu *
|
||||||
use ../nulib/shared.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 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 }
|
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 ──────────────────────────────────────────────────────────────────
|
# ── Validate ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
def "main validate" [target?: string] { missing-target "validate" $target }
|
def "main validate" [target?: string] { missing-target "validate" $target }
|
||||||
|
|
|
||||||
208
reflection/defaults/workflow.ncl
Normal file
208
reflection/defaults/workflow.ncl
Normal file
|
|
@ -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 = {},
|
||||||
|
}
|
||||||
|
|
@ -66,6 +66,11 @@
|
||||||
default = true,
|
default = true,
|
||||||
help = "Thin bash wrapper that sets ONTOREF_ROOT and ONTOREF_PROJECT_ROOT, then delegates to ontoref entry point." },
|
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 ───────────────────────────────────────────────────────────
|
# ── Validation ───────────────────────────────────────────────────────────
|
||||||
{ type = "section_header", name = "validation_header",
|
{ type = "section_header", name = "validation_header",
|
||||||
title = "Validation", border_top = true, border_bottom = true },
|
title = "Validation", border_top = true, border_bottom = true },
|
||||||
|
|
@ -80,7 +85,7 @@
|
||||||
title = "Review", border_top = true },
|
title = "Review", border_top = true },
|
||||||
|
|
||||||
{ type = "section", name = "review_note",
|
{ 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",
|
{ type = "confirm", name = "ready_to_generate",
|
||||||
prompt = "Generate the adopt script?",
|
prompt = "Generate the adopt script?",
|
||||||
|
|
|
||||||
57
reflection/migrations/0014-workflow-layer-model.ncl
Normal file
57
reflection/migrations/0014-workflow-layer-model.ncl
Normal file
|
|
@ -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
|
||||||
|
",
|
||||||
|
}
|
||||||
498
reflection/modules/workflow.nu
Normal file
498
reflection/modules/workflow.nu
Normal file
|
|
@ -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 <id> — generate one layer
|
||||||
|
# workflow generate --provider <tag> — 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<string>]: nothing -> string {
|
||||||
|
$args | str join ' '
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build a tool invocation string from tool name + args, trimmed.
|
||||||
|
def tool-cmd [tool: string, args: list<string>]: 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 { [] })
|
||||||
|
}
|
||||||
100
reflection/schemas/workflow.ncl
Normal file
100
reflection/schemas/workflow.ncl
Normal file
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
@ -73,7 +73,20 @@ if ($wrapper_dest | path exists) {
|
||||||
}
|
}
|
||||||
{% endif %}
|
{% 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 %}
|
{% if validate_after %}
|
||||||
print ""
|
print ""
|
||||||
|
|
|
||||||
70
reflection/templates/ontology/workflow.ncl
Normal file
70
reflection/templates/ontology/workflow.ncl
Normal file
|
|
@ -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 = {},
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue