pub mod catalog;
use std::collections::BTreeMap;
use camino::Utf8Path;
use serde::{Deserialize, Serialize};
use crate::diagnostic::{Diagnostic, Reason};
use crate::error::RkError;
use crate::landing::manifest::{self, CheckoutMode, Integration, Provider, Style};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ReleaseMode {
Automatic,
External,
None,
}
impl ReleaseMode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Automatic => "automatic",
Self::External => "external",
Self::None => "none",
}
}
pub fn parse(raw: &str) -> Result<Self, RkError> {
match raw {
"automatic" => Ok(Self::Automatic),
"external" => Ok(Self::External),
"none" => Ok(Self::None),
other => Err(RkError::Usage(format!(
"unknown release mode '{other}'; the modes are: automatic, external, none"
))),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ReleaseIntent {
pub mode: ReleaseMode,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub driver: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub style: Option<Style>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub line_prefix: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProfileSnapshot {
pub technologies: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub forge: Option<String>,
pub release: ReleaseIntent,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GitWorkflow {
pub trunk: String,
pub checkout_mode: CheckoutMode,
#[serde(default = "crate::landing::manifest::integration_forge")]
pub integration: Integration,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapabilityRequests {
#[serde(default)]
pub nix_packaging: bool,
#[serde(default)]
pub reporting_policy: bool,
#[serde(default)]
pub scorecard: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub code_scanning: Option<Provider>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Source {
Flag,
Config,
Record,
Observation,
Default,
}
impl Source {
#[must_use]
pub const fn rank(self) -> u8 {
match self {
Self::Flag => 0,
Self::Config => 1,
Self::Record => 2,
Self::Observation => 3,
Self::Default => 4,
}
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Flag => "flag",
Self::Config => "config",
Self::Record => "record",
Self::Observation => "observation",
Self::Default => "default",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(rename_all = "kebab-case", tag = "state")]
pub enum Proposal {
None,
Automatic {
driver: String,
},
Ambiguous {
drivers: Vec<String>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Params {
profile: ProfileSnapshot,
git: GitWorkflow,
capabilities: CapabilityRequests,
repo: String,
security_contact: String,
security_response: String,
required_check: String,
required_workflow: String,
}
#[derive(Default)]
pub struct Inputs<'a> {
pub technologies: &'a [String],
pub forge: Option<&'a str>,
pub repo: Option<&'a str>,
pub release_mode: Option<ReleaseMode>,
pub release_driver: Option<&'a str>,
pub style: Option<Style>,
pub trunk: Option<&'a str>,
pub checkout_mode: Option<CheckoutMode>,
pub integration: Option<Integration>,
pub required_check: Option<&'a str>,
pub required_workflow: Option<&'a str>,
pub nix: Option<bool>,
pub reporting_policy: Option<bool>,
pub scorecard: Option<bool>,
pub code_scanning: Option<Option<Provider>>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Purpose {
Init,
Preview,
Upgrade,
Adopt,
}
#[derive(Debug, Clone)]
pub struct Resolved {
pub params: Params,
pub sources: BTreeMap<&'static str, Source>,
pub unknown: Vec<String>,
pub proposal: Option<Proposal>,
}
pub fn canonical_category(raw: &str) -> Result<String, String> {
let lowered = raw.trim().to_ascii_lowercase();
let shaped = lowered
.chars()
.next()
.is_some_and(|c| c.is_ascii_lowercase() || c.is_ascii_digit())
&& lowered
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-');
if shaped {
Ok(lowered)
} else {
Err(format!(
"{raw:?} is not a category name; one is lowercase letters and digits with hyphens inside, as [a-z0-9][a-z0-9-]*"
))
}
}
pub fn canonical_list(key: &str, raw: &[String]) -> Result<Vec<String>, String> {
let mut out = Vec::with_capacity(raw.len());
for value in raw {
let name = canonical_category(value).map_err(|reason| format!("{key}: {reason}"))?;
if out.contains(&name) {
return Err(format!("{key} names {name} twice; each technology once"));
}
out.push(name);
}
out.sort();
Ok(out)
}
fn push_shell_word(out: &mut String, value: &str) {
let inert = !value.is_empty()
&& value.bytes().all(|byte| {
byte.is_ascii_alphanumeric()
|| matches!(
byte,
b'_' | b'@' | b'%' | b'+' | b'=' | b':' | b',' | b'.' | b'/' | b'-'
)
});
if inert {
out.push_str(value);
return;
}
out.push('\'');
out.push_str(&value.replace('\'', "'\"'\"'"));
out.push('\'');
}
impl Params {
#[must_use]
pub fn from_record(record: &manifest::Manifest) -> Self {
Self {
profile: record.profile.clone(),
git: record.git.clone(),
capabilities: record.capabilities.clone(),
repo: record.parameters.repo.clone(),
security_contact: record.parameters.security_contact.clone(),
security_response: record.parameters.security_response.clone(),
required_check: gate_answer(
record,
|(check, _)| check,
|parameters| ¶meters.required_check,
),
required_workflow: gate_answer(
record,
|(_, workflow)| workflow,
|parameters| ¶meters.required_workflow,
),
}
}
pub fn resolve(
target: &Utf8Path,
flags: &Inputs<'_>,
config: Option<&crate::config::Config>,
record: Option<&manifest::Manifest>,
purpose: Purpose,
) -> Result<Self, RkError> {
resolve(target, flags, config, record, purpose).map(|resolved| resolved.params)
}
#[must_use]
pub const fn profile(&self) -> &ProfileSnapshot {
&self.profile
}
#[must_use]
pub const fn git(&self) -> &GitWorkflow {
&self.git
}
#[must_use]
pub const fn capabilities(&self) -> &CapabilityRequests {
&self.capabilities
}
#[must_use]
pub fn technologies(&self) -> &[String] {
&self.profile.technologies
}
#[must_use]
pub fn forge(&self) -> Option<&str> {
self.profile.forge.as_deref()
}
#[must_use]
pub const fn release_mode(&self) -> ReleaseMode {
self.profile.release.mode
}
#[must_use]
pub fn driver(&self) -> Option<&str> {
self.profile.release.driver.as_deref()
}
#[must_use]
pub const fn style(&self) -> Option<Style> {
self.profile.release.style
}
#[must_use]
pub const fn nix_packaging(&self) -> bool {
self.capabilities.nix_packaging
}
#[must_use]
pub const fn reporting_policy(&self) -> bool {
self.capabilities.reporting_policy
}
#[must_use]
pub const fn scorecard(&self) -> bool {
self.capabilities.scorecard
}
#[must_use]
pub const fn code_scanning(&self) -> Option<Provider> {
self.capabilities.code_scanning
}
#[must_use]
pub fn repo(&self) -> &str {
&self.repo
}
#[must_use]
pub const fn checkout_mode(&self) -> CheckoutMode {
self.git.checkout_mode
}
#[must_use]
pub const fn integration(&self) -> Integration {
self.git.integration
}
#[must_use]
pub fn trunk(&self) -> &str {
&self.git.trunk
}
#[must_use]
pub fn line_prefix(&self) -> &str {
self.profile
.release
.line_prefix
.as_deref()
.unwrap_or(crate::config::LINE_PREFIX_DEFAULT)
}
#[must_use]
pub fn security_contact(&self) -> &str {
&self.security_contact
}
#[must_use]
pub fn security_response(&self) -> &str {
&self.security_response
}
#[must_use]
pub fn required_check(&self) -> &str {
&self.required_check
}
#[must_use]
pub fn required_workflow(&self) -> &str {
&self.required_workflow
}
#[must_use]
pub fn canonical_flags(&self) -> String {
let mut out = String::new();
for technology in &self.profile.technologies {
out.push_str(" --technology ");
out.push_str(technology);
}
if let Some(forge) = &self.profile.forge {
out.push_str(" --forge ");
out.push_str(forge);
}
if !self.repo.is_empty() && self.repo != crate::projection::REPO_PLACEHOLDER {
out.push_str(" --repo ");
out.push_str(&self.repo);
}
out.push_str(" --release-mode ");
out.push_str(self.profile.release.mode.as_str());
if let Some(driver) = &self.profile.release.driver {
out.push_str(" --release-driver ");
out.push_str(driver);
}
if let Some(style) = self.profile.release.style {
out.push_str(" --release-style ");
out.push_str(style.as_str());
}
out.push_str(" --trunk ");
out.push_str(&self.git.trunk);
out.push_str(" --checkout-mode ");
out.push_str(self.git.checkout_mode.as_str());
out.push_str(" --integration ");
out.push_str(self.git.integration.as_str());
if !self.required_check.is_empty() {
out.push_str(" --required-check ");
push_shell_word(&mut out, &self.required_check);
}
if !self.required_workflow.is_empty() {
out.push_str(" --required-workflow ");
push_shell_word(&mut out, &self.required_workflow);
}
out
}
#[must_use]
pub fn capability_flags(&self) -> String {
let mut out = String::new();
if self.capabilities.nix_packaging {
out.push_str(" --nix-packaging");
}
if self.capabilities.reporting_policy {
out.push_str(" --reporting-policy");
}
if self.capabilities.scorecard {
out.push_str(" --scorecard");
}
out.push_str(" --code-scanning ");
out.push_str(
self.capabilities
.code_scanning
.map_or("off", Provider::as_str),
);
out
}
#[must_use]
pub fn capability_toggles(&self) -> String {
let word = |on: bool| if on { "on" } else { "off" };
format!(
" --nix-packaging {} --reporting-policy {} --scorecard {} --code-scanning {}",
word(self.capabilities.nix_packaging),
word(self.capabilities.reporting_policy),
word(self.capabilities.scorecard),
self.capabilities
.code_scanning
.map_or("off", Provider::as_str)
)
}
}
#[cfg(test)]
impl Params {
pub(crate) fn for_test(repo: &str, style: Option<Style>) -> Self {
Self {
profile: ProfileSnapshot {
technologies: vec!["rust".to_owned()],
forge: Some("github".to_owned()),
release: ReleaseIntent {
mode: ReleaseMode::Automatic,
driver: Some("rust".to_owned()),
style,
line_prefix: Some(crate::config::LINE_PREFIX_DEFAULT.to_owned()),
},
},
git: GitWorkflow {
trunk: crate::config::TRUNK_DEFAULT.to_owned(),
checkout_mode: CheckoutMode::LinkedWorktree,
integration: Integration::Local,
},
capabilities: CapabilityRequests {
nix_packaging: false,
reporting_policy: true,
scorecard: false,
code_scanning: None,
},
repo: repo.to_owned(),
security_contact: String::new(),
security_response: crate::config::RESPONSE_DEFAULT.to_owned(),
required_check: "gate".to_owned(),
required_workflow: "ci".to_owned(),
}
}
pub(crate) fn for_test_security(contact: &str, response: &str) -> Self {
Self {
security_contact: contact.to_owned(),
security_response: response.to_owned(),
..Self::for_test("acme/widget", Some(Style::Trunk))
}
}
pub(crate) fn for_test_release_less(
technologies: &[&str],
forge: Option<&str>,
mode: ReleaseMode,
) -> Self {
let mut params = Self::for_test("acme/widget", None);
params.profile.technologies = technologies.iter().map(|t| (*t).to_owned()).collect();
params.profile.forge = forge.map(str::to_owned);
params.profile.release = ReleaseIntent {
mode,
driver: None,
style: None,
line_prefix: None,
};
params.capabilities.reporting_policy = false;
if forge.is_none() {
params.repo = String::new();
}
params
}
pub(crate) fn set_pair_for_test(&mut self, driver: &str, forge: &str) {
self.profile.technologies = vec![driver.to_owned()];
self.profile.release.driver = Some(driver.to_owned());
self.profile.forge = Some(forge.to_owned());
}
pub(crate) const fn set_checkout_mode_for_test(&mut self, mode: CheckoutMode) {
self.git.checkout_mode = mode;
}
pub(crate) const fn set_integration_for_test(&mut self, mode: Integration) {
self.git.integration = mode;
}
pub(crate) const fn set_nix_for_test(&mut self, nix: bool) {
self.capabilities.nix_packaging = nix;
}
pub(crate) const fn set_scorecard_for_test(&mut self, scorecard: bool) {
self.capabilities.scorecard = scorecard;
}
pub(crate) const fn set_code_scanning_for_test(&mut self, provider: Option<Provider>) {
self.capabilities.code_scanning = provider;
}
}
fn invalid_release(message: impl std::fmt::Display) -> RkError {
RkError::Usage(format!(
"{message}; an automatic release names a driver among profile.technologies and a style, and an external or none release names neither"
))
}
#[must_use]
pub fn gate_defaults(
profile: &ProfileSnapshot,
git: &GitWorkflow,
) -> Option<(&'static str, &'static str)> {
let consuming = profile.forge.as_deref() == Some("github")
&& profile.release.mode == ReleaseMode::Automatic
&& profile.release.style == Some(Style::Trunk)
&& git.integration == Integration::Local;
consuming.then_some(("gate", "ci"))
}
fn gate_answer(
record: &manifest::Manifest,
pick: impl Fn((&'static str, &'static str)) -> &'static str,
held: impl Fn(&manifest::Parameters) -> &String,
) -> String {
let recorded = held(&record.parameters);
if recorded.is_empty() {
gate_defaults(&record.profile, &record.git)
.map_or_else(String::new, |pair| pick(pair).to_owned())
} else {
recorded.clone()
}
}
fn answered<T>(chain: [(Option<T>, Source); 5]) -> Option<(T, Source)> {
chain
.into_iter()
.find_map(|(value, source)| value.map(|value| (value, source)))
}
#[allow(
clippy::too_many_lines,
reason = "the resolution is one precedence walk per field, and splitting it would hide that every field walks the same chain"
)]
pub fn resolve(
target: &Utf8Path,
flags: &Inputs<'_>,
config: Option<&crate::config::Config>,
record: Option<&manifest::Manifest>,
purpose: Purpose,
) -> Result<Resolved, RkError> {
let mut sources: BTreeMap<&'static str, Source> = BTreeMap::new();
let observed = crate::detect::observe(target.as_std_path());
let known_drivers = catalog::known_drivers();
let (technologies, source) = answered([
(
(!flags.technologies.is_empty()).then(|| flags.technologies.to_vec()),
Source::Flag,
),
(
config.and_then(|c| c.profile.technologies.clone()),
Source::Config,
),
(
record.map(|r| r.profile.technologies.clone()),
Source::Record,
),
(
Some(
observed
.technologies
.iter()
.map(|t| (*t).to_owned())
.collect(),
),
Source::Observation,
),
(None, Source::Default),
])
.unwrap_or_else(|| (Vec::new(), Source::Default));
let technologies =
canonical_list("profile.technologies", &technologies).map_err(RkError::Usage)?;
sources.insert("profile.technologies", source);
let forge_flag = flags
.forge
.map(|name| canonical_category(name).map_err(RkError::Usage))
.transpose()?;
let (forge, source) = answered([
(forge_flag.map(Some), Source::Flag),
(
config
.and_then(|c| c.profile.forge.clone())
.map(|value| if value.is_empty() { None } else { Some(value) }),
Source::Config,
),
(record.map(|r| r.profile.forge.clone()), Source::Record),
(
observed.forge.map(|forge| Some(forge.as_str().to_owned())),
Source::Observation,
),
(Some(None), Source::Default),
])
.unwrap_or((None, Source::Default));
let forge = forge
.map(|name| canonical_category(&name).map_err(RkError::Usage))
.transpose()?;
sources.insert("profile.forge", source);
let (repo, source) = answered([
(flags.repo.map(str::to_owned), Source::Flag),
(
config
.map(|c| c.project.repo.clone())
.filter(|value| !value.is_empty()),
Source::Config,
),
(
record
.map(|r| r.parameters.repo.clone())
.filter(|value| !value.is_empty()),
Source::Record,
),
(observed.repo.clone(), Source::Observation),
(None, Source::Default),
])
.map_or((None, Source::Default), |(value, source)| {
(Some(value), source)
});
sources.insert("project.repo", source);
let release_bearing: Vec<String> = technologies
.iter()
.filter(|name| known_drivers.contains(name))
.cloned()
.collect();
let proposal = match release_bearing.as_slice() {
[] => Proposal::None,
[one] => Proposal::Automatic {
driver: one.clone(),
},
many => Proposal::Ambiguous {
drivers: many.to_vec(),
},
};
let proposed_mode = match &proposal {
Proposal::Automatic { .. } | Proposal::Ambiguous { .. } => ReleaseMode::Automatic,
Proposal::None => ReleaseMode::None,
};
let (mode, source) = answered([
(flags.release_mode, Source::Flag),
(config.and_then(|c| c.profile.release.mode), Source::Config),
(record.map(|r| r.profile.release.mode), Source::Record),
(Some(proposed_mode), Source::Observation),
(None, Source::Default),
])
.unwrap_or((ReleaseMode::None, Source::Default));
let mode_source = source;
sources.insert("profile.release.mode", mode_source);
let mode_answered_above = mode_source != Source::Observation;
let proposal = (!mode_answered_above).then_some(proposal);
let driver_flag = flags
.release_driver
.map(|name| canonical_category(name).map_err(RkError::Usage))
.transpose()?;
let (driver, driver_source) = answered([
(driver_flag, Source::Flag),
(
config.and_then(|c| c.profile.release.driver.clone()),
Source::Config,
),
(
record.and_then(|r| r.profile.release.driver.clone()),
Source::Record,
),
(
match &proposal {
Some(Proposal::Automatic { driver }) if mode == ReleaseMode::Automatic => {
Some(driver.clone())
}
_ => None,
},
Source::Observation,
),
(None, Source::Default),
])
.map_or((None, Source::Default), |(value, source)| {
(Some(value), source)
});
let (style, style_source) = answered([
(flags.style, Source::Flag),
(config.and_then(|c| c.profile.release.style), Source::Config),
(record.and_then(|r| r.profile.release.style), Source::Record),
(None, Source::Observation),
(
(mode == ReleaseMode::Automatic && matches!(purpose, Purpose::Init | Purpose::Preview))
.then_some(Style::Trunk),
Source::Default,
),
])
.map_or((None, Source::Default), |(value, source)| {
(Some(value), source)
});
let (line_prefix, prefix_source) = answered([
(None, Source::Flag),
(
config.and_then(|c| c.profile.release.line_prefix.clone()),
Source::Config,
),
(
record.and_then(|r| r.profile.release.line_prefix.clone()),
Source::Record,
),
(None, Source::Observation),
(
(mode == ReleaseMode::Automatic).then(|| crate::config::LINE_PREFIX_DEFAULT.to_owned()),
Source::Default,
),
])
.map_or((None, Source::Default), |(value, source)| {
(Some(value), source)
});
let reporting = purpose == Purpose::Preview;
let release = match mode {
ReleaseMode::Automatic => {
if let Some(Proposal::Ambiguous { drivers }) = &proposal
&& driver.is_none()
&& !reporting
{
return Err(RkError::Usage(format!(
"the target carries more than one release-bearing technology, {}, and nothing names the driver; pass --release-driver <name>, or set profile.release.driver in {}",
drivers.join(" and "),
crate::config::CONFIG_PATH
)));
}
if forge.is_none() && !reporting {
let message = observed.host.map_or_else(
|| "no forge detected: the target has no origin remote, and an automatic release needs one".to_owned(),
|host| format!("no forge detected: the host {host} is not recognized, and an automatic release needs one"),
);
return Err(RkError::refusal(
Diagnostic::new(Reason::ForgeUndetected, message)
.expected("a github.com or gitlab remote, or --forge")
.action("pass --forge <github|gitlab>, or --release-mode none for a project that releases nothing"),
));
}
if driver.is_none() && !reporting {
return Err(invalid_release(format!(
"profile.release.mode is automatic and no driver is named; pass --release-driver <{}>",
known_drivers.join("|")
)));
}
if let Some(driver) = &driver
&& !technologies.contains(driver)
&& !reporting
{
return Err(invalid_release(format!(
"profile.release.driver names {driver}, which profile.technologies does not carry ({})",
if technologies.is_empty() {
"empty".to_owned()
} else {
technologies.join(", ")
}
)));
}
let style = match (style, purpose) {
(None, Purpose::Upgrade | Purpose::Adopt) => {
return Err(RkError::Usage(
"the target carries no style parameter; set profile.release.style in .release-kit/config.toml or pass --release-style <trunk|lines>".into(),
));
}
(None, _) => Style::Trunk,
(Some(style), _) => style,
};
sources.insert("profile.release.driver", driver_source);
sources.insert("profile.release.style", style_source);
sources.insert("profile.release.line_prefix", prefix_source);
ReleaseIntent {
mode,
driver,
style: Some(style),
line_prefix: Some(
line_prefix.unwrap_or_else(|| crate::config::LINE_PREFIX_DEFAULT.to_owned()),
),
}
}
ReleaseMode::External | ReleaseMode::None => {
for (key, present, value_source) in [
("profile.release.driver", driver.is_some(), driver_source),
("profile.release.style", style.is_some(), style_source),
(
"profile.release.line_prefix",
line_prefix.is_some(),
prefix_source,
),
] {
if present
&& matches!(value_source, Source::Flag | Source::Config)
&& value_source.rank() <= mode_source.rank()
{
return Err(invalid_release(format!(
"{key} is set while profile.release.mode is {}",
mode.as_str()
)));
}
}
ReleaseIntent {
mode,
driver: None,
style: None,
line_prefix: None,
}
}
};
let adapter_known = forge
.as_deref()
.is_some_and(|name| crate::detect::Forge::parse(name).is_some());
let repo = match (forge.is_some(), adapter_known, repo) {
(false, _, _) => String::new(),
(true, false, repo) => repo.unwrap_or_default(),
(true, true, Some(repo)) => repo,
(true, true, None) if purpose == Purpose::Preview => {
crate::projection::REPO_PLACEHOLDER.to_owned()
}
(true, true, None) => return Err(crate::landing::repo_unresolved()),
};
let (trunk, source) = answered([
(flags.trunk.map(str::to_owned), Source::Flag),
(config.and_then(|c| c.git.trunk.clone()), Source::Config),
(record.map(|r| r.git.trunk.clone()), Source::Record),
(None, Source::Observation),
(
Some(crate::config::TRUNK_DEFAULT.to_owned()),
Source::Default,
),
])
.unwrap_or_else(|| (crate::config::TRUNK_DEFAULT.to_owned(), Source::Default));
sources.insert("git.trunk", source);
let (checkout_mode, source) = answered([
(flags.checkout_mode, Source::Flag),
(config.and_then(|c| c.git.checkout_mode), Source::Config),
(record.map(|r| r.git.checkout_mode), Source::Record),
(None, Source::Observation),
(
Some(if purpose == Purpose::Adopt {
CheckoutMode::MainWorktree
} else {
CheckoutMode::LinkedWorktree
}),
Source::Default,
),
])
.unwrap_or((CheckoutMode::LinkedWorktree, Source::Default));
sources.insert("git.checkout_mode", source);
let (integration, source) = answered([
(flags.integration, Source::Flag),
(config.and_then(|c| c.git.integration), Source::Config),
(record.map(|r| r.git.integration), Source::Record),
(None, Source::Observation),
(
Some(if purpose == Purpose::Adopt {
Integration::Forge
} else {
Integration::Local
}),
Source::Default,
),
])
.unwrap_or((Integration::Local, Source::Default));
sources.insert("git.integration", source);
let (nix_packaging, source) = answered([
(flags.nix, Source::Flag),
(
config.and_then(|c| c.capabilities.nix_packaging),
Source::Config,
),
(record.map(|r| r.capabilities.nix_packaging), Source::Record),
(None, Source::Observation),
(Some(false), Source::Default),
])
.unwrap_or((false, Source::Default));
sources.insert("capabilities.nix_packaging", source);
let (reporting_policy, source) = answered([
(flags.reporting_policy, Source::Flag),
(
config.and_then(|c| c.capabilities.reporting_policy),
Source::Config,
),
(
record.map(|r| r.capabilities.reporting_policy),
Source::Record,
),
(None, Source::Observation),
(
Some(release.mode == ReleaseMode::Automatic),
Source::Default,
),
])
.unwrap_or((false, Source::Default));
sources.insert("capabilities.reporting_policy", source);
let (scorecard, source) = answered([
(flags.scorecard, Source::Flag),
(
config.and_then(|c| c.capabilities.scorecard),
Source::Config,
),
(record.map(|r| r.capabilities.scorecard), Source::Record),
(None, Source::Observation),
(Some(false), Source::Default),
])
.unwrap_or((false, Source::Default));
sources.insert("capabilities.scorecard", source);
let configured_scanning = config
.and_then(|c| c.capabilities.code_scanning.as_deref())
.map(Provider::parse)
.transpose()?;
let (code_scanning, source) = answered([
(flags.code_scanning, Source::Flag),
(configured_scanning, Source::Config),
(record.map(|r| r.capabilities.code_scanning), Source::Record),
(None, Source::Observation),
(Some(None), Source::Default),
])
.unwrap_or((None, Source::Default));
sources.insert("capabilities.code_scanning", source);
let (security_contact, source) = answered([
(None, Source::Flag),
(
config.and_then(|c| c.security.contact.clone()),
Source::Config,
),
(
record.map(|r| r.parameters.security_contact.clone()),
Source::Record,
),
(None, Source::Observation),
(Some(String::new()), Source::Default),
])
.unwrap_or((String::new(), Source::Default));
let security_contact =
crate::config::canonical_contact(&security_contact).map_err(crate::config::invalid)?;
sources.insert("security.contact", source);
let (security_response, source) = answered([
(None, Source::Flag),
(
config.and_then(|c| c.security.response.clone()),
Source::Config,
),
(
record.map(|r| r.parameters.security_response.clone()),
Source::Record,
),
(None, Source::Observation),
(
Some(crate::config::RESPONSE_DEFAULT.to_owned()),
Source::Default,
),
])
.unwrap_or_else(|| (crate::config::RESPONSE_DEFAULT.to_owned(), Source::Default));
let security_response =
crate::config::canonical_response(&security_response).map_err(crate::config::invalid)?;
sources.insert("security.response", source);
let gate_default = gate_defaults(
&ProfileSnapshot {
technologies: technologies.clone(),
forge: forge.clone(),
release: release.clone(),
},
&GitWorkflow {
trunk: trunk.clone(),
checkout_mode,
integration,
},
);
let (required_check, source) = answered([
(flags.required_check.map(str::to_owned), Source::Flag),
(
config
.map(|c| c.setup.required_check.clone())
.filter(|name| !name.is_empty()),
Source::Config,
),
(
record
.map(|r| r.parameters.required_check.clone())
.filter(|name| !name.is_empty()),
Source::Record,
),
(None, Source::Observation),
(
Some(gate_default.map_or_else(String::new, |(check, _)| check.to_owned())),
Source::Default,
),
])
.unwrap_or((String::new(), Source::Default));
sources.insert("setup.required_check", source);
let (required_workflow, source) = answered([
(flags.required_workflow.map(str::to_owned), Source::Flag),
(
config
.map(|c| c.setup.required_workflow.clone())
.filter(|name| !name.is_empty()),
Source::Config,
),
(
record
.map(|r| r.parameters.required_workflow.clone())
.filter(|name| !name.is_empty()),
Source::Record,
),
(None, Source::Observation),
(
Some(gate_default.map_or_else(String::new, |(_, workflow)| workflow.to_owned())),
Source::Default,
),
])
.unwrap_or((String::new(), Source::Default));
sources.insert("setup.required_workflow", source);
if forge.as_deref() == Some("gitlab") {
for (flag, supplied) in [
("--required-check", flags.required_check),
("--required-workflow", flags.required_workflow),
] {
if supplied.is_some() {
return Err(RkError::Usage(format!(
"{flag} is not a GitLab answer: the forge requires the whole pipeline through one project setting and names no individual check"
)));
}
}
}
let mut unknown: Vec<String> = technologies
.iter()
.filter(|name| !known_drivers.contains(name))
.map(|name| format!("technology {name}"))
.collect();
if let Some(name) = &forge
&& crate::detect::Forge::parse(name).is_none()
{
unknown.push(format!("forge {name}"));
}
Ok(Resolved {
params: Params {
profile: ProfileSnapshot {
technologies,
forge,
release,
},
git: GitWorkflow {
trunk,
checkout_mode,
integration,
},
capabilities: CapabilityRequests {
nix_packaging,
reporting_policy,
scorecard,
code_scanning,
},
repo,
security_contact,
security_response,
required_check,
required_workflow,
},
sources,
unknown,
proposal,
})
}