docs_update.rs 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. //! A scheduled job to post a PR to update the documentation on rust-lang/rust.
  2. use crate::db::jobs::JobSchedule;
  3. use crate::github::{self, GitTreeEntry, GithubClient, Issue, Repository};
  4. use anyhow::Context;
  5. use anyhow::Result;
  6. use cron::Schedule;
  7. use reqwest::Client;
  8. use std::fmt::Write;
  9. use std::str::FromStr;
  10. /// This is the repository where the commits will be created.
  11. const WORK_REPO: &str = "rustbot/rust";
  12. /// This is the repository where the PR will be created.
  13. const DEST_REPO: &str = "rust-lang/rust";
  14. /// This is the branch in `WORK_REPO` to create the commits.
  15. const BRANCH_NAME: &str = "docs-update";
  16. const SUBMODULES: &[&str] = &[
  17. "src/doc/book",
  18. "src/doc/edition-guide",
  19. "src/doc/embedded-book",
  20. "src/doc/nomicon",
  21. "src/doc/reference",
  22. "src/doc/rust-by-example",
  23. "src/doc/rustc-dev-guide",
  24. ];
  25. const TITLE: &str = "Update books";
  26. pub fn job() -> JobSchedule {
  27. JobSchedule {
  28. name: "docs_update".to_string(),
  29. // Around 9am Pacific time on every Monday.
  30. schedule: Schedule::from_str("0 00 17 * * Mon *").unwrap(),
  31. metadata: serde_json::Value::Null,
  32. }
  33. }
  34. pub async fn handle_job() -> Result<()> {
  35. // Only run every other week. Doing it every week can be a bit noisy, and
  36. // (rarely) a PR can take longer than a week to merge (like if there are
  37. // CI issues). `Schedule` does not allow expressing this, so check it
  38. // manually.
  39. //
  40. // This is set to run the first week after a release, and the week just
  41. // before a release. That allows getting the latest changes in the next
  42. // release, accounting for possibly taking a few days for the PR to land.
  43. let today = chrono::Utc::today().naive_utc();
  44. let base = chrono::naive::NaiveDate::from_ymd(2015, 12, 10);
  45. let duration = today.signed_duration_since(base);
  46. let weeks = duration.num_weeks();
  47. if weeks % 2 != 0 {
  48. tracing::trace!("skipping job, this is an odd week");
  49. return Ok(());
  50. }
  51. tracing::trace!("starting docs-update");
  52. docs_update()
  53. .await
  54. .context("failed to process docs update")?;
  55. Ok(())
  56. }
  57. pub async fn docs_update() -> Result<Option<Issue>> {
  58. let gh = GithubClient::new_with_default_token(Client::new());
  59. let work_repo = gh.repository(WORK_REPO).await?;
  60. work_repo
  61. .merge_upstream(&gh, &work_repo.default_branch)
  62. .await?;
  63. let updates = get_submodule_updates(&gh, &work_repo).await?;
  64. if updates.is_empty() {
  65. tracing::trace!("no updates this week?");
  66. return Ok(None);
  67. }
  68. create_commit(&gh, &work_repo, &updates).await?;
  69. Ok(Some(create_pr(&gh, &updates).await?))
  70. }
  71. struct Update {
  72. path: String,
  73. new_hash: String,
  74. pr_body: String,
  75. }
  76. async fn get_submodule_updates(
  77. gh: &GithubClient,
  78. repo: &github::Repository,
  79. ) -> Result<Vec<Update>> {
  80. let mut updates = Vec::new();
  81. for submodule_path in SUBMODULES {
  82. tracing::trace!("checking submodule {submodule_path}");
  83. let submodule = repo.submodule(gh, submodule_path, None).await?;
  84. let submodule_repo = submodule.repository(gh).await?;
  85. let latest_commit = submodule_repo
  86. .get_reference(gh, &format!("heads/{}", submodule_repo.default_branch))
  87. .await?;
  88. if submodule.sha == latest_commit.object.sha {
  89. tracing::trace!(
  90. "skipping submodule {submodule_path}, no changes sha={}",
  91. submodule.sha
  92. );
  93. continue;
  94. }
  95. let current_hash = submodule.sha;
  96. let new_hash = latest_commit.object.sha;
  97. let pr_body = generate_pr_body(gh, &submodule_repo, &current_hash, &new_hash).await?;
  98. let update = Update {
  99. path: submodule.path,
  100. new_hash,
  101. pr_body,
  102. };
  103. updates.push(update);
  104. }
  105. Ok(updates)
  106. }
  107. async fn generate_pr_body(
  108. gh: &GithubClient,
  109. repo: &github::Repository,
  110. oldest: &str,
  111. newest: &str,
  112. ) -> Result<String> {
  113. let recent_commits: Vec<_> = repo
  114. .recent_commits(gh, &repo.default_branch, oldest, newest)
  115. .await?;
  116. if recent_commits.is_empty() {
  117. anyhow::bail!(
  118. "unexpected empty set of commits for {} oldest={oldest} newest={newest}",
  119. repo.full_name
  120. );
  121. }
  122. let mut body = format!(
  123. "## {}\n\
  124. \n\
  125. {} commits in {}..{}\n\
  126. {} to {}\n\
  127. \n",
  128. repo.full_name,
  129. recent_commits.len(),
  130. oldest,
  131. newest,
  132. recent_commits.first().unwrap().committed_date,
  133. recent_commits.last().unwrap().committed_date,
  134. );
  135. for commit in recent_commits {
  136. write!(body, "- {}", commit.title).unwrap();
  137. if let Some(num) = commit.pr_num {
  138. write!(body, " ({}#{})", repo.full_name, num).unwrap();
  139. }
  140. body.push('\n');
  141. }
  142. Ok(body)
  143. }
  144. async fn create_commit(
  145. gh: &GithubClient,
  146. rust_repo: &Repository,
  147. updates: &[Update],
  148. ) -> Result<()> {
  149. let master_ref = rust_repo
  150. .get_reference(gh, &format!("heads/{}", rust_repo.default_branch))
  151. .await?;
  152. let master_commit = rust_repo.git_commit(gh, &master_ref.object.sha).await?;
  153. let tree_entries: Vec<_> = updates
  154. .iter()
  155. .map(|update| GitTreeEntry {
  156. path: update.path.clone(),
  157. mode: "160000".to_string(),
  158. object_type: "commit".to_string(),
  159. sha: update.new_hash.clone(),
  160. })
  161. .collect();
  162. let new_tree = rust_repo
  163. .update_tree(gh, &master_commit.tree.sha, &tree_entries)
  164. .await?;
  165. let commit = rust_repo
  166. .create_commit(gh, TITLE, &[&master_ref.object.sha], &new_tree.sha)
  167. .await?;
  168. rust_repo
  169. .update_reference(gh, &format!("heads/{BRANCH_NAME}"), &commit.sha)
  170. .await?;
  171. Ok(())
  172. }
  173. async fn create_pr(gh: &GithubClient, updates: &[Update]) -> Result<Issue> {
  174. let dest_repo = gh.repository(DEST_REPO).await?;
  175. let mut body = String::new();
  176. for update in updates {
  177. write!(body, "{}\n", update.pr_body).unwrap();
  178. }
  179. let username = WORK_REPO.split('/').next().unwrap();
  180. let head = format!("{username}:{BRANCH_NAME}");
  181. let pr = dest_repo
  182. .new_pr(gh, TITLE, &head, &dest_repo.default_branch, &body)
  183. .await?;
  184. tracing::debug!("created PR {}", pr.html_url);
  185. Ok(pr)
  186. }