A GitHub Action to generate PDF diffs for Typst documents.
Warning
This project is in early development. The API may change in future releases.
- Builds Typst documents from separate base and head revisions.
- Generates diff PDFs with
diff-pdf. - Uploads head PDFs and diff PDFs as workflow artifacts.
- Optionally posts a pull request (PR) comment.
The action requires a Linux runner with passwordless sudo apt-get, gh, and jq,
which the GitHub-hosted Ubuntu runner images provide.
The following workflow runs on PRs, compares the PR head against the merge-base (the commit where the PR branched off the base branch), uploads the generated PDFs, and posts a PR comment.
name: Generate Typst PDF diff
on:
pull_request:
types:
- opened
- synchronize
- reopened
jobs:
generate-typst-pdf-diff:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Generate Typst PDF diff
uses: conjikidow/typst-pdf-diff-action@v0.4.0
with:
target-files: main.typThe examples reference actions by tag for readability. For production workflows, consider pinning each action to a full-length commit SHA, as GitHub recommends. Releases of this action are immutable, so its own tags are already locked to a single commit.
If your Typst project uses submodules, set submodules: recursive and pass a
token that can access your repository and those submodules.
name: Generate Typst PDF diff
on:
pull_request:
types:
- opened
- synchronize
- reopened
env:
TYPST_TARGET_FILES: paper/main.typ slides/main.typ
jobs:
generate-typst-pdf-diff:
runs-on: ubuntu-latest
permissions: {}
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ vars.GH_APP_CLIENT_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
repositories: |
${{ github.event.repository.name }}
private-submodule
permission-contents: read
permission-pull-requests: write
- name: Generate Typst PDF diff
uses: conjikidow/typst-pdf-diff-action@v0.4.0
with:
target-files: ${{ env.TYPST_TARGET_FILES }}
github-token: ${{ steps.app-token.outputs.token }}
submodules: recursiveImportant
Without owner or repositories, the token reaches only the repository the workflow runs in,
so list every repository it has to read, this one included.
A GitHub App installation token is scoped to a single account,
so it cannot read submodules owned by another user or organization.
actions/checkout fails the whole job when any submodule cannot be fetched.
If your repository and its submodules span several owners, prepare the working trees yourself as described in
Bring Your Own Working Trees.
For non-PR events, set head-revision and base-revision explicitly if you do not want
to rely on the action's automatic revision resolution.
The token passed to github-token is only reached by the steps that talk to GitHub,
so what it needs depends on which of them run.
| Scope | Needed for |
|---|---|
contents: read |
Resolving the revisions and checking them out. Not needed when head-dir and base-dir are set. |
pull-requests: write |
Posting the PR comment and deleting earlier ones. Not needed when post-comment is false. |
The default ${{ github.token }} carries whatever the workflow grants it,
so grant those scopes in the job, as the first example above does.
The permissions: block does not reach a token you pass yourself.
A GitHub App installation token, as in the second example above, needs the same access granted to the app itself.
None of the steps that act on your repository use the job's own GITHUB_TOKEN,
which is why that example zeroes it with permissions: {}.
| Name | Description | Required | Default |
|---|---|---|---|
target-files |
Space-separated Typst entrypoint files to compile. | Yes | - |
typst-version |
Version of Typst to use. | No | 'latest' |
submodules |
Submodule mode passed to actions/checkout: false, true, or recursive. |
No | false |
head-revision |
Head revision to compare, as a branch, tag, or commit SHA. Defaults to the PR head SHA or github.sha. |
No | '' |
base-revision |
Base revision to compare, as a branch, tag, or commit SHA. Defaults to the merge-base of the PR or github.event.before. |
No | '' |
post-comment |
Whether to post a PR comment with the diff results. | No | true |
comment-mode |
Handling of earlier comments: replace deletes them, append keeps them. |
No | replace |
fail-on-comment-error |
Whether to fail the action when posting or deleting comments fails. | No | false |
upload-artifacts |
Whether to upload the head and diff PDFs as workflow artifacts. | No | true |
github-token |
Token used to authenticate with GitHub. Needs contents: read for the checkout and pull-requests: write for the comment. |
No | ${{ github.token }} |
target-files is interpreted as a space-separated list, for example main.typ appendix.typ.
Note
post-comment: true is intended for pull_request events.
On other events, the action skips the PR comment.
These are only needed when the action cannot check out the sources itself. See Bring Your Own Working Trees.
| Name | Description | Required | Default |
|---|---|---|---|
head-dir |
Existing working tree to build the head revision from. Requires base-dir. |
No | '' |
base-dir |
Existing working tree to build the base revision from. Requires head-dir. |
No | '' |
Both must be set together, and submodules, head-revision, and base-revision must stay at their defaults,
because the action checks out nothing in this mode.
Any other combination fails immediately.
| Name | Description |
|---|---|
has-diff |
true when at least one target file produces a diff PDF. |
head-artifact-url |
URL of the uploaded head PDF artifact. Empty when upload-artifacts is false. |
diff-artifact-url |
URL of the uploaded diff PDF artifact. Empty when no diff artifact was uploaded. |
Set head-dir and base-dir when the action cannot check out the sources itself,
for example when your repository and its submodules live under more than one owner and therefore need separate tokens.
The action then builds and compares the directories you provide, and checks out nothing.
Resolving the base revision is then up to you. For a PR, compare against the merge-base rather than the base branch tip, or the diff will also contain unrelated changes merged into the base branch in the meantime.
name: Generate Typst PDF diff
on:
pull_request:
types:
- opened
- synchronize
- reopened
jobs:
generate-typst-pdf-diff:
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Generate GitHub App token
id: app-token
uses: actions/create-github-app-token@v3
with:
client-id: ${{ vars.GH_APP_CLIENT_ID }}
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
owner: submodule-owner
repositories: private-submodule
permission-contents: read
- name: Resolve the merge-base
id: merge-base
env:
GH_TOKEN: ${{ github.token }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
sha=$(gh api "repos/${GITHUB_REPOSITORY}/compare/${PR_BASE_SHA}...${PR_HEAD_SHA}" --jq '.merge_base_commit.sha')
echo "sha=${sha}" >> "${GITHUB_OUTPUT}"
- name: Check out the head revision
uses: actions/checkout@v7
with:
path: head-src
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false
- name: Check out the base revision
uses: actions/checkout@v7
with:
path: base-src
ref: ${{ steps.merge-base.outputs.sha }}
persist-credentials: false
- name: Check out the required submodules
env:
GH_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
git config --global --add url."https://x-access-token:${GH_TOKEN}@github.com/".insteadOf git@github.com:
git config --global --add url."https://x-access-token:${GH_TOKEN}@github.com/".insteadOf https://github.com/
for dir in head-src base-src; do
git -C "${dir}" submodule update --init --depth 1 private-submodule
done
- name: Generate Typst PDF diff
uses: conjikidow/typst-pdf-diff-action@v0.4.0
with:
target-files: paper/main.typ
head-dir: head-src
base-dir: base-srcThat example uses a single token for its submodules. When your submodules span several owners, qualify each rewrite with the owner so that the longest match wins. Identical prefixes resolve to whichever token was configured first, which silently sends one owner's token to another owner.
git config --global --add \
url."https://x-access-token:${TOKEN_A}@github.com/owner-a/".insteadOf git@github.com:owner-a/
git config --global --add \
url."https://x-access-token:${TOKEN_B}@github.com/owner-b/".insteadOf git@github.com:owner-b/The action writes its build output to build/ in the workspace,
so do not point head-dir or base-dir inside that directory.
Caution
The action cannot verify that the directories you supply hold the revisions you intended.
- Resolves the base and head revisions, and checks them out into separate directories.
Both steps are skipped when
head-dirandbase-dirare provided. - Installs Typst and
diff-pdf. - Builds PDFs for all
target-filesfrom both revisions. - Generates diff PDFs with
diff-pdf. - Uploads head PDFs and diff PDFs as artifacts when enabled.
- Builds a Markdown summary and optionally posts a PR comment.
Licensed under either of MIT license or Apache License, Version 2.0 at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this software by you, as defined in the Apache-2.0 license, shall be dually licensed as above, without any additional terms or conditions.
Contributions, bug reports, and feedback are always welcome! Thank you for helping improve this project for everyone!