From 7f0723a953bbb29bdfb48748f752d49232f5ca58 Mon Sep 17 00:00:00 2001 From: Thomas Durand Date: Thu, 5 Mar 2026 01:52:45 +0100 Subject: [PATCH 1/2] feat: added a slug-prefix parameter for link anchors Motivation: when using a matrix job, or more than one kind of tests in a same workflow, we can end up with multiple summaries at the same time. This will lead to multiple anchors and html id that will no longer be unique. This prefix option allow to disambiguate those anchors ; and keep them functional. --- README.md | 4 ++++ __tests__/utils/slugger.test.ts | 28 ++++++++++++++++++++++++++++ action.yml | 4 ++++ dist/index.js | 8 ++++++-- src/main.ts | 5 ++++- src/report/get-report.ts | 2 ++ src/utils/slugger.ts | 2 +- 7 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 __tests__/utils/slugger.test.ts diff --git a/README.md b/README.md index bdced5c..bc28d53 100644 --- a/README.md +++ b/README.md @@ -162,6 +162,10 @@ jobs: # Allows you to generate reports for Actions Summary # https://github.blog/2022-05-09-supercharging-github-actions-with-job-summaries/ use-actions-summary: 'true' + + # Prefix used when generating report anchor slugs. + # Useful to avoid collisions when multiple reports are rendered together. + slug-prefix: '' # Optionally specify a title (Heading level 1) for the report. Leading and trailing whitespace are ignored. # This is useful for separating your test report from other sections in the build summary. diff --git a/__tests__/utils/slugger.test.ts b/__tests__/utils/slugger.test.ts new file mode 100644 index 0000000..e2ad534 --- /dev/null +++ b/__tests__/utils/slugger.test.ts @@ -0,0 +1,28 @@ +import {DEFAULT_OPTIONS} from '../../src/report/get-report.js' +import {slug} from '../../src/utils/slugger.js' + +describe('slugger', () => { + it('adds prefix from report options to generated slug', () => { + const result = slug('r0s1', { + ...DEFAULT_OPTIONS, + slugPrefix: 'prefix-' + }) + + expect(result).toEqual({ + id: 'user-content-prefix-r0s1', + link: '#user-content-prefix-r0s1' + }) + }) + + it('sanitizes custom prefix using existing slug normalization', () => { + const result = slug('r0', { + ...DEFAULT_OPTIONS, + slugPrefix: ' my /custom_prefix?.' + }) + + expect(result).toEqual({ + id: 'user-content-my-customprefix-r0', + link: '#user-content-my-customprefix-r0' + }) + }) +}) diff --git a/action.yml b/action.yml index 8dbc85c..f66ac45 100644 --- a/action.yml +++ b/action.yml @@ -88,6 +88,10 @@ inputs: https://github.com/orgs/github/teams/engineering/discussions/871 default: 'true' required: false + slug-prefix: + description: Prefix used when generating report anchor slugs + required: false + default: '' badge-title: description: Customize badge title required: false diff --git a/dist/index.js b/dist/index.js index 171caa3..f49930a 100644 --- a/dist/index.js +++ b/dist/index.js @@ -55869,7 +55869,7 @@ function getExceptionSource(stackTrace, trackedFiles, getRelativePath) { ;// CONCATENATED MODULE: ./lib/utils/slugger.js function slug(name, options) { - const slugId = name + const slugId = `${options.slugPrefix}${name}` .trim() .replace(/_/g, '') .replace(/[./\\]/g, '-') @@ -55891,6 +55891,7 @@ const MAX_ACTIONS_SUMMARY_LENGTH = 1048576; const DEFAULT_OPTIONS = { listSuites: 'all', listTests: 'all', + slugPrefix: '', baseUrl: '', onlySummary: false, useActionsSummary: true, @@ -57890,6 +57891,7 @@ class TestReporter { workDirInput = getInput('working-directory', { required: false }); onlySummary = getInput('only-summary', { required: false }) === 'true'; useActionsSummary = getInput('use-actions-summary', { required: false }) === 'true'; + slugPrefix = getInput('slug-prefix', { required: false }); badgeTitle = getInput('badge-title', { required: false }); reportTitle = getInput('report-title', { required: false }); collapsed = getInput('collapsed', { required: false }); @@ -57989,7 +57991,7 @@ class TestReporter { throw error; } } - const { listSuites, listTests, onlySummary, useActionsSummary, badgeTitle, reportTitle, collapsed } = this; + const { listSuites, listTests, slugPrefix, onlySummary, useActionsSummary, badgeTitle, reportTitle, collapsed } = this; const passed = results.reduce((sum, tr) => sum + tr.passed, 0); const failed = results.reduce((sum, tr) => sum + tr.failed, 0); const skipped = results.reduce((sum, tr) => sum + tr.skipped, 0); @@ -57999,6 +58001,7 @@ class TestReporter { const summary = getReport(results, { listSuites, listTests, + slugPrefix, baseUrl, onlySummary, useActionsSummary, @@ -58027,6 +58030,7 @@ class TestReporter { const summary = getReport(results, { listSuites, listTests, + slugPrefix, baseUrl, onlySummary, useActionsSummary, diff --git a/src/main.ts b/src/main.ts index 888fd73..ead81b3 100644 --- a/src/main.ts +++ b/src/main.ts @@ -49,6 +49,7 @@ class TestReporter { readonly workDirInput = core.getInput('working-directory', {required: false}) readonly onlySummary = core.getInput('only-summary', {required: false}) === 'true' readonly useActionsSummary = core.getInput('use-actions-summary', {required: false}) === 'true' + readonly slugPrefix = core.getInput('slug-prefix', {required: false}) readonly badgeTitle = core.getInput('badge-title', {required: false}) readonly reportTitle = core.getInput('report-title', {required: false}) readonly collapsed = core.getInput('collapsed', {required: false}) as 'auto' | 'always' | 'never' @@ -174,7 +175,7 @@ class TestReporter { } } - const {listSuites, listTests, onlySummary, useActionsSummary, badgeTitle, reportTitle, collapsed} = this + const {listSuites, listTests, slugPrefix, onlySummary, useActionsSummary, badgeTitle, reportTitle, collapsed} = this const passed = results.reduce((sum, tr) => sum + tr.passed, 0) const failed = results.reduce((sum, tr) => sum + tr.failed, 0) @@ -188,6 +189,7 @@ class TestReporter { { listSuites, listTests, + slugPrefix, baseUrl, onlySummary, useActionsSummary, @@ -219,6 +221,7 @@ class TestReporter { const summary = getReport(results, { listSuites, listTests, + slugPrefix, baseUrl, onlySummary, useActionsSummary, diff --git a/src/report/get-report.ts b/src/report/get-report.ts index 63617b0..325e28c 100644 --- a/src/report/get-report.ts +++ b/src/report/get-report.ts @@ -11,6 +11,7 @@ const MAX_ACTIONS_SUMMARY_LENGTH = 1048576 export interface ReportOptions { listSuites: 'all' | 'failed' | 'none' listTests: 'all' | 'failed' | 'none' + slugPrefix: string baseUrl: string onlySummary: boolean useActionsSummary: boolean @@ -22,6 +23,7 @@ export interface ReportOptions { export const DEFAULT_OPTIONS: ReportOptions = { listSuites: 'all', listTests: 'all', + slugPrefix: '', baseUrl: '', onlySummary: false, useActionsSummary: true, diff --git a/src/utils/slugger.ts b/src/utils/slugger.ts index 656a81b..f12b49e 100644 --- a/src/utils/slugger.ts +++ b/src/utils/slugger.ts @@ -4,7 +4,7 @@ import {ReportOptions} from '../report/get-report.js' export function slug(name: string, options: ReportOptions): {id: string; link: string} { - const slugId = name + const slugId = `${options.slugPrefix}${name}` .trim() .replace(/_/g, '') .replace(/[./\\]/g, '-') From 08dfe2715bd4e19464b54dc425e5ec036ae5b49e Mon Sep 17 00:00:00 2001 From: Thomas Durand Date: Thu, 5 Mar 2026 15:18:06 +0100 Subject: [PATCH 2/2] feat: removing parameter, generating slug prefix and providing it as an output --- README.md | 5 +---- action.yml | 6 ++---- dist/index.js | 6 +++++- src/main.ts | 4 +++- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index bc28d53..f92e3a2 100644 --- a/README.md +++ b/README.md @@ -162,10 +162,6 @@ jobs: # Allows you to generate reports for Actions Summary # https://github.blog/2022-05-09-supercharging-github-actions-with-job-summaries/ use-actions-summary: 'true' - - # Prefix used when generating report anchor slugs. - # Useful to avoid collisions when multiple reports are rendered together. - slug-prefix: '' # Optionally specify a title (Heading level 1) for the report. Leading and trailing whitespace are ignored. # This is useful for separating your test report from other sections in the build summary. @@ -216,6 +212,7 @@ jobs: | time | Test execution time [ms] | | url | Check run URL | | url_html | Check run URL HTML | +| slug_prefix| Random anchor links slug prefix generated for the summary headers | ## Supported formats diff --git a/action.yml b/action.yml index f66ac45..f3bab88 100644 --- a/action.yml +++ b/action.yml @@ -88,10 +88,6 @@ inputs: https://github.com/orgs/github/teams/engineering/discussions/871 default: 'true' required: false - slug-prefix: - description: Prefix used when generating report anchor slugs - required: false - default: '' badge-title: description: Customize badge title required: false @@ -126,6 +122,8 @@ outputs: description: Check run URL url_html: description: Check run URL HTML + slug_prefix: + description: Random prefix added to generated report anchor slugs for this action run runs: using: 'node20' main: 'dist/index.js' diff --git a/dist/index.js b/dist/index.js index f49930a..06ca127 100644 --- a/dist/index.js +++ b/dist/index.js @@ -55383,6 +55383,8 @@ function getOctokit(token, options, ...additionalPlugins) { return new GitHubWithPlugins(getOctokitOptions(token, options)); } //# sourceMappingURL=github.js.map +// EXTERNAL MODULE: external "node:crypto" +var external_node_crypto_ = __nccwpck_require__(7598); // EXTERNAL MODULE: ./node_modules/adm-zip/adm-zip.js var adm_zip = __nccwpck_require__(1316); // EXTERNAL MODULE: ./node_modules/picomatch/index.js @@ -57864,6 +57866,7 @@ class NetteTesterJunitParser { + async function main() { try { @@ -57891,7 +57894,7 @@ class TestReporter { workDirInput = getInput('working-directory', { required: false }); onlySummary = getInput('only-summary', { required: false }) === 'true'; useActionsSummary = getInput('use-actions-summary', { required: false }) === 'true'; - slugPrefix = getInput('slug-prefix', { required: false }); + slugPrefix = `tr-${(0,external_node_crypto_.randomBytes)(4).toString('base64url')}-`; badgeTitle = getInput('badge-title', { required: false }); reportTitle = getInput('report-title', { required: false }); collapsed = getInput('collapsed', { required: false }); @@ -57965,6 +57968,7 @@ class TestReporter { setOutput('failed', failed); setOutput('skipped', skipped); setOutput('time', time); + setOutput('slug_prefix', this.slugPrefix); if (this.failOnError && isFailed) { setFailed(`Failed test were found and 'fail-on-error' option is set to ${this.failOnError}`); return; diff --git a/src/main.ts b/src/main.ts index ead81b3..fc6ea16 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,7 @@ import * as core from '@actions/core' import * as github from '@actions/github' import {GitHub} from '@actions/github/lib/utils' +import {randomBytes} from 'node:crypto' import {ArtifactProvider} from './input-providers/artifact-provider.js' import {LocalFileProvider} from './input-providers/local-file-provider.js' @@ -49,7 +50,7 @@ class TestReporter { readonly workDirInput = core.getInput('working-directory', {required: false}) readonly onlySummary = core.getInput('only-summary', {required: false}) === 'true' readonly useActionsSummary = core.getInput('use-actions-summary', {required: false}) === 'true' - readonly slugPrefix = core.getInput('slug-prefix', {required: false}) + readonly slugPrefix = `tr-${randomBytes(4).toString('base64url')}-` readonly badgeTitle = core.getInput('badge-title', {required: false}) readonly reportTitle = core.getInput('report-title', {required: false}) readonly collapsed = core.getInput('collapsed', {required: false}) as 'auto' | 'always' | 'never' @@ -145,6 +146,7 @@ class TestReporter { core.setOutput('failed', failed) core.setOutput('skipped', skipped) core.setOutput('time', time) + core.setOutput('slug_prefix', this.slugPrefix) if (this.failOnError && isFailed) { core.setFailed(`Failed test were found and 'fail-on-error' option is set to ${this.failOnError}`)