Skip to content
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
12 changes: 12 additions & 0 deletions .github/dependabot.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: 2
updates:
- package-ecosystem: github-actions
directory: '/'
schedule:
interval: weekly
cooldown:
default-days: 14
groups:
ci:
patterns:
- '*'
105 changes: 105 additions & 0 deletions .github/workflows/format-lint-comment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# This takes the results of "format-lint.yaml" and creates a comment on the PR to explain
# them.
#
# See "format-lint.yaml" for more details on why this workflow split is needed.

name: PR comment

on:
workflow_run:
workflows: [Format and lint]
types: [completed]

jobs:
comment:
runs-on: ubuntu-latest
if: github.event.workflow_run.event == 'pull_request'
permissions:
pull-requests: write
actions: read
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
Comment thread
etiennebacher marked this conversation as resolved.

- name: Download panache results
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
continue-on-error: true
with:
name: panache-results
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
path: /tmp/panache

- name: Download jarl results
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
continue-on-error: true
with:
name: jarl-results
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
path: /tmp/jarl

- name: Read results
id: results
run: |
echo "pr_number=$(cat /tmp/panache/pr-number.txt 2>/dev/null)" >> "$GITHUB_OUTPUT"
echo "panache_result=$(cat /tmp/panache/panache-result.txt 2>/dev/null)" >> "$GITHUB_OUTPUT"
echo "jarl_result=$(cat /tmp/jarl/jarl-result.txt 2>/dev/null)" >> "$GITHUB_OUTPUT"

# After reading the results of Panache and Jarl, we have several possible outcomes:
# - both pass: if we had created a comment before because at least one of them was
# failing, then we update this comment. Otherwise, we don't open a comment just to
# say that this is passing.
# - Jarl, Panache, or both fail: we create a comment with a custom template to explain
# what failed and how to fix it locally. This should help new contributors when
# they see that CI for linting and formatting fails.

# This is needed to know if we need to create or update a comment.
- name: Find existing comment
if: steps.results.outputs.pr_number != ''
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
id: find-comment
with:
issue-number: ${{ steps.results.outputs.pr_number }}
comment-author: 'github-actions[bot]'
body-includes: '<!-- format-lint-check -->'

# Build the comment body depending on Panache and Jarl results. For the case where both
# fail, we concatenate the two single-failure templates.
#
# Note that the case with both succeeding produces a template only if we have
# "$COMMENT_ID", i.e. if we already created a comment before. If we didn't, then
# `steps.build.outputs.ready` is false and the last step isn't triggered.
- name: Build comment
id: build
if: steps.results.outputs.pr_number != ''
env:
PANACHE_RESULT: ${{ steps.results.outputs.panache_result }}
JARL_RESULT: ${{ steps.results.outputs.jarl_result }}
COMMENT_ID: ${{ steps.find-comment.outputs.comment-id }}
# The "echo" between the two "cat" ensures a new line between the two templates.
run: |
if [[ "$PANACHE_RESULT" == "failure" && "$JARL_RESULT" == "failure" ]]; then
{
cat .github/workflows/templates/panache-only.md
echo
echo "---"
echo
cat .github/workflows/templates/jarl-only.md
} > /tmp/comment.md
elif [[ "$PANACHE_RESULT" == "failure" ]]; then
cp .github/workflows/templates/panache-only.md /tmp/comment.md
elif [[ "$JARL_RESULT" == "failure" ]]; then
cp .github/workflows/templates/jarl-only.md /tmp/comment.md
elif [[ -n "$COMMENT_ID" ]]; then
cp .github/workflows/templates/all-pass.md /tmp/comment.md
fi
[[ -f /tmp/comment.md ]] && echo "ready=true" >> "$GITHUB_OUTPUT" || echo "ready=false" >> "$GITHUB_OUTPUT"

- name: Create or update comment
if: steps.build.outputs.ready == 'true' && steps.results.outputs.pr_number != ''
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
with:
comment-id: ${{ steps.find-comment.outputs.comment-id }}
issue-number: ${{ steps.results.outputs.pr_number }}
body-path: /tmp/comment.md
edit-mode: replace
68 changes: 68 additions & 0 deletions .github/workflows/format-lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# This workflow runs Panache for formatting and Jarl for linting. It stores artifacts about the
# outcome of each tool and the PR number. Those artifacts are then used in the workflow
# "format-lint-comment.yaml" to post a comment so that it's clearer for external
# contributors.
#
# This could have been done in a single workflow with a "pull_request_target" trigger but
# this might have some security issues (this is unlikely in practice but still better to
# be ahead of it):
# https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/

name: Format and lint

on:
push:
branches: [main]
pull_request:
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true

jobs:
format-panache:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Run Panache
id: check
uses: jolars/panache-action@a1e4e68c74a41e6b9dade9c93095a262a23aa48c # v1
with:
lint: "false"
continue-on-error: true
- name: Save results
if: github.event_name == 'pull_request'
run: |
echo "${{ steps.check.outcome }}" > /tmp/panache-result.txt
echo "${{ github.event.pull_request.number }}" > /tmp/pr-number.txt
- name: Upload artifacts
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: panache-results
path: |
/tmp/panache-result.txt
/tmp/pr-number.txt
- if: steps.check.outcome == 'failure'
run: exit 1

