github_releases.rs 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  1. use crate::{
  2. changelogs::Changelog,
  3. config::GitHubReleasesConfig,
  4. github::{CreateEvent, CreateKind, Event},
  5. handlers::Context,
  6. };
  7. use anyhow::Context as _;
  8. use octocrab::Page;
  9. use std::{collections::HashMap, time::Duration};
  10. use tracing as log;
  11. pub(super) async fn handle(
  12. ctx: &Context,
  13. event: &Event,
  14. config: &GitHubReleasesConfig,
  15. ) -> anyhow::Result<()> {
  16. // Only allow commit pushed to the changelog branch or tags being created.
  17. match event {
  18. Event::Push(push) if push.git_ref == format!("refs/heads/{}", config.changelog_branch) => {}
  19. Event::Create(CreateEvent {
  20. ref_type: CreateKind::Tag,
  21. ..
  22. }) => {}
  23. _ => return Ok(()),
  24. }
  25. log::info!("handling github releases");
  26. log::debug!("loading the changelog");
  27. let content = load_changelog(ctx, event, config).await.with_context(|| {
  28. format!(
  29. "failed to load changelog file {} from repo {} in branch {}",
  30. config.changelog_path,
  31. event.repo().full_name,
  32. config.changelog_branch
  33. )
  34. })?;
  35. let changelog = Changelog::parse(config.format, &content)?;
  36. log::debug!("loading the git tags");
  37. let tags = load_paginated(
  38. ctx,
  39. &format!("repos/{}/git/matching-refs/tags", event.repo().full_name),
  40. |git_ref: &GitRef| {
  41. git_ref
  42. .name
  43. .strip_prefix("refs/tags/")
  44. .unwrap_or(git_ref.name.as_str())
  45. .to_string()
  46. },
  47. )
  48. .await?;
  49. log::debug!("loading the existing releases");
  50. let releases = load_paginated(
  51. ctx,
  52. &format!("repos/{}/releases", event.repo().full_name),
  53. |release: &Release| release.tag_name.clone(),
  54. )
  55. .await?;
  56. for tag in tags.keys() {
  57. if let Some(expected_body) = changelog.version(tag) {
  58. let expected_name = format!("{} {}", config.project_name, tag);
  59. if let Some(release) = releases.get(tag) {
  60. if release.name != expected_name || release.body != expected_body {
  61. log::info!("updating release {} on {}", tag, event.repo().full_name);
  62. let _: serde_json::Value = ctx
  63. .octocrab
  64. .patch(
  65. &release.url,
  66. Some(&serde_json::json!({
  67. "name": expected_name,
  68. "body": expected_body,
  69. })),
  70. )
  71. .await?;
  72. } else {
  73. // Avoid waiting for the delay below.
  74. continue;
  75. }
  76. } else {
  77. log::info!("creating release {} on {}", tag, event.repo().full_name);
  78. let e: octocrab::Result<serde_json::Value> = ctx
  79. .octocrab
  80. .post(
  81. format!("repos/{}/releases", event.repo().full_name),
  82. Some(&serde_json::json!({
  83. "tag_name": tag,
  84. "name": expected_name,
  85. "body": expected_body,
  86. })),
  87. )
  88. .await;
  89. match e {
  90. Ok(v) => log::debug!("created release: {:?}", v),
  91. Err(e) => {
  92. log::error!("Failed to create release: {:?}", e);
  93. // Don't stop creating future releases just because this
  94. // one failed.
  95. }
  96. }
  97. }
  98. log::debug!("sleeping for one second to avoid hitting any rate limit");
  99. tokio::time::sleep(Duration::from_secs(1)).await;
  100. } else {
  101. log::trace!(
  102. "skipping tag {} since it doesn't have a changelog entry",
  103. tag
  104. );
  105. }
  106. }
  107. Ok(())
  108. }
  109. async fn load_changelog(
  110. ctx: &Context,
  111. event: &Event,
  112. config: &GitHubReleasesConfig,
  113. ) -> anyhow::Result<String> {
  114. let resp = ctx
  115. .github
  116. .raw_file(
  117. &event.repo().full_name,
  118. &config.changelog_branch,
  119. &config.changelog_path,
  120. )
  121. .await?
  122. .ok_or_else(|| anyhow::Error::msg("missing file"))?;
  123. Ok(String::from_utf8(resp)?)
  124. }
  125. async fn load_paginated<T, R, F>(ctx: &Context, url: &str, key: F) -> anyhow::Result<HashMap<R, T>>
  126. where
  127. T: serde::de::DeserializeOwned,
  128. R: Eq + PartialEq + std::hash::Hash,
  129. F: Fn(&T) -> R,
  130. {
  131. let mut current_page: Page<T> = ctx.octocrab.get::<Page<T>, _, ()>(url, None).await?;
  132. let mut items = current_page
  133. .take_items()
  134. .into_iter()
  135. .map(|val| (key(&val), val))
  136. .collect::<HashMap<R, T>>();
  137. while let Some(mut new_page) = ctx.octocrab.get_page::<T>(&current_page.next).await? {
  138. items.extend(
  139. new_page
  140. .take_items()
  141. .into_iter()
  142. .map(|val| (key(&val), val)),
  143. );
  144. current_page = new_page;
  145. }
  146. Ok(items)
  147. }
  148. #[derive(Debug, serde::Deserialize)]
  149. struct GitRef {
  150. #[serde(rename = "ref")]
  151. name: String,
  152. }
  153. #[derive(Debug, serde::Deserialize)]
  154. struct Release {
  155. url: String,
  156. tag_name: String,
  157. name: String,
  158. body: String,
  159. }