浏览代码

Merge pull request #811 from pietroalbini/github-releases

Synchronize GitHub Releases with the in-repo changelog
Mark Rousskov 4 年之前
父节点
当前提交
87d3fafc75
共有 10 个文件被更改,包括 575 次插入4 次删除
  1. 111 0
      Cargo.lock
  2. 1 0
      Cargo.toml
  3. 46 0
      src/changelogs/mod.rs
  4. 151 0
      src/changelogs/rustc.rs
  5. 12 0
      src/config.rs
  6. 41 3
      src/github.rs
  7. 19 0
      src/handlers.rs
  8. 167 0
      src/handlers/github_releases.rs
  9. 2 1
      src/handlers/notification.rs
  10. 25 0
      src/lib.rs

+ 111 - 0
Cargo.lock

@@ -24,6 +24,15 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "ansi_term"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b"
+dependencies = [
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "anyhow"
 version = "1.0.32"
@@ -177,6 +186,21 @@ dependencies = [
  "time",
 ]
 
+[[package]]
+name = "clap"
+version = "2.33.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags",
+ "strsim",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
 [[package]]
 name = "cloudabi"
 version = "0.1.0"
@@ -186,6 +210,25 @@ dependencies = [
  "bitflags",
 ]
 
+[[package]]
+name = "comrak"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d325e4f2ffff52ca77d995bb675494d5364aa332499d5f7c7fbb28c25e671f6"
+dependencies = [
+ "clap",
+ "entities",
+ "lazy_static",
+ "pest",
+ "pest_derive",
+ "regex",
+ "shell-words",
+ "twoway",
+ "typed-arena",
+ "unicode_categories",
+ "xdg",
+]
+
 [[package]]
 name = "core-foundation"
 version = "0.7.0"
@@ -274,6 +317,12 @@ dependencies = [
  "cfg-if",
 ]
 
+[[package]]
+name = "entities"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5320ae4c3782150d900b79807611a59a99fc9a1d61d686faafc24b93fc8d7ca"
+
 [[package]]
 name = "env_logger"
 version = "0.7.1"
@@ -1489,6 +1538,12 @@ dependencies = [
  "opaque-debug 0.3.0",
 ]
 
+[[package]]
+name = "shell-words"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fa3938c99da4914afedd13bf3d79bcb6c277d1b2c398d23257a304d9e1b074"
+
 [[package]]
 name = "siphasher"
 version = "0.3.3"
@@ -1551,6 +1606,12 @@ dependencies = [
  "unicode-normalization",
 ]
 
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
 [[package]]
 name = "subtle"
 version = "2.2.3"
@@ -1606,6 +1667,15 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
 [[package]]
 name = "thread_local"
 version = "1.0.1"
@@ -1752,6 +1822,7 @@ dependencies = [
  "anyhow",
  "async-trait",
  "chrono",
+ "comrak",
  "dotenv",
  "env_logger",
  "futures",
@@ -1786,6 +1857,22 @@ version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642"
 
+[[package]]
+name = "twoway"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b40075910de3a912adbd80b5d8bad6ad10a23eeb1f5bf9d4006839e899ba5bc"
+dependencies = [
+ "memchr",
+ "unchecked-index",
+]
+
+[[package]]
+name = "typed-arena"
+version = "1.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9b2228007eba4120145f785df0f6c92ea538f5a3635a612ecf4e334c8c1446d"
+
 [[package]]
 name = "typenum"
 version = "1.12.0"
@@ -1798,6 +1885,12 @@ version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
 
+[[package]]
+name = "unchecked-index"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c"
+
 [[package]]
 name = "unicase"
 version = "2.6.0"
@@ -1837,6 +1930,12 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
 
+[[package]]
+name = "unicode_categories"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
+
 [[package]]
 name = "url"
 version = "2.1.1"
@@ -1864,6 +1963,12 @@ version = "0.2.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c"
 
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
 [[package]]
 name = "version_check"
 version = "0.9.2"
@@ -2042,3 +2147,9 @@ dependencies = [
  "winapi 0.2.8",
  "winapi-build",
 ]
+
+[[package]]
+name = "xdg"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d089681aa106a86fade1b0128fb5daf07d5867a509ab036d99988dec80429a57"

+ 1 - 0
Cargo.toml

@@ -34,6 +34,7 @@ postgres-native-tls = "0.3"
 native-tls = "0.2"
 serde_path_to_error = "0.1.2"
 octocrab = "0.5"
+comrak = "0.8.2"
 
 [dependencies.serde]
 version = "1"

+ 46 - 0
src/changelogs/mod.rs

@@ -0,0 +1,46 @@
+mod rustc;
+
+use comrak::{nodes::AstNode, Arena, ComrakOptions, ComrakRenderOptions};
+use std::collections::HashMap;
+
+#[derive(Copy, Clone, PartialEq, Eq, Debug, serde::Deserialize)]
+#[serde(rename_all = "kebab-case")]
+pub(crate) enum ChangelogFormat {
+    Rustc,
+}
+
+pub(crate) struct Changelog {
+    versions: HashMap<String, String>,
+}
+
+impl Changelog {
+    pub(crate) fn parse(format: ChangelogFormat, content: &str) -> anyhow::Result<Self> {
+        match format {
+            ChangelogFormat::Rustc => rustc::RustcFormat::new(&Arena::new()).parse(content),
+        }
+    }
+
+    pub(crate) fn version(&self, version: &str) -> Option<&str> {
+        self.versions.get(version).map(|s| s.as_str())
+    }
+}
+
+fn render_for_github_releases<'a>(document: &'a AstNode<'a>) -> anyhow::Result<String> {
+    let mut content = Vec::new();
+    comrak::format_commonmark(
+        document,
+        &ComrakOptions {
+            render: ComrakRenderOptions {
+                // Prevent column width line breaks from appearing in the generated release
+                // notes. GitHub Releases insert <br>s for every line break in the markdown,
+                // mangling the output.
+                width: std::usize::MAX,
+
+                ..ComrakRenderOptions::default()
+            },
+            ..ComrakOptions::default()
+        },
+        &mut content,
+    )?;
+    Ok(String::from_utf8(content)?)
+}

+ 151 - 0
src/changelogs/rustc.rs

@@ -0,0 +1,151 @@
+use super::Changelog;
+use comrak::{
+    nodes::{Ast, AstNode, NodeHeading, NodeValue},
+    Arena, ComrakOptions,
+};
+use std::cell::RefCell;
+use std::collections::HashMap;
+
+pub(super) struct RustcFormat<'a> {
+    arena: &'a Arena<AstNode<'a>>,
+    current_h1: Option<String>,
+    result: Changelog,
+}
+
+impl<'a> RustcFormat<'a> {
+    pub(super) fn new(arena: &'a Arena<AstNode<'a>>) -> Self {
+        RustcFormat {
+            arena,
+            current_h1: None,
+            result: Changelog {
+                versions: HashMap::new(),
+            },
+        }
+    }
+
+    pub(super) fn parse(mut self, content: &str) -> anyhow::Result<Changelog> {
+        let ast = comrak::parse_document(&self.arena, &content, &ComrakOptions::default());
+
+        let mut section_ast = Vec::new();
+        for child in ast.children() {
+            let child_data = child.data.borrow();
+
+            if let NodeValue::Heading(NodeHeading { level: 1, .. }) = child_data.value {
+                if let Some(h1) = self.current_h1.take() {
+                    self.store_version(h1, section_ast)?;
+                }
+
+                self.current_h1 = Some(String::from_utf8(child_data.content.clone())?);
+                section_ast = Vec::new();
+            } else {
+                section_ast.push(child);
+            }
+        }
+        if let Some(h1) = self.current_h1.take() {
+            self.store_version(h1, section_ast)?;
+        }
+
+        Ok(self.result)
+    }
+
+    fn store_version(&mut self, h1: String, body: Vec<&'a AstNode<'a>>) -> anyhow::Result<()> {
+        // Create a document with only the contents of this section
+        let document = self
+            .arena
+            .alloc(AstNode::new(RefCell::new(Ast::new(NodeValue::Document))));
+        for child in &body {
+            document.append(child);
+        }
+
+        let content = super::render_for_github_releases(document)?;
+
+        if let Some(version) = h1.split(' ').nth(1) {
+            self.result.versions.insert(version.to_string(), content);
+        } else {
+            println!("skipped version, invalid header: {}", h1);
+        }
+
+        Ok(())
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    const CONTENT: &str = "\
+Version 1.45.2 (2020-08-03)
+==========================
+
+* [Fix bindings in tuple struct patterns][74954]
+* [Link in another section][69033]
+* Very very very very very very very very very very very long line that has some
+  linebreaks here and there
+
+[74954]: https://github.com/rust-lang/rust/issues/74954
+
+Version 1.45.1 (2020-07-30)
+==========================
+
+* [Fix const propagation with references.][73613]
+* [rustfmt accepts rustfmt_skip in cfg_attr again.][73078]
+
+[73613]: https://github.com/rust-lang/rust/pull/73613
+[73078]: https://github.com/rust-lang/rust/issues/73078
+
+Version 1.44.0 (2020-06-04)
+==========================
+
+Language
+--------
+- [You can now use `async/.await` with `#[no_std]` enabled.][69033]
+
+**Syntax-only changes**
+
+- [Expansion-driven outline module parsing][69838]
+```rust
+#[cfg(FALSE)]
+mod foo {
+    mod bar {
+        mod baz; // `foo/bar/baz.rs` doesn't exist, but no error!
+    }
+}
+```
+
+These are still rejected semantically, so you will likely receive an error but
+these changes can be seen and parsed by macros and conditional compilation.
+
+Internal Only
+-------------
+These changes provide no direct user facing benefits, but represent significant
+improvements to the internals and overall performance of rustc and
+related tools.
+
+- [dep_graph Avoid allocating a set on when the number reads are small.][69778]
+
+[69033]: https://github.com/rust-lang/rust/pull/69033/
+[69838]: https://github.com/rust-lang/rust/pull/69838/
+[69778]: https://github.com/rust-lang/rust/pull/69778/
+";
+
+    const EXPECTED_1_45_2: &str = "\
+- [Fix bindings in tuple struct patterns](https://github.com/rust-lang/rust/issues/74954)
+- [Link in another section](https://github.com/rust-lang/rust/pull/69033/)
+- Very very very very very very very very very very very long line that has some linebreaks here and there
+";
+
+    #[test]
+    fn test_changelog_parsing() -> anyhow::Result<()> {
+        let arena = Arena::new();
+        let parsed = RustcFormat::new(&arena).parse(CONTENT)?;
+
+        // Ensure the right markdown is generated from each version
+        let version_1_45_2 = parsed.version("1.45.2").expect("missing version 1.45.2");
+        assert_eq!(EXPECTED_1_45_2, version_1_45_2);
+
+        let version_1_44_0 = parsed.version("1.44.0").expect("missing version 1.44.0");
+        assert!(version_1_44_0.contains("Avoid allocating a set"));
+
+        Ok(())
+    }
+}

+ 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,
             }
         );
     }

