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:
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
|
```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.
|
||||||
|
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
|
// 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(())
|
||||||
}
|
}
|
@ -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 {
|
||||||
|
@ -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
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
|
session: Day 1 Afternoon
|
||||||
|
target_minutes: 180
|
||||||
---
|
---
|
||||||
|
|
||||||
# Welcome Back
|
# Welcome Back
|
||||||
|
@ -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
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
session: Day 2 Afternoon
|
session: Day 2 Afternoon
|
||||||
|
target_minutes: 180
|
||||||
---
|
---
|
||||||
|
|
||||||
# Welcome Back
|
# Welcome Back
|
||||||
|
@ -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
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
session: Day 3 Afternoon
|
session: Day 3 Afternoon
|
||||||
|
target_minutes: 180
|
||||||
---
|
---
|
||||||
|
|
||||||
# Welcome Back
|
# Welcome Back
|
||||||
|
@ -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
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
---
|
---
|
||||||
session: Day 4 Afternoon
|
session: Day 4 Afternoon
|
||||||
|
target_minutes: 180
|
||||||
---
|
---
|
||||||
|
|
||||||
# Welcome Back
|
# Welcome Back
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user