Skip to content

Commit

Permalink
Comment PRs with updated schedule information (#1576)
Browse files Browse the repository at this point in the history
This adds a GH action to add a comment to every PR giving the updated
course schedule with the PR merged.

To accomplish this, I broke `mdbook-course` into a library and two
binaries, allowing the mdbook content to be loaded dynamically outside
of an `mdbook build` invocation.

I think this is a net benefit, but possible improvements include:
* diffing the "before" and "after" schedules and only making the comment
when those are not the same (or replacing the comment with "no schedule
changes")
* including per-segment timing behind `<details>` (with a few minutes
effort I couldn't get this to play nicely with the markdown lists)

---------

Co-authored-by: Martin Geisler <[email protected]>
  • Loading branch information
djmitche and mgeisler authored Jan 12, 2024
1 parent 23ba2aa commit 4c0833a
Show file tree
Hide file tree
Showing 15 changed files with 151 additions and 56 deletions.
27 changes: 27 additions & 0 deletions .github/workflows/course-schedule.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: "Course Schedule Updates"
on:
pull_request:
paths:
- "src/**.md"

jobs:
course-schedule:
runs-on: ubuntu-latest
name: Make Course Schedule Comment
permissions:
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v3

- name: Setup Rust cache
uses: ./.github/workflows/setup-rust-cache

- name: Generate Schedule
run: cargo run -p mdbook-course --bin course-schedule > course-schedule.md

- name: Comment PR
uses: thollander/actions-comment-pull-request@v2
with:
filePath: course-schedule.md
comment_tag: course-schedule
10 changes: 10 additions & 0 deletions mdbook-course/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ below:

```yaml
minutes: NNN
target_minutes: NNN
course: COURSE NAME
session: SESSION NAME
```
Expand Down Expand Up @@ -59,6 +60,9 @@ require in the `minutes` field. This information is summed, with breaks
automatically added between segments, to give time estimates for segments,
sessions, and courses.

Each session should list a `target_minutes` that is the target duration of the
session.

## Directives

Within the course material, the following directives can be used:
Expand All @@ -73,3 +77,9 @@ Within the course material, the following directives can be used:
These will be replaced with a markdown outline of the current segment, session,
or course. The last directive can refer to another course by name and is used in
the "Running the Course" section.

# Course-Schedule Comments

The `course-schedule` binary generates Markdown output that is included in a
GitHub pull request comment, based on the information provided in the above
format.
61 changes: 61 additions & 0 deletions mdbook-course/src/bin/course-schedule.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use mdbook::MDBook;
use mdbook_course::course::{Course, Courses};
use mdbook_course::markdown::duration;

fn main() {
pretty_env_logger::init();
let root_dir = ".";
let mdbook = MDBook::load(root_dir).expect("Unable to load the book");
let (courses, _) = Courses::extract_structure(mdbook.book)
.expect("Unable to extract course structure");

println!("## Course Schedule");
println!("With this pull request applied, the course schedule is as follows:");
for course in &courses {
print_summary(course);
}
}

fn timediff(actual: u64, target: u64, slop: u64) -> String {
if actual > target + slop {
format!(
"{} (\u{23f0} *{} too long*)",
duration(actual),
duration(actual - target),
)
} else if actual < target - slop {
format!("{}: ({} short)", duration(actual), duration(target - actual),)
} else {
format!("{}", duration(actual))
}
}

fn print_summary(course: &Course) {
if course.target_minutes() == 0 {
return;
}
println!("### {}", course.name);
println!("_{}_", timediff(course.minutes(), course.target_minutes(), 15));

for session in course {
println!(
"* {} - _{}_",
session.name,
timediff(session.minutes(), session.target_minutes(), 5)
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.

mod course;
mod frontmatter;
mod markdown;
mod replacements;
mod timing_info;

use crate::course::{Course, Courses};
use crate::markdown::duration;
use clap::{Arg, Command};
use mdbook::book::BookItem;
use mdbook::preprocess::CmdPreprocessor;
use mdbook_course::course::Courses;
use mdbook_course::{replacements, timing_info};
use std::io::{stdin, stdout};
use std::process;

Expand All @@ -46,47 +40,9 @@ fn main() {
}
}

fn timediff(actual: u64, target: u64) -> String {
if actual > target {
format!(
"{}: {} OVER TARGET {}",
duration(actual),
duration(actual - target),
duration(target)
)
} else if actual < target {
format!(
"{}: {} shorter than target {}",
duration(actual),
duration(target - actual),
duration(target)
)
} else {
format!("{}: right on time", duration(actual))
}
}

fn print_summary(fundamentals: &Course) {
eprintln!("Fundamentals: {}", timediff(fundamentals.minutes(), 8 * 3 * 60));

eprintln!("Sessions:");
for session in fundamentals {
eprintln!(" {}: {}", session.name, timediff(session.minutes(), 3 * 60));
for segment in session {
eprintln!(" {}: {}", segment.name, duration(segment.minutes()));
}
}
}

fn preprocess() -> anyhow::Result<()> {
let (ctx, book) = CmdPreprocessor::parse_input(stdin())?;
let (_, book) = CmdPreprocessor::parse_input(stdin())?;
let (courses, mut book) = Courses::extract_structure(book)?;
let verbose = ctx
.config
.get_preprocessor("course")
.and_then(|t| t.get("verbose"))
.and_then(|v| v.as_bool())
.unwrap_or_default();

book.for_each_mut(|chapter| {
if let BookItem::Chapter(chapter) = chapter {
Expand All @@ -108,15 +64,6 @@ fn preprocess() -> anyhow::Result<()> {
}
});

// Print a summary of times for the "Fundamentals" course.
// Translations with a POT-Creation-Date before 2023-11-29 (when
// we merged #1073) will have no frontmatter.
if verbose {
if let Some(fundamentals) = courses.find_course("Fundamentals") {
print_summary(fundamentals);
}
}

serde_json::to_writer(stdout(), &book)?;
Ok(())
}
22 changes: 22 additions & 0 deletions mdbook-course/src/course.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ pub struct Course {
pub struct Session {
pub name: String,
pub segments: Vec<Segment>,
target_minutes: u64,
}

/// A Segment is a collection of slides with a related theme.
Expand Down Expand Up @@ -143,6 +144,7 @@ impl Courses {
{
let course = courses.course_mut(course_name);
let session = course.session_mut(session_name);
session.target_minutes += frontmatter.target_minutes.unwrap_or(0);
session.add_segment(frontmatter, chapter)?;
}
}
Expand Down Expand Up @@ -232,6 +234,15 @@ impl Course {
self.into_iter().map(|s| s.minutes()).sum()
}

/// Return the target duration of this course, as the sum of all segment
/// target durations.
///
/// This includes breaks between segments, but does not count time between
/// sessions.
pub fn target_minutes(&self) -> u64 {
self.into_iter().map(|s| s.target_minutes()).sum()
}

/// Generate a Markdown schedule for this course, for placement at the given
/// path.
pub fn schedule(&self, at_source_path: impl AsRef<Path>) -> String {
Expand Down Expand Up @@ -333,6 +344,17 @@ impl Session {
* BREAK_DURATION;
instructional_time + breaks
}

/// Return the target duration of this session.
///
/// This includes breaks between segments.
pub fn target_minutes(&self) -> u64 {
if self.target_minutes > 0 {
self.target_minutes
} else {
self.minutes()
}
}
}

impl<'a> IntoIterator for &'a Session {
Expand Down
1 change: 1 addition & 0 deletions mdbook-course/src/frontmatter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ use serde::Deserialize;
#[derive(Deserialize, Debug, Default)]
pub struct Frontmatter {
pub minutes: Option<u64>,
pub target_minutes: Option<u64>,
pub course: Option<String>,
pub session: Option<String>,
}
Expand Down
19 changes: 19 additions & 0 deletions mdbook-course/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

pub mod course;
pub mod frontmatter;
pub mod markdown;
pub mod replacements;
pub mod timing_info;
1 change: 1 addition & 0 deletions src/welcome-day-1-afternoon.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
session: Day 1 Afternoon
target_minutes: 180
---

# Welcome Back
Expand Down
1 change: 1 addition & 0 deletions src/welcome-day-1.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
minutes: 5
course: Fundamentals
session: Day 1 Morning
target_minutes: 180
---

# Welcome to Day 1
Expand Down
1 change: 1 addition & 0 deletions src/welcome-day-2-afternoon.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
session: Day 2 Afternoon
target_minutes: 180
---

# Welcome Back
Expand Down
1 change: 1 addition & 0 deletions src/welcome-day-2.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
minutes: 3
course: Fundamentals
session: Day 2 Morning
target_minutes: 180
---

# Welcome to Day 2
Expand Down
1 change: 1 addition & 0 deletions src/welcome-day-3-afternoon.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
session: Day 3 Afternoon
target_minutes: 180
---

# Welcome Back
Expand Down
1 change: 1 addition & 0 deletions src/welcome-day-3.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
minutes: 3
course: Fundamentals
session: Day 3 Morning
target_minutes: 180
---

# Welcome to Day 3
Expand Down
1 change: 1 addition & 0 deletions src/welcome-day-4-afternoon.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
session: Day 4 Afternoon
target_minutes: 180
---

# Welcome Back
Expand Down
1 change: 1 addition & 0 deletions src/welcome-day-4.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
minutes: 3
course: Fundamentals
session: Day 4 Morning
target_minutes: 180
---

# Welcome to Day 4
Expand Down

0 comments on commit 4c0833a

Please sign in to comment.