1
0
mirror of https://github.com/google/comprehensive-rust.git synced 2025-06-08 10:36:17 +02:00

Comment PRs with updated schedule information (#1576)

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 <mgeisler@google.com>
This commit is contained in:
Dustin J. Mitchell 2024-01-12 10:53:09 -05:00 committed by GitHub
parent 23ba2aa42f
commit 4c0833a22e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 151 additions and 56 deletions

27
.github/workflows/course-schedule.yml vendored Normal file
View File

@ -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

View File

@ -13,6 +13,7 @@ below:
```yaml ```yaml
minutes: NNN minutes: NNN
target_minutes: NNN
course: COURSE NAME course: COURSE NAME
session: SESSION NAME session: SESSION NAME
``` ```
@ -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, automatically added between segments, to give time estimates for segments,
sessions, and courses. sessions, and courses.
Each session should list a `target_minutes` that is the target duration of the
session.
## Directives ## Directives
Within the course material, the following directives can be used: Within the course material, the following directives can be used:
@ -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, 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 or course. The last directive can refer to another course by name and is used in
the "Running the Course" section. 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.

View File

@ -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)
);
}
}

View File

@ -12,17 +12,11 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // 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 clap::{Arg, Command};
use mdbook::book::BookItem; use mdbook::book::BookItem;
use mdbook::preprocess::CmdPreprocessor; use mdbook::preprocess::CmdPreprocessor;
use mdbook_course::course::Courses;
use mdbook_course::{replacements, timing_info};
use std::io::{stdin, stdout}; use std::io::{stdin, stdout};
use std::process; use std::process;
@ -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<()> { 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 (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| { book.for_each_mut(|chapter| {
if let BookItem::Chapter(chapter) = chapter { if let BookItem::Chapter(chapter) = chapter {
@ -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)?; serde_json::to_writer(stdout(), &book)?;
Ok(()) Ok(())
} }

View File

@ -72,6 +72,7 @@ pub struct Course {
pub struct Session { pub struct Session {
pub name: String, pub name: String,
pub segments: Vec<Segment>, pub segments: Vec<Segment>,
target_minutes: u64,
} }
/// A Segment is a collection of slides with a related theme. /// A Segment is a collection of slides with a related theme.
@ -143,6 +144,7 @@ impl Courses {
{ {
let course = courses.course_mut(course_name); let course = courses.course_mut(course_name);
let session = course.session_mut(session_name); let session = course.session_mut(session_name);
session.target_minutes += frontmatter.target_minutes.unwrap_or(0);
session.add_segment(frontmatter, chapter)?; session.add_segment(frontmatter, chapter)?;
} }
} }
@ -232,6 +234,15 @@ impl Course {
self.into_iter().map(|s| s.minutes()).sum() 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 /// Generate a Markdown schedule for this course, for placement at the given
/// path. /// path.
pub fn schedule(&self, at_source_path: impl AsRef<Path>) -> String { pub fn schedule(&self, at_source_path: impl AsRef<Path>) -> String {
@ -333,6 +344,17 @@ impl Session {
* BREAK_DURATION; * BREAK_DURATION;
instructional_time + breaks 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 { impl<'a> IntoIterator for &'a Session {

View File

@ -20,6 +20,7 @@ use serde::Deserialize;
#[derive(Deserialize, Debug, Default)] #[derive(Deserialize, Debug, Default)]
pub struct Frontmatter { pub struct Frontmatter {
pub minutes: Option<u64>, pub minutes: Option<u64>,
pub target_minutes: Option<u64>,
pub course: Option<String>, pub course: Option<String>,
pub session: Option<String>, pub session: Option<String>,
} }

19
mdbook-course/src/lib.rs Normal file
View File

@ -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;

View File

@ -1,5 +1,6 @@
--- ---
session: Day 1 Afternoon session: Day 1 Afternoon
target_minutes: 180
--- ---
# Welcome Back # Welcome Back

View File

@ -2,6 +2,7 @@
minutes: 5 minutes: 5
course: Fundamentals course: Fundamentals
session: Day 1 Morning session: Day 1 Morning
target_minutes: 180
--- ---
# Welcome to Day 1 # Welcome to Day 1

View File

@ -1,5 +1,6 @@
--- ---
session: Day 2 Afternoon session: Day 2 Afternoon
target_minutes: 180
--- ---
# Welcome Back # Welcome Back

View File

@ -2,6 +2,7 @@
minutes: 3 minutes: 3
course: Fundamentals course: Fundamentals
session: Day 2 Morning session: Day 2 Morning
target_minutes: 180
--- ---
# Welcome to Day 2 # Welcome to Day 2

View File

@ -1,5 +1,6 @@
--- ---
session: Day 3 Afternoon session: Day 3 Afternoon
target_minutes: 180
--- ---
# Welcome Back # Welcome Back

View File

@ -2,6 +2,7 @@
minutes: 3 minutes: 3
course: Fundamentals course: Fundamentals
session: Day 3 Morning session: Day 3 Morning
target_minutes: 180
--- ---
# Welcome to Day 3 # Welcome to Day 3

View File

@ -1,5 +1,6 @@
--- ---
session: Day 4 Afternoon session: Day 4 Afternoon
target_minutes: 180
--- ---
# Welcome Back # Welcome Back

View File

@ -2,6 +2,7 @@
minutes: 3 minutes: 3
course: Fundamentals course: Fundamentals
session: Day 4 Morning session: Day 4 Morning
target_minutes: 180
--- ---
# Welcome to Day 4 # Welcome to Day 4