浏览代码

implement synchronization for github releases

Pietro Albini 4 年之前
父节点
当前提交
c5f9daef56
共有 4 个文件被更改,包括 196 次插入0 次删除
  1. 12 0
      src/config.rs
  2. 2 0
      src/github.rs
  3. 15 0
      src/handlers.rs
  4. 167 0
      src/handlers/github_releases.rs

+ 12 - 0
src/config.rs

@@ -1,3 +1,4 @@
+use crate::changelogs::ChangelogFormat;
 use crate::github::GithubClient;
 use std::collections::{HashMap, HashSet};
 use std::fmt;
@@ -25,6 +26,7 @@ pub(crate) struct Config {
     pub(crate) glacier: Option<GlacierConfig>,
     pub(crate) autolabel: Option<AutolabelConfig>,
     pub(crate) notify_zulip: Option<NotifyZulipConfig>,
+    pub(crate) github_releases: Option<GitHubReleasesConfig>,
 }
 
 #[derive(PartialEq, Eq, Debug, serde::Deserialize)]
@@ -150,6 +152,15 @@ pub(crate) async fn get(gh: &GithubClient, repo: &str) -> Result<Arc<Config>, Co
     }
 }
 
+#[derive(PartialEq, Eq, Debug, serde::Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub(crate) struct GitHubReleasesConfig {
+    pub(crate) format: ChangelogFormat,
+    pub(crate) project_name: String,
+    pub(crate) changelog_path: String,
+    pub(crate) changelog_branch: String,
+}
+
 fn get_cached_config(repo: &str) -> Option<Result<Arc<Config>, ConfigurationError>> {
     let cache = CONFIG_CACHE.read().unwrap();
     cache.get(repo).and_then(|(config, fetch_time)| {
@@ -272,6 +283,7 @@ mod tests {
                 glacier: None,
                 autolabel: None,
                 notify_zulip: None,
+                github_releases: None,
             }
         );
     }

+ 2 - 0
src/github.rs

@@ -863,6 +863,8 @@ pub struct CreateEvent {
 
 #[derive(Debug, serde::Deserialize)]
 pub struct PushEvent {
+    #[serde(rename = "ref")]
+    pub git_ref: String,
     repository: Repository,
     sender: User,
 }

+ 15 - 0
src/handlers.rs

@@ -25,6 +25,7 @@ impl fmt::Display for HandlerError {
 
 mod assign;
 mod autolabel;
+mod github_releases;
 mod glacier;
 mod major_change;
 mod milestone_prs;
@@ -72,6 +73,20 @@ pub async fn handle(ctx: &Context, event: &Event) -> Vec<HandlerError> {
         );
     }
 
+    if let Some(ghr_config) = config
+        .as_ref()
+        .ok()
+        .and_then(|c| c.github_releases.as_ref())
+    {
+        if let Err(e) = github_releases::handle(ctx, event, ghr_config).await {
+            log::error!(
+                "failed to process event {:?} with github_releases handler: {:?}",
+                event,
+                e
+            );
+        }
+    }
+
     errors
 }
 

+ 167 - 0
src/handlers/github_releases.rs

@@ -0,0 +1,167 @@
+use crate::{
+    changelogs::Changelog,
+    config::GitHubReleasesConfig,
+    github::{CreateEvent, CreateKind, Event},
+    handlers::Context,
+};
+use anyhow::Context as _;
+use octocrab::Page;
+use std::{collections::HashMap, time::Duration};
+
+pub(super) async fn handle(
+    ctx: &Context,
+    event: &Event,
+    config: &GitHubReleasesConfig,
+) -> anyhow::Result<()> {
+    // Only allow commit pushed to the changelog branch or tags being created.
+    match event {
+        Event::Push(push) if push.git_ref == format!("refs/heads/{}", config.changelog_branch) => {}
+        Event::Create(CreateEvent {
+            ref_type: CreateKind::Tag,
+            ..
+        }) => {}
+        _ => return Ok(()),
+    }
+
+    log::info!("handling github releases");
+
+    log::debug!("loading the changelog");
+    let content = load_changelog(ctx, event, config).await.with_context(|| {
+        format!(
+            "failed to load changelog file {} from repo {} in branch {}",
+            config.changelog_path,
+            event.repo_name(),
+            config.changelog_branch
+        )
+    })?;
+    let changelog = Changelog::parse(config.format, &content)?;
+
+    log::debug!("loading the git tags");
+    let tags = load_paginated(
+        ctx,
+        &format!("repos/{}/git/matching-refs/tags", event.repo_name()),
+        |git_ref: &GitRef| {
+            git_ref
+                .name
+                .strip_prefix("refs/tags/")
+                .unwrap_or(git_ref.name.as_str())
+                .to_string()
+        },
+    )
+    .await?;
+
+    log::debug!("loading the existing releases");
+    let releases = load_paginated(
+        ctx,
+        &format!("repos/{}/releases", event.repo_name()),
+        |release: &Release| release.tag_name.clone(),
+    )
+    .await?;
+
+    for tag in tags.keys() {
+        if let Some(expected_body) = changelog.version(tag) {
+            let expected_name = format!("{} {}", config.project_name, tag);
+
+            if let Some(release) = releases.get(tag) {
+                if release.name != expected_name || release.body != expected_body {
+                    log::info!("updating release {} on {}", tag, event.repo_name());
+                    let _: serde_json::Value = ctx
+                        .octocrab
+                        .patch(
+                            &release.url,
+                            Some(&serde_json::json!({
+                                "name": expected_name,
+                                "body": expected_body,
+                            })),
+                        )
+                        .await?;
+                } else {
+                    // Avoid waiting for the delay below.
+                    continue;
+                }
+            } else {
+                log::info!("creating release {} on {}", tag, event.repo_name());
+                let _: serde_json::Value = ctx
+                    .octocrab
+                    .post(
+                        format!("repos/{}/releases", event.repo_name()),
+                        Some(&serde_json::json!({
+                            "tag_name": tag,
+                            "name": expected_name,
+                            "body": expected_body,
+                        })),
+                    )
+                    .await?;
+            }
+
+            log::debug!("sleeping for one second to avoid hitting any rate limit");
+            tokio::time::delay_for(Duration::from_secs(1)).await;
+        } else {
+            log::trace!(
+                "skipping tag {} since it doesn't have a changelog entry",
+                tag
+            );
+        }
+    }
+
+    Ok(())
+}
+
+async fn load_changelog(
+    ctx: &Context,
+    event: &Event,
+    config: &GitHubReleasesConfig,
+) -> anyhow::Result<String> {
+    let resp = ctx
+        .github
+        .raw_file(
+            event.repo_name(),
+            &config.changelog_branch,
+            &config.changelog_path,
+        )
+        .await?
+        .ok_or_else(|| anyhow::Error::msg("missing file"))?;
+
+    Ok(String::from_utf8(resp)?)
+}
+
+async fn load_paginated<T, R, F>(ctx: &Context, url: &str, key: F) -> anyhow::Result<HashMap<R, T>>
+where
+    T: serde::de::DeserializeOwned,
+    R: Eq + PartialEq + std::hash::Hash,
+    F: Fn(&T) -> R,
+{
+    let mut current_page: Page<T> = ctx.octocrab.get::<Page<T>, _, ()>(url, None).await?;
+
+    let mut items = current_page
+        .take_items()
+        .into_iter()
+        .map(|val| (key(&val), val))
+        .collect::<HashMap<R, T>>();
+
+    while let Some(mut new_page) = ctx.octocrab.get_page::<T>(&current_page.next).await? {
+        items.extend(
+            new_page
+                .take_items()
+                .into_iter()
+                .map(|val| (key(&val), val)),
+        );
+        current_page = new_page;
+    }
+
+    Ok(items)
+}
+
+#[derive(Debug, serde::Deserialize)]
+struct GitRef {
+    #[serde(rename = "ref")]
+    name: String,
+}
+
+#[derive(Debug, serde::Deserialize)]
+struct Release {
+    url: String,
+    tag_name: String,
+    name: String,
+    body: String,
+}