TL;DR

  • To create a GitHub Actions job summary from a Deno script, the script must run with --allow-env, --allow-read, --allow-sys and --allow-write permissions.
  • Use Deno --no-prompt to 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_SUMMARY

But 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:

Terminal window
error: Top-level await promise never resolved

The code

mod.ts
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:

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-prompt flag is passed to the deno command.

https://docs.deno.com/runtime/fundamentals/security/#permissions

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:

Terminal window
error: Uncaught (in promise) NotCapable: Requires sys access to "uid", run again with the --allow-sys flag

Example run task

deno.json
{
"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
}
}

Resources