mod activity;
mod command_record;
pub mod dismiss;
mod effects;
pub mod export;
pub(crate) mod github;
pub mod metrics;
pub(crate) mod owner;
pub(crate) mod pass_starts;
mod process_identity;
mod reading;
pub(crate) use reading::Framing;
pub(crate) use reading::catalog;
pub(crate) use reading::run_for_pull_request;
mod recording;
mod run_metrics;
pub(crate) mod session;
pub(crate) mod session_signals;
pub(crate) mod spans;
pub mod status;
pub mod stop;
pub(crate) mod trace;
mod usual_range;
use std::fmt::{self, Display};
use std::path::{Path, PathBuf};
use std::sync::{LazyLock, Mutex, MutexGuard, PoisonError};
use crate::config::UserConfig;
use crate::github::ListedIssue;
use crate::harness::Choice;
use crate::issue::{IssueUrl, Repo};
use crate::session::Logged;
use activity::Kind;
pub use command_record::StageName as Stage;
pub(crate) use command_record::{Issue, TicketRecord, TicketState, experiment_rechecked};
use effects::OnMachine;
use recording::Record;
fn unix_user(action: &str) -> anyhow::Result<String> {
use anyhow::Context;
let failure = format!("could not identify the Unix user asking for {action}");
let user = std::process::Command::new("id")
.arg("-un")
.output()
.with_context(|| failure.clone())?;
if !user.status.success() {
anyhow::bail!(failure);
}
Ok(String::from_utf8(user.stdout)?.trim().to_owned())
}
pub(crate) fn display_name(stored: &str) -> &str {
match stored {
"Run" => "Ticket run",
"dispatched Run" => "dispatched Ticket run",
other => other,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandKind {
Issue,
Architect,
Pickup,
Secure,
Scoring,
Experiment,
}
impl CommandKind {
pub fn folder(self) -> &'static str {
match self {
Self::Issue => "issue",
Self::Architect => "architect",
Self::Pickup => "pickup",
Self::Secure => "secure",
Self::Scoring => "scoring",
Self::Experiment => "experiment",
}
}
pub fn named(name: &str) -> Option<Self> {
match name {
"issue" => Some(Self::Issue),
"architect" => Some(Self::Architect),
"pickup" => Some(Self::Pickup),
"secure" => Some(Self::Secure),
"scoring" => Some(Self::Scoring),
"experiment" => Some(Self::Experiment),
_ => None,
}
}
}
pub enum Begin<'a> {
Run(&'a IssueUrl),
ChildRun(&'a str, CommandKind),
ArchitectRun,
PickupRun,
SecurityRun,
ScoringRun,
Experiment,
}
#[derive(Clone, Copy)]
pub enum Work<'a> {
Run(&'a IssueUrl),
BaseFix(&'a IssueUrl),
SpecRun(&'a IssueUrl),
PickupRun(&'a ListedIssue),
ArchitectRun(&'a Repo),
SecurityRun(&'a Repo),
ScoringRun(&'a Repo),
Experiment(&'a Repo),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Pass {
Architect,
Pickup,
Security,
}
impl Pass {
pub(crate) fn name(self) -> &'static str {
match self {
Self::Architect => "Architect",
Self::Pickup => "Pickup",
Self::Security => "Security",
}
}
}
impl Display for Pass {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} run", self.name())
}
}
impl serde::Serialize for Pass {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(self)
}
}
static RECORD: LazyLock<Mutex<Record<OnMachine>>> =
LazyLock::new(|| Mutex::new(Record::new(OnMachine)));
const PARENT_RECORD: &str = "THIRDSHIFT_PARENT_RECORD";
const PARENT_DISPATCH: &str = "THIRDSHIFT_PARENT_DISPATCH";
pub fn begin(begin: Begin) {
let mut record = record();
if matches!(begin, Begin::ChildRun(..)) {
record.inherit_owner(owner::Owner::inherited());
record.child_of(std::env::var_os(PARENT_RECORD).map(PathBuf::from));
record.child_dispatch(std::env::var(PARENT_DISPATCH).ok());
}
record.begin(begin);
}
pub fn inherit_record(command: &mut std::process::Command) {
if let Some(path) = record().record_path() {
command.env(PARENT_RECORD, path);
} else {
command.env_remove(PARENT_RECORD);
}
if let Some(issue) = record().dispatch_issue() {
command.env(PARENT_DISPATCH, issue);
} else {
command.env_remove(PARENT_DISPATCH);
}
}
pub(crate) fn tickets(tickets: Vec<TicketRecord>) {
record().tickets(tickets);
}
pub fn configured(config: &UserConfig) {
record().configured(config);
}
pub(crate) fn pass_fired(repo: &Repo) {
record().pass_fired(repo);
}
pub fn started(work: Work, harness: &Choice) {
record().started(work, harness);
}
pub fn skipped(pass: Pass, repo: &Repo, reason: impl Display) {
record().skipped(pass, repo, reason);
}
pub(crate) fn activity(message: impl Display) {
record().activity(message);
}
pub fn ended(outcome: impl Display) {
record().ended(outcome);
}
pub fn issue_title(title: Option<String>) {
record().issue_title(title);
}
pub fn stage(stage: Stage) {
record().stage(stage, None);
}
pub fn session(session: Logged, path: &Path) {
record().stage(session.stage(), Some(path));
}
pub fn issue_branch(branch: Option<&str>) {
record().issue_branch(branch);
}
pub(crate) fn unaddressed_findings(counts: Option<crate::pull_request::UnaddressedFindings>) {
record().unaddressed_findings(counts);
}
pub fn finished(account: &crate::run_ending::Account<'_>) {
record().finished(account);
}
pub(crate) fn experiment(description: &crate::evals::experiment::Description) {
record().experiment(description);
}
pub(crate) fn experiment_finished(outcome: &str, cause: Option<&str>) {
record().experiment_finished(outcome, cause);
}
pub fn stop_requested() -> Option<stop::Request> {
if !crate::interrupt::requested() {
return None;
}
record().stop_request()
}
pub fn dispatch_ended(ended: &crate::run::Ended) {
record().dispatch_ended(&crate::run_ending::Account::of_run(ended));
}
pub fn dispatching(work: Work, harness: &Choice) {
record().dispatching(work, harness);
}
pub fn show_held() {
record().show_held();
}
pub fn root(repo: &Repo) -> PathBuf {
record().root(repo)
}
pub fn command_kind() -> CommandKind {
record().command_kind()
}
pub(crate) fn record_for_pull_request(root: &Path, url: &str) -> anyhow::Result<Option<PathBuf>> {
Ok(run_for_pull_request(root, url)?.map(|(path, _)| path))
}
pub fn session_dir(repo: &Repo) -> PathBuf {
let record = record();
record
.root(repo)
.join("commands")
.join(record.command_kind().folder())
}
pub fn stamp() -> String {
record().stamp()
}
pub fn command_log_path() -> Option<PathBuf> {
record().command_log_path()
}
pub(crate) fn captured_owner() -> Result<owner::Owner, String> {
record().captured_owner()
}
pub(crate) fn trace_context() -> Option<trace::Context> {
record().trace_context()
}
pub(crate) fn otlp_endpoint() -> Option<String> {
record().otlp_endpoint()
}
pub fn eprint(line: &str) {
record().eprint(line);
}
pub fn print(line: &str) {
record().print(line);
}
fn root_under(logs_dir: &Path, repo: &Repo) -> PathBuf {
logs_dir.join(&repo.owner).join(&repo.name)
}
impl<'a> Work<'a> {
fn initial_stage(self) -> Option<Stage> {
match self {
Work::Run(_) | Work::BaseFix(_) => Some(Stage::Implement),
Work::SpecRun(_) => Some(Stage::Tickets),
Work::ArchitectRun(_) => Some(Stage::ArchitectureReview),
Work::ScoringRun(_) => Some(Stage::Sampling),
Work::PickupRun(_) | Work::SecurityRun(_) => None,
Work::Experiment(_) => Some(Stage::ArmBuilds),
}
}
fn repo(self) -> Repo {
match self {
Work::Run(issue) | Work::BaseFix(issue) | Work::SpecRun(issue) => issue.repo(),
Work::PickupRun(listed) => listed.issue.repo(),
Work::ArchitectRun(repo)
| Work::SecurityRun(repo)
| Work::ScoringRun(repo)
| Work::Experiment(repo) => repo.clone(),
}
}
fn kind(self) -> Kind {
match self {
Work::Run(_) | Work::BaseFix(_) => Kind::Run,
Work::SpecRun(_) => Kind::SpecRun,
Work::PickupRun(_) => Kind::Pass(Pass::Pickup),
Work::ArchitectRun(_) => Kind::Pass(Pass::Architect),
Work::SecurityRun(_) => Kind::Pass(Pass::Security),
Work::ScoringRun(_) => Kind::ScoringRun,
Work::Experiment(_) => Kind::Experiment,
}
}
pub(crate) fn issue_url(self) -> Option<&'a IssueUrl> {
match self {
Work::Run(issue) | Work::BaseFix(issue) | Work::SpecRun(issue) => Some(issue),
Work::PickupRun(listed) => Some(&listed.issue),
Work::ArchitectRun(_)
| Work::SecurityRun(_)
| Work::ScoringRun(_)
| Work::Experiment(_) => None,
}
}
fn issue(self) -> Option<u64> {
self.issue_url().map(|issue| issue.number)
}
}
fn record() -> MutexGuard<'static, Record<OnMachine>> {
RECORD.lock().unwrap_or_else(PoisonError::into_inner)
}