Skip to content

Commit 41d178e

Browse files
authored
Merge pull request #46 from Mark-Simulacrum/promote-branches
Support branch promotion
2 parents 2b82a75 + e745ef6 commit 41d178e

File tree

4 files changed

+208
-2
lines changed

4 files changed

+208
-2
lines changed

src/branching.rs

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
use crate::Context;
2+
3+
impl Context {
4+
/// Let $stable, $beta, $master be the tips of each branch (when starting).
5+
///
6+
/// * Set stable to $beta.
7+
/// * Set beta to $master (ish, look for the version bump).
8+
/// * Create a rust-lang/cargo branch for the appropriate beta commit.
9+
/// * Post a PR against the newly created beta branch bump src/ci/channel to `beta`.
10+
pub fn do_branching(&mut self) -> anyhow::Result<()> {
11+
let mut github = if let Some(github) = self.config.github() {
12+
github
13+
} else {
14+
eprintln!("Skipping branching -- github credentials not configured");
15+
return Ok(());
16+
};
17+
let mut token = github.token("rust-lang/rust")?;
18+
let prebump_sha = token.last_commit_for_file("src/version")?;
19+
let beta_sha = token.get_ref("heads/beta")?;
20+
21+
let stable_version = token.read_file(Some("stable"), "src/version")?;
22+
let beta_version = token.read_file(Some("beta"), "src/version")?;
23+
let future_beta_version = token.read_file(Some(&prebump_sha), "src/version")?;
24+
25+
// Check that we've not already promoted. Rather than trying to assert
26+
// +1 version numbers, we instead have a simpler check that all the
27+
// versions are unique -- before promotion we should have:
28+
//
29+
// * stable @ 1.61.0
30+
// * beta @ 1.62.0
31+
// * prebump @ 1.63.0
32+
//
33+
// and after promotion we will have (if we were to read the files again):
34+
//
35+
// * stable @ 1.62.0
36+
// * beta @ 1.63.0
37+
// * prebump @ 1.63.0
38+
//
39+
// In this state, if we try to promote again, we want to bail out. The
40+
// stable == beta check isn't as useful, but still nice to have.
41+
if stable_version.content()? == beta_version.content()? {
42+
anyhow::bail!(
43+
"Stable and beta have the same version: {}; refusing to promote branches.",
44+
stable_version.content()?.trim()
45+
);
46+
}
47+
if beta_version.content()? == future_beta_version.content()? {
48+
anyhow::bail!(
49+
"Beta and pre-bump master ({}) have the same version: {}; refusing to promote branches.",
50+
prebump_sha,
51+
beta_version.content()?.trim()
52+
);
53+
}
54+
55+
// No need to disable branch protection, as the promote-release app is
56+
// specifically authorized to force-push to these branches.
57+
token.update_ref("heads/stable", &beta_sha, true)?;
58+
token.update_ref("heads/beta", &prebump_sha, true)?;
59+
60+
let cargo_sha = token
61+
.read_file(Some(&prebump_sha), "src/tools/cargo")?
62+
.submodule_sha()
63+
.to_owned();
64+
65+
let mut github = self.config.github().unwrap();
66+
let mut token = github.token("rust-lang/cargo")?;
67+
let new_beta = future_beta_version.content()?.trim().to_owned();
68+
token.create_ref(&dbg!(format!("heads/rust-{}", new_beta)), &cargo_sha)?;
69+
70+
Ok(())
71+
}
72+
}

src/config.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,40 @@ impl std::fmt::Display for Channel {
4747
}
4848
}
4949

50+
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
51+
pub(crate) enum Action {
52+
/// This is the default action, what we'll do if the environment variable
53+
/// isn't set. It takes the configured channel and pushes artifacts into the
54+
/// appropriate buckets, taking care of other helper tasks along the way.
55+
PromoteRelease,
56+
57+
/// This promotes the branches up a single release:
58+
///
59+
/// Let $stable, $beta, $master be the tips of each branch (when starting).
60+
///
61+
/// * Set stable to $beta.
62+
/// * Set beta to $master (ish, look for the version bump).
63+
/// * Create a rust-lang/cargo branch for the appropriate beta commit.
64+
/// * Post a PR against the newly created beta branch bump src/ci/channel to `beta`.
65+
PromoteBranches,
66+
}
67+
68+
impl FromStr for Action {
69+
type Err = Error;
70+
71+
fn from_str(input: &str) -> Result<Self, Self::Err> {
72+
match input {
73+
"promote-release" => Ok(Action::PromoteRelease),
74+
"promote-branches" => Ok(Action::PromoteBranches),
75+
_ => anyhow::bail!("unknown channel: {}", input),
76+
}
77+
}
78+
}
79+
5080
pub(crate) struct Config {
81+
/// This is the action we're expecting to take.
82+
pub(crate) action: Action,
83+
5184
/// The channel we're currently releasing.
5285
pub(crate) channel: Channel,
5386
/// CloudFront distribution ID for doc.rust-lang.org.
@@ -153,6 +186,7 @@ pub(crate) struct Config {
153186
impl Config {
154187
pub(crate) fn from_env() -> Result<Self, Error> {
155188
Ok(Self {
189+
action: default_env("ACTION", Action::PromoteRelease)?,
156190
bypass_startup_checks: bool_env("BYPASS_STARTUP_CHECKS")?,
157191
channel: require_env("CHANNEL")?,
158192
cloudfront_doc_id: require_env("CLOUDFRONT_DOC_ID")?,

src/github.rs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,38 @@ impl RepositoryClient<'_> {
226226
Ok(())
227227
}
228228

229+
pub(crate) fn update_ref(&mut self, name: &str, sha: &str, force: bool) -> anyhow::Result<()> {
230+
// This mostly exists to make sure the request is successful rather than
231+
// really checking the created ref (which we already know).
232+
#[derive(serde::Deserialize)]
233+
struct CreatedRef {
234+
#[serde(rename = "ref")]
235+
#[allow(unused)]
236+
ref_: String,
237+
}
238+
#[derive(serde::Serialize)]
239+
struct UpdateRefInternal<'a> {
240+
sha: &'a str,
241+
force: bool,
242+
}
243+
244+
self.start_new_request()?;
245+
// We want curl to read the request body, so configure POST.
246+
self.github.client.post(true)?;
247+
// However, the actual request should be a PATCH request.
248+
self.github.client.custom_request("PATCH")?;
249+
self.github.client.url(&format!(
250+
"https://api.github.com/repos/{repository}/git/refs/{name}",
251+
repository = self.repo,
252+
))?;
253+
self.github
254+
.client
255+
.with_body(UpdateRefInternal { sha, force })
256+
.send_with_response::<CreatedRef>()?;
257+
258+
Ok(())
259+
}
260+
229261
pub(crate) fn workflow_dispatch(&mut self, workflow: &str, branch: &str) -> anyhow::Result<()> {
230262
#[derive(serde::Serialize)]
231263
struct Request<'a> {
@@ -309,6 +341,71 @@ impl RepositoryClient<'_> {
309341
.send()?;
310342
Ok(())
311343
}
344+
345+
/// Returns the last commit (SHA) on a repository's default branch which changed
346+
/// the passed path.
347+
pub(crate) fn last_commit_for_file(&mut self, path: &str) -> anyhow::Result<String> {
348+
#[derive(serde::Deserialize)]
349+
struct CommitData {
350+
sha: String,
351+
}
352+
self.start_new_request()?;
353+
self.github.client.get(true)?;
354+
self.github.client.url(&format!(
355+
"https://api.github.com/repos/{repo}/commits?path={path}",
356+
repo = self.repo
357+
))?;
358+
let mut commits = self
359+
.github
360+
.client
361+
.without_body()
362+
.send_with_response::<Vec<CommitData>>()?;
363+
if commits.is_empty() {
364+
anyhow::bail!("No commits for path {:?}", path);
365+
}
366+
Ok(commits.remove(0).sha)
367+
}
368+
369+
/// Returns the contents of the file
370+
pub(crate) fn read_file(&mut self, sha: Option<&str>, path: &str) -> anyhow::Result<GitFile> {
371+
self.start_new_request()?;
372+
self.github.client.get(true)?;
373+
self.github.client.url(&format!(
374+
"https://api.github.com/repos/{repo}/contents/{path}{maybe_ref}",
375+
repo = self.repo,
376+
maybe_ref = sha.map(|s| format!("?ref={}", s)).unwrap_or_default()
377+
))?;
378+
self.github
379+
.client
380+
.without_body()
381+
.send_with_response::<GitFile>()
382+
}
383+
}
384+
385+
#[derive(Debug, serde::Deserialize)]
386+
#[serde(tag = "type", rename_all = "lowercase")]
387+
pub(crate) enum GitFile {
388+
File { encoding: String, content: String },
389+
Submodule { sha: String },
390+
}
391+
392+
impl GitFile {
393+
pub(crate) fn submodule_sha(&self) -> &str {
394+
if let GitFile::Submodule { sha } = self {
395+
sha
396+
} else {
397+
panic!("{:?} not a submodule", self);
398+
}
399+
}
400+
401+
pub(crate) fn content(&self) -> anyhow::Result<String> {
402+
if let GitFile::File { encoding, content } = self {
403+
assert_eq!(encoding, "base64");
404+
Ok(String::from_utf8(base64::decode(&content.trim())?)?)
405+
} else {
406+
panic!("content() on {:?}", self);
407+
}
408+
}
312409
}
313410

314411
#[derive(Copy, Clone)]

src/main.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#![allow(clippy::rc_buffer)]
22

3+
mod branching;
34
mod build_manifest;
45
mod config;
56
mod curl_helper;
@@ -71,8 +72,10 @@ impl Context {
7172

7273
fn run(&mut self) -> Result<(), Error> {
7374
let _lock = self.lock()?;
74-
self.do_release()?;
75-
75+
match self.config.action {
76+
config::Action::PromoteRelease => self.do_release()?,
77+
config::Action::PromoteBranches => self.do_branching()?,
78+
}
7679
Ok(())
7780
}
7881

0 commit comments

Comments
 (0)