diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index 882bd3d..a8cc614 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -19,7 +19,7 @@ jobs: include: - os: ubuntu-24.04 timeout: timeout - - os: macos-14 + - os: macos-15 timeout: gtimeout runs-on: ${{ matrix.os }} timeout-minutes: 5 @@ -27,7 +27,7 @@ jobs: - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 with: persist-credentials: false - - if: matrix.os == 'macos-14' + - if: matrix.os == 'macos-15' run: brew install coreutils timeout-minutes: 2 - run: ${{ matrix.timeout }} -k 5 120 rustup toolchain install 1.95 --profile minimal @@ -38,14 +38,14 @@ jobs: - run: ${{ matrix.timeout }} -k 5 270 cargo +1.95 nextest run --locked - run: ${{ matrix.timeout }} -k 5 60 cargo +1.95 test --manifest-path fixtures/handoff/Cargo.toml --locked --tests - run: ${{ matrix.timeout }} -k 5 60 cargo +1.95 test --manifest-path examples/spendcheck/Cargo.toml --locked --tests - - if: matrix.os == 'macos-14' + - if: matrix.os == 'macos-15' run: shasum -a256 target/debug/allstack > target/debug/allstack.sha256 - - if: matrix.os == 'macos-14' + - if: matrix.os == 'macos-15' run: git rev-parse HEAD > target/debug/allstack.commit - - if: matrix.os == 'macos-14' + - if: matrix.os == 'macos-15' uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f with: - name: allstack-macos-14 + name: allstack-macos-15 path: | target/debug/allstack target/debug/allstack.sha256 diff --git a/src/domain.rs b/src/domain.rs index f0c76ca..4f9420f 100644 --- a/src/domain.rs +++ b/src/domain.rs @@ -362,3 +362,27 @@ pub fn spec_path_valid(path: &str) -> bool { .split('/') .all(|part| !part.is_empty() && part != ".." && !part.starts_with('.')) } + +/// The managed commit message. The run marker joins the message's closing +/// trailer block, because git reads trailers only from the last paragraph and a +/// repository may require its own (`Release-note:`, `Signed-off-by:`). +pub fn managed_commit_message(message: &str, run_id: &str) -> String { + let message = message.trim_end(); + let marker = format!("Allstack-Run: {run_id}"); + let closes_with_trailers = message + .rsplit_once("\n\n") + .is_some_and(|(_, last)| last.lines().all(is_trailer)); + if closes_with_trailers { + format!("{message}\n{marker}") + } else { + format!("{message}\n\n{marker}") + } +} + +fn is_trailer(line: &str) -> bool { + line.split_once(": ").is_some_and(|(key, value)| { + !key.is_empty() + && !value.trim().is_empty() + && key.chars().all(|c| c.is_ascii_alphanumeric() || c == '-') + }) +} diff --git a/src/infrastructure/runtime.rs b/src/infrastructure/runtime.rs index eb4f78c..a581102 100644 --- a/src/infrastructure/runtime.rs +++ b/src/infrastructure/runtime.rs @@ -9,7 +9,9 @@ use super::{ }; use crate::{ application::ports::{Platform, Start}, - domain::{Policy, Publication, Run, Stage, Verification, WorkflowSelection}, + domain::{ + Policy, Publication, Run, Stage, Verification, WorkflowSelection, managed_commit_message, + }, }; use anyhow::{Context, Result, ensure}; use serde_json::{Value, json}; @@ -316,7 +318,8 @@ impl Platform for Runtime { if count == 1 { args.push("--amend"); } - args.extend(["-m", message, "-m", &marker]); + let full_message = managed_commit_message(message, &run.id); + args.extend(["-m", &full_message]); git::call(&self.state, &self.repo, &args)?; git::clean(&self.state, &self.repo)?; git::call(&self.state, &self.repo, &["rev-parse", "HEAD"]) diff --git a/tests/domain.rs b/tests/domain.rs index 225b19f..54ceb06 100644 --- a/tests/domain.rs +++ b/tests/domain.rs @@ -1,5 +1,26 @@ mod common; -use allstack::domain::{Event, Host, Policy, Stage, validate_transition}; +use allstack::domain::{Event, Host, Policy, Stage, managed_commit_message, validate_transition}; + +#[test] +fn the_run_marker_joins_the_closing_trailer_block() { + let message = "feat(x): add y\n\nBody.\n\nRelease-note: Users can y.\n"; + assert_eq!( + managed_commit_message(message, "run-1"), + "feat(x): add y\n\nBody.\n\nRelease-note: Users can y.\nAllstack-Run: run-1" + ); +} + +#[test] +fn the_run_marker_starts_its_own_block_after_prose_or_a_bare_subject() { + assert_eq!( + managed_commit_message("fix: add y\n\nA body line.", "run-1"), + "fix: add y\n\nA body line.\n\nAllstack-Run: run-1" + ); + assert_eq!( + managed_commit_message("fix: add y", "run-1"), + "fix: add y\n\nAllstack-Run: run-1" + ); +} #[test] fn stale_host_and_unknown_action_cannot_execute() { diff --git a/tests/workspace.rs b/tests/workspace.rs index a9f0107..611218c 100644 --- a/tests/workspace.rs +++ b/tests/workspace.rs @@ -49,7 +49,8 @@ impl Fixture { .map(String::from) .collect(), cwd: self.repo.clone(), - seconds: 10, + // The large-checkout case stages 10,010 files in one call. + seconds: 60, environment: BTreeMap::new(), input: None, directory: self.state.join("process"),