From 3cd71475f098358f70efa018ee7e6fbe9817dcef Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 27 Oct 2026 19:32:33 +0100 Subject: [PATCH 1/2] feat(cli): add snapshot command for PR preview versions Signed-off-by: Test --- docs/site/docs-en/reference/cli.md | 41 +++++ docs/site/docs-fr/reference/cli.md | 41 +++++ src/cli.rs | 33 ++++ src/monorepo/check.rs | 10 +- src/monorepo/mod.rs | 2 + src/monorepo/snapshot.rs | 216 ++++++++++++++++++++++ src/monorepo/snapshot/tests.rs | 279 +++++++++++++++++++++++++++++ 7 files changed, 618 insertions(+), 4 deletions(-) create mode 100644 src/monorepo/snapshot.rs create mode 100644 src/monorepo/snapshot/tests.rs diff --git a/docs/site/docs-en/reference/cli.md b/docs/site/docs-en/reference/cli.md index 79428b1c..8bf6a086 100644 --- a/docs/site/docs-en/reference/cli.md +++ b/docs/site/docs-en/reference/cli.md @@ -84,6 +84,47 @@ At the end it lists the commits, the files written and the tags created, then de --- +## `ferrflow snapshot` + +Stamp every package with pending changes with a throwaway pre-release version, for a PR preview build. + +```bash +ferrflow snapshot --tag [--from-zero] [--no-git] [--run-hooks] +``` + +| Flag | Description | +| ---------------- | ------------------------------------------------------------------------ | +| `--tag ` | Pre-release suffix, e.g. `pr-123`. Must be a valid semver pre-release | +| `--from-zero` | Use `0.0.0-.` instead of the computed next version | +| `--no-git` | Skip the clean working tree check, for checkouts CI already modified | +| `--run-hooks` | Run the `pre_publish` and `post_publish` hooks with the snapshot version | + +The next version is computed from every commit since each package's last release, then its pre-release part is replaced: `1.3.0` becomes `1.3.0-pr-123.abc1234`, where `abc1234` is the short HEAD commit. Only versioned files are written. There is no changelog, commit, tag or push, and configured publishers do not run, so nothing can move a `latest` dist-tag. With `--dry-run` nothing is written. + +It prints one JSON object on stdout: + +```json +{ + "tag": "pr-123", + "commit": "abc1234", + "dry_run": false, + "packages": [ + { + "name": "cli", + "version": "1.3.0-pr-123.abc1234", + "previous_version": "1.2.0" + } + ] +} +``` + +```bash +ferrflow snapshot --tag "pr-$PR_NUMBER" > snapshot.json +npm publish --tag "pr-$PR_NUMBER" +``` + +--- + ## `ferrflow check` Preview what `ferrflow release` would do without making any changes. diff --git a/docs/site/docs-fr/reference/cli.md b/docs/site/docs-fr/reference/cli.md index 8ce68a87..ee079e95 100644 --- a/docs/site/docs-fr/reference/cli.md +++ b/docs/site/docs-fr/reference/cli.md @@ -53,6 +53,47 @@ Avec `releaseCommitMode: "pr"`, le clone reçoit le commit et les tags qui arriv --- +## `ferrflow snapshot` + +Donner à chaque package qui a des changements en attente une version de pré-release jetable, pour un build de preview de PR. + +```bash +ferrflow snapshot --tag [--from-zero] [--no-git] [--run-hooks] +``` + +| Option | Description | +| ----------------- | ------------------------------------------------------------------------------------- | +| `--tag ` | Suffixe de pré-release, par exemple `pr-123`. Doit être une pré-release semver valide | +| `--from-zero` | Utiliser `0.0.0-.` au lieu de la prochaine version calculée | +| `--no-git` | Ne pas exiger un working tree propre, pour un checkout déjà modifié par la CI | +| `--run-hooks` | Exécuter les hooks `pre_publish` et `post_publish` avec la version snapshot | + +La prochaine version est calculée à partir de tous les commits depuis la dernière release de chaque package, puis sa partie pré-release est remplacée : `1.3.0` devient `1.3.0-pr-123.abc1234`, où `abc1234` est le commit HEAD abrégé. Seuls les fichiers versionnés sont écrits. Pas de changelog, de commit, de tag ni de push, et les publishers configurés ne tournent pas, donc rien ne peut déplacer un dist-tag `latest`. Avec `--dry-run`, rien n'est écrit. + +La commande affiche un objet JSON sur stdout : + +```json +{ + "tag": "pr-123", + "commit": "abc1234", + "dry_run": false, + "packages": [ + { + "name": "cli", + "version": "1.3.0-pr-123.abc1234", + "previous_version": "1.2.0" + } + ] +} +``` + +```bash +ferrflow snapshot --tag "pr-$PR_NUMBER" > snapshot.json +npm publish --tag "pr-$PR_NUMBER" +``` + +--- + ## `ferrflow check` Prévisualiser ce que `ferrflow release` ferait sans effectuer de changements. diff --git a/src/cli.rs b/src/cli.rs index 72952db2..3e5af78d 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -84,6 +84,22 @@ pub enum Commands { #[arg(long, help = "Keep the clone and print its path")] keep: bool, }, + /// Stamp pending packages with a throwaway `-.` version for a PR preview. + /// Writes versioned files only: no changelog, commit, tag or push. + Snapshot { + /// Pre-release suffix, e.g. `pr-123`. + #[arg(long, value_name = "SUFFIX")] + tag: String, + /// Use `0.0.0-.` instead of the computed next version. + #[arg(long)] + from_zero: bool, + /// Skip the clean working tree check. + #[arg(long)] + no_git: bool, + /// Run the pre_publish and post_publish hooks. Configured publishers are not run. + #[arg(long)] + run_hooks: bool, + }, Publish { packages: Vec, #[arg(short = 'a', long)] @@ -226,6 +242,7 @@ impl Commands { Commands::Check { .. } => "check", Commands::Release { .. } => "release", Commands::ShadowRelease { .. } => "shadow-release", + Commands::Snapshot { .. } => "snapshot", Commands::Publish { .. } => "publish", Commands::Changelog => "changelog", Commands::Init { .. } => "init", @@ -317,6 +334,22 @@ impl Cli { Commands::ShadowRelease { keep } => { crate::shadow::run(self.config.as_deref(), self.verbose, keep) } + Commands::Snapshot { + tag, + from_zero, + no_git, + run_hooks, + } => crate::monorepo::snapshot( + self.config.as_deref(), + &crate::monorepo::SnapshotOptions { + tag: &tag, + from_zero, + no_git, + run_hooks, + dry_run: self.dry_run, + verbose: self.verbose, + }, + ), Commands::Publish { packages, all } => crate::publish::run( self.config.as_deref(), &packages, diff --git a/src/monorepo/check.rs b/src/monorepo/check.rs index 7390f4fd..dea0fda0 100644 --- a/src/monorepo/check.rs +++ b/src/monorepo/check.rs @@ -101,11 +101,13 @@ pub(super) fn plan_json(config_path: Option<&Path>, channel: Option<&str>) -> Re let repo = open_repo(&std::env::current_dir()?)?; let root = get_repo_root(&repo)?; let config = Config::load(&root, config_path)?; - let mut timing = Timing::new(false); + plan_json_at(&root, &config, channel) +} +pub(super) fn plan_json_at(root: &Path, config: &Config, channel: Option<&str>) -> Result { let out = run_release_logic( - &root, - &config, + root, + config, true, false, true, @@ -117,7 +119,7 @@ pub(super) fn plan_json(config_path: Option<&Path>, channel: Option<&str>) -> Re false, false, false, - &mut timing, + &mut Timing::new(false), )?; Ok(out.json.unwrap_or_else(|| r#"{"packages":[]}"#.to_string())) diff --git a/src/monorepo/mod.rs b/src/monorepo/mod.rs index bcc4f603..e3fb4be1 100644 --- a/src/monorepo/mod.rs +++ b/src/monorepo/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod preview; mod release; pub(crate) mod release_output; pub(crate) mod run; +mod snapshot; mod types; mod util; mod version_source; @@ -18,6 +19,7 @@ pub use graph_report::run as graph; pub use plan_interactive::run as plan_interactive; pub use release::release; pub use run::why; +pub use snapshot::{SnapshotOptions, snapshot}; #[cfg(test)] mod tests; diff --git a/src/monorepo/snapshot.rs b/src/monorepo/snapshot.rs new file mode 100644 index 00000000..7ad97bfa --- /dev/null +++ b/src/monorepo/snapshot.rs @@ -0,0 +1,216 @@ +use anyhow::{Context, Result, bail}; +use semver::{BuildMetadata, Prerelease, Version}; +use serde::Serialize; +use std::path::Path; + +use crate::config::{Config, PackageConfig}; +use crate::formats::write_version; +use crate::git::{get_repo_root, open_repo}; +use crate::hooks::{HookContext, HookPackage, HookPoint, resolve_hook, resolve_policy, run_hook}; + +use super::check::plan_json_at; +use super::types::{CheckPackage, CheckResult}; +use super::util::collect_dirty_files; + +pub struct SnapshotOptions<'a> { + pub tag: &'a str, + pub from_zero: bool, + pub no_git: bool, + pub run_hooks: bool, + pub dry_run: bool, + pub verbose: bool, +} + +#[derive(Debug, Serialize)] +struct SnapshotReport { + tag: String, + commit: String, + dry_run: bool, + packages: Vec, +} + +#[derive(Debug, Serialize)] +struct SnapshotPackage { + name: String, + version: String, + previous_version: String, + #[serde(skip)] + bump_type: String, +} + +pub fn snapshot(config_path: Option<&Path>, opts: &SnapshotOptions<'_>) -> Result<()> { + let repo = open_repo(&std::env::current_dir()?)?; + let root = get_repo_root(&repo)?; + let config = Config::load(&root, config_path)?; + let report = run(&root, config, opts)?; + println!("{}", serde_json::to_string(&report)?); + Ok(()) +} + +fn run(root: &Path, mut config: Config, opts: &SnapshotOptions<'_>) -> Result { + config.workspace.recover_missed_releases = true; + let suffix = parse_suffix(opts.tag)?; + let repo = open_repo(root)?; + + if !opts.no_git { + let mut dirty: Vec = collect_dirty_files(&repo).into_iter().collect(); + if !dirty.is_empty() { + dirty.sort(); + bail!( + "working tree has uncommitted changes ({}); commit them or pass --no-git", + dirty.join(", ") + ); + } + } + + let commit = repo + .head_id() + .context("snapshot needs a commit to stamp the version with")? + .to_string()[..7] + .to_string(); + + let plan: CheckResult = serde_json::from_str(&plan_json_at(root, &config, None)?)?; + let packages = plan + .packages + .into_iter() + .filter_map(|pkg| snapshot_package(pkg, &suffix, &commit, opts.from_zero).transpose()) + .collect::>>()?; + + if !opts.dry_run { + for pkg in &packages { + for vf in &package_config(&config, &pkg.name)?.versioned_files { + write_version(vf, root, &pkg.version)?; + } + } + } + + if opts.run_hooks { + for point in [HookPoint::PrePublish, HookPoint::PostPublish] { + run_publish_hooks(root, &config, &packages, opts, point)?; + } + } + + Ok(SnapshotReport { + tag: opts.tag.to_string(), + commit, + dry_run: opts.dry_run, + packages, + }) +} + +fn parse_suffix(tag: &str) -> Result { + if tag.is_empty() { + bail!("--tag must not be empty"); + } + Prerelease::new(tag).with_context(|| { + format!("--tag `{tag}` is not a valid semver pre-release (use [0-9A-Za-z-], dot-separated)") + }) +} + +fn snapshot_package( + pkg: CheckPackage, + suffix: &Prerelease, + commit: &str, + from_zero: bool, +) -> Result> { + let Some(next) = pkg.next_version else { + return Ok(None); + }; + let version = snapshot_version(&next, suffix, commit, from_zero) + .with_context(|| format!("package '{}': cannot snapshot version `{next}`", pkg.name))?; + Ok(Some(SnapshotPackage { + name: pkg.name, + version, + previous_version: pkg.current_version, + bump_type: pkg.bump_type, + })) +} + +fn snapshot_version( + next: &str, + suffix: &Prerelease, + commit: &str, + from_zero: bool, +) -> Result { + let mut version = if from_zero { + Version::new(0, 0, 0) + } else { + Version::parse(next)? + }; + let commit_id = if commit.starts_with('0') && commit.bytes().all(|b| b.is_ascii_digit()) { + format!("g{commit}") + } else { + commit.to_string() + }; + version.pre = Prerelease::new(&format!("{suffix}.{commit_id}"))?; + version.build = BuildMetadata::EMPTY; + Ok(version.to_string()) +} + +fn package_config<'a>(config: &'a Config, name: &str) -> Result<&'a PackageConfig> { + config + .packages + .iter() + .find(|p| p.name == name) + .with_context(|| format!("package '{name}' not found in config")) +} + +fn run_publish_hooks( + root: &Path, + config: &Config, + packages: &[SnapshotPackage], + opts: &SnapshotOptions<'_>, + point: HookPoint, +) -> Result<()> { + let all_packages: Vec = packages + .iter() + .map(|p| HookPackage { + name: p.name.clone(), + version: p.version.clone(), + bump: p.bump_type.clone(), + }) + .collect(); + let ws_hooks = config.workspace.hooks.as_ref(); + + for pkg in packages { + let pkg_config = package_config(config, &pkg.name)?; + let pkg_hooks = pkg_config.hooks.as_ref(); + let Some(cmd) = resolve_hook(pkg_hooks, ws_hooks, point) else { + continue; + }; + let ctx = HookContext { + package: pkg.name.clone(), + old_version: pkg.previous_version.clone(), + new_version: pkg.version.clone(), + bump_type: pkg.bump_type.clone(), + tag: String::new(), + dry_run: opts.dry_run, + package_path: root + .join(pkg_config.path.trim_start_matches("./")) + .to_string_lossy() + .into_owned(), + channel: Some(opts.tag.to_string()), + error_code: None, + monorepo: config.is_monorepo(), + is_prerelease: true, + changelog: String::new(), + commits: Vec::new(), + bumped_files: Vec::new(), + all_packages: all_packages.clone(), + release_url: None, + }; + run_hook( + point, + &cmd, + &ctx, + resolve_policy(pkg_hooks, ws_hooks), + opts.dry_run, + opts.verbose, + root, + )?; + } + Ok(()) +} + +#[cfg(test)] +mod tests; diff --git a/src/monorepo/snapshot/tests.rs b/src/monorepo/snapshot/tests.rs new file mode 100644 index 00000000..a95b9de6 --- /dev/null +++ b/src/monorepo/snapshot/tests.rs @@ -0,0 +1,279 @@ +use super::*; +use crate::test_utils::{commit_file, git, init_repo}; + +fn suffix(s: &str) -> Prerelease { + Prerelease::new(s).unwrap() +} + +fn opts(tag: &str) -> SnapshotOptions<'_> { + SnapshotOptions { + tag, + from_zero: false, + no_git: false, + run_hooks: false, + dry_run: false, + verbose: false, + } +} + +#[test] +fn version_appends_suffix_and_commit_to_next_version() { + assert_eq!( + snapshot_version("1.2.3", &suffix("pr-123"), "abc1234", false).unwrap(), + "1.2.3-pr-123.abc1234" + ); +} + +#[test] +fn version_replaces_existing_prerelease_and_build_metadata() { + assert_eq!( + snapshot_version("2.0.0-beta.4+build.7", &suffix("pr-9"), "abc1234", false).unwrap(), + "2.0.0-pr-9.abc1234" + ); +} + +#[test] +fn from_zero_ignores_the_computed_version() { + assert_eq!( + snapshot_version("3.4.5", &suffix("pr-1"), "abc1234", true).unwrap(), + "0.0.0-pr-1.abc1234" + ); +} + +#[test] +fn numeric_commit_with_leading_zero_stays_valid_semver() { + let v = snapshot_version("1.0.0", &suffix("pr-1"), "0123456", false).unwrap(); + assert_eq!(v, "1.0.0-pr-1.g0123456"); + Version::parse(&v).unwrap(); +} + +#[test] +fn non_semver_next_version_is_an_error() { + assert!(snapshot_version("not-a-version", &suffix("pr-1"), "abc1234", false).is_err()); +} + +#[test] +fn suffix_must_be_a_valid_prerelease() { + assert!(parse_suffix("pr-123").is_ok()); + assert!(parse_suffix("pr.123").is_ok()); + assert!(parse_suffix("").is_err()); + assert!(parse_suffix("pr_123").is_err()); + assert!(parse_suffix("pr..1").is_err()); + assert!(parse_suffix("pr/1").is_err()); +} + +fn package_json(version: &str) -> String { + format!("{{\n \"name\": \"x\",\n \"version\": \"{version}\"\n}}\n") +} + +fn monorepo(extra_ws: &str) -> (tempfile::TempDir, Config) { + let (dir, _repo) = init_repo(); + let root = dir.path(); + std::fs::write( + root.join(".ferrflow"), + format!( + r#"{{"workspace": {{"releaseCommitMode": "none"{extra_ws}}}, "package": [ + {{"name": "api", "path": "api", "versionedFiles": [{{"path": "api/package.json", "format": "json"}}]}}, + {{"name": "web", "path": "web", "versionedFiles": [{{"path": "web/package.json", "format": "json"}}]}} + ]}}"# + ), + ) + .unwrap(); + for pkg in ["api", "web"] { + std::fs::create_dir_all(root.join(pkg)).unwrap(); + std::fs::write(root.join(pkg).join("package.json"), package_json("1.0.0")).unwrap(); + } + git(root, &["add", "."]); + git(root, &["commit", "-m", "chore: init"]); + git(root, &["tag", "api@v1.0.0"]); + git(root, &["tag", "web@v1.0.0"]); + commit_file(root, "api/index.js", "x", "feat(api): add x", 1_900_000_000); + + let config = Config::load(root, Some(&root.join(".ferrflow"))).unwrap(); + (dir, config) +} + +fn head(root: &Path) -> String { + git(root, &["rev-parse", "HEAD"]).trim().to_string() +} + +fn version_in(root: &Path, pkg: &str) -> String { + let raw = std::fs::read_to_string(root.join(pkg).join("package.json")).unwrap(); + serde_json::from_str::(&raw).unwrap()["version"] + .as_str() + .unwrap() + .to_string() +} + +#[test] +fn snapshots_only_packages_with_pending_changes() { + let (dir, config) = monorepo(""); + let root = dir.path(); + let sha = head(root)[..7].to_string(); + + let report = run(root, config, &opts("pr-123")).unwrap(); + + assert_eq!(report.commit, sha); + assert_eq!(report.packages.len(), 1); + assert_eq!(report.packages[0].name, "api"); + assert_eq!(report.packages[0].previous_version, "1.0.0"); + assert_eq!(report.packages[0].version, format!("1.1.0-pr-123.{sha}")); + assert_eq!(version_in(root, "api"), format!("1.1.0-pr-123.{sha}")); + assert_eq!(version_in(root, "web"), "1.0.0"); +} + +#[test] +fn counts_every_commit_since_the_last_release_not_just_head() { + let (dir, config) = monorepo(""); + let root = dir.path(); + commit_file(root, "README.md", "docs", "docs: readme", 1_900_000_100); + + let report = run(root, config, &opts("pr-2")).unwrap(); + + assert_eq!(report.packages.len(), 1); + assert_eq!(report.packages[0].name, "api"); +} + +#[test] +fn leaves_history_and_tags_untouched() { + let (dir, config) = monorepo(""); + let root = dir.path(); + let before_head = head(root); + let before_tags = git(root, &["tag", "--list"]); + + run(root, config, &opts("pr-1")).unwrap(); + + assert_eq!(head(root), before_head); + assert_eq!(git(root, &["tag", "--list"]), before_tags); +} + +#[test] +fn dry_run_writes_nothing() { + let (dir, config) = monorepo(""); + let root = dir.path(); + + let report = run( + root, + config, + &SnapshotOptions { + dry_run: true, + ..opts("pr-1") + }, + ) + .unwrap(); + + assert!(report.dry_run); + assert!(report.packages[0].version.starts_with("1.1.0-pr-1.")); + assert_eq!(version_in(root, "api"), "1.0.0"); +} + +#[test] +fn dirty_tree_is_refused_without_no_git() { + let (dir, config) = monorepo(""); + let root = dir.path(); + std::fs::write(root.join("api/index.js"), "edited").unwrap(); + + let err = run(root, config, &opts("pr-1")).unwrap_err(); + + assert!(format!("{err:#}").contains("--no-git"), "{err:#}"); + assert_eq!(version_in(root, "api"), "1.0.0"); +} + +#[test] +fn no_git_runs_on_a_dirty_tree() { + let (dir, config) = monorepo(""); + let root = dir.path(); + std::fs::write(root.join("api/index.js"), "edited").unwrap(); + + let report = run( + root, + config, + &SnapshotOptions { + no_git: true, + ..opts("pr-1") + }, + ) + .unwrap(); + + assert_eq!(report.packages.len(), 1); + assert!(version_in(root, "api").starts_with("1.1.0-pr-1.")); +} + +#[test] +fn invalid_tag_fails_before_touching_files() { + let (dir, config) = monorepo(""); + let root = dir.path(); + + assert!(run(root, config, &opts("pr_1")).is_err()); + assert_eq!(version_in(root, "api"), "1.0.0"); +} + +#[test] +fn json_report_shape_is_stable() { + let (dir, config) = monorepo(""); + let report = run(dir.path(), config, &opts("pr-7")).unwrap(); + + let json = serde_json::to_value(&report).unwrap(); + let mut keys: Vec<&str> = json + .as_object() + .unwrap() + .keys() + .map(String::as_str) + .collect(); + keys.sort(); + assert_eq!(keys, ["commit", "dry_run", "packages", "tag"]); + assert_eq!(json["tag"], "pr-7"); + + let mut pkg_keys: Vec<&str> = json["packages"][0] + .as_object() + .unwrap() + .keys() + .map(String::as_str) + .collect(); + pkg_keys.sort(); + assert_eq!(pkg_keys, ["name", "previous_version", "version"]); +} + +#[test] +fn publish_hooks_see_the_snapshot_version() { + let (dir, config) = monorepo( + r#", "hooks": {"prePublish": "echo pre $FERRFLOW_PACKAGE $FERRFLOW_NEW_VERSION $FERRFLOW_CHANNEL >> hooks.log", "postPublish": "echo post $FERRFLOW_PACKAGE >> hooks.log"}"#, + ); + let root = dir.path(); + let sha = head(root)[..7].to_string(); + + run( + root, + config, + &SnapshotOptions { + run_hooks: true, + no_git: true, + ..opts("pr-5") + }, + ) + .unwrap(); + let log = std::fs::read_to_string(root.join("hooks.log")).unwrap(); + assert_eq!( + log, + format!("pre api 1.1.0-pr-5.{sha} pr-5\npost api\n"), + "pre_publish must fire for every package before any post_publish" + ); +} + +#[test] +fn hooks_do_not_run_unless_asked() { + let (dir, config) = monorepo(r#", "hooks": {"prePublish": "touch hooks.log"}"#); + let root = dir.path(); + + run( + root, + config, + &SnapshotOptions { + no_git: true, + ..opts("pr-5") + }, + ) + .unwrap(); + + assert!(!root.join("hooks.log").exists()); +} From 242fface1c3fdc74f6d03a88cb518da54b120b27 Mon Sep 17 00:00:00 2001 From: Test Date: Tue, 27 Oct 2026 19:35:08 +0100 Subject: [PATCH 2/2] fix(cli): refresh lockfiles in snapshot when updateLockfiles is on Signed-off-by: Test --- docs/site/docs-en/reference/cli.md | 2 +- docs/site/docs-fr/reference/cli.md | 2 +- src/monorepo/snapshot.rs | 7 +++- src/monorepo/snapshot/tests.rs | 52 ++++++++++++++++++++++++++++++ 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/docs/site/docs-en/reference/cli.md b/docs/site/docs-en/reference/cli.md index 8bf6a086..11feb453 100644 --- a/docs/site/docs-en/reference/cli.md +++ b/docs/site/docs-en/reference/cli.md @@ -99,7 +99,7 @@ ferrflow snapshot --tag [--from-zero] [--no-git] [--run-hooks] | `--no-git` | Skip the clean working tree check, for checkouts CI already modified | | `--run-hooks` | Run the `pre_publish` and `post_publish` hooks with the snapshot version | -The next version is computed from every commit since each package's last release, then its pre-release part is replaced: `1.3.0` becomes `1.3.0-pr-123.abc1234`, where `abc1234` is the short HEAD commit. Only versioned files are written. There is no changelog, commit, tag or push, and configured publishers do not run, so nothing can move a `latest` dist-tag. With `--dry-run` nothing is written. +The next version is computed from every commit since each package's last release, then its pre-release part is replaced: `1.3.0` becomes `1.3.0-pr-123.abc1234`, where `abc1234` is the short HEAD commit. Only versioned files are written, plus lockfiles when `updateLockfiles` is on. There is no changelog, commit, tag or push, and configured publishers do not run, so nothing can move a `latest` dist-tag. With `--dry-run` nothing is written. It prints one JSON object on stdout: diff --git a/docs/site/docs-fr/reference/cli.md b/docs/site/docs-fr/reference/cli.md index ee079e95..401556be 100644 --- a/docs/site/docs-fr/reference/cli.md +++ b/docs/site/docs-fr/reference/cli.md @@ -68,7 +68,7 @@ ferrflow snapshot --tag [--from-zero] [--no-git] [--run-hooks] | `--no-git` | Ne pas exiger un working tree propre, pour un checkout déjà modifié par la CI | | `--run-hooks` | Exécuter les hooks `pre_publish` et `post_publish` avec la version snapshot | -La prochaine version est calculée à partir de tous les commits depuis la dernière release de chaque package, puis sa partie pré-release est remplacée : `1.3.0` devient `1.3.0-pr-123.abc1234`, où `abc1234` est le commit HEAD abrégé. Seuls les fichiers versionnés sont écrits. Pas de changelog, de commit, de tag ni de push, et les publishers configurés ne tournent pas, donc rien ne peut déplacer un dist-tag `latest`. Avec `--dry-run`, rien n'est écrit. +La prochaine version est calculée à partir de tous les commits depuis la dernière release de chaque package, puis sa partie pré-release est remplacée : `1.3.0` devient `1.3.0-pr-123.abc1234`, où `abc1234` est le commit HEAD abrégé. Seuls les fichiers versionnés sont écrits, ainsi que les lockfiles quand `updateLockfiles` est activé. Pas de changelog, de commit, de tag ni de push, et les publishers configurés ne tournent pas, donc rien ne peut déplacer un dist-tag `latest`. Avec `--dry-run`, rien n'est écrit. La commande affiche un objet JSON sur stdout : diff --git a/src/monorepo/snapshot.rs b/src/monorepo/snapshot.rs index 7ad97bfa..a4fa99df 100644 --- a/src/monorepo/snapshot.rs +++ b/src/monorepo/snapshot.rs @@ -9,6 +9,7 @@ use crate::git::{get_repo_root, open_repo}; use crate::hooks::{HookContext, HookPackage, HookPoint, resolve_hook, resolve_policy, run_hook}; use super::check::plan_json_at; +use super::run::refresh_lockfiles; use super::types::{CheckPackage, CheckResult}; use super::util::collect_dirty_files; @@ -78,9 +79,13 @@ fn run(root: &Path, mut config: Config, opts: &SnapshotOptions<'_>) -> Result