mirror of
https://github.com/google/comprehensive-rust.git
synced 2025-03-31 01:38:16 +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:
parent
23ba2aa42f
commit
4c0833a22e
27
.github/workflows/course-schedule.yml
vendored
Normal file
27
.github/workflows/course-schedule.yml
vendored
Normal 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
|
@ -13,6 +13,7 @@ below:
|
||||
|
||||
```yaml
|
||||
minutes: NNN
|
||||
target_minutes: NNN
|
||||
course: COURSE 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,
|
||||
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:
|
||||
@ -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
mdbook-course/src/bin/course-schedule.rs
Normal file
61
mdbook-course/src/bin/course-schedule.rs
Normal 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)
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
||||
@ -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 {
|
||||
@ -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(())
|
||||
}
|
@ -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.
|
||||
@ -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)?;
|
||||
}
|
||||
}
|
||||
@ -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 {
|
||||
@ -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 {
|
||||
|
@ -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>,
|
||||
}
|
||||
|
19
mdbook-course/src/lib.rs
Normal file
19
mdbook-course/src/lib.rs
Normal 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;
|
@ -1,5 +1,6 @@
|
||||
---
|
||||
session: Day 1 Afternoon
|
||||
target_minutes: 180
|
||||
---
|
||||
|
||||
# Welcome Back
|
||||
|
@ -2,6 +2,7 @@
|
||||
minutes: 5
|
||||
course: Fundamentals
|
||||
session: Day 1 Morning
|
||||
target_minutes: 180
|
||||
---
|
||||
|
||||
# Welcome to Day 1
|
||||
|
@ -1,5 +1,6 @@
|
||||
---
|
||||
session: Day 2 Afternoon
|
||||
target_minutes: 180
|
||||
---
|
||||
|
||||
# Welcome Back
|
||||
|
@ -2,6 +2,7 @@
|
||||
minutes: 3
|
||||
course: Fundamentals
|
||||
session: Day 2 Morning
|
||||
target_minutes: 180
|
||||
---
|
||||
|
||||
# Welcome to Day 2
|
||||
|
@ -1,5 +1,6 @@
|
||||
---
|
||||
session: Day 3 Afternoon
|
||||
target_minutes: 180
|
||||
---
|
||||
|
||||
# Welcome Back
|
||||
|
@ -2,6 +2,7 @@
|
||||
minutes: 3
|
||||
course: Fundamentals
|
||||
session: Day 3 Morning
|
||||
target_minutes: 180
|
||||
---
|
||||
|
||||
# Welcome to Day 3
|
||||
|
@ -1,5 +1,6 @@
|
||||
---
|
||||
session: Day 4 Afternoon
|
||||
target_minutes: 180
|
||||
---
|
||||
|
||||
# Welcome Back
|
||||
|
@ -2,6 +2,7 @@
|
||||
minutes: 3
|
||||
course: Fundamentals
|
||||
session: Day 4 Morning
|
||||
target_minutes: 180
|
||||
---
|
||||
|
||||
# Welcome to Day 4
|
||||
|
Loading…
x
Reference in New Issue
Block a user