TL;DR
- To create a GitHub Actions job summary from a Deno script, the script must run with
--allow-env,--allow-read,--allow-sysand--allow-writepermissions.- Use Deno
--no-promptto turn permission prompts into immediate errors when running interactively.
GitHub Actions has a cool feature to create job summaries. This lets you add custom Markdown and/or HTML to a job and show it on the summary page of a workflow run (e.g. to create custom reports).
How to create a job summary
The simplest way is to write to the $GITHUB_STEP_SUMMARY environment variable:
steps: - name: Create job summary run: | echo "My report" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "This is a Markdown list:" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "- One" >> $GITHUB_STEP_SUMMARY echo "- Two" >> $GITHUB_STEP_SUMMARYBut there’s also the @actions/core toolkit that allows doing the above in JavaScript/TypeScript:
import * as core from "@actions/core"
await core.summary .addHeading("My report") .addEOL() .addRaw("This is a Markdown list:") .addEOL() .addList(["One", "Two"]) .write()The problem
I was using the @actions/core toolkit in a Deno script (executed in a workflow job) to create a job summary.
But my job would always fail.
For some reason, the Promise creating the job summary would never resolve:
error: Top-level await promise never resolvedThe code
import * as core from "npm:@actions/core"
export async function createJobSummary<T extends Record<string, any>>(items: T[], title: string) { let buff = core.summary.addHeading(title).addEOL() if (items.length < 1) { buff = buff.addRaw(`No data`).addEOL() } else { const columns = Object.keys(items[0]) const header = columns.map((col) => { return { data: col.toUpperCase(), header: true, } }) const rows = items.map((item) => { return columns.map((col) => String(item[col])) }) buff = buff.addTable([header, ...rows]) } return buff.write()}Why it fails
Creating a job summary essentially writes to a file. This becomes obvious when checking the toolkit’s write code.
But by default, Deno doesn’t have access to sensitive APIs. For example, it does not have permission to access the file system.
The error hid the underlying cause: the toolkit could not access the summary file without the required permissions.
The fix
Job summaries write to a file, so the Deno script must run with --allow-write permission.
But interestingly this didn’t fix the issue: my job would still fail with the same error.
This left me puzzled.
So after a while I just tried running the script with --allow-all permission, and it worked.
Turns out that the toolkit also:
- Reads an environment variable and checks whether the summary file is readable and writable.
- Uses Node compatibility APIs that access system information.
And Deno requires explicit permission to:
So the script must be run with --allow-env, --allow-read, --allow-sys and --allow-write permissions to create a job summary.
Improving permission errors
Debugging permission errors as described above isn’t great. Can we make the script fail (faster) with a better error?
Deno prompts for missing permissions when running interactively.
We can disable this behavior with --no-prompt:
Prompts are not shown if stdout/stderr are not a TTY, or when the
--no-promptflag is passed to thedenocommand.
When this flag is used, Deno returns a clearer permission error (and fails faster). Prompts are already disabled when stdout and stderr are not attached to a TTY, as is normally the case in GitHub Actions:
error: Uncaught (in promise) NotCapable: Requires sys access to "uid", run again with the --allow-sys flagExample run task
{ "tasks": { "run": "deno run --allow-env --allow-read --allow-sys --allow-write --no-prompt mod.ts" }, "imports": { "@actions/core": "npm:@actions/core@^1.11.1" }, "fmt": { "semiColons": false }}