Skip to content

Commit 2b82a75

Browse files
authored
Merge pull request #45 from Mark-Simulacrum/dev-blog
Post internals + blog PR after publishing dev-static releases
2 parents 2b1037a + 37d33fd commit 2b82a75

File tree

5 files changed

+415
-78
lines changed

5 files changed

+415
-78
lines changed

src/config.rs

+90
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::discourse::Discourse;
12
use crate::github::Github;
23
use crate::Context;
34
use anyhow::{Context as _, Error};
@@ -113,6 +114,27 @@ pub(crate) struct Config {
113114
/// Should be a org/repo code, e.g., rust-lang/rust.
114115
pub(crate) rustc_tag_repository: Option<String>,
115116

117+
/// Where to publish new blog PRs.
118+
///
119+
/// We create a new PR announcing releases in this repository; currently we
120+
/// don't automatically merge it (but that might change in the future).
121+
///
122+
/// Should be a org/repo code, e.g., rust-lang/blog.rust-lang.org.
123+
pub(crate) blog_repository: Option<String>,
124+
125+
/// The expected release date, for the blog post announcing dev-static
126+
/// releases. Expected to be in YYYY-MM-DD format.
127+
///
128+
/// This is used to produce the expected release date in blog posts and to
129+
/// generate the release notes URL (targeting stable branch on
130+
/// rust-lang/rust).
131+
pub(crate) scheduled_release_date: Option<chrono::NaiveDate>,
132+
133+
/// These are Discourse configurations for where to post dev-static
134+
/// announcements. Currently we only post dev release announcements.
135+
pub(crate) discourse_api_key: Option<String>,
136+
pub(crate) discourse_api_user: Option<String>,
137+
116138
/// This is a github app private key, used for the release steps which
117139
/// require action on GitHub (e.g., kicking off a new thanks GHA build,
118140
/// opening pull requests against the blog for dev releases, promoting
@@ -151,6 +173,10 @@ impl Config {
151173
upload_dir: require_env("UPLOAD_DIR")?,
152174
wip_recompress: bool_env("WIP_RECOMPRESS")?,
153175
rustc_tag_repository: maybe_env("RUSTC_TAG_REPOSITORY")?,
176+
blog_repository: maybe_env("BLOG_REPOSITORY")?,
177+
scheduled_release_date: maybe_env("BLOG_SCHEDULED_RELEASE_DATE")?,
178+
discourse_api_user: maybe_env("DISCOURSE_API_USER")?,
179+
discourse_api_key: maybe_env("DISCOURSE_API_KEY")?,
154180
github_app_key: maybe_env("GITHUB_APP_KEY")?,
155181
github_app_id: maybe_env("GITHUB_APP_ID")?,
156182
})
@@ -163,6 +189,70 @@ impl Config {
163189
None
164190
}
165191
}
192+
pub(crate) fn discourse(&self) -> Option<Discourse> {
193+
if let (Some(key), Some(user)) = (&self.discourse_api_key, &self.discourse_api_user) {
194+
Some(Discourse::new(
195+
"https://internals.rust-lang.org".to_owned(),
196+
user.clone(),
197+
key.clone(),
198+
))
199+
} else {
200+
None
201+
}
202+
}
203+
204+
pub(crate) fn blog_contents(
205+
&self,
206+
release: &str,
207+
archive_date: &str,
208+
for_blog: bool,
209+
internals_url: Option<&str>,
210+
) -> Option<String> {
211+
let scheduled_release_date = self.scheduled_release_date?;
212+
let release_notes_url = format!(
213+
"https://github.com/rust-lang/rust/blob/stable/RELEASES.md#version-{}-{}",
214+
release.replace('.', ""),
215+
scheduled_release_date.format("%Y-%m-%d"),
216+
);
217+
let human_date = scheduled_release_date.format("%B %d");
218+
let internals = internals_url
219+
.map(|url| format!("You can leave feedback on the [internals thread]({url})."))
220+
.unwrap_or_default();
221+
let prefix = if for_blog {
222+
format!(
223+
r#"---
224+
layout: post
225+
title: "{} pre-release testing"
226+
author: Release automation
227+
team: The Release Team <https://www.rust-lang.org/governance/teams/release>
228+
---{}"#,
229+
release, "\n\n",
230+
)
231+
} else {
232+
String::new()
233+
};
234+
Some(format!(
235+
"{prefix}The {release} pre-release is ready for testing. The release is scheduled for
236+
{human_date}. [Release notes can be found here.][relnotes]
237+
238+
You can try it out locally by running:
239+
240+
```plain
241+
RUSTUP_DIST_SERVER=https://dev-static.rust-lang.org rustup update stable
242+
```
243+
244+
The index is <https://dev-static.rust-lang.org/dist/{archive_date}/index.html>.
245+
246+
{internals}
247+
248+
The release team is also thinking about changes to our pre-release process:
249+
we'd love your feedback [on this GitHub issue][feedback].
250+
251+
[relnotes]: {release_notes_url}
252+
[feedback]: https://github.com/rust-lang/release-team/issues/16
253+
"
254+
))
255+
}
166256
}
167257

