Skip to content

Post internals + blog PR after publishing dev-static releases #45

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jul 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions src/config.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::discourse::Discourse;
use crate::github::Github;
use crate::Context;
use anyhow::{Context as _, Error};
Expand Down Expand Up @@ -113,6 +114,27 @@ pub(crate) struct Config {
/// Should be a org/repo code, e.g., rust-lang/rust.
pub(crate) rustc_tag_repository: Option<String>,

/// Where to publish new blog PRs.
///
/// We create a new PR announcing releases in this repository; currently we
/// don't automatically merge it (but that might change in the future).
///
/// Should be a org/repo code, e.g., rust-lang/blog.rust-lang.org.
pub(crate) blog_repository: Option<String>,

/// The expected release date, for the blog post announcing dev-static
/// releases. Expected to be in YYYY-MM-DD format.
///
/// This is used to produce the expected release date in blog posts and to
/// generate the release notes URL (targeting stable branch on
/// rust-lang/rust).
pub(crate) scheduled_release_date: Option<chrono::NaiveDate>,

/// These are Discourse configurations for where to post dev-static
/// announcements. Currently we only post dev release announcements.
pub(crate) discourse_api_key: Option<String>,
pub(crate) discourse_api_user: Option<String>,

/// This is a github app private key, used for the release steps which
/// require action on GitHub (e.g., kicking off a new thanks GHA build,
/// opening pull requests against the blog for dev releases, promoting
Expand Down Expand Up @@ -151,6 +173,10 @@ impl Config {
upload_dir: require_env("UPLOAD_DIR")?,
wip_recompress: bool_env("WIP_RECOMPRESS")?,
rustc_tag_repository: maybe_env("RUSTC_TAG_REPOSITORY")?,
blog_repository: maybe_env("BLOG_REPOSITORY")?,
scheduled_release_date: maybe_env("BLOG_SCHEDULED_RELEASE_DATE")?,
discourse_api_user: maybe_env("DISCOURSE_API_USER")?,
discourse_api_key: maybe_env("DISCOURSE_API_KEY")?,
github_app_key: maybe_env("GITHUB_APP_KEY")?,
github_app_id: maybe_env("GITHUB_APP_ID")?,
})
Expand All @@ -163,6 +189,70 @@ impl Config {
None
}
}
pub(crate) fn discourse(&self) -> Option<Discourse> {
if let (Some(key), Some(user)) = (&self.discourse_api_key, &self.discourse_api_user) {
Some(Discourse::new(
"https://internals.rust-lang.org".to_owned(),
user.clone(),
key.clone(),
))
} else {
None
}
}

pub(crate) fn blog_contents(
&self,
release: &str,
archive_date: &str,
for_blog: bool,
internals_url: Option<&str>,
) -> Option<String> {
let scheduled_release_date = self.scheduled_release_date?;
let release_notes_url = format!(
"https://github.com/rust-lang/rust/blob/stable/RELEASES.md#version-{}-{}",
release.replace('.', ""),
scheduled_release_date.format("%Y-%m-%d"),
);
let human_date = scheduled_release_date.format("%B %d");
let internals = internals_url
.map(|url| format!("You can leave feedback on the [internals thread]({url})."))
.unwrap_or_default();
let prefix = if for_blog {
format!(
r#"---
layout: post
title: "{} pre-release testing"
author: Release automation
team: The Release Team <https://www.rust-lang.org/governance/teams/release>
---{}"#,
release, "\n\n",
)
} else {
String::new()
};
Some(format!(
"{prefix}The {release} pre-release is ready for testing. The release is scheduled for
{human_date}. [Release notes can be found here.][relnotes]

You can try it out locally by running:

```plain
RUSTUP_DIST_SERVER=https://dev-static.rust-lang.org rustup update stable
```

The index is <https://dev-static.rust-lang.org/dist/{archive_date}/index.html>.

{internals}

The release team is also thinking about changes to our pre-release process:
we'd love your feedback [on this GitHub issue][feedback].

[relnotes]: {release_notes_url}
[feedback]: https://github.com/rust-lang/release-team/issues/16
"
))
}
}

