1
0
Fork 0
toolkit/packages/core/src/summary.ts

364 lines
9.3 KiB
TypeScript
Raw Normal View History

2022-03-02 01:55:43 +00:00
import {EOL} from 'os'
import {constants, promises} from 'fs'
const {access, appendFile, writeFile} = promises
2022-02-23 23:09:05 +00:00
export const SUMMARY_ENV_VAR = 'GITHUB_STEP_SUMMARY'
export const SUMMARY_DOCS_URL =
2022-05-05 17:29:20 +00:00
'https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary'
export type SummaryTableRow = (SummaryTableCell | string)[]
2022-03-02 17:10:01 +00:00
export interface SummaryTableCell {
/**
* Cell content
*/
data: string
/**
* Render cell as header
* (optional) default: false
*/
header?: boolean
/**
* Number of columns the cell extends
* (optional) default: '1'
*/
colspan?: string
/**
* Number of rows the cell extends
* (optional) default: '1'
*/
rowspan?: string
}
2022-02-23 23:09:05 +00:00
export interface SummaryImageOptions {
/**
* The width of the image in pixels. Must be an integer without a unit.
* (optional)
*/
width?: string
/**
* The height of the image in pixels. Must be an integer without a unit.
* (optional)
*/
height?: string
}
2022-03-08 21:37:20 +00:00
export interface SummaryWriteOptions {
/**
* Replace all existing content in summary file with buffer contents
* (optional) default: false
*/
overwrite?: boolean
}
2022-05-05 17:29:20 +00:00
class Summary {
private _buffer: string
private _filePath?: string
2022-02-23 23:09:05 +00:00
constructor() {
this._buffer = ''
2022-02-23 23:09:05 +00:00
}
/**
* Finds the summary file path from the environment, rejects if env var is not found or file does not exist
* Also checks r/w permissions.
2022-02-23 23:09:05 +00:00
*
* @returns step summary file path
*/
private async filePath(): Promise<string> {
if (this._filePath) {
return this._filePath
}
const pathFromEnv = process.env[SUMMARY_ENV_VAR]
if (!pathFromEnv) {
2022-02-23 23:09:05 +00:00
throw new Error(
2022-05-05 17:29:20 +00:00
`Unable to find environment variable for $${SUMMARY_ENV_VAR}. Check if your runtime environment supports job summaries.`
2022-02-23 23:09:05 +00:00
)
}
try {
await access(pathFromEnv, constants.R_OK | constants.W_OK)
} catch {
throw new Error(
`Unable to access summary file: '${pathFromEnv}'. Check if the file has correct read/write permissions.`
)
}
this._filePath = pathFromEnv
return this._filePath
}
2022-02-23 23:09:05 +00:00
/**
* Wraps content in an HTML tag, adding any HTML attributes
*
* @param {string} tag HTML tag to wrap
* @param {string | null} content content within the tag
* @param {[attribute: string]: string} attrs key-value list of HTML attributes to add
*
* @returns {string} content wrapped in HTML element
*/
private wrap(
tag: string,
content: string | null,
attrs: {[attribute: string]: string} = {}
): string {
const htmlAttrs = Object.entries(attrs)
.map(([key, value]) => ` ${key}="${value}"`)
.join('')
if (!content) {
return `<${tag}${htmlAttrs}>`
}
return `<${tag}${htmlAttrs}>${content}</${tag}>`
}
/**
* Writes text in the buffer to the summary buffer file and empties buffer. Will append by default.
*
* @param {SummaryWriteOptions} [options] (optional) options for write operation
2022-02-23 23:09:05 +00:00
*
2022-05-05 17:29:20 +00:00
* @returns {Promise<Summary>} summary instance
2022-02-23 23:09:05 +00:00
*/
2022-05-05 17:29:20 +00:00
async write(options?: SummaryWriteOptions): Promise<Summary> {
2022-03-08 21:37:20 +00:00
const overwrite = !!options?.overwrite
2022-02-23 23:09:05 +00:00
const filePath = await this.filePath()
const writeFunc = overwrite ? writeFile : appendFile
await writeFunc(filePath, this._buffer, {encoding: 'utf8'})
return this.emptyBuffer()
}
/**
* Clears the summary buffer and wipes the summary file
*
2022-05-05 17:29:20 +00:00
* @returns {Summary} summary instance
*/
2022-05-05 17:29:20 +00:00
async clear(): Promise<Summary> {
2022-03-08 21:37:20 +00:00
return this.emptyBuffer().write({overwrite: true})
}
/**
* Returns the current summary buffer as a string
*
* @returns {string} string of summary buffer
*/
stringify(): string {
return this._buffer
}
2022-02-23 23:09:05 +00:00
/**
* If the summary buffer is empty
*
* @returns {boolen} true if the buffer is empty
*/
isEmptyBuffer(): boolean {
return this._buffer.length === 0
2022-02-23 23:09:05 +00:00
}
/**
* Resets the summary buffer without writing to summary file
2022-02-23 23:09:05 +00:00
*
2022-05-05 17:29:20 +00:00
* @returns {Summary} summary instance
2022-02-23 23:09:05 +00:00
*/
2022-05-05 17:29:20 +00:00
emptyBuffer(): Summary {
this._buffer = ''
2022-02-23 23:09:05 +00:00
return this
}
/**
2022-03-02 01:55:43 +00:00
* Adds raw text to the summary buffer
*
* @param {string} text content to add
2022-03-03 04:49:17 +00:00
* @param {boolean} [addEOL=false] (optional) append an EOL to the raw text (default: false)
2022-02-23 23:09:05 +00:00
*
2022-05-05 17:29:20 +00:00
* @returns {Summary} summary instance
2022-02-23 23:09:05 +00:00
*/
2022-05-05 17:29:20 +00:00
addRaw(text: string, addEOL = false): Summary {
this._buffer += text
2022-03-03 04:49:17 +00:00
return addEOL ? this.addEOL() : this
2022-02-23 23:09:05 +00:00
}
/**
2022-03-02 01:55:43 +00:00
* Adds the operating system-specific end-of-line marker to the buffer
*
2022-05-05 17:29:20 +00:00
* @returns {Summary} summary instance
2022-02-23 23:09:05 +00:00
*/
2022-05-05 17:29:20 +00:00
addEOL(): Summary {
2022-03-03 04:49:17 +00:00
return this.addRaw(EOL)
2022-02-23 23:09:05 +00:00
}
/**
* Adds an HTML codeblock to the summary buffer
2022-02-23 23:09:05 +00:00
*
* @param {string} code content to render within fenced code block
* @param {string} lang (optional) language to syntax highlight code
2022-02-23 23:09:05 +00:00
*
2022-05-05 17:29:20 +00:00
* @returns {Summary} summary instance
2022-02-23 23:09:05 +00:00
*/
2022-05-05 17:29:20 +00:00
addCodeBlock(code: string, lang?: string): Summary {
const attrs = {
...(lang && {lang})
}
const element = this.wrap('pre', this.wrap('code', code), attrs)
2022-03-03 04:49:17 +00:00
return this.addRaw(element).addEOL()
2022-02-23 23:09:05 +00:00
}
/**
* Adds an HTML list to the summary buffer
2022-02-23 23:09:05 +00:00
*
* @param {string[]} items list of items to render
2022-03-03 04:49:17 +00:00
* @param {boolean} [ordered=false] (optional) if the rendered list should be ordered or not (default: false)
2022-02-23 23:09:05 +00:00
*
2022-05-05 17:29:20 +00:00
* @returns {Summary} summary instance
2022-02-23 23:09:05 +00:00
*/
2022-05-05 17:29:20 +00:00
addList(items: string[], ordered = false): Summary {
const tag = ordered ? 'ol' : 'ul'
const listItems = items.map(item => this.wrap('li', item)).join('')
const element = this.wrap(tag, listItems)
2022-03-03 04:49:17 +00:00
return this.addRaw(element).addEOL()
2022-02-23 23:09:05 +00:00
}
/**
* Adds an HTML table to the summary buffer
2022-02-23 23:09:05 +00:00
*
* @param {SummaryTableCell[]} rows table rows
2022-02-23 23:09:05 +00:00
*
2022-05-05 17:29:20 +00:00
* @returns {Summary} summary instance
2022-02-23 23:09:05 +00:00
*/
2022-05-05 17:29:20 +00:00
addTable(rows: SummaryTableRow[]): Summary {
const tableBody = rows
2022-02-23 23:09:05 +00:00
.map(row => {
const cells = row
.map(cell => {
if (typeof cell === 'string') {
return this.wrap('td', cell)
}
const {header, data, colspan, rowspan} = cell
const tag = header ? 'th' : 'td'
const attrs = {
...(colspan && {colspan}),
...(rowspan && {rowspan})
}
return this.wrap(tag, data, attrs)
})
.join('')
return this.wrap('tr', cells)
2022-02-23 23:09:05 +00:00
})
.join('')
const element = this.wrap('table', tableBody)
2022-03-03 04:49:17 +00:00
return this.addRaw(element).addEOL()
2022-02-23 23:09:05 +00:00
}
/**
* Adds a collapsable HTML details element to the summary buffer
2022-02-23 23:09:05 +00:00
*
* @param {string} label text for the closed state
* @param {string} content collapsable content
*
2022-05-05 17:29:20 +00:00
* @returns {Summary} summary instance
2022-02-23 23:09:05 +00:00
*/
2022-05-05 17:29:20 +00:00
addDetails(label: string, content: string): Summary {
const element = this.wrap('details', this.wrap('summary', label) + content)
2022-03-03 04:49:17 +00:00
return this.addRaw(element).addEOL()
2022-02-23 23:09:05 +00:00
}
/**
* Adds an HTML image tag to the summary buffer
*
* @param {string} src path to the image you to embed
* @param {string} alt text description of the image
2022-03-03 04:49:17 +00:00
* @param {SummaryImageOptions} options (optional) addition image attributes
*
2022-05-05 17:29:20 +00:00
* @returns {Summary} summary instance
*/
2022-05-05 17:29:20 +00:00
addImage(src: string, alt: string, options?: SummaryImageOptions): Summary {
const {width, height} = options || {}
const attrs = {
...(width && {width}),
...(height && {height})
}
const element = this.wrap('img', null, {src, alt, ...attrs})
2022-03-03 04:49:17 +00:00
return this.addRaw(element).addEOL()
}
/**
* Adds an HTML section heading element
*
* @param {string} text heading text
* @param {number | string} [level=1] (optional) the heading level, default: 1
*
2022-05-05 17:29:20 +00:00
* @returns {Summary} summary instance
*/
2022-05-05 17:29:20 +00:00
addHeading(text: string, level?: number | string): Summary {
const tag = `h${level}`
const allowedTag = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag)
? tag
: 'h1'
const element = this.wrap(allowedTag, text)
2022-03-03 04:49:17 +00:00
return this.addRaw(element).addEOL()
}
/**
* Adds an HTML thematic break (<hr>) to the summary buffer
*
2022-05-05 17:29:20 +00:00
* @returns {Summary} summary instance
*/
2022-05-05 17:29:20 +00:00
addSeparator(): Summary {
const element = this.wrap('hr', null)
2022-03-03 04:49:17 +00:00
return this.addRaw(element).addEOL()
}
/**
* Adds an HTML line break (<br>) to the summary buffer
*
2022-05-05 17:29:20 +00:00
* @returns {Summary} summary instance
*/
2022-05-05 17:29:20 +00:00
addBreak(): Summary {
const element = this.wrap('br', null)
2022-03-03 04:49:17 +00:00
return this.addRaw(element).addEOL()
}
/**
* Adds an HTML blockquote to the summary buffer
*
* @param {string} text quote text
* @param {string} cite (optional) citation url
*
2022-05-05 17:29:20 +00:00
* @returns {Summary} summary instance
*/
2022-05-05 17:29:20 +00:00
addQuote(text: string, cite?: string): Summary {
const attrs = {
...(cite && {cite})
}
const element = this.wrap('blockquote', text, attrs)
2022-03-03 04:49:17 +00:00
return this.addRaw(element).addEOL()
}
2022-03-02 17:10:01 +00:00
/**
* Adds an HTML anchor tag to the summary buffer
*
* @param {string} text link text/content
* @param {string} href hyperlink
*
2022-05-05 17:29:20 +00:00
* @returns {Summary} summary instance
2022-03-02 17:10:01 +00:00
*/
2022-05-05 17:29:20 +00:00
addLink(text: string, href: string): Summary {
2022-03-02 17:10:01 +00:00
const element = this.wrap('a', text, {href})
2022-03-03 04:49:17 +00:00
return this.addRaw(element).addEOL()
2022-03-02 17:10:01 +00:00
}
2022-02-23 23:09:05 +00:00
}
const _summary = new Summary()
/**
* @deprecated use `core.summary`
*/
export const markdownSummary = _summary
export const summary = _summary