1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
//! Atomic, per-issue exclusion between Pickup runs on different machines,
//! and between a Pickup run and the Architect run or Security run that marks
//! its own issue ready and dispatches it. Label edits and ordinary Issue
//! branch pushes cannot arbitrate that race. A separate origin ref is
//! created only if absent, then held through dispatch.
use anyhow::{Context, Result};
use crate::git::Git;
use crate::issue::IssueUrl;
use crate::progress;
pub struct Reservation {
launch: Git,
reference: String,
token: String,
}
impl Reservation {
/// Create a unique commit and atomically reserve `issue` with an empty
/// lease. Existing reservations are never adopted, expired or overwritten.
/// Errors with an uncertain remote outcome retain the ref for recovery.
pub fn take(launch: &Git, issue: &IssueUrl) -> Result<Option<Self>> {
Self::take_ref(launch, reference(issue.number), &issue.url)
}
/// Review and Fix runs elect one owner for a PR across machines.
pub(crate) fn pull_request(
launch: &Git,
pr: &crate::review::PullRequestUrl,
) -> Result<Option<Self>> {
Self::take_ref(
launch,
format!("refs/thirdshift/reservations/{}", pr.issue.number),
&pr.url,
)
}
/// Reserve `issue`, which this run published and is about to mark
/// ready and dispatch itself, as a Pickup run reserves its Ready issue,
/// so that an overlapping Pickup run passes it over. Fails if another
/// run already holds it.
pub fn of_published(launch: &Git, issue: &IssueUrl) -> Result<Self> {
Self::take(launch, issue)?.with_context(|| {
format!(
"#{} is already reserved on origin, at {}",
issue.number,
reference(issue.number)
)
})
}
/// Serialize CI-red Ticket publication across all Base branches and hosts.
pub fn ci_red(launch: &Git) -> Result<Option<Self>> {
Self::take_ref(
launch,
"refs/heads/thirdshift/pickup/ci-red".into(),
"CI-red Ticket filing",
)
}
fn take_ref(launch: &Git, reference: String, subject: &str) -> Result<Option<Self>> {
let nonce = tempfile::Builder::new().rand_bytes(32).tempfile()?;
// A reservation carries no source tree or parent: naming a Base branch
// permits a Launch directory with unrelated, unpublished local work.
let tree = launch.run(&["mktree"])?;
let message = format!(
"thirdshift Pickup reservation for {}\n\nToken: {}\nHost: {}\nLaunch: {}\nProcess: {}\nStarted: {}",
subject,
nonce.path().display(),
crate::host::name().unwrap_or_else(|| "unknown".into()),
launch.dir().display(),
std::process::id(),
chrono::Utc::now(),
);
let token = launch.run(&[
"-c",
"commit.gpgSign=false",
"commit-tree",
&tree,
"-m",
&message,
])?;
let pushed = launch.run(&[
"push",
"--no-verify",
&format!("--force-with-lease={reference}:"),
"origin",
&format!("{token}:{reference}"),
]);
if let Err(error) = pushed {
let remote = launch.run(&["ls-remote", "--refs", "origin", &reference])
.with_context(|| format!(
"{error:#}; could not verify reservation {reference}; check origin by hand (our token {token})"
))?;
if let Some((other, _)) = remote.split_once('\t')
&& other != token
{
return Ok(None);
}
return Err(error).with_context(|| format!(
"could not reserve {reference}; check origin by hand before retrying (our token {token})"
));
}
Ok(Some(Self {
launch: launch.completion(),
reference,
token,
}))
}
}
/// The ref that reserves issue `number` on origin.
pub fn reference(number: u64) -> String {
format!("refs/heads/thirdshift/pickup/{number}")
}
impl Drop for Reservation {
fn drop(&mut self) {
// Finish after interruption, and never delete another owner's token.
if let Err(error) = self.launch.run(&[
"push",
"--no-verify",
&format!("--force-with-lease={}:{}", self.reference, self.token),
"origin",
&format!(":{}", self.reference),
]) {
progress::warn(
&error,
format_args!(
"could not release Pickup reservation {}; check origin by hand (our token {})",
self.reference, self.token
),
);
}
}
}