mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-04 02:33:56 +00:00
Some checks failed
Rust / Format (push) Waiting to run
Rust / Clippy (push) Waiting to run
Rust / Generated Docs (push) Waiting to run
Rust / Test (Linux) (push) Waiting to run
Rust / Test (macOS) (push) Waiting to run
TypeScript / Typecheck (push) Has been cancelled
TypeScript / Test (push) Has been cancelled
TypeScript / Build (push) Has been cancelled
## Summary
Enforce Cargo lockfile use across CI and release automation so jobs fail
on stale `Cargo.lock` state instead of resolving dependencies
implicitly. This adds `--locked` to Rust CI, release builds/tests,
nightly release tagging, the TypeScript workflow's embedded Rust build,
and helper-owned Cargo calls in `fabro-dev`.
This also fixes the Linux CI flake exposed by the PR: canceling a
durably blocked in-process run could take the abort path while the
workflow was still unwinding a human-input gate, causing
`run.failed(cancelled)` to be followed by `run.unblocked`. That invalid
event order broke projection rebuilds and made `GET /runs/{id}` return
404. Cancellation now uses the durable lifecycle status when selecting
the in-process blocked-run path, so the pending interview is cancelled
before the terminal event is emitted.
The release command's intentional `cargo update --workspace` step is
unchanged, because that step updates `Cargo.lock` after bumping the
workspace version.
## Testing
- `cargo nextest run --locked -p fabro-dev --features dev -E
'test(dry_run_computes_stable_version_from_date) |
test(dry_run_prints_equivalent_build_commands)'`
- `cargo --locked dev release --dry-run --skip-tests --release-date
2026-01-01`
- `cargo --locked dev docs check`
- `cargo nextest run --locked -p fabro-server --features test-support
cancel_durably_blocked_in_process_run_cancels_pending_interview_without_abort_signal
--status-level fail --final-status-level fail --show-progress none`
- `cargo nextest run --locked -p fabro-server --features test-support
--test it scenario::lifecycle --profile ci --status-level fail
--final-status-level fail --show-progress none --no-fail-fast`
- Linux Docker stress reproduction: `cargo nextest run --locked -p
fabro-server --features test-support --test it
scenario::lifecycle::full_http_lifecycle_cancel --profile ci
--stress-count 200 --status-level fail --final-status-level fail
--show-progress none`
- `cargo +nightly-2026-04-14 fmt --check --all`
- `cargo +nightly-2026-04-14 clippy --locked -p fabro-dev --features dev
--all-targets -- -D warnings`
- `cargo +nightly-2026-04-14 clippy --locked -p fabro-server --features
test-support --all-targets -- -D warnings`
- `git diff --check`
Full `cargo nextest run --locked -p fabro-dev --features dev` currently
has two unrelated policy-test failures:
`policy::catalog_builtin_references_stay_in_allowlist` and
`policy::workflow_template_rendering_call_sites_stay_in_allowlist`.
---
[](https://github.com/EveryInc/compound-engineering-plugin)
🤖 Generated with GPT-5 via [Codex](https://openai.com/codex)
---------
Co-authored-by: Release Repro <release-repro@example.com>
226 lines
5.9 KiB
Rust
226 lines
5.9 KiB
Rust
use std::path::Path;
|
|
use std::process::Command;
|
|
|
|
use super::{fabro_dev, output_text, write_file};
|
|
|
|
#[expect(
|
|
clippy::disallowed_methods,
|
|
reason = "integration tests intentionally shell out to git in temporary fixture repositories"
|
|
)]
|
|
fn git(root: &Path, args: &[&str]) {
|
|
let output = Command::new("git")
|
|
.args(args)
|
|
.current_dir(root)
|
|
.output()
|
|
.expect("git should run");
|
|
assert!(
|
|
output.status.success(),
|
|
"git {:?} failed\nstdout:\n{}\nstderr:\n{}",
|
|
args,
|
|
output_text(&output.stdout),
|
|
output_text(&output.stderr)
|
|
);
|
|
}
|
|
|
|
fn release_fixture() -> tempfile::TempDir {
|
|
let fixture = tempfile::tempdir().expect("creating fixture");
|
|
write_file(
|
|
fixture.path(),
|
|
"Cargo.toml",
|
|
r#"[workspace]
|
|
members = []
|
|
|
|
[workspace.package]
|
|
version = "0.1.0"
|
|
"#,
|
|
);
|
|
git(fixture.path(), &["init"]);
|
|
git(fixture.path(), &["config", "user.name", "Release Test"]);
|
|
git(fixture.path(), &[
|
|
"config",
|
|
"user.email",
|
|
"release-test@example.com",
|
|
]);
|
|
git(fixture.path(), &["add", "Cargo.toml"]);
|
|
git(fixture.path(), &["commit", "-m", "initial"]);
|
|
fixture
|
|
}
|
|
|
|
#[test]
|
|
fn help_lists_release_flags() {
|
|
let output = fabro_dev()
|
|
.args(["release", "--help"])
|
|
.assert()
|
|
.success()
|
|
.get_output()
|
|
.clone();
|
|
let stdout = output_text(&output.stdout);
|
|
|
|
for flag in ["--dry-run", "--skip-tests", "--release-date", "--nightly"] {
|
|
assert!(
|
|
stdout.contains(flag),
|
|
"release help should list {flag}:\n{stdout}"
|
|
);
|
|
}
|
|
|
|
assert!(
|
|
!stdout.contains("[PRERELEASE_LABEL]"),
|
|
"release help should not list a prerelease positional:\n{stdout}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dry_run_computes_stable_version_from_date() {
|
|
let fixture = release_fixture();
|
|
|
|
let output = fabro_dev()
|
|
.args([
|
|
"release",
|
|
"--root",
|
|
fixture
|
|
.path()
|
|
.to_str()
|
|
.expect("fixture path should be utf-8"),
|
|
"--release-date",
|
|
"2026-01-01",
|
|
"--dry-run",
|
|
])
|
|
.assert()
|
|
.success()
|
|
.get_output()
|
|
.clone();
|
|
let stdout = output_text(&output.stdout);
|
|
|
|
assert!(
|
|
stdout.contains("Releasing 0.100.0 (tag v0.100.0)"),
|
|
"dry-run should compute base version from date:\n{stdout}"
|
|
);
|
|
assert!(
|
|
stdout.contains("cargo --locked dev spa refresh"),
|
|
"dry-run should print one SPA refresh command:\n{stdout}"
|
|
);
|
|
assert!(
|
|
!stdout.contains("git diff --exit-code -- lib/crates/fabro-spa/assets"),
|
|
"dry-run should not print a separate SPA asset diff command:\n{stdout}"
|
|
);
|
|
assert!(
|
|
stdout.contains("git tag -a v0.100.0 -m v0.100.0"),
|
|
"dry-run should print release tag command:\n{stdout}"
|
|
);
|
|
assert!(
|
|
stdout.contains(
|
|
"unset GH_TOKEN GITHUB_TOKEN && SEGMENT_WRITE_KEY=fake-for-local-smoke cargo nextest run --locked"
|
|
),
|
|
"dry-run should show release tests without inherited GitHub tokens:\n{stdout}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dry_run_increments_existing_prerelease_number() {
|
|
let fixture = release_fixture();
|
|
git(fixture.path(), &["tag", "v0.100.0-nightly.0"]);
|
|
|
|
let output = fabro_dev()
|
|
.args([
|
|
"release",
|
|
"--root",
|
|
fixture
|
|
.path()
|
|
.to_str()
|
|
.expect("fixture path should be utf-8"),
|
|
"--release-date",
|
|
"2026-01-01",
|
|
"--dry-run",
|
|
"--nightly",
|
|
])
|
|
.assert()
|
|
.success()
|
|
.get_output()
|
|
.clone();
|
|
let stdout = output_text(&output.stdout);
|
|
|
|
assert!(
|
|
stdout.contains("Releasing 0.100.0-nightly.1 (tag v0.100.0-nightly.1)"),
|
|
"dry-run should increment existing nightly tag:\n{stdout}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn positional_nightly_fails_with_clap_error() {
|
|
let output = fabro_dev()
|
|
.args(["release", "nightly"])
|
|
.assert()
|
|
.failure()
|
|
.code(2)
|
|
.get_output()
|
|
.clone();
|
|
let stderr = output_text(&output.stderr);
|
|
|
|
assert!(
|
|
stderr.contains("unexpected argument 'nightly'"),
|
|
"nightly positional should be rejected by clap:\n{stderr}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dry_run_reports_skip_tests_without_running_release_tests() {
|
|
let fixture = release_fixture();
|
|
|
|
let output = fabro_dev()
|
|
.args([
|
|
"release",
|
|
"--root",
|
|
fixture
|
|
.path()
|
|
.to_str()
|
|
.expect("fixture path should be utf-8"),
|
|
"--release-date",
|
|
"2026-01-01",
|
|
"--dry-run",
|
|
"--skip-tests",
|
|
])
|
|
.assert()
|
|
.success()
|
|
.get_output()
|
|
.clone();
|
|
let stdout = output_text(&output.stdout);
|
|
|
|
assert!(
|
|
stdout.contains("--skip-tests set, would skip release-mode test smoke"),
|
|
"dry-run should report skip-tests behavior:\n{stdout}"
|
|
);
|
|
assert!(
|
|
!stdout.contains("cargo nextest run"),
|
|
"dry-run with skip-tests should not print release test command:\n{stdout}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn dirty_worktree_errors_unless_dry_run() {
|
|
let fixture = release_fixture();
|
|
write_file(fixture.path(), "dirty.txt", "dirty\n");
|
|
|
|
let output = fabro_dev()
|
|
.args([
|
|
"release",
|
|
"--root",
|
|
fixture
|
|
.path()
|
|
.to_str()
|
|
.expect("fixture path should be utf-8"),
|
|
"--release-date",
|
|
"2026-01-01",
|
|
"--skip-tests",
|
|
])
|
|
.assert()
|
|
.failure()
|
|
.code(1)
|
|
.get_output()
|
|
.clone();
|
|
let stderr = output_text(&output.stderr);
|
|
|
|
assert!(
|
|
stderr.contains("working tree is dirty"),
|
|
"dirty non-dry-run release should fail before mutating:\n{stderr}"
|
|
);
|
|
}
|