Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Import release automation tool from website repo #981

Merged
merged 1 commit into from
Aug 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
360 changes: 356 additions & 4 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ members = [
"models/star",
"models/test",

"tools/automator",
"tools/export-validator",
"tools/release-operator",
]
Expand Down
19 changes: 19 additions & 0 deletions tools/automator/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "automator"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
anyhow = "1.0.62"
chrono = "0.4.22"
octocrab = "0.17.0"
url = "2.2.2"

[dependencies.clap]
version = "3.2.17"
features = ["derive"]

[dependencies.tokio]
version = "1.20.1"
features = ["full"]
19 changes: 19 additions & 0 deletions tools/automator/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# `automator`

CLI tool that automates Fornjot development tasks (mostly release automation).

## Usage

Install `automator`, so you can use it in other repositories (most relevantly, the [website repository](https://github.com/hannobraun/www.fornjot.app)):

``` sh
cargo install --path tools/automator/
```

To learn how to use it, run the following command:

``` sh
automator --help
```

Please also refer to the [release procedure](../../RELEASES.md), which is the main use case for `automator`, as of this writing.
122 changes: 122 additions & 0 deletions tools/automator/src/announcement.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
use std::{fmt::Write, path::PathBuf};

use anyhow::Context;
use chrono::{Date, Datelike, Utc};
use tokio::{
fs::{self, File},
io::AsyncWriteExt,
};

use crate::pull_requests::PullRequest;

pub async fn create_release_announcement(
last_release_date: Date<Utc>,
version: String,
) -> anyhow::Result<()> {
let now = Utc::now();

let year = now.year();
let week = now.iso_week().week();

let pull_requests =
PullRequest::fetch_since_last_release(last_release_date)
.await?
.into_values();

let mut file = create_file(year, week).await?;
generate_announcement(week, version, pull_requests, &mut file).await?;

Ok(())
}

async fn create_file(year: i32, week: u32) -> anyhow::Result<File> {
let dir =
PathBuf::from(format!("content/blog/weekly-release/{year}-w{week}"));
let file = dir.join("index.md");

fs::create_dir_all(&dir).await.with_context(|| {
format!("Failed to create directory `{}`", dir.display())
})?;
let file = File::create(&file).await.with_context(|| {
format!("Failed to create file `{}`", file.display())
})?;

Ok(file)
}

async fn generate_announcement(
week: u32,
version: String,
pull_requests: impl IntoIterator<Item = PullRequest>,
file: &mut File,
) -> anyhow::Result<()> {
let mut pull_request_links = String::new();

for PullRequest { number, html_url } in pull_requests {
let link = format!("[#{number}]: {html_url}\n");

pull_request_links.push_str(&link);
}

let mut buf = String::new();
write!(
buf,
"\
+++
title = \"Weekly Release - 2022-W{week}\"

[extra]
version = \"{version}\"
+++

**TASK: Write introduction.**


### Sponsors

Fornjot is supported by [@webtrax-oz](https://github.com/webtrax-oz), [@lthiery](https://github.com/lthiery), [@Yatekii](https://github.com/Yatekii), [@martindederer](https://github.com/martindederer), [@hobofan](https://github.com/hobofan), [@ahdinosaur](https://github.com/ahdinosaur), [@thawkins](https://github.com/thawkins), [@bollian](https://github.com/bollian), [@rozgo](https://github.com/rozgo), and [my other awesome sponsors](https://github.com/sponsors/hannobraun). Thank you!

If you want Fornjot to be stable and sustainable long-term, please consider [supporting me](https://github.com/sponsors/hannobraun) too.


### End-user improvements

Improvements to Fornjot and its documentation that are visible to end-users.

**TASK: Add end-user improvements.**


### Ecosystem improvements

Improvements to the Fornjot ecosystem that are relevant to developers who are building on top of Fornjot components.

#### `fj-kernel`

**TASK: Add ecosystem improvements.**


### Internal Improvements

Improvements that are relevant to developers working on Fornjot itself.

**TASK: Add internal improvements.**


### Issue of the Week

**TASK: Write.**


### Outlook

**TASK: Write.**


{pull_request_links}\
"
)?;

file.write_all(buf.as_bytes()).await?;

Ok(())
}
24 changes: 24 additions & 0 deletions tools/automator/src/args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use chrono::{Date, NaiveDate, Utc};

#[derive(clap::Parser)]
pub enum Args {
CreateReleaseAnnouncement(CreateReleaseAnnouncement),
}

impl Args {
pub fn parse() -> Self {
<Self as clap::Parser>::parse()
}
}

#[derive(clap::Parser)]
pub struct CreateReleaseAnnouncement {
pub last_release_date: NaiveDate,
pub version: String,
}

impl CreateReleaseAnnouncement {
pub fn last_release_date(&self) -> Date<Utc> {
Date::from_utc(self.last_release_date, Utc)
}
}
9 changes: 9 additions & 0 deletions tools/automator/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
mod announcement;
mod args;
mod pull_requests;
mod run;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
run::run().await
}
69 changes: 69 additions & 0 deletions tools/automator/src/pull_requests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
use std::collections::BTreeMap;

use anyhow::anyhow;
use chrono::{Date, Utc};
use octocrab::params::{pulls::Sort, Direction, State};
use url::Url;

pub struct PullRequest {
pub number: u64,
pub html_url: Url,
}

impl PullRequest {
pub async fn fetch_since_last_release(
last_release_date: Date<Utc>,
) -> anyhow::Result<BTreeMap<u64, Self>> {
let mut pull_requests = BTreeMap::new();
let mut page = 1u32;

'outer: loop {
println!("Fetching page {}...", page);
let pull_request_page = octocrab::instance()
.pulls("hannobraun", "Fornjot")
.list()
.state(State::Closed)
.sort(Sort::Updated)
.direction(Direction::Descending)
.per_page(100) // this is the maximum number of results per page
.page(page)
.send()
.await?;

for pull_request in pull_request_page.items {
if let Some(updated_at) = pull_request.updated_at {
if updated_at.date() < last_release_date {
// This pull request has been updated before the last
// release. Since we sort pull requests by
// updated-descending, that means all following pull
// requests have been updated before the last release,
// and thus couldn't have been merged after.
break 'outer;
}
}

if let Some(merged_at) = pull_request.merged_at {
if merged_at.date() >= last_release_date {
let number = pull_request.number;
let html_url =
pull_request.html_url.ok_or_else(|| {
anyhow!("Pull request is missing URL")
})?;

let pull_request = Self { number, html_url };

pull_requests.insert(pull_request.number, pull_request);
}
}
}

if pull_request_page.next.is_some() {
page += 1;
} else {
break;
}
}

Ok(pull_requests)
}
}
15 changes: 15 additions & 0 deletions tools/automator/src/run.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use anyhow::Context;

use crate::{announcement::create_release_announcement, args::Args};

pub async fn run() -> anyhow::Result<()> {
match Args::parse() {
Args::CreateReleaseAnnouncement(args) => {
create_release_announcement(args.last_release_date(), args.version)
.await
.context("Failed to create release announcement")?;
}
}

Ok(())
}