use std::io::{self, Write};
use runner_manager_domain::attempt::{active_count, active_count_for};
use runner_manager_domain::model::{RefreshInterval, StartMode, Timestamp};
use runner_manager_domain::policy::{PolicyMode, ScalePolicy};
use runner_manager_domain::store::Store;
use runner_manager_github::rest::refreshes_per_hour;
use runner_manager_platform::service::InstallRecord;
use serde::Serialize;
use super::host::{FALLBACK_COST_MULTIPLE, HostBudget, local_host, max_repository_targets};
use super::workspace;
use super::{CliError, Context, Failure, StatusArgs, write_failed};
pub const SCHEMA_VERSION: u32 = 1;
#[cfg(test)]
const SCHEMA_V1_HOST_FIELDS: &[&str] = &[
"architecture",
"capacity",
"configured",
"display_name",
"headroom",
"id",
"in_use",
"os",
"refresh_interval_secs",
"service_start_mode",
];
#[cfg(test)]
const SCHEMA_V1_POLICY_FIELDS: &[&str] = &[
"active_attempts",
"enabled",
"id",
"max_capacity",
"min_capacity",
"mode",
"routing_labels",
"scope",
"state",
"target",
];
#[derive(Debug, Clone, Serialize)]
pub struct StatusDocument {
pub schema_version: u32,
pub generated_at: Timestamp,
pub product: Product,
pub github_contacted: bool,
pub credential: Credential,
pub host: HostSnapshot,
pub budget: BudgetSnapshot,
pub policies: Vec<PolicySnapshot>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Product {
pub name: &'static str,
pub version: &'static str,
pub service_binary_version: Option<String>,
}
fn binary_version(path: &std::path::Path) -> Option<String> {
let output = std::process::Command::new(path)
.arg("--version")
.output()
.ok()?;
if !output.status.success() {
return None;
}
String::from_utf8(output.stdout)
.ok()?
.split_whitespace()
.last()
.map(str::to_string)
}
fn installed_service_version(context: &Context) -> Option<String> {
let record = InstallRecord::read(context.paths()).ok().flatten()?;
binary_version(&record.binary)
}
#[derive(Debug, Clone, Serialize)]
pub struct Credential {
pub present: bool,
pub unreadable: Option<String>,
pub store_scope: String,
pub store_location: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct HostSnapshot {
pub configured: bool,
pub id: Option<String>,
pub display_name: Option<String>,
pub os: Option<String>,
pub architecture: Option<String>,
pub capacity: u16,
pub in_use: u16,
pub headroom: u16,
pub service_start_mode: String,
pub refresh_interval_secs: u16,
pub runner_root: Option<String>,
pub runner_root_source: String,
pub configured_runner_root: Option<String>,
pub runner_root_unavailable: Option<String>,
pub active_ephemeral_attempts: u16,
pub cleanup_blocked_ephemeral_attempts: u16,
}
#[derive(Debug, Clone, Serialize)]
pub struct BudgetSnapshot {
pub interval_secs: u16,
pub refreshes_per_hour: u32,
pub projected_requests_per_hour: u32,
pub projection_is_floor: bool,
pub allowance_requests_per_hour: u32,
pub ceiling_requests_per_hour: u32,
pub headroom_requests_per_hour: u32,
pub exceeds_allowance: bool,
pub max_repository_targets: u32,
pub best_case_multiple_when_paging: u32,
}
#[derive(Debug, Clone, Serialize)]
pub struct PolicySnapshot {
pub id: String,
pub target: String,
pub scope: String,
pub mode: String,
pub state: String,
pub enabled: bool,
pub min_capacity: u16,
pub max_capacity: Option<u16>,
pub routing_labels: Vec<String>,
pub active_attempts: u16,
pub cleanup_blocked_attempts: u16,
pub workspace_mode: String,
pub workspace_root: Option<String>,
pub workspace_effective_root: Option<String>,
pub workspace_root_source: String,
pub workspace_slots: Vec<SlotSnapshot>,
}
#[derive(Debug, Clone, Serialize)]
pub struct SlotSnapshot {
pub slot: u16,
pub attempt: String,
pub state: String,
pub cleanup_blocked: bool,
}
impl From<&workspace::SlotLease> for SlotSnapshot {
fn from(lease: &workspace::SlotLease) -> Self {
Self {
slot: lease.slot,
attempt: lease.attempt.clone(),
state: lease.state.clone(),
cleanup_blocked: lease.cleanup_blocked,
}
}
}
impl PolicySnapshot {
fn of(
policy: &ScalePolicy,
active_attempts: u16,
workspace: &workspace::RepositoryWorkspace,
) -> Self {
Self {
id: policy.id.to_string(),
target: policy.target.slug(),
scope: workspace::scope_token(policy.target.scope()).to_string(),
mode: match policy.mode() {
PolicyMode::MonitorOnly => "monitor_only",
PolicyMode::Autoscale(_) => "autoscale",
}
.to_string(),
state: policy.state().to_string(),
enabled: policy.enabled(),
min_capacity: policy.min_capacity(),
max_capacity: policy.max_capacity().map(std::num::NonZeroU16::get),
routing_labels: policy
.routing_labels()
.map(registration_labels)
.unwrap_or_default(),
active_attempts,
cleanup_blocked_attempts: workspace.attempts.cleanup_blocked,
workspace_mode: workspace.kind().to_string(),
workspace_root: workspace
.policy
.root()
.map(|root| root.as_str().to_string()),
workspace_effective_root: workspace.effective_root().map(str::to_string),
workspace_root_source: workspace.root_source().to_string(),
workspace_slots: workspace.leases.iter().map(SlotSnapshot::from).collect(),
}
}
}
fn registration_labels(labels: &runner_manager_domain::policy::RoutingLabels) -> Vec<String> {
labels.as_registration_labels()
}
pub fn snapshot(context: &Context) -> Result<StatusDocument, CliError> {
let store = context.store()?;
let host = local_host(&store)?;
let attempts = store.attempts().map_err(|source| {
CliError::new(
Failure::LocalState,
format!("cannot read this host's attempt journal: {source}"),
)
})?;
let policies = store.policies().map_err(|source| {
CliError::with_remedy(
Failure::LocalState,
format!("cannot read this host's policies: {source}"),
"runner-manager host show",
)
})?;
let start_mode = host
.as_ref()
.map_or_else(StartMode::default, |h| h.service_start_mode);
let interval = host
.as_ref()
.map_or_else(RefreshInterval::default, |h| h.refresh_interval);
let capacity = host
.as_ref()
.map_or(super::DEFAULT_HOST_CAPACITY, |h| h.host_capacity());
let in_use = active_count(attempts.iter());
let secrets = context.secret_store(start_mode)?;
let (present, unreadable) = match secrets.load() {
Ok(value) => (value.is_some(), None),
Err(source) => (false, Some(source.to_string())),
};
let targets: Vec<_> = policies.iter().map(|p| p.target.clone()).collect();
let budget = HostBudget::of(interval, &targets);
let runner_root = workspace::host_root(context.paths(), host.as_ref());
let ephemeral = workspace::host_affected_attempts(&store)?;
let workspaces = policies
.iter()
.map(|policy| workspace::repository_workspace(&store, &runner_root, policy))
.collect::<Result<Vec<_>, CliError>>()?;
Ok(StatusDocument {
schema_version: SCHEMA_VERSION,
generated_at: context.clock().now(),
product: Product {
name: env!("CARGO_PKG_NAME"),
version: env!("CARGO_PKG_VERSION"),
service_binary_version: installed_service_version(context),
},
github_contacted: false,
credential: Credential {
present,
unreadable,
store_scope: secrets.scope().to_string(),
store_location: secrets.location(),
},
host: HostSnapshot {
configured: host.is_some(),
id: host.as_ref().map(|h| h.id.to_string()),
display_name: host.as_ref().map(|h| h.display_name.clone()),
os: host.as_ref().map(|h| h.os.to_string()),
architecture: host.as_ref().map(|h| h.architecture.to_string()),
capacity,
in_use,
headroom: capacity.saturating_sub(in_use),
service_start_mode: start_mode.to_string(),
refresh_interval_secs: interval.as_secs(),
runner_root: runner_root.effective_text().map(str::to_string),
runner_root_source: runner_root.source().as_token().to_string(),
configured_runner_root: runner_root
.configured
.as_ref()
.map(|root| root.as_str().to_string()),
runner_root_unavailable: runner_root.unavailable.clone(),
active_ephemeral_attempts: ephemeral.active,
cleanup_blocked_ephemeral_attempts: ephemeral.cleanup_blocked,
},
budget: BudgetSnapshot {
interval_secs: interval.as_secs(),
refreshes_per_hour: refreshes_per_hour(interval),
projected_requests_per_hour: budget.requests_per_hour(),
projection_is_floor: budget.is_floor(),
allowance_requests_per_hour: budget.allowance(),
ceiling_requests_per_hour: budget.ceiling(),
headroom_requests_per_hour: budget.headroom(),
exceeds_allowance: budget.exceeds_allowance(),
max_repository_targets: max_repository_targets(interval),
best_case_multiple_when_paging: FALLBACK_COST_MULTIPLE,
},
policies: policies
.iter()
.zip(workspaces.iter())
.map(|(policy, workspace)| {
PolicySnapshot::of(
policy,
active_count_for(policy.id, attempts.iter()),
workspace,
)
})
.collect(),
})
}
pub fn dispatch(context: &Context, args: &StatusArgs, out: &mut dyn Write) -> Result<(), CliError> {
let failed = write_failed("this host's status");
let document = snapshot(context)?;
if args.json {
write_json(out, &document)
} else {
write_text(out, &document)
}
.map_err(failed)
}
fn write_json(out: &mut dyn Write, document: &StatusDocument) -> io::Result<()> {
serde_json::to_writer_pretty(&mut *out, document)
.map_err(|source| io::Error::other(source.to_string()))?;
writeln!(out)
}
fn write_text(out: &mut dyn Write, document: &StatusDocument) -> io::Result<()> {
writeln!(
out,
"{} {}",
document.product.name, document.product.version
)?;
writeln!(out, " as of {}", document.generated_at)?;
writeln!(out)?;
if document.host.configured {
writeln!(
out,
"Host: {} ({} {})",
document.host.display_name.as_deref().unwrap_or("unnamed"),
document.host.os.as_deref().unwrap_or("unknown"),
document.host.architecture.as_deref().unwrap_or("unknown"),
)?;
} else {
writeln!(
out,
"Host: not configured yet; the values below are the defaults."
)?;
}
writeln!(
out,
" capacity {} in use of {} ({} free)",
document.host.in_use, document.host.capacity, document.host.headroom
)?;
writeln!(
out,
" service start mode {}",
document.host.service_start_mode
)?;
writeln!(
out,
" runner root {} ({})",
document
.host
.runner_root
.as_deref()
.unwrap_or("unavailable"),
document.host.runner_root_source.replace('_', "-"),
)?;
if let Some(reason) = &document.host.runner_root_unavailable {
writeln!(out, " runner root problem {reason}")?;
}
writeln!(
out,
" ephemeral paths {} active, {} awaiting cleanup",
document.host.active_ephemeral_attempts, document.host.cleanup_blocked_ephemeral_attempts
)?;
writeln!(
out,
" credential {} in the {}-scoped store",
match (&document.credential.unreadable, document.credential.present) {
(Some(_), _) => "not readable by this account",
(None, true) => "present",
(None, false) => "absent",
},
document.credential.store_scope
)?;
if let Some(reason) = &document.credential.unreadable {
writeln!(out, " credential problem {reason}")?;
}
writeln!(
out,
" GitHub contacted no (this is a local snapshot; `auth status` asks GitHub)"
)?;
writeln!(out)?;
writeln!(out, "Policies ({})", document.policies.len())?;
if document.policies.is_empty() {
writeln!(out, " none yet -- `repo add` or `org add` creates one.")?;
}
for policy in &document.policies {
writeln!(
out,
" {:<40} {:<13} {:<10} {} active",
policy.target, policy.mode, policy.state, policy.active_attempts
)?;
writeln!(
out,
" {:<40} {} attempt in {}",
"",
policy.workspace_mode,
policy
.workspace_effective_root
.as_deref()
.unwrap_or("an unresolved root"),
)?;
for slot in &policy.workspace_slots {
writeln!(
out,
" {:<40} slot s{} {}",
"",
slot.slot,
if slot.cleanup_blocked {
"cleanup blocked; quarantined until remediation"
} else {
"leased by a live attempt"
}
)?;
}
}
writeln!(out)?;
writeln!(
out,
"Shared REST budget: {} requests/hour projected of {} this host may spend",
document.budget.projected_requests_per_hour, document.budget.allowance_requests_per_hour
)?;
writeln!(
out,
" about {} repository targets fit at a {}s interval (best case; a repository whose",
document.budget.max_repository_targets, document.budget.interval_secs
)?;
writeln!(
out,
" counts have to walk pages costs up to {}x that)",
document.budget.best_case_multiple_when_paging
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::Value;
fn document() -> StatusDocument {
StatusDocument {
schema_version: SCHEMA_VERSION,
generated_at: chrono::DateTime::from_timestamp(1_787_270_400, 0).unwrap(),
product: Product {
name: "runner-manager",
version: "0.1.0",
service_binary_version: None,
},
github_contacted: false,
credential: Credential {
present: true,
unreadable: None,
store_scope: "machine".to_string(),
store_location: "C:/ProgramData/runner-manager/secrets".to_string(),
},
host: HostSnapshot {
configured: true,
id: Some("00000000-0000-0000-0000-000000000001".to_string()),
display_name: Some("home-win".to_string()),
os: Some("windows".to_string()),
architecture: Some("x64".to_string()),
capacity: 2,
in_use: 1,
headroom: 1,
service_start_mode: "boot".to_string(),
refresh_interval_secs: 60,
runner_root: Some("C:/rman".to_string()),
runner_root_source: "platform_default".to_string(),
configured_runner_root: None,
runner_root_unavailable: None,
active_ephemeral_attempts: 1,
cleanup_blocked_ephemeral_attempts: 0,
},
budget: BudgetSnapshot {
interval_secs: 60,
refreshes_per_hour: 60,
projected_requests_per_hour: 180,
projection_is_floor: false,
allowance_requests_per_hour: 2500,
ceiling_requests_per_hour: 5000,
headroom_requests_per_hour: 2320,
exceeds_allowance: false,
max_repository_targets: 13,
best_case_multiple_when_paging: FALLBACK_COST_MULTIPLE,
},
policies: vec![PolicySnapshot {
id: "00000000-0000-0000-0000-000000000010".to_string(),
target: "owner/repo".to_string(),
scope: "repository".to_string(),
mode: "autoscale".to_string(),
state: "active".to_string(),
enabled: true,
min_capacity: 0,
max_capacity: Some(1),
routing_labels: vec!["rm-home-win-x64".to_string()],
active_attempts: 1,
cleanup_blocked_attempts: 0,
workspace_mode: "persistent".to_string(),
workspace_root: Some("D:/ci-cache/project".to_string()),
workspace_effective_root: Some("D:/ci-cache/project".to_string()),
workspace_root_source: "repository".to_string(),
workspace_slots: vec![SlotSnapshot {
slot: 2,
attempt: "00000000-0000-0000-0000-000000000020".to_string(),
state: "busy".to_string(),
cleanup_blocked: false,
}],
}],
}
}
fn emitted() -> Value {
let mut buffer = Vec::new();
write_json(&mut buffer, &document()).expect("writing to a Vec");
serde_json::from_slice(&buffer).expect("the document must be valid JSON")
}
fn keys(value: &Value, pointer: &str) -> Vec<String> {
let at = value
.pointer(pointer)
.unwrap_or_else(|| panic!("the document must carry {pointer}"));
let object = at
.as_object()
.unwrap_or_else(|| panic!("{pointer} must be an object, and is {at}"));
let mut names: Vec<String> = object.keys().cloned().collect();
names.sort();
names
}
#[test]
fn the_documented_schema_is_the_one_that_is_emitted() {
let emitted = emitted();
assert_eq!(
keys(&emitted, ""),
[
"budget",
"credential",
"generated_at",
"github_contacted",
"host",
"policies",
"product",
"schema_version",
]
);
assert_eq!(
keys(&emitted, "/product"),
["name", "service_binary_version", "version"]
);
assert_eq!(
keys(&emitted, "/credential"),
["present", "store_location", "store_scope", "unreadable"]
);
assert_eq!(
keys(&emitted, "/host"),
[
"active_ephemeral_attempts",
"architecture",
"capacity",
"cleanup_blocked_ephemeral_attempts",
"configured",
"configured_runner_root",
"display_name",
"headroom",
"id",
"in_use",
"os",
"refresh_interval_secs",
"runner_root",
"runner_root_source",
"runner_root_unavailable",
"service_start_mode",
]
);
assert_eq!(
keys(&emitted, "/budget"),
[
"allowance_requests_per_hour",
"best_case_multiple_when_paging",
"ceiling_requests_per_hour",
"exceeds_allowance",
"headroom_requests_per_hour",
"interval_secs",
"max_repository_targets",
"projected_requests_per_hour",
"projection_is_floor",
"refreshes_per_hour",
]
);
assert_eq!(
keys(&emitted, "/policies/0"),
[
"active_attempts",
"cleanup_blocked_attempts",
"enabled",
"id",
"max_capacity",
"min_capacity",
"mode",
"routing_labels",
"scope",
"state",
"target",
"workspace_effective_root",
"workspace_mode",
"workspace_root",
"workspace_root_source",
"workspace_slots",
]
);
assert_eq!(
keys(&emitted, "/policies/0/workspace_slots/0"),
["attempt", "cleanup_blocked", "slot", "state"]
);
}
#[test]
fn the_workspace_fields_are_additive_so_the_version_does_not_move() {
let emitted = emitted();
let host = keys(&emitted, "/host");
for field in SCHEMA_V1_HOST_FIELDS {
assert!(
host.iter().any(|name| name == field),
"host.{field} was promised at schema v1 and is gone; that is a breaking \
change and needs SCHEMA_VERSION bumped, not this list edited"
);
}
let policy = keys(&emitted, "/policies/0");
for field in SCHEMA_V1_POLICY_FIELDS {
assert!(
policy.iter().any(|name| name == field),
"policies[].{field} was promised at schema v1 and is gone"
);
}
assert_eq!(
SCHEMA_VERSION, 1,
"the workspace fields are additions, and an addition is compatible: a consumer \
reading only the fields it knows is unaffected"
);
}
#[test]
fn the_workspace_fields_are_structured_rather_than_rendered() {
let emitted = emitted();
assert_eq!(
emitted["host"]["runner_root_source"],
Value::from("platform_default"),
"the source is a token, not the hyphenated badge `host show` prints"
);
assert_eq!(emitted["host"]["configured_runner_root"], Value::Null);
assert!(emitted["host"]["active_ephemeral_attempts"].is_u64());
assert!(emitted["host"]["cleanup_blocked_ephemeral_attempts"].is_u64());
let policy = &emitted["policies"][0];
assert_eq!(policy["workspace_mode"], Value::from("persistent"));
assert_eq!(
policy["workspace_root_source"],
Value::from("repository"),
"the third source `d1` names: this repository's own setting, not the host's"
);
assert_eq!(
policy["workspace_root"],
Value::from("D:/ci-cache/project"),
"a persistent policy's root is its own, not the host's"
);
let slots = policy["workspace_slots"].as_array().expect("an array");
assert_eq!(slots.len(), 1);
assert_eq!(
slots[0]["slot"],
Value::from(2),
"the slot is a number, so a consumer never parses `s2`"
);
assert!(
slots[0]["cleanup_blocked"].as_bool().is_some(),
"quarantine is a boolean, not a state string a consumer has to know the \
vocabulary of"
);
let mut ephemeral = document();
ephemeral.policies[0].workspace_mode = "ephemeral".to_string();
ephemeral.policies[0].workspace_root = None;
ephemeral.policies[0].workspace_effective_root = ephemeral.host.runner_root.clone();
ephemeral.policies[0].workspace_root_source = "platform_default".to_string();
ephemeral.policies[0].workspace_slots.clear();
let mut buffer = Vec::new();
write_json(&mut buffer, &ephemeral).unwrap();
let value: Value = serde_json::from_slice(&buffer).unwrap();
assert_eq!(value["policies"][0]["workspace_root"], Value::Null);
assert_eq!(
value["policies"][0]["workspace_root_source"], value["host"]["runner_root_source"],
"an ephemeral policy inherits the host's source, and says which one it is"
);
assert_eq!(
value["policies"][0]["workspace_effective_root"],
value["host"]["runner_root"]
);
assert!(
value["policies"][0]["workspace_slots"]
.as_array()
.expect("an array")
.is_empty()
);
}
#[test]
fn no_rendering_enumerates_what_is_inside_a_workspace() {
let mut buffer = Vec::new();
write_text(&mut buffer, &document()).unwrap();
let text = String::from_utf8(buffer).unwrap();
assert!(
text.contains("persistent attempt in D:/ci-cache/project"),
"the human rendering names the root: {text}"
);
assert!(
!text.contains("_work"),
"a status line that named the retained directory would be one step from \
listing it: {text}"
);
}
#[test]
fn an_unreadable_store_is_reported_rather_than_read_as_absent() {
let mut document = document();
document.credential.present = false;
document.credential.unreadable = Some("only root may read /var/db/SystemKey".to_string());
let mut buffer = Vec::new();
write_text(&mut buffer, &document).unwrap();
let text = String::from_utf8(buffer).unwrap();
assert!(
text.contains("credential not readable by this account"),
"{text}"
);
assert!(
text.contains("credential problem only root may read /var/db/SystemKey"),
"the store's own words are what name the remedy: {text}"
);
assert!(
!text.contains("credential absent"),
"an unreadable store has not answered the question, and `absent` is the one \
answer that sends an operator to overwrite it: {text}"
);
assert!(text.contains("Policies (1)"), "{text}");
}
#[test]
fn the_document_carries_its_own_version() {
assert_eq!(emitted()["schema_version"], Value::from(SCHEMA_VERSION));
}
#[test]
fn a_scripted_consumer_reads_it_without_special_casing() {
let emitted = emitted();
let capacity = emitted["host"]["capacity"]
.as_u64()
.expect("capacity is a number, not a string");
let in_use = emitted["host"]["in_use"].as_u64().expect("a number");
assert_eq!(capacity, 2);
assert_eq!(in_use, 1);
assert_eq!(
emitted["host"]["headroom"].as_u64().expect("a number"),
capacity - in_use,
"headroom must be derivable and consistent, so a consumer can trust either"
);
assert!(
emitted["credential"]["present"]
.as_bool()
.expect("a boolean, so a consumer never string-matches on it")
);
assert!(
!emitted["github_contacted"].as_bool().expect("a boolean"),
"the document must say plainly that it is a local snapshot"
);
let policies = emitted["policies"].as_array().expect("an array");
assert_eq!(policies.len(), 1);
assert_eq!(policies[0]["target"], Value::from("owner/repo"));
assert_eq!(policies[0]["max_capacity"], Value::from(1));
assert!(
policies[0]["routing_labels"].is_array(),
"labels are an array, not a comma-separated string a consumer has to split"
);
let mut monitor_only = document();
monitor_only.policies[0].max_capacity = None;
monitor_only.policies[0].mode = "monitor_only".to_string();
let mut buffer = Vec::new();
write_json(&mut buffer, &monitor_only).unwrap();
let value: Value = serde_json::from_slice(&buffer).unwrap();
assert_eq!(value["policies"][0]["max_capacity"], Value::Null);
}
#[test]
fn the_target_ceiling_carries_its_caveat_into_both_renderings() {
assert_eq!(
emitted()["budget"]["best_case_multiple_when_paging"],
Value::from(FALLBACK_COST_MULTIPLE),
"a consumer reading `max_repository_targets` must be able to read the multiple \
it can be wrong by from the same document"
);
let mut buffer = Vec::new();
write_text(&mut buffer, &document()).unwrap();
let text = String::from_utf8(buffer).unwrap();
assert!(text.contains("best case"), "{text}");
assert!(
text.contains(&format!("{FALLBACK_COST_MULTIPLE}x")),
"{text}"
);
}
#[test]
fn nothing_is_printed_around_the_json() {
let mut buffer = Vec::new();
write_json(&mut buffer, &document()).unwrap();
let text = String::from_utf8(buffer).unwrap();
assert!(text.starts_with('{'), "got: {text}");
assert!(
text.ends_with("}\n"),
"got the tail: {:?}",
&text[text.len().saturating_sub(8)..]
);
serde_json::from_str::<Value>(&text).expect("parses whole");
}
#[test]
fn no_field_of_the_document_is_a_place_to_put_a_credential() {
fn walk(value: &Value, path: &str, found: &mut Vec<String>) {
match value {
Value::Object(fields) => {
for (name, child) in fields {
let here = format!("{path}/{name}");
let lowered = name.to_ascii_lowercase();
for forbidden in ["token", "secret", "device_code", "password", "key"] {
if lowered.contains(forbidden) {
found.push(here.clone());
}
}
walk(child, &here, found);
}
}
Value::Array(items) => {
for (index, child) in items.iter().enumerate() {
walk(child, &format!("{path}/{index}"), found);
}
}
_ => {}
}
}
let mut offenders = Vec::new();
walk(&emitted(), "", &mut offenders);
assert!(
offenders.is_empty(),
"these fields name a credential, and `f1` requires this document to carry none: \
{offenders:?}"
);
let mut buffer = Vec::new();
write_json(&mut buffer, &document()).unwrap();
let text = String::from_utf8(buffer).unwrap();
for prefix in ["ghu_", "gho_", "ghs_", "ghp_"] {
assert!(!text.contains(prefix), "found {prefix:?} in: {text}");
}
}
}