+ 41 - 3
src/github.rs

@@ -847,61 +847,99 @@ pub enum QueryKind {
     Count,
 }
 
+#[derive(Debug, serde::Deserialize)]
+#[serde(rename_all = "snake_case")]
+pub enum CreateKind {
+    Branch,
+    Tag,
+}
+
+#[derive(Debug, serde::Deserialize)]
+pub struct CreateEvent {
+    pub ref_type: CreateKind,
+    repository: Repository,
+    sender: User,
+}
+
+#[derive(Debug, serde::Deserialize)]
+pub struct PushEvent {
+    #[serde(rename = "ref")]
+    pub git_ref: String,
+    repository: Repository,
+    sender: User,
+}
+
 #[derive(Debug)]
 pub enum Event {
+    Create(CreateEvent),
     IssueComment(IssueCommentEvent),
     Issue(IssuesEvent),
+    Push(PushEvent),
 }
 
 impl Event {
     pub fn repo_name(&self) -> &str {
         match self {
+            Event::Create(event) => &event.repository.full_name,
             Event::IssueComment(event) => &event.repository.full_name,
             Event::Issue(event) => &event.repository.full_name,
+            Event::Push(event) => &event.repository.full_name,
         }
     }
 
     pub fn issue(&self) -> Option<&Issue> {
         match self {
+            Event::Create(_) => None,
             Event::IssueComment(event) => Some(&event.issue),
             Event::Issue(event) => Some(&event.issue),
+            Event::Push(_) => None,
         }
     }
 
     /// This will both extract from IssueComment events but also Issue events
     pub fn comment_body(&self) -> Option<&str> {
         match self {
+            Event::Create(_) => None,
             Event::Issue(e) => Some(&e.issue.body),
             Event::IssueComment(e) => Some(&e.comment.body),
+            Event::Push(_) => None,
         }
     }
 
     /// This will both extract from IssueComment events but also Issue events
     pub fn comment_from(&self) -> Option<&str> {
         match self {
+            Event::Create(_) => None,
             Event::Issue(e) => Some(&e.changes.as_ref()?.body.from),
             Event::IssueComment(e) => Some(&e.changes.as_ref()?.body.from),
+            Event::Push(_) => None,
         }
     }
 
     pub fn html_url(&self) -> Option<&str> {
         match self {
+            Event::Create(_) => None,
             Event::Issue(e) => Some(&e.issue.html_url),
             Event::IssueComment(e) => Some(&e.comment.html_url),
+            Event::Push(_) => None,
         }
     }
 
     pub fn user(&self) -> &User {
         match self {
+            Event::Create(e) => &e.sender,
             Event::Issue(e) => &e.issue.user,
             Event::IssueComment(e) => &e.comment.user,
+            Event::Push(e) => &e.sender,
         }
     }
 
-    pub fn time(&self) -> chrono::DateTime<FixedOffset> {
+    pub fn time(&self) -> Option<chrono::DateTime<FixedOffset>> {
         match self {
-            Event::Issue(e) => e.issue.created_at.into(),
-            Event::IssueComment(e) => e.comment.updated_at.into(),
+            Event::Create(_) => None,
+            Event::Issue(e) => Some(e.issue.created_at.into()),
+            Event::IssueComment(e) => Some(e.comment.updated_at.into()),
+            Event::Push(_) => None,
         }
     }
 }

+ 19 - 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
 }
 