lint-jarl:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
- name: Check
id: check
uses: etiennebacher/setup-jarl@eabf7e572f2991d165059007531b9bbe2987e39c # v0.1.1
continue-on-error: true
- name: Save result
if: github.event_name == 'pull_request'
run: echo "${{ steps.check.outcome }}" > /tmp/jarl-result.txt
- name: Upload artifact
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: jarl-results
path: /tmp/jarl-result.txt
- if: steps.check.outcome == 'failure'
run: exit 1
2 changes: 2 additions & 0 deletions .github/workflows/templates/all-pass.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!-- format-lint-check -->
:white_check_mark: All formatting and linting checks passed!
14 changes: 14 additions & 0 deletions .github/workflows/templates/jarl-only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!-- format-lint-check -->
:x: This Pull Request failed our automated **linting checks**. Please resolve these prior to requesting review. We use Jarl to automatically lint R code. Please [install it](https://jarl.etiennebacher.com/#installation) (you may need to close and reopen your IDE, e.g. RStudio or Positron, after that) and run the following command in the terminal (not the R console) to find linting issues:

```sh
jarl check .
```

Some of these issues might be automatically fixed with the following command:

```sh
jarl check . --fix
```

<sub>This comment will be automatically updated once linting checks pass.</sub>
8 changes: 8 additions & 0 deletions .github/workflows/templates/panache-only.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!-- format-lint-check -->
:x: This Pull Request failed our automated **formatting checks**. Please resolve these prior to requesting review. We use Panache to automatically format R code chunks in Quarto files. Please [install it](https://panache.bz/getting-started.html#installation) (you may need to close and reopen your IDE, e.g. RStudio or Positron, after that) and run the following command in the terminal (not the R console) to reformat the code:

```sh
panache format **/*.qmd
```

<sub>This comment will be automatically updated once formatting checks pass.</sub>
49 changes: 25 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ Directory of teaching modules built by the Palaeoverse team.
Every module should contain Quarto (.qmd) content that renders a long-form website page (for async learning) plus a reveal.js slide deck (for live teaching). There are many ways to accomplish this; we've outlined and templated the three strategies that we think work well below. Pick one of them and start from its template in
[`_templates`](_templates):

| | Strategy | Start from |
| --- | --- | --- |
| 1 | **One file, slide layout by filter.** A single `index.qmd` registers the [`web_and_slides.lua`](web_and_slides.lua) filter, which turns your prose into speaker notes and breaks the remaining content into slides for you (ideal if you are new to Quarto). | [`template_single_file.qmd`](_templates/template_single_file.qmd) |
| 2 | **One file, interleaved by hand.** A single `index.qmd` using plain Quarto conditional content (ideal if you are comfortable with Quarto syntax and don't have too many customizations). | [`template_single_file_interleaved.qmd`](_templates/template_single_file_interleaved.qmd) |
| 3 | **Two separate files.** A long-form document and a slide deck, maintained side by side (ideal if you really need customized content for both formats). | [`template_long_format.qmd`](_templates/template_long_format.qmd) + [`template_slides.qmd`](_templates/template_slides.qmd) |
| | Strategy | Start from |
| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| 1 | **One file, slide layout by filter.** A single `index.qmd` registers the [`web_and_slides.lua`](web_and_slides.lua) filter, which turns your prose into speaker notes and breaks the remaining content into slides for you (ideal if you are new to Quarto). | [`template_single_file.qmd`](_templates/template_single_file.qmd) |
| 2 | **One file, interleaved by hand.** A single `index.qmd` using plain Quarto conditional content (ideal if you are comfortable with Quarto syntax and don't have too many customizations). | [`template_single_file_interleaved.qmd`](_templates/template_single_file_interleaved.qmd) |
| 3 | **Two separate files.** A long-form document and a slide deck, maintained side by side (ideal if you really need customized content for both formats). | [`template_long_format.qmd`](_templates/template_long_format.qmd) + [`template_slides.qmd`](_templates/template_slides.qmd) |

Strategies 1 and 2 render one file to both formats, so Quarto links the page and
the deck for you under "Other Formats". Strategy 3 renders two independent
Expand All @@ -32,6 +32,7 @@ layout it applies). You can get there two ways:

- **Tag as you go.** Copy [`template_single_file.qmd`](_templates/template_single_file.qmd)
and write with the blocks from the start.

- **Write long-form first, then convert.** Draft the module as an ordinary
written tutorial (see [`template_single_file_draft.qmd`](_templates/template_single_file_draft.qmd) for an example)
and run `web_and_slides.r` over it once:
Expand All @@ -53,11 +54,11 @@ regenerate.
Once the filter is registered and copied into the module directory, you get the
following automatic behavior:

- slides close after each figure
- deeper headings become their own slides
- callouts keep their boxes (and collapsed callouts stay collapsed)
- content on long slides is shrunk as needed
- back-to-back code chunks are revealed one at a time
- slides close after each figure
- deeper headings become their own slides
- callouts keep their boxes (and collapsed callouts stay collapsed)
- content on long slides is shrunk as needed
- back-to-back code chunks are revealed one at a time

## 2. One file, interleaved by hand

Expand All @@ -67,11 +68,11 @@ the way._
Same single-file idea, but using Quarto's own conditional content instead of our
classes, so there is nothing to generate and nothing to register:

| Class | Behavior |
| --- | --- |
| `::: {.notes}` | Quarto renders it as prose on the page and as speaker notes in the slide deck |
| `::: {.content-visible when-format="revealjs"}` | Content is only rendered on the slides, not on the page |
| `::: {.content-hidden when-format="revealjs"}` | Content is only rendered on the page, not on the slides |
| Class | Behavior |
| ----------------------------------------------- | ----------------------------------------------------------------------------- |
| `::: {.notes}` | Quarto renders it as prose on the page and as speaker notes in the slide deck |
| `::: {.content-visible when-format="revealjs"}` | Content is only rendered on the slides, not on the page |
| `::: {.content-hidden when-format="revealjs"}` | Content is only rendered on the page, not on the slides |

In exchange for the control you take on the work the
filter was doing: slides do not close after a figure, deeper headings do not
Expand Down Expand Up @@ -126,10 +127,10 @@ This section covers **strategy 1** in detail: the front matter is much the same
whichever strategy you pick, but the classes and the automation below come from
the two files at the root of this repo.

| File | Role |
| --- | --- |
| `web_and_slides.lua` | Pandoc/Quarto filter, applied at render time. Decides what appears on the website, what appears on the slides, and how the slides are broken up. |
| `web_and_slides.r` | One-shot authoring helper. Converts a finished long-form document into the tagged form the filter expects. |
| File | Role |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `web_and_slides.lua` | Pandoc/Quarto filter, applied at render time. Decides what appears on the website, what appears on the slides, and how the slides are broken up. |
| `web_and_slides.r` | One-shot authoring helper. Converts a finished long-form document into the tagged form the filter expects. |

## Front matter

Expand Down Expand Up @@ -168,11 +169,11 @@ everything below; `web_and_slides.r` adds it for you, or you can copy it.

Three fenced-div (`:::`) classes control where content lands:

| Class | Website Tutorial | Slides |
| --- | --- | --- |
| `.narration` | normal prose | speaker notes |
| `.slides-only` | dropped | shown on the slide |
| `.html-only` | shown | dropped |
| Class | Website Tutorial | Slides |
| -------------- | ---------------- | ------------------ |
| `.narration` | normal prose | speaker notes |
| `.slides-only` | dropped | shown on the slide |
| `.html-only` | shown | dropped |

Anything not wrapped in one of these appears in both outputs. So the usual shape
of a module is: headings and code chunks shared by both outputs, the connecting
Expand Down
1 change: 1 addition & 0 deletions _templates/template_long_format.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Same code chunk but not executed:

```{r}
#| eval: false

plot(mtcars)
```

Expand Down
8 changes: 5 additions & 3 deletions _templates/template_single_file.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ the page and separate notes on the slide.
:::

::: {.slides-only}
- a bullet summary for the slide
- which would be redundant on the page
- a bullet summary for the slide
- which would be redundant on the page
:::

::: {.html-only}
Expand Down Expand Up @@ -112,13 +112,15 @@ A callout gets a slide of its own. The content is un-boxed and the slide is titl

::: {.callout-note}
## A note

This box is dropped on the slide, which lets a figure inside it stretch to fit.
:::

::: {.callout-caution collapse="true"}
## An exercise solution

A *collapsed* callout is the exception: it keeps its box. Since clicking on
slides can be awkward, the collapsed content is held back as a fragment and
slides can be awkward, the collapsed content is held back as a fragment and
revealed when you advance. On the website it stays a click-to-open box.
:::

Expand Down
14 changes: 8 additions & 6 deletions _templates/template_single_file_draft.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,20 @@ plot(mtcars$hp, mtcars$mpg)

Lists, tables, images and blockquotes are left alone by the script so they appear in both outputs as-is:

- a list item
- another list item
- a list item
- another list item

A callout is left alone too, and gets a slide of its own:

::: {.callout-note}
## A note

Give a callout a heading like this and the heading becomes the slide title.
:::

::: {.callout-caution collapse="true"}
## An exercise solution

A collapsed callout stays collapsed: on the website it is a click-to-open box, and
on the slides it keeps its box with the content hidden until you advance.
:::
Expand All @@ -70,10 +72,10 @@ on the slides it keeps its box with the content hidden until you advance.

Once you have run the script, look through the output for:

1. headings the script flagged as awkward for slides,
2. places where the two outputs should differ: add `::: {.slides-only}` or
`::: {.html-only}` blocks by hand,
3. narration blocks that should be split or merged.
1. headings the script flagged as awkward for slides,
2. places where the two outputs should differ: add `::: {.slides-only}` or
`::: {.html-only}` blocks by hand,
3. narration blocks that should be split or merged.

From then on, edit the converted file. Keep this draft only if you would rather
keep iterating on the prose and re-generate.
Loading
Loading