forked from EduCraft/curriculum-project-hub
chore: restore checker CI gate
This commit is contained in:
@@ -19,8 +19,10 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v5
|
- uses: actions/checkout@v5
|
||||||
|
|
||||||
- name: Install Rust toolchain (stable + clippy + rustfmt)
|
# Keep local rustup, rustfmt output, clippy, and CI on one reproducible
|
||||||
uses: dtolnay/rust-toolchain@stable
|
# compiler release. Update this together with rust-toolchain.toml.
|
||||||
|
- name: Install Rust toolchain (1.92.0 + clippy + rustfmt)
|
||||||
|
uses: dtolnay/rust-toolchain@1.92.0
|
||||||
with:
|
with:
|
||||||
components: clippy, rustfmt
|
components: clippy, rustfmt
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Restore the checker formatting gate
|
# Restore the checker formatting gate
|
||||||
|
|
||||||
Type: task
|
Type: task
|
||||||
Status: open
|
Status: resolved
|
||||||
|
|
||||||
## Question
|
## Question
|
||||||
|
|
||||||
@@ -9,3 +9,32 @@ Apply the repository's configured rustfmt output to the currently reported
|
|||||||
Rust files, verify that no semantic changes are introduced, and restore
|
Rust files, verify that no semantic changes are introduced, and restore
|
||||||
`cargo fmt --all --check` so the checker CI gate is green.
|
`cargo fmt --all --check` so the checker CI gate is green.
|
||||||
|
|
||||||
|
## Answer
|
||||||
|
|
||||||
|
The exact CI command failed deterministically on seven Rust files and an
|
||||||
|
isolated single-file rustfmt check reproduced the same output. The source
|
||||||
|
changes were formatting-only, but the underlying gate was not reproducible:
|
||||||
|
the repository had no `rust-toolchain.toml` and checker CI installed floating
|
||||||
|
`stable`, so local rustfmt, clippy and CI could change independently.
|
||||||
|
|
||||||
|
The checker toolchain is now pinned to Rust 1.92.0 with `rustfmt` and `clippy`
|
||||||
|
in both `rust-toolchain.toml` and the Gitea workflow. The seven files were
|
||||||
|
formatted with that toolchain. Running the complete CI path then surfaced three
|
||||||
|
test helpers accepting `&PathBuf` instead of `&Path` and a Clippy name collision
|
||||||
|
between `Shell` and `PowerShell`; those were corrected without changing CLI
|
||||||
|
values or behavior.
|
||||||
|
|
||||||
|
Verification:
|
||||||
|
|
||||||
|
```text
|
||||||
|
cargo fmt --all --check PASS (twice)
|
||||||
|
cargo clippy --all-targets --all-features -- -D warnings
|
||||||
|
PASS
|
||||||
|
cargo test --all-features --workspace PASS (53 tests)
|
||||||
|
cph completions zsh / power-shell PASS
|
||||||
|
```
|
||||||
|
|
||||||
|
The final Rust diff consists of rustfmt layout plus four borrowed-Path
|
||||||
|
signature generalizations and the internal `Shell` → `CompletionTarget` type
|
||||||
|
rename. No expression, string value, command behavior, or public Rust API was
|
||||||
|
changed.
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ rollback and recovery, and no known critical security or data-integrity gaps.
|
|||||||
unrelated Organizations is not an acceptable destination state.
|
unrelated Organizations is not an acceptable destination state.
|
||||||
- Use the `diagnosing-bugs` loop for every reproduced defect. Prefer explicit
|
- Use the `diagnosing-bugs` loop for every reproduced defect. Prefer explicit
|
||||||
failures and observable critical paths over fallback behavior.
|
failures and observable critical paths over fallback behavior.
|
||||||
- Work on `saas-production-readiness`; make small conventional commits after
|
- Work directly on `main` per developer authorization; make small conventional
|
||||||
verified, independently reversible milestones. Do not push without explicit
|
commits after verified, independently reversible milestones. Do not push
|
||||||
authorization.
|
without explicit authorization.
|
||||||
- Local `lake build` currently requires a configured elan toolchain; CI remains
|
- Local `lake build` currently requires a configured elan toolchain; CI remains
|
||||||
the authoritative configured Lean build environment until that local toolchain
|
the authoritative configured Lean build environment until that local toolchain
|
||||||
is available.
|
is available.
|
||||||
@@ -43,6 +43,7 @@ rollback and recovery, and no known critical security or data-integrity gaps.
|
|||||||
- [Audit backup, migration, and disaster recovery safety](issues/06-audit-backup-migration-recovery.md) — separate persistent state from releases, define a database/workspace checkpoint and recovery objectives, make migrations compatibility-gated, and prove complete off-host backup sets through isolated restore and cutover drills.
|
- [Audit backup, migration, and disaster recovery safety](issues/06-audit-backup-migration-recovery.md) — separate persistent state from releases, define a database/workspace checkpoint and recovery objectives, make migrations compatibility-gated, and prove complete off-host backup sets through isolated restore and cutover drills.
|
||||||
- [Audit the accepted product surface for production completeness](issues/07-audit-product-surface-completeness.md) — retain the guarded Organization APIs and single-app Feishu vertical slice, but require explicit Organization-scoped Feishu/provider connections and external-identity namespaces, private platform and Organization admin control planes, and the full ADR-0022 capacity surface before release.
|
- [Audit the accepted product surface for production completeness](issues/07-audit-product-surface-completeness.md) — retain the guarded Organization APIs and single-app Feishu vertical slice, but require explicit Organization-scoped Feishu/provider connections and external-identity namespaces, private platform and Organization admin control planes, and the full ADR-0022 capacity surface before release.
|
||||||
- [Decide the platform-administrator identity and audit boundary](issues/42-decide-platform-admin-identity-audit.md) — use a dedicated platform Feishu issuer, one peer administrator role, guarded bootstrap and identity-bound invitations, revocable server-side sessions, atomic append-only Platform Audit, and dual-factor offline recovery without a standing break-glass account.
|
- [Decide the platform-administrator identity and audit boundary](issues/42-decide-platform-admin-identity-audit.md) — use a dedicated platform Feishu issuer, one peer administrator role, guarded bootstrap and identity-bound invitations, revocable server-side sessions, atomic append-only Platform Audit, and dual-factor offline recovery without a standing break-glass account.
|
||||||
|
- [Restore the checker formatting gate](issues/09-restore-checker-format-gate.md) — pin local and CI Rust/rustfmt/clippy to 1.92.0, apply the canonical workspace format, and keep the full fmt + clippy + 53-test checker gate green.
|
||||||
|
|
||||||
## Fog
|
## Fog
|
||||||
|
|
||||||
|
|||||||
@@ -72,3 +72,7 @@ examples/ ← 样例工程文件(如 TH-141),流水线的真实输入
|
|||||||
## CI
|
## CI
|
||||||
|
|
||||||
`.gitea/workflows/spec-check.yml` 在每次 push / PR 时于 `spec/` 下跑 `lake build`,确保契约始终 type-check 通过(从第一天起就是"绿"的)。这是良构 gate,见宪法第 2 条。
|
`.gitea/workflows/spec-check.yml` 在每次 push / PR 时于 `spec/` 下跑 `lake build`,确保契约始终 type-check 通过(从第一天起就是"绿"的)。这是良构 gate,见宪法第 2 条。
|
||||||
|
|
||||||
|
Rust checker 的本地与 CI 工具链由根 `rust-toolchain.toml` 固定;`.gitea/workflows/checker-check.yml`
|
||||||
|
必须安装同一精确版本并执行 `cargo fmt --all --check`、Clippy `-D warnings` 与 workspace
|
||||||
|
全测试。升级 Rust 时这两处必须在同一提交更新并通过完整 checker gate。
|
||||||
|
|||||||
@@ -360,8 +360,13 @@ pub fn target_is_shell(root: &Path, target: &str) -> bool {
|
|||||||
.iter()
|
.iter()
|
||||||
.find(|t| t.name == target)
|
.find(|t| t.name == target)
|
||||||
.is_some_and(|t| {
|
.is_some_and(|t| {
|
||||||
t.steps.iter().any(|s| matches!(s, cph_model::Step::Shell { .. }))
|
t.steps
|
||||||
&& !t.steps.iter().any(|s| matches!(s, cph_model::Step::TypstCompile { .. }))
|
.iter()
|
||||||
|
.any(|s| matches!(s, cph_model::Step::Shell { .. }))
|
||||||
|
&& !t
|
||||||
|
.steps
|
||||||
|
.iter()
|
||||||
|
.any(|s| matches!(s, cph_model::Step::TypstCompile { .. }))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,9 +375,17 @@ fn run_one_shell(cwd: &Path, run: &str) -> ShellStepOutcome {
|
|||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
let output = if cfg!(target_os = "windows") {
|
let output = if cfg!(target_os = "windows") {
|
||||||
Command::new("cmd").arg("/C").arg(run).current_dir(cwd).output()
|
Command::new("cmd")
|
||||||
|
.arg("/C")
|
||||||
|
.arg(run)
|
||||||
|
.current_dir(cwd)
|
||||||
|
.output()
|
||||||
} else {
|
} else {
|
||||||
Command::new("sh").arg("-c").arg(run).current_dir(cwd).output()
|
Command::new("sh")
|
||||||
|
.arg("-c")
|
||||||
|
.arg(run)
|
||||||
|
.current_dir(cwd)
|
||||||
|
.output()
|
||||||
};
|
};
|
||||||
|
|
||||||
match output {
|
match output {
|
||||||
@@ -704,7 +717,11 @@ fn collect_image_assets(eng_root: &Path, build_root: &Path, body: &str) -> Resul
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Err(e) = std::fs::copy(&src, &dst) {
|
if let Err(e) = std::fs::copy(&src, &dst) {
|
||||||
return Err(format!("failed to copy '{}' → '{}': {e}", src.display(), dst.display()));
|
return Err(format!(
|
||||||
|
"failed to copy '{}' → '{}': {e}",
|
||||||
|
src.display(),
|
||||||
|
dst.display()
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -763,7 +780,8 @@ pub fn target_is_markdown_assemble(root: &Path, target: &str) -> bool {
|
|||||||
t.steps
|
t.steps
|
||||||
.iter()
|
.iter()
|
||||||
.any(|s| matches!(s, cph_model::Step::AssembleMarkdown { .. }))
|
.any(|s| matches!(s, cph_model::Step::AssembleMarkdown { .. }))
|
||||||
&& !t.steps
|
&& !t
|
||||||
|
.steps
|
||||||
.iter()
|
.iter()
|
||||||
.any(|s| matches!(s, cph_model::Step::TypstCompile { .. }))
|
.any(|s| matches!(s, cph_model::Step::TypstCompile { .. }))
|
||||||
})
|
})
|
||||||
@@ -833,7 +851,11 @@ fn compile_targets(lesson: &cph_model::Lesson) -> Vec<&str> {
|
|||||||
lesson
|
lesson
|
||||||
.targets
|
.targets
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|t| t.steps.iter().any(|s| matches!(s, cph_model::Step::TypstCompile { .. })))
|
.filter(|t| {
|
||||||
|
t.steps
|
||||||
|
.iter()
|
||||||
|
.any(|s| matches!(s, cph_model::Step::TypstCompile { .. }))
|
||||||
|
})
|
||||||
.map(|t| t.name.as_str())
|
.map(|t| t.name.as_str())
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
//! fixtures built in a temp dir. The typst Engine is pointed at the repo's real
|
//! fixtures built in a temp dir. The typst Engine is pointed at the repo's real
|
||||||
//! `render/` package via `Engine::with_render_dir`.
|
//! `render/` package via `Engine::with_render_dir`.
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use cph_diag::DiagCode;
|
use cph_diag::DiagCode;
|
||||||
use cph_typst::Engine;
|
use cph_typst::Engine;
|
||||||
@@ -138,7 +138,7 @@ path = "segments/does-not-exist"
|
|||||||
|
|
||||||
/// Write a throwaway one-segment lesson into `tmp`, with the given `[targets.x]`
|
/// Write a throwaway one-segment lesson into `tmp`, with the given `[targets.x]`
|
||||||
/// body spliced in. Returns nothing; the caller runs `check`.
|
/// body spliced in. Returns nothing; the caller runs `check`.
|
||||||
fn write_segment_lesson_with_target(tmp: &PathBuf, target_body: &str) {
|
fn write_segment_lesson_with_target(tmp: &Path, target_body: &str) {
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
tmp.join("manifest.toml"),
|
tmp.join("manifest.toml"),
|
||||||
format!(
|
format!(
|
||||||
@@ -264,7 +264,7 @@ template = "exports/handout.typ"
|
|||||||
|
|
||||||
/// Write a one-segment lesson with a `file-tree` shell target whose `run` is the
|
/// Write a one-segment lesson with a `file-tree` shell target whose `run` is the
|
||||||
/// given command. Returns nothing; the caller runs `run_shell_target`.
|
/// given command. Returns nothing; the caller runs `run_shell_target`.
|
||||||
fn write_shell_target_lesson(tmp: &PathBuf, run: &str) {
|
fn write_shell_target_lesson(tmp: &Path, run: &str) {
|
||||||
std::fs::write(
|
std::fs::write(
|
||||||
tmp.join("manifest.toml"),
|
tmp.join("manifest.toml"),
|
||||||
format!(
|
format!(
|
||||||
@@ -365,7 +365,10 @@ run = "printf SHOULD_NOT_RUN > build/ran.txt"
|
|||||||
|
|
||||||
let report = cph_check::run_shell_target(&tmp, &engine(), "assets");
|
let report = cph_check::run_shell_target(&tmp, &engine(), "assets");
|
||||||
assert!(!report.ok);
|
assert!(!report.ok);
|
||||||
assert!(report.outcomes.is_empty(), "no command should run on a broken lesson");
|
assert!(
|
||||||
|
report.outcomes.is_empty(),
|
||||||
|
"no command should run on a broken lesson"
|
||||||
|
);
|
||||||
assert!(report.check.has_errors());
|
assert!(report.check.has_errors());
|
||||||
assert!(
|
assert!(
|
||||||
!tmp.join("build").join("ran.txt").exists(),
|
!tmp.join("build").join("ran.txt").exists(),
|
||||||
@@ -380,7 +383,7 @@ run = "printf SHOULD_NOT_RUN > build/ran.txt"
|
|||||||
/// content file, plus a `slides` target that assembles them. The `which` slice
|
/// content file, plus a `slides` target that assembles them. The `which` slice
|
||||||
/// controls which segments get a `slides.md` (so the "skip absent" path can be
|
/// controls which segments get a `slides.md` (so the "skip absent" path can be
|
||||||
/// exercised). Returns nothing; the caller runs `run_markdown_assemble_target`.
|
/// exercised). Returns nothing; the caller runs `run_markdown_assemble_target`.
|
||||||
fn write_markdown_assemble_target_lesson(tmp: &PathBuf, slides: &[(&str, &str)]) {
|
fn write_markdown_assemble_target_lesson(tmp: &Path, slides: &[(&str, &str)]) {
|
||||||
let mut parts = String::new();
|
let mut parts = String::new();
|
||||||
for (i, (name, _)) in slides.iter().enumerate() {
|
for (i, (name, _)) in slides.iter().enumerate() {
|
||||||
if i > 0 {
|
if i > 0 {
|
||||||
@@ -424,10 +427,7 @@ field = "slides"
|
|||||||
#[test]
|
#[test]
|
||||||
fn target_is_markdown_assemble_detects_shape() {
|
fn target_is_markdown_assemble_detects_shape() {
|
||||||
let tmp = tempdir();
|
let tmp = tempdir();
|
||||||
write_markdown_assemble_target_lesson(
|
write_markdown_assemble_target_lesson(&tmp, &[("a", "# A"), ("b", "# B")]);
|
||||||
&tmp,
|
|
||||||
&[("a", "# A"), ("b", "# B")],
|
|
||||||
);
|
|
||||||
assert!(cph_check::target_is_markdown_assemble(&tmp, "slides"));
|
assert!(cph_check::target_is_markdown_assemble(&tmp, "slides"));
|
||||||
// The student target doesn't exist here → falls through to typst path → false.
|
// The student target doesn't exist here → falls through to typst path → false.
|
||||||
assert!(!cph_check::target_is_markdown_assemble(&tmp, "student"));
|
assert!(!cph_check::target_is_markdown_assemble(&tmp, "student"));
|
||||||
@@ -439,7 +439,10 @@ fn run_markdown_assemble_target_concatenates_in_parts_order() {
|
|||||||
let tmp = tempdir();
|
let tmp = tempdir();
|
||||||
write_markdown_assemble_target_lesson(
|
write_markdown_assemble_target_lesson(
|
||||||
&tmp,
|
&tmp,
|
||||||
&[("intro", "# 规则一\n- 范围"), ("rule2", "# 规则二\n$8\\times$")],
|
&[
|
||||||
|
("intro", "# 规则一\n- 范围"),
|
||||||
|
("rule2", "# 规则二\n$8\\times$"),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
let report = cph_check::run_markdown_assemble_target(&tmp, &engine(), "slides");
|
let report = cph_check::run_markdown_assemble_target(&tmp, &engine(), "slides");
|
||||||
@@ -667,6 +670,6 @@ fn tempdir() -> PathBuf {
|
|||||||
p
|
p
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cleanup(p: &PathBuf) {
|
fn cleanup(p: &Path) {
|
||||||
let _ = std::fs::remove_dir_all(p);
|
let _ = std::fs::remove_dir_all(p);
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-16
@@ -51,12 +51,12 @@ enum Command {
|
|||||||
/// completion file, e.g. `cph completions zsh > ~/.zfunc/_cph`.
|
/// completion file, e.g. `cph completions zsh > ~/.zfunc/_cph`.
|
||||||
Completions {
|
Completions {
|
||||||
/// Which shell to generate completions for.
|
/// Which shell to generate completions for.
|
||||||
shell: Shell,
|
shell: CompletionTarget,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
|
#[derive(Debug, Clone, Copy, clap::ValueEnum)]
|
||||||
enum Shell {
|
enum CompletionTarget {
|
||||||
Bash,
|
Bash,
|
||||||
Zsh,
|
Zsh,
|
||||||
Fish,
|
Fish,
|
||||||
@@ -82,16 +82,16 @@ fn main() -> ExitCode {
|
|||||||
/// Emit a shell-completion script for `shell` to stdout. The script is built
|
/// Emit a shell-completion script for `shell` to stdout. The script is built
|
||||||
/// from the same `Cli` clap definition above, so it tracks subcommands/flags as
|
/// from the same `Cli` clap definition above, so it tracks subcommands/flags as
|
||||||
/// they evolve.
|
/// they evolve.
|
||||||
fn run_completions(shell: Shell) -> ExitCode {
|
fn run_completions(shell: CompletionTarget) -> ExitCode {
|
||||||
use clap::CommandFactory;
|
use clap::CommandFactory;
|
||||||
use clap_complete::Shell as CompShell;
|
use clap_complete::Shell as CompShell;
|
||||||
|
|
||||||
let sh = match shell {
|
let sh = match shell {
|
||||||
Shell::Bash => CompShell::Bash,
|
CompletionTarget::Bash => CompShell::Bash,
|
||||||
Shell::Zsh => CompShell::Zsh,
|
CompletionTarget::Zsh => CompShell::Zsh,
|
||||||
Shell::Fish => CompShell::Fish,
|
CompletionTarget::Fish => CompShell::Fish,
|
||||||
Shell::PowerShell => CompShell::PowerShell,
|
CompletionTarget::PowerShell => CompShell::PowerShell,
|
||||||
Shell::Elvish => CompShell::Elvish,
|
CompletionTarget::Elvish => CompShell::Elvish,
|
||||||
};
|
};
|
||||||
let mut cmd = Cli::command();
|
let mut cmd = Cli::command();
|
||||||
clap_complete::generate(sh, &mut cmd, "cph", &mut std::io::stdout());
|
clap_complete::generate(sh, &mut cmd, "cph", &mut std::io::stdout());
|
||||||
@@ -188,7 +188,10 @@ fn run_build(
|
|||||||
/// explicit `cph build --target <name>`, and each command is printed before it
|
/// explicit `cph build --target <name>`, and each command is printed before it
|
||||||
/// runs so the user sees exactly what is executed.
|
/// runs so the user sees exactly what is executed.
|
||||||
fn run_shell_build(path: &std::path::Path, engine: &Engine, target: &str) -> ExitCode {
|
fn run_shell_build(path: &std::path::Path, engine: &Engine, target: &str) -> ExitCode {
|
||||||
eprintln!("running shell-step target '{target}' (commands execute in {})", path.display());
|
eprintln!(
|
||||||
|
"running shell-step target '{target}' (commands execute in {})",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
let report = cph_check::run_shell_target(path, engine, target);
|
let report = cph_check::run_shell_target(path, engine, target);
|
||||||
print_diagnostics(&report.check);
|
print_diagnostics(&report.check);
|
||||||
|
|
||||||
@@ -196,7 +199,10 @@ fn run_shell_build(path: &std::path::Path, engine: &Engine, target: &str) -> Exi
|
|||||||
// Refused before running anything (check errors / unknown target / no
|
// Refused before running anything (check errors / unknown target / no
|
||||||
// shell steps): the diagnostics above explain why.
|
// shell steps): the diagnostics above explain why.
|
||||||
if report.check.has_errors() {
|
if report.check.has_errors() {
|
||||||
eprintln!("build refused: {} errors (fix the lesson first)", report.check.error_count());
|
eprintln!(
|
||||||
|
"build refused: {} errors (fix the lesson first)",
|
||||||
|
report.check.error_count()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
eprintln!("build failed: target '{target}' has no shell steps to run");
|
eprintln!("build failed: target '{target}' has no shell steps to run");
|
||||||
}
|
}
|
||||||
@@ -212,13 +218,19 @@ fn run_shell_build(path: &std::path::Path, engine: &Engine, target: &str) -> Exi
|
|||||||
eprint!("{}", outcome.stderr);
|
eprint!("{}", outcome.stderr);
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"step failed (exit {})",
|
"step failed (exit {})",
|
||||||
outcome.status.map(|c| c.to_string()).unwrap_or_else(|| "signal".into())
|
outcome
|
||||||
|
.status
|
||||||
|
.map(|c| c.to_string())
|
||||||
|
.unwrap_or_else(|| "signal".into())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if report.ok {
|
if report.ok {
|
||||||
println!("shell-step target '{target}' completed: {} step(s) ok", report.outcomes.len());
|
println!(
|
||||||
|
"shell-step target '{target}' completed: {} step(s) ok",
|
||||||
|
report.outcomes.len()
|
||||||
|
);
|
||||||
ExitCode::SUCCESS
|
ExitCode::SUCCESS
|
||||||
} else {
|
} else {
|
||||||
eprintln!("shell-step target '{target}' failed");
|
eprintln!("shell-step target '{target}' failed");
|
||||||
@@ -231,13 +243,19 @@ fn run_shell_build(path: &std::path::Path, engine: &Engine, target: &str) -> Exi
|
|||||||
/// shell path, this is opt-in — only on an explicit `cph build --target <name>`,
|
/// shell path, this is opt-in — only on an explicit `cph build --target <name>`,
|
||||||
/// and never in `check`.
|
/// and never in `check`.
|
||||||
fn run_markdown_assemble_build(path: &std::path::Path, engine: &Engine, target: &str) -> ExitCode {
|
fn run_markdown_assemble_build(path: &std::path::Path, engine: &Engine, target: &str) -> ExitCode {
|
||||||
eprintln!("assembling markdown target '{target}' (reading parts under {})", path.display());
|
eprintln!(
|
||||||
|
"assembling markdown target '{target}' (reading parts under {})",
|
||||||
|
path.display()
|
||||||
|
);
|
||||||
let report = cph_check::run_markdown_assemble_target(path, engine, target);
|
let report = cph_check::run_markdown_assemble_target(path, engine, target);
|
||||||
print_diagnostics(&report.check);
|
print_diagnostics(&report.check);
|
||||||
|
|
||||||
if report.outcomes.is_empty() && !report.ok {
|
if report.outcomes.is_empty() && !report.ok {
|
||||||
if report.check.has_errors() {
|
if report.check.has_errors() {
|
||||||
eprintln!("build refused: {} errors (fix the lesson first)", report.check.error_count());
|
eprintln!(
|
||||||
|
"build refused: {} errors (fix the lesson first)",
|
||||||
|
report.check.error_count()
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
eprintln!("build failed: target '{target}' has no assemble-markdown steps to run");
|
eprintln!("build failed: target '{target}' has no assemble-markdown steps to run");
|
||||||
}
|
}
|
||||||
@@ -250,7 +268,11 @@ fn run_markdown_assemble_build(path: &std::path::Path, engine: &Engine, target:
|
|||||||
outcome.field, outcome.parts_read
|
outcome.field, outcome.parts_read
|
||||||
);
|
);
|
||||||
if let Some(out_rel) = &outcome.written {
|
if let Some(out_rel) = &outcome.written {
|
||||||
println!("wrote {} ({} bytes)", path.join(out_rel).display(), outcome.body.len());
|
println!(
|
||||||
|
"wrote {} ({} bytes)",
|
||||||
|
path.join(out_rel).display(),
|
||||||
|
outcome.body.len()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if let Some(err) = &outcome.error {
|
if let Some(err) = &outcome.error {
|
||||||
eprintln!("assemble failed: {err}");
|
eprintln!("assemble failed: {err}");
|
||||||
@@ -258,7 +280,10 @@ fn run_markdown_assemble_build(path: &std::path::Path, engine: &Engine, target:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if report.ok {
|
if report.ok {
|
||||||
println!("assemble-markdown target '{target}' completed: {} step(s) ok", report.outcomes.len());
|
println!(
|
||||||
|
"assemble-markdown target '{target}' completed: {} step(s) ok",
|
||||||
|
report.outcomes.len()
|
||||||
|
);
|
||||||
ExitCode::SUCCESS
|
ExitCode::SUCCESS
|
||||||
} else {
|
} else {
|
||||||
eprintln!("assemble-markdown target '{target}' failed");
|
eprintln!("assemble-markdown target '{target}' failed");
|
||||||
|
|||||||
@@ -737,7 +737,11 @@ fn parse_target(name: String, value: toml::Value, diags: &mut Vec<Diagnostic>) -
|
|||||||
/// A well-formed but empty `covers = []` yields `Some(vec![])`: a target that
|
/// A well-formed but empty `covers = []` yields `Some(vec![])`: a target that
|
||||||
/// explicitly renders no kind (every used kind then draws a `renderIgnored`
|
/// explicitly renders no kind (every used kind then draws a `renderIgnored`
|
||||||
/// warning) — distinct from an absent key.
|
/// warning) — distinct from an absent key.
|
||||||
fn parse_covers(name: &str, value: toml::Value, diags: &mut Vec<Diagnostic>) -> Option<Vec<String>> {
|
fn parse_covers(
|
||||||
|
name: &str,
|
||||||
|
value: toml::Value,
|
||||||
|
diags: &mut Vec<Diagnostic>,
|
||||||
|
) -> Option<Vec<String>> {
|
||||||
let items = match value {
|
let items = match value {
|
||||||
toml::Value::Array(items) => items,
|
toml::Value::Array(items) => items,
|
||||||
_ => {
|
_ => {
|
||||||
|
|||||||
@@ -324,7 +324,11 @@ fn cph_version_mismatch_is_an_error_diagnostic() {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|d| d.code == DiagCode::CphVersionMismatch)
|
.filter(|d| d.code == DiagCode::CphVersionMismatch)
|
||||||
.collect();
|
.collect();
|
||||||
assert_eq!(mm.len(), 1, "expected one CphVersionMismatch, got {diags:?}");
|
assert_eq!(
|
||||||
|
mm.len(),
|
||||||
|
1,
|
||||||
|
"expected one CphVersionMismatch, got {diags:?}"
|
||||||
|
);
|
||||||
assert!(
|
assert!(
|
||||||
mm[0].message.contains("99.99.99") && mm[0].message.contains(CPH_VERSION),
|
mm[0].message.contains("99.99.99") && mm[0].message.contains(CPH_VERSION),
|
||||||
"diagnostic should name both versions, got: {}",
|
"diagnostic should name both versions, got: {}",
|
||||||
|
|||||||
@@ -526,7 +526,11 @@ mod tests {
|
|||||||
// ADR-0015: `slides`/`transcript` are markdown content fields (`x-cph-content: "md"`),
|
// ADR-0015: `slides`/`transcript` are markdown content fields (`x-cph-content: "md"`),
|
||||||
// distinct from the `.typ` content fields.
|
// distinct from the `.typ` content fields.
|
||||||
let s = schema_for("segment").unwrap();
|
let s = schema_for("segment").unwrap();
|
||||||
let slides = s.content_fields.iter().find(|f| f.name == "slides").unwrap();
|
let slides = s
|
||||||
|
.content_fields
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.name == "slides")
|
||||||
|
.unwrap();
|
||||||
assert_eq!(slides.ext, "md");
|
assert_eq!(slides.ext, "md");
|
||||||
assert!(!slides.required);
|
assert!(!slides.required);
|
||||||
let transcript = s
|
let transcript = s
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ pub fn resolve_render_dir() -> PathBuf {
|
|||||||
ensure_extracted().unwrap_or_else(|_| {
|
ensure_extracted().unwrap_or_else(|_| {
|
||||||
// Last-resort: extract under the OS temp dir. Still correct, just not
|
// Last-resort: extract under the OS temp dir. Still correct, just not
|
||||||
// cached across processes.
|
// cached across processes.
|
||||||
let fallback = std::env::temp_dir().join(format!("cph-render-{}", env!("CARGO_PKG_VERSION")));
|
let fallback =
|
||||||
|
std::env::temp_dir().join(format!("cph-render-{}", env!("CARGO_PKG_VERSION")));
|
||||||
let _ = extract_to(&fallback);
|
let _ = extract_to(&fallback);
|
||||||
fallback
|
fallback
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[toolchain]
|
||||||
|
channel = "1.92.0"
|
||||||
|
components = ["clippy", "rustfmt"]
|
||||||
|
profile = "minimal"
|
||||||
Reference in New Issue
Block a user