fn maybe_env<R>(name: &str) -> Result<Option<R>, Error>
Expand Down
68 changes: 68 additions & 0 deletions src/curl_helper.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
use anyhow::Context;
use curl::easy::Easy;

pub trait BodyExt {
fn with_body<S>(&mut self, body: S) -> Request<'_, S>;
fn without_body(&mut self) -> Request<'_, ()>;
}

impl BodyExt for Easy {
fn with_body<S>(&mut self, body: S) -> Request<'_, S> {
Request {
body: Some(body),
client: self,
}
}
fn without_body(&mut self) -> Request<'_, ()> {
Request {
body: None,
client: self,
}
}
}

pub struct Request<'a, S> {
body: Option<S>,
client: &'a mut Easy,
}

impl<S: serde::Serialize> Request<'_, S> {
pub fn send_with_response<T: serde::de::DeserializeOwned>(self) -> anyhow::Result<T> {
use std::io::Read;
let mut response = Vec::new();
let body = self.body.map(|body| serde_json::to_vec(&body).unwrap());
{
let mut transfer = self.client.transfer();
// The unwrap in the read_function is basically guaranteed to not
// happen: reading into a slice can't fail. We can't use `?` since the
// return type inside transfer isn't compatible with io::Error.
if let Some(mut body) = body.as_deref() {
transfer.read_function(move |dest| Ok(body.read(dest).unwrap()))?;
}
transfer.write_function(|new_data| {
response.extend_from_slice(new_data);
Ok(new_data.len())
})?;
transfer.perform()?;
}
serde_json::from_slice(&response)
.with_context(|| format!("{}", String::from_utf8_lossy(&response)))
}

pub fn send(self) -> anyhow::Result<()> {
use std::io::Read;
let body = self.body.map(|body| serde_json::to_vec(&body).unwrap());
{
let mut transfer = self.client.transfer();
// The unwrap in the read_function is basically guaranteed to not
// happen: reading into a slice can't fail. We can't use `?` since the
// return type inside transfer isn't compatible with io::Error.
if let Some(mut body) = body.as_deref() {
transfer.read_function(move |dest| Ok(body.read(dest).unwrap()))?;
}
transfer.perform()?;
}

Ok(())
}
}
69 changes: 69 additions & 0 deletions src/discourse.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use crate::curl_helper::BodyExt;
use curl::easy::Easy;

pub struct Discourse {
root: String,
api_key: String,
api_username: String,
client: Easy,
}

impl Discourse {
pub fn new(root: String, api_username: String, api_key: String) -> Discourse {
Discourse {
root,
api_key,
api_username,
client: Easy::new(),
}
}

fn start_new_request(&mut self) -> anyhow::Result<()> {
self.client.reset();
self.client.useragent("rust-lang/promote-release")?;
let mut headers = curl::easy::List::new();
headers.append(&format!("Api-Key: {}", self.api_key))?;
headers.append(&format!("Api-Username: {}", self.api_username))?;
headers.append("Content-Type: application/json")?;
self.client.http_headers(headers)?;
Ok(())
}

/// Returns a URL to the topic
pub fn create_topic(
&mut self,
category: u32,
title: &str,
body: &str,
) -> anyhow::Result<String> {
#[derive(serde::Serialize)]
struct Request<'a> {
title: &'a str,
#[serde(rename = "raw")]
body: &'a str,
category: u32,
archetype: &'a str,
}
#[derive(serde::Deserialize)]
struct Response {
topic_id: u32,
topic_slug: String,
}
self.start_new_request()?;
self.client.post(true)?;
self.client.url(&format!("{}/posts.json", self.root))?;
let resp = self
.client
.with_body(Request {
title,
body,
category,
archetype: "regular",
})
.send_with_response::<Response>()?;
Ok(format!(
"{}/t/{}/{}",
self.root, resp.topic_slug, resp.topic_id
))
}
}
Loading