Date posted: 08 Jun 2023, 4 minutes to read

Writing to the $GITHUB_STEP_SUMMARY with the core npm package

Every time I need to write to the GITHUB_STEP_SUMMARY in GitHub Actions from the actions/github-script action (or from Typescript), I need to search for the blogpost that announced it’s existence. So I’m writing this blogpost to make it easier for myself to find it a lot easier, including some working examples.

Photo of around 20 white puzzle pieces against a white background

Photo by Markus Winkler on Unsplash

The code for the summaries lives in the actions/core package on npm, but figuring out how to use it can be a bit hard. The only example I’ve seen is in the blogpost I mentioned.

  import * as core from '@actions/core' 
  await core.summary
  .addHeading('Test Results')
  .addCodeBlock(generateTestResults(), "js")
    [{data: 'File', header: true}, {data: 'Result', header: true}],
    ['foo.js', 'Pass ✅'],
    ['bar.js', 'Fail ❌'],
    ['test.js', 'Pass ✅']
  .addLink('View staging deployment!', '')

This does a lot of things at the same time, but we get the general idea that you can:

  • add headings
  • add code blocks
  • add tables
  • add links And at the end you need to write the summary itself, which will be added to the file in the GITHUB_STEP_SUMMARY environment variable.

Working with the table output

There are no methods to break the table into chunks, like:

  1. Add a header
  2. Add a row

The only method there is, is adding the table in one go, with each row as an array of objects, and some configuration in the first row as that will define if the cell is a header or not. So assuming you have an array of results that you want to show, you can convert that array with properties into an array of rows, with each property value being an item in the row array.

The interesting thing I ran into, is that the row cells must be a string. Sending in integers for example does not work. Take the following example:

await core.summary
                        [{data: 'Topic', header: true}, {data: 'Count', header: true}, {data: 'Public', header: true}],
                        ['foo.js' , "1", "2"],
                        ['bar.js' , '3', '4'],
                        ['test.js', 100, 200]

In this example, all rows will be added to the summary, and as long as the content is a valid string, it will be shown in the table as well. In this example, the values in the last row are integers, and they will be not visible in the table.

Screenshot of the table output, with the integer values missing in the last row

A full example of creating the header array with hardcoded cells, and then adding the rows from an array of objects can be seen below. Here I have an array stored as output in a previous step, so I read that file and map it (as string values!) to an array containing the rows. The next step is to join the two arrays (header + summary) and pass that to the addTable method.

- name: Show information in the GITHUB_STEP_SUMMARY
    uses: actions/github-script@v6
    summaryFile: $
    script: see below in other markup for better readability
        const fs = require('fs')
        const summary = fs.readFileSync(process.env.summaryFile, 'utf8')

        // make the heading array for the core.summary method
        const headingArray = [{data: 'Topic', header: true}, {data: 'Count', header: true}, {data: 'Public', header: true},{data: 'Internal', header: true},{data: 'Private', header: true}]
        // convert the summary array into an array that can be passed into the core.summary method
        const summaryArray = JSON.parse(summary).map(t => [, t.count.toString(), t.public.toString(), t.internal.toString(), t.private.toString()])

        // join the two arrays
        const tableArray = [headingArray, ...summaryArray]

        await core.summary
                .addHeading(`Topics used on repos in the [${}] organization`)

Writing raw text to the summary

If you want to add some lines of text to the summary with this, then let me save you some time on figuring this out (writing this for a friend 🙈):
You are writing the raw text as Markdown, which I often forget. That means that everything has a meaning, especially after a header!

Here is an example of some of my logging:. The end of lines are also needed!

    await core.summary.addHeading("Repo info")
                      .addRaw(`Total repos: ${repos.length}  `).addEOL()
                      .addRaw(`Large repos: ${largerRepoCount}  `).addEOL()
                      .addRaw(`Gitattributes: ${largerRepoHasGitAttributes}  `).addEOL()

Need to add a mermaid diagram?

Notice the quotes in the exampe below, that’s where I went wrong the first few times:

await core.summary.addHeading("Repo info")
                  .addRaw(`pie title Repo size overview`).addEOL()
                  .addRaw(`"Large (${largerRepoCount})": ${largerRepoCount}`).addEOL()
                  .addRaw(`"Small (${normalRepoCount)": ${normalRepoCount}`).addEOL()                          

Note: Keep in mind that the core.summary.write() method writes the entire buffer to the summary file, but does not clean that buffer. That means that if you use write(), then add more info and write() again, you will get the first buffer as well as the second buffer! I have not found a way to clean the buffer, so make sure you only call the write() only once!