Skip to content

Commit 4adaec4

Browse files
Create and push rust-lang/rust tags for new stable releases
We could consider expanding this to beta's as well (or even nightly), but for now this matches what the normal release process does well enough. It also won't currently do anything in production (we need to configure the PROMOTE_RELEASE_REPOSITORY with push credentials and PROMOTE_RELEASE_PUSH_TAGS), but will still exercise the tag creation, which is good. Code is intended to be somewhat extensible so that we can add Cargo tagging pretty easily.
1 parent 5f36dc7 commit 4adaec4

File tree

6 files changed

+390
-0
lines changed

6 files changed

+390
-0
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ rayon = "1.4.0"
2222
sha2 = "0.10"
2323
hex = "0.4.2"
2424
pgp = "0.8"
25+
rsa = "0.6"
26+
base64 = "0.13"
2527
chrono = "0.4.19"
2628
git2 = "0.14"
2729
tempfile = "3.1.0"

src/config.rs

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use crate::github::Github;
12
use crate::Context;
23
use anyhow::{Context as _, Error};
34
use std::env::VarError;
@@ -103,6 +104,28 @@ pub(crate) struct Config {
103104
/// Whether to skip invalidating the CloudFront distributions. This is useful when running the
104105
/// release process locally, without access to the production AWS account.
105106
pub(crate) skip_cloudfront_invalidations: bool,
107+
108+
/// Where to tag stable rustc releases.
109+
///
110+
/// This repository should have content write permissions with the github
111+
/// app configuration.
112+
///
113+
/// Should be a org/repo code, e.g., rust-lang/rust.
114+
pub(crate) rustc_tag_repository: Option<String>,
115+
116+
/// This is a github app private key, used for the release steps which
117+
/// require action on GitHub (e.g., kicking off a new thanks GHA build,
118+
/// opening pull requests against the blog for dev releases, promoting
119+
/// branches). Not all of this is implemented yet but it's all going to use
120+
/// tokens retrieved from the github app here.
121+
///
122+
/// Currently this isn't really exercised in CI, but that might change in
123+
/// the future with a github app scoped to a 'fake' org or something like
124+
/// that.
125+
pub(crate) github_app_key: Option<String>,
126+
127+
/// The app ID associated with the private key being passed.
128+
pub(crate) github_app_id: Option<u32>,
106129
}
107130

108131
impl Config {
@@ -127,8 +150,19 @@ impl Config {
127150
storage_class: default_env("UPLOAD_STORAGE_CLASS", "INTELLIGENT_TIERING".into())?,
128151
upload_dir: require_env("UPLOAD_DIR")?,
129152
wip_recompress: bool_env("WIP_RECOMPRESS")?,
153+
rustc_tag_repository: maybe_env("RUSTC_TAG_REPOSITORY")?,
154+
github_app_key: maybe_env("GITHUB_APP_KEY")?,
155+
github_app_id: maybe_env("GITHUB_APP_ID")?,
130156
})
131157
}
158+
159+
pub(crate) fn github(&self) -> Option<Github> {
160+
if let (Some(key), Some(id)) = (&self.github_app_key, self.github_app_id) {
161+
Some(Github::new(key, id))
162+
} else {
163+
None
164+
}
165+
}
132166
}
133167

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

