Skip to content

Commit 6aad92b

Browse files
committed
Validate scans input, skip lockfile writes, and warn on name mismatches
1 parent c451932 commit 6aad92b

5 files changed

Lines changed: 46 additions & 11 deletions

File tree

.github/actions/find/src/pluginManager/index.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,7 @@ export async function loadCustomPlugins() {
7777
await loadPluginsFromPath({pluginsPath, skipBuiltInPlugins: BUILT_IN_PLUGINS})
7878
}
7979

80-
// Curated first-party packages allowed to be installed and loaded from NPM.
81-
// Kept intentionally small while the plugin system is being prototyped.
80+
// First-party packages allowed to be installed and loaded from NPM.
8281
const FIRST_PARTY_NPM_PLUGINS = ['@github/accessibility-scanner-alt-text-plugin']
8382

8483
// exported for mocking/testing. not for actual use
@@ -89,7 +88,7 @@ export async function loadNpmPlugins(npmPlugins: NpmPluginRequest[]) {
8988
core.info('Loading NPM plugins')
9089

9190
for (const request of npmPlugins) {
92-
// Only install curated first-party packages.
91+
// Only install first-party packages.
9392
if (!FIRST_PARTY_NPM_PLUGINS.includes(request.package)) {
9493
core.warning(`Skipping NPM plugin '${request.package}' because it is not a first-party package`)
9594
continue
@@ -106,6 +105,14 @@ export async function loadNpmPlugins(npmPlugins: NpmPluginRequest[]) {
106105
continue
107106
}
108107

108+
// The requested name (in 'scans') gates invocation, so a mismatch means the plugin would load but never run.
109+
if (plugin.name !== request.name) {
110+
core.warning(
111+
`Skipping NPM plugin '${request.package}' because it exported name '${plugin.name}', which does not match requested name '${request.name}'`,
112+
)
113+
continue
114+
}
115+
109116
// Built-in and local plugins take precedence over NPM ones of the same name.
110117
if (plugins.some(existing => existing.name === plugin.name)) {
111118
core.info(`Skipping NPM plugin '${plugin.name}' because a plugin with that name is already loaded`)

.github/actions/find/src/pluginManager/npmPluginLoader.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import {execFileSync} from 'child_process'
22
import * as core from '@actions/core'
33
import type {NpmPluginRequest, Plugin} from './types.js'
44

5-
// Install the package at runtime
5+
// Install the package at runtime.
66
export function installNpmPackage(spec: string) {
7-
execFileSync('npm', ['install', spec, '--no-save', '--ignore-scripts'], {stdio: 'inherit'})
7+
execFileSync('npm', ['install', spec, '--no-save', '--no-package-lock', '--ignore-scripts'], {stdio: 'inherit'})
88
}
99

1010
// Install and import a single NPM-published plugin

.github/actions/find/src/scansContextProvider.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@ type ScanEntry = string | {name?: unknown; package?: unknown; version?: unknown}
1616
export function getScansContext() {
1717
if (!scansContext) {
1818
const scansInput = core.getInput('scans', {required: false})
19-
const rawScans = JSON.parse(scansInput || '[]') as ScanEntry[]
19+
const parsed = JSON.parse(scansInput || '[]')
20+
// Fail early with a clear message instead of a cryptic 'not iterable' error later.
21+
if (!Array.isArray(parsed)) {
22+
throw new Error(`'scans' input must be a JSON array, got: ${scansInput}`)
23+
}
24+
const rawScans = parsed as ScanEntry[]
2025

2126
// scansToPerform holds the name of every scan/plugin to run
2227
const scansToPerform: string[] = []

.github/actions/find/tests/findForUrl.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,17 @@ describe('findForUrl', () => {
116116
expect(loadedPlugins[0].default).toHaveBeenCalledTimes(1)
117117
expect(loadedPlugins[1].default).toHaveBeenCalledTimes(0)
118118
})
119+
120+
it('forwards object-form NPM plugin entries to loadPlugins', async () => {
121+
loadedPlugins = []
122+
actionInput = JSON.stringify([{name: 'alt-text-scan', package: '@github/accessibility-scanner-alt-text-plugin'}])
123+
clearAll()
124+
125+
await findForUrl('test.com')
126+
expect(pluginManager.loadPlugins).toHaveBeenCalledWith([
127+
{name: 'alt-text-scan', package: '@github/accessibility-scanner-alt-text-plugin', version: undefined},
128+
])
129+
})
119130
})
120131

121132
describe('axe finding categorization', () => {

.github/actions/find/tests/npmPluginLoader.test.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,16 @@ describe('npmPluginLoader', () => {
1919
})
2020

2121
describe('installNpmPackage', () => {
22-
it('installs with --no-save and --ignore-scripts', () => {
22+
it('installs with --no-save, --no-package-lock and --ignore-scripts', () => {
2323
const execSpy = vi.spyOn(childProcess, 'execFileSync').mockImplementation(() => Buffer.from(''))
2424
npmPluginLoader.installNpmPackage('some-pkg@1.0.0')
25-
expect(execSpy).toHaveBeenCalledWith('npm', ['install', 'some-pkg@1.0.0', '--no-save', '--ignore-scripts'], {
26-
stdio: 'inherit',
27-
})
25+
expect(execSpy).toHaveBeenCalledWith(
26+
'npm',
27+
['install', 'some-pkg@1.0.0', '--no-save', '--no-package-lock', '--ignore-scripts'],
28+
{
29+
stdio: 'inherit',
30+
},
31+
)
2832
})
2933
})
3034

@@ -34,7 +38,7 @@ describe('npmPluginLoader', () => {
3438
await npmPluginLoader.loadPluginViaNpm({name: 'p', package: 'nonexistent-pkg-xyz', version: '2.3.4'})
3539
expect(execSpy).toHaveBeenCalledWith(
3640
'npm',
37-
['install', 'nonexistent-pkg-xyz@2.3.4', '--no-save', '--ignore-scripts'],
41+
['install', 'nonexistent-pkg-xyz@2.3.4', '--no-save', '--no-package-lock', '--ignore-scripts'],
3842
{stdio: 'inherit'},
3943
)
4044
})
@@ -79,6 +83,14 @@ describe('loadNpmPlugins', () => {
7983
expect(pluginManager.getPlugins().length).toBe(0)
8084
})
8185

86+
it('skips an NPM plugin whose exported name does not match the requested name', async () => {
87+
vi.spyOn(npmPluginLoader, 'loadPluginViaNpm').mockResolvedValue({name: 'actual-name', default: vi.fn()})
88+
const warnSpy = vi.spyOn(core, 'warning').mockImplementation(() => {})
89+
await pluginManager.loadNpmPlugins([{name: 'requested-name', package: ALLOWED}])
90+
expect(warnSpy).toHaveBeenCalled()
91+
expect(pluginManager.getPlugins().length).toBe(0)
92+
})
93+
8294
it('skips an NPM plugin whose name collides with an already-loaded plugin', async () => {
8395
pluginManager.getPlugins().push({name: 'dup', default: vi.fn()})
8496
vi.spyOn(npmPluginLoader, 'loadPluginViaNpm').mockResolvedValue({name: 'dup', default: vi.fn()})

0 commit comments

Comments
 (0)