@@ -134,6 +149,10 @@ macro_rules! command_handlers {
                     log::debug!("skipping event, comment was {:?}", e.action);
                     return;
                 }
+                Event::Push(_) | Event::Create(_) => {
+                    log::debug!("skipping unsupported event");
+                    return;
+                }
             }
 
             let input = Input::new(&body, &ctx.username);

+ 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,
+}

+ 2 - 1
src/handlers/notification.rs

@@ -33,6 +33,7 @@ pub async fn handle(ctx: &Context, event: &Event) -> anyhow::Result<()> {
     let short_description = match event {
         Event::Issue(e) => e.issue.title.clone(),
         Event::IssueComment(e) => format!("Comment on {}", e.issue.title),
+        Event::Push(_) | Event::Create(_) => return Ok(()),
     };
 
     let mut caps = parser::get_mentions(body)
@@ -110,7 +111,7 @@ pub async fn handle(ctx: &Context, event: &Event) -> anyhow::Result<()> {
                     user_id: user.id.unwrap(),
                     origin_url: event.html_url().unwrap().to_owned(),
                     origin_html: body.to_owned(),
-                    time: event.time(),
+                    time: event.time().unwrap(),
                     short_description: Some(short_description.clone()),
                     team_name: team_name.clone(),
                 },

+ 25 - 0
src/lib.rs

@@ -10,6 +10,7 @@ use std::fmt;
 
 pub mod actions;
 pub mod agenda;
+mod changelogs;
 pub mod config;
 pub mod db;
 pub mod github;
@@ -29,6 +30,8 @@ pub enum EventName {
     PullRequestReviewComment,
     IssueComment,
     Issue,
+    Push,
+    Create,
     Other,
 }
 
@@ -41,6 +44,8 @@ impl std::str::FromStr for EventName {
             "issue_comment" => EventName::IssueComment,
             "pull_request" => EventName::PullRequest,
             "issues" => EventName::Issue,
+            "push" => EventName::Push,
+            "create" => EventName::Create,
             _ => EventName::Other,
         })
     }
@@ -57,6 +62,8 @@ impl fmt::Display for EventName {
                 EventName::IssueComment => "issue_comment",
                 EventName::Issue => "issues",
                 EventName::PullRequest => "pull_request",
+                EventName::Push => "push",
+                EventName::Create => "create",
                 EventName::Other => "other",
             }
         )
@@ -150,6 +157,24 @@ pub async fn webhook(
 
             github::Event::Issue(payload)
         }
+        EventName::Push => {
+            let payload = deserialize_payload::<github::PushEvent>(&payload)
+                .with_context(|| format!("{:?} failed to deserialize", event))
+                .map_err(anyhow::Error::from)?;
+
+            log::info!("handling push event {:?}", payload);
+
+            github::Event::Push(payload)
+        }
+        EventName::Create => {
+            let payload = deserialize_payload::<github::CreateEvent>(&payload)
+                .with_context(|| format!("{:?} failed to deserialize", event))
+                .map_err(anyhow::Error::from)?;
+
+            log::info!("handling create event {:?}", payload);
+
+            github::Event::Create(payload)
+        }
         // Other events need not be handled
         EventName::Other => {
             return Ok(false);