src/github.rs

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
use anyhow::Context;
2+
use curl::easy::Easy;
3+
use rsa::pkcs1::DecodeRsaPrivateKey;
4+
use sha2::Digest;
5+
use std::time::SystemTime;
6+
7+
pub(crate) struct Github {
8+
key: rsa::RsaPrivateKey,
9+
id: u32,
10+
client: Easy,
11+
}
12+
13+
pub(crate) struct RepositoryClient<'a> {
14+
github: &'a mut Github,
15+
repo: String,
16+
token: String,
17+
}
18+
19+
impl Github {
20+
pub(crate) fn new(key: &str, id: u32) -> Github {
21+
Github {
22+
key: rsa::RsaPrivateKey::from_pkcs1_pem(key).unwrap(),
23+
id,
24+
client: Easy::new(),
25+
}
26+
}
27+
28+
fn jwt(&self) -> String {
29+
let now = SystemTime::now()
30+
.duration_since(SystemTime::UNIX_EPOCH)
31+
.unwrap()
32+
.as_secs();
33+
let payload = serde_json::json! {{
34+
"iat": now - 10,
35+
"exp": now + 60,
36+
"iss": self.id,
37+
}};
38+
let header = r#"{"alg":"RS256","typ":"JWT"}"#;
39+
let payload = serde_json::to_string(&payload).unwrap();
40+
41+
let encoding = base64::URL_SAFE_NO_PAD;
42+
let signature = self
43+
.key
44+
.sign(
45+
rsa::padding::PaddingScheme::PKCS1v15Sign {
46+
hash: Some(rsa::hash::Hash::SHA2_256),
47+
},
48+
&sha2::Sha256::new()
49+
.chain_update(format!(
50+
"{}.{}",
51+
base64::encode_config(&header, encoding),
52+
base64::encode_config(&payload, encoding),
53+
))
54+
.finalize(),
55+
)
56+
.unwrap();
57+
format!(
58+
"{}.{}.{}",
59+
base64::encode_config(&header, encoding),
60+
base64::encode_config(&payload, encoding),
61+
base64::encode_config(&signature, encoding),
62+
)
63+
}
64+
65+
fn start_jwt_request(&mut self) -> anyhow::Result<()> {
66+
self.client.reset();
67+
let mut headers = curl::easy::List::new();
68+
headers.append(&format!("Authorization: Bearer {}", self.jwt()))?;
69+
self.client.http_headers(headers)?;
70+
Ok(())
71+
}
72+
73+
pub(crate) fn token(&mut self, repository: &str) -> anyhow::Result<RepositoryClient<'_>> {
74+
self.start_jwt_request()?;
75+
self.client.get(true)?;
76+
self.client.url(&format!(
77+
"https://api.github.com/repos/{}/installation",
78+
repository
79+
))?;
80+
#[derive(serde::Deserialize)]
81+
struct InstallationResponse {
82+
id: u32,
83+
}
84+
let installation_id = send_request::<InstallationResponse>(&mut self.client)?.id;
85+
86+
self.start_jwt_request()?;
87+
self.client.post(true)?;
88+
self.client.url(&format!(
89+
"https://api.github.com/app/installations/{installation_id}/access_tokens"
90+
))?;
91+
#[derive(serde::Deserialize)]
92+
struct TokenResponse {
93+
token: String,
94+
}
95+
let token = send_request::<TokenResponse>(&mut self.client)?.token;
96+
Ok(RepositoryClient {
97+
github: self,
98+
repo: repository.to_owned(),
99+
token,
100+
})
101+
}
102+
}
103+
104+
impl RepositoryClient<'_> {
105+
fn start_new_request(&mut self) -> anyhow::Result<()> {
106+
self.github.client.reset();
107+
let mut headers = curl::easy::List::new();
108+
headers.append(&format!("Authorization: token {}", self.token))?;
109+
self.github.client.http_headers(headers)?;
110+
Ok(())
111+
}
112+
113+
pub(crate) fn tag(&mut self, tag: CreateTag<'_>) -> anyhow::Result<()> {
114+
#[derive(serde::Deserialize)]
115+
struct CreatedTag {
116+
sha: String,
117+
}
118+
self.start_new_request()?;
119+
self.github.client.post(true)?;
120+
self.github.client.url(&format!(
121+
"https://api.github.com/repos/{repository}/git/tags",
122+
repository = self.repo,
123+
))?;
124+
let created = send_request_body::<CreatedTag, _>(
125+
&mut self.github.client,
126+
CreateTagInternal {
127+
tag: tag.tag_name,
128+
message: tag.message,
129+
object: tag.commit,
130+
type_: "commit",
131+
tagger: CreateTagTaggerInternal {
132+
name: tag.tagger_name,
133+
email: tag.tagger_email,
134+
},
135+
},
136+
)?;
137+
138+
// This mostly exists to make sure the request is successful rather than
139+
// really checking the created ref (which we already know).
140+
#[derive(serde::Deserialize)]
141+
struct CreatedTagRef {
142+
#[serde(rename = "ref")]
143+
#[allow(unused)]
144+
ref_: String,
145+
}
146+
self.start_new_request()?;
147+
self.github.client.post(true)?;
148+
self.github.client.url(&format!(
149+
"https://api.github.com/repos/{repository}/git/refs",
150+
repository = self.repo,
151+
))?;
152+
send_request_body::<CreatedTagRef, _>(
153+
&mut self.github.client,
154+
CreateRefInternal {
155+
ref_: &format!("refs/tags/{}", tag.tag_name),
156+
sha: &created.sha,
157+
},
158+
)?;
159+
160+
Ok(())
161+
}
162+
}
163+
164+
#[derive(Copy, Clone)]
165+
pub(crate) struct CreateTag<'a> {
166+
pub(crate) commit: &'a str,
167+
pub(crate) tag_name: &'a str,
168+
pub(crate) message: &'a str,
169+
pub(crate) tagger_name: &'a str,
170+
pub(crate) tagger_email: &'a str,
171+
}
172+
173+
#[derive(serde::Serialize)]
174+
struct CreateTagInternal<'a> {
175+
tag: &'a str,
176+
message: &'a str,
177+
/// sha of the object being tagged
178+
object: &'a str,
179+
#[serde(rename = "type")]
180+
type_: &'a str,
181+
tagger: CreateTagTaggerInternal<'a>,
182+
}
183+
184+
#[derive(serde::Serialize)]
185+
struct CreateTagTaggerInternal<'a> {
186+
name: &'a str,
187+
email: &'a str,
188+
}
189+
190+
#[derive(serde::Serialize)]
191+
struct CreateRefInternal<'a> {
192+
#[serde(rename = "ref")]
193+
ref_: &'a str,
194+
sha: &'a str,
195+
}
196+
197+
fn send_request_body<T: serde::de::DeserializeOwned, S: serde::Serialize>(
198+
client: &mut Easy,
199+
body: S,
200+
) -> anyhow::Result<T> {
201+
use std::io::Read;
202+
client.useragent("rust-lang/promote-release").unwrap();
203+
let mut response = Vec::new();
204+
let body = serde_json::to_vec(&body).unwrap();
205+
{
206+
let mut transfer = client.transfer();
207+
let mut body = &body[..];
208+
// The unwrap in the read_function is basically guaranteed to not
209+
// happen: reading into a slice can't fail. We can't use `?` since the
210+
// return type inside transfer isn't compatible with io::Error.
211+
transfer.read_function(move |dest| Ok(body.read(dest).unwrap()))?;
212+
transfer.write_function(|new_data| {
213+
response.extend_from_slice(new_data);
214+
Ok(new_data.len())
215+
})?;
216+
transfer.perform()?;
217+
}
218+
serde_json::from_slice(&response)
219+
.with_context(|| format!("{}", String::from_utf8_lossy(&response)))
220+
}
221+
222+
fn send_request<T: serde::de::DeserializeOwned>(client: &mut Easy) -> anyhow::Result<T> {
223+
client.useragent("rust-lang/promote-release").unwrap();
224+
let mut response = Vec::new();
225+
{
226+
let mut transfer = client.transfer();
227+
transfer.write_function(|new_data| {
228+
response.extend_from_slice(new_data);
229+
Ok(new_data.len())
230+
})?;
231+
transfer.perform()?;
232+
}
233+
serde_json::from_slice(&response)
234+
.with_context(|| format!("{}", String::from_utf8_lossy(&response)))
235+
}

0 commit comments

Comments
 (0)