168258
fn maybe_env<R>(name: &str) -> Result<Option<R>, Error>

src/curl_helper.rs

+68
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
use anyhow::Context;
2+
use curl::easy::Easy;
3+
4+
pub trait BodyExt {
5+
fn with_body<S>(&mut self, body: S) -> Request<'_, S>;
6+
fn without_body(&mut self) -> Request<'_, ()>;
7+
}
8+
9+
impl BodyExt for Easy {
10+
fn with_body<S>(&mut self, body: S) -> Request<'_, S> {
11+
Request {
12+
body: Some(body),
13+
client: self,
14+
}
15+
}
16+
fn without_body(&mut self) -> Request<'_, ()> {
17+
Request {
18+
body: None,
19+
client: self,
20+
}
21+
}
22+
}
23+
24+
pub struct Request<'a, S> {
25+
body: Option<S>,
26+
client: &'a mut Easy,
27+
}
28+
29+
impl<S: serde::Serialize> Request<'_, S> {
30+
pub fn send_with_response<T: serde::de::DeserializeOwned>(self) -> anyhow::Result<T> {
31+
use std::io::Read;
32+
let mut response = Vec::new();
33+
let body = self.body.map(|body| serde_json::to_vec(&body).unwrap());
34+
{
35+
let mut transfer = self.client.transfer();
36+
// The unwrap in the read_function is basically guaranteed to not
37+
// happen: reading into a slice can't fail. We can't use `?` since the
38+
// return type inside transfer isn't compatible with io::Error.
39+
if let Some(mut body) = body.as_deref() {
40+
transfer.read_function(move |dest| Ok(body.read(dest).unwrap()))?;
41+
}
42+
transfer.write_function(|new_data| {
43+
response.extend_from_slice(new_data);
44+
Ok(new_data.len())
45+
})?;
46+
transfer.perform()?;
47+
}
48+
serde_json::from_slice(&response)
49+
.with_context(|| format!("{}", String::from_utf8_lossy(&response)))
50+
}
51+
52+
pub fn send(self) -> anyhow::Result<()> {
53+
use std::io::Read;
54+
let body = self.body.map(|body| serde_json::to_vec(&body).unwrap());
55+
{
56+
let mut transfer = self.client.transfer();
57+
// The unwrap in the read_function is basically guaranteed to not
58+
// happen: reading into a slice can't fail. We can't use `?` since the
59+
// return type inside transfer isn't compatible with io::Error.
60+
if let Some(mut body) = body.as_deref() {
61+
transfer.read_function(move |dest| Ok(body.read(dest).unwrap()))?;
62+
}
63+
transfer.perform()?;
64+
}
65+
66+
Ok(())
67+
}
68+
}

src/discourse.rs

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
use crate::curl_helper::BodyExt;
2+
use curl::easy::Easy;
3+
4+
pub struct Discourse {
5+
root: String,
6+
api_key: String,
7+
api_username: String,
8+
client: Easy,
9+
}
10+
11+
impl Discourse {
12+
pub fn new(root: String, api_username: String, api_key: String) -> Discourse {
13+
Discourse {
14+
root,
15+
api_key,
16+
api_username,
17+
client: Easy::new(),
18+
}
19+
}
20+
21+
fn start_new_request(&mut self) -> anyhow::Result<()> {
22+
self.client.reset();
23+
self.client.useragent("rust-lang/promote-release")?;
24+
let mut headers = curl::easy::List::new();
25+
headers.append(&format!("Api-Key: {}", self.api_key))?;
26+
headers.append(&format!("Api-Username: {}", self.api_username))?;
27+
headers.append("Content-Type: application/json")?;
28+
self.client.http_headers(headers)?;
29+
Ok(())
30+
}
31+
32+
/// Returns a URL to the topic
33+
pub fn create_topic(
34+
&mut self,
35+
category: u32,
36+
title: &str,
37+
body: &str,
38+
) -> anyhow::Result<String> {
39+
#[derive(serde::Serialize)]
40+
struct Request<'a> {
41+
title: &'a str,
42+
#[serde(rename = "raw")]
43+
body: &'a str,
44+
category: u32,
45+
archetype: &'a str,
46+
}
47+
#[derive(serde::Deserialize)]
48+
struct Response {
49+
topic_id: u32,
50+
topic_slug: String,
51+
}
52+
self.start_new_request()?;
53+
self.client.post(true)?;
54+
self.client.url(&format!("{}/posts.json", self.root))?;
55+
let resp = self
56+
.client
57+
.with_body(Request {
58+
title,
59+
body,
60+
category,
61+
archetype: "regular",
62+
})
63+
.send_with_response::<Response>()?;
64+
Ok(format!(
65+
"{}/t/{}/{}",
66+
self.root, resp.topic_slug, resp.topic_id
67+
))
68+
}
69+
}

0 commit comments

Comments
 (0)