Skip to content

Commit c26de62

Browse files
committed
Add knip, publint, actionlint as specialized linters
Add three specialized linters to complement strict TS + Prettier — each catches a class of problem neither of those sees. - knip: dead code and unused exports. Found: two unused test helpers (setGithubEnv, useMockAgent, TestContext interface). Removed. Would have caught the original @octokit/rest / actions-toolkit / node-fetch leftovers automatically if it had been in place from the start. - publint: npm package hygiene. Added 'type: commonjs' (explicit module type), 'private: true' (this project ships as a GitHub Action, not via npm), and 'files: [dist/, action.yml]' (if it ever is published, only the artifacts users need get packaged). - actionlint: GitHub Actions workflow linter. Found nodejs.yml pinning actions/setup-node@v3 (deprecated) and testing on Node 18/20 when the action.yml runtime is node24. Bumped to setup-node@v4 and the matrix to 20/22/24. Swapped nock devDep (not used since we moved to undici MockAgent in the earlier test-harness commit) for an explicit 'undici: ^5.29' devDep — this resolves the version knip was flagging as 'unlisted' and pins it to match what @actions/github resolves, which is required for MockAgent interception to work in-process. Scripts: - lint:knip, lint:publint, lint (runs format:check + both). - CI workflow runs 'npm run lint' and 'npm test' before the build. - New .github/workflows/actionlint.yml runs actionlint on any change to .github/workflows/**.
1 parent e551d66 commit c26de62

15 files changed

Lines changed: 3602 additions & 2569 deletions

.github/workflows/actionlint.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: actionlint
2+
3+
on:
4+
push:
5+
branches: ['*']
6+
pull_request:
7+
branches: [master]
8+
paths:
9+
- '.github/workflows/**'
10+
11+
jobs:
12+
actionlint:
13+
runs-on: ubuntu-latest
14+
steps:
15+
- uses: actions/checkout@v4
16+
- name: Install and run actionlint
17+
run: |
18+
bash <(curl -sSfL https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
19+
./actionlint -color

.github/workflows/nodejs.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ jobs:
1515
runs-on: ubuntu-latest
1616
strategy:
1717
matrix:
18-
node-version: [18.x, 20.x]
18+
node-version: [20.x, 22.x, 24.x]
1919
steps:
2020
- name: Initialize Energy Estimation
2121
uses: green-coding-berlin/eco-ci-energy-estimation@v1
@@ -29,11 +29,15 @@ jobs:
2929
task: get-measurement
3030
label: 'repository checkout'
3131
- name: Use Node.js ${{ matrix.node-version }}
32-
uses: actions/setup-node@v3
32+
uses: actions/setup-node@v4
3333
with:
3434
node-version: ${{ matrix.node-version }}
3535
- name: Npm install
3636
run: npm ci
37+
- name: Lint (prettier + knip + publint)
38+
run: npm run lint
39+
- name: Npm test
40+
run: npm test
3741
- name: Npm build
3842
run: npm run build --if-present
3943
- name: Checkout Repo Measurement

__tests__/integration/errorPaths.test.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ describe('error paths', () => {
8484

8585
// The action reports the failure through core.setFailed. It does not
8686
// silently retry (v6 @actions/github does not ship plugin-retry).
87-
expect(watch.failures.join('\n')).toMatch(/Could not retrieve repository contents|Could not update the JSON file/)
87+
expect(watch.failures.join('\n')).toMatch(
88+
/Could not retrieve repository contents|Could not update the JSON file/
89+
)
8890
watch.restore()
8991
})
9092

@@ -145,9 +147,9 @@ describe('error paths', () => {
145147
body: 'i have read the cla document and i hereby sign the cla',
146148
user: { login: 'alice', id: 1001 }
147149
})
148-
fake.repo('acme', 'widgets').addWorkflow('cla-check', [
149-
{ id: 777, conclusion: 'failure' }
150-
])
150+
fake
151+
.repo('acme', 'widgets')
152+
.addWorkflow('cla-check', [{ id: 777, conclusion: 'failure' }])
151153

152154
// Rerun-workflow-run fails at the 'listWorkflowRuns' step.
153155
fake.injectFailure({
@@ -178,7 +180,9 @@ describe('error paths', () => {
178180

179181
// The signature should still have been recorded even though the rerun
180182
// request failed.
181-
const sigFile = fake.repo('acme', 'widgets').getFile('signatures/v1/cla.json') as {
183+
const sigFile = fake
184+
.repo('acme', 'widgets')
185+
.getFile('signatures/v1/cla.json') as {
182186
signedContributors: Array<{ name: string }>
183187
}
184188
expect(sigFile.signedContributors.map(c => c.name)).toContain('alice')

__tests__/integration/pagination.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,9 @@ describe('pagination', () => {
6262
)
6363
// alice's signing comment lives on page 2. If pagination were broken,
6464
// newSigned would be empty.
65-
expect(result.newSigned.map((c: { name: string }) => c.name)).toEqual(['alice'])
65+
expect(result.newSigned.map((c: { name: string }) => c.name)).toEqual([
66+
'alice'
67+
])
6668
})
6769

6870
it('fake emits rel="next" only when there are more pages', async () => {

__tests__/integration/preVsPostRegression.test.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,13 @@ function scenarioEnv(
9090
}
9191
}
9292

93-
import {createHash} from 'crypto'
93+
import { createHash } from 'crypto'
9494

9595
interface NormalizedRequest {
9696
method: string
9797
path: string
9898
status: number
99-
bodyHash: string // stable hash of the canonicalized request body
99+
bodyHash: string // stable hash of the canonicalized request body
100100
bodyShape: string // tag, e.g. 'empty' | 'json' | 'text', for readable diffs
101101
}
102102

@@ -106,8 +106,8 @@ interface NormalizedRequest {
106106
* diff, but any semantic change — a missing field, a new field, a different
107107
* commit message — still produces a distinct hash.
108108
*/
109-
function canonicalBody(raw: string): {hash: string; shape: string} {
110-
if (!raw) return {hash: 'empty', shape: 'empty'}
109+
function canonicalBody(raw: string): { hash: string; shape: string } {
110+
if (!raw) return { hash: 'empty', shape: 'empty' }
111111
let parsed: unknown
112112
try {
113113
parsed = JSON.parse(raw)
@@ -126,7 +126,7 @@ function canonicalBody(raw: string): {hash: string; shape: string} {
126126

127127
function normalizeLog(log: FakeGitHubHttp['requestLog']): NormalizedRequest[] {
128128
return log.map(e => {
129-
const {hash, shape} = canonicalBody(e.body)
129+
const { hash, shape } = canonicalBody(e.body)
130130
return {
131131
method: e.method,
132132
path: decodeURIComponent(e.path.split('?')[0] || ''),
@@ -256,8 +256,16 @@ describe('pre- vs post-refactor: HTTP-level behaviour is unchanged', () => {
256256
// focused unit tests to pin down body shape.
257257
const bodySensitive = (r: NormalizedRequest): boolean => {
258258
if (r.method === 'POST' && r.path === '/graphql') return false
259-
if (r.method === 'PATCH' && r.path.startsWith('/repos/acme/widgets/issues/comments/')) return false
260-
if (r.method === 'PUT' && r.path === '/repos/acme/widgets/contents/signatures/cla.json') return false
259+
if (
260+
r.method === 'PATCH' &&
261+
r.path.startsWith('/repos/acme/widgets/issues/comments/')
262+
)
263+
return false
264+
if (
265+
r.method === 'PUT' &&
266+
r.path === '/repos/acme/widgets/contents/signatures/cla.json'
267+
)
268+
return false
261269
return true
262270
}
263271
const key = (r: NormalizedRequest) =>

__tests__/testHelpers/context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as github from '@actions/github'
22

3-
export interface TestContext {
3+
interface TestContext {
44
owner: string
55
repo: string
66
issueNumber: number

__tests__/testHelpers/env.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,6 @@ export function setInput(name: string, value: string): void {
1212
trackedKeys.add(key)
1313
}
1414

15-
export function setGithubEnv(vars: Record<string, string | undefined>): void {
16-
for (const [k, v] of Object.entries(vars)) {
17-
if (v === undefined) {
18-
delete process.env[k]
19-
} else {
20-
process.env[k] = v
21-
}
22-
trackedKeys.add(k)
23-
}
24-
}
25-
2615
export function resetEnv(): void {
2716
for (const key of trackedKeys) {
2817
delete process.env[key]

__tests__/testHelpers/fakeGithubCore.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -220,13 +220,21 @@ export function createFakeGitHubCore(): FakeGitHubCore {
220220
path
221221
})
222222
})
223-
addRoute(getRoutes, '/repos/:owner/:repo/issues/:num/comments', (m, _body, query) => {
224-
const owner = decodeURIComponent(m[1]!)
225-
const name = decodeURIComponent(m[2]!)
226-
const num = parseInt(m[3]!, 10)
227-
const all = getRepo(owner, name).comments.get(num) || []
228-
return paginate(all, query, `/repos/${owner}/${name}/issues/${num}/comments`)
229-
})
223+
addRoute(
224+
getRoutes,
225+
'/repos/:owner/:repo/issues/:num/comments',
226+
(m, _body, query) => {
227+
const owner = decodeURIComponent(m[1]!)
228+
const name = decodeURIComponent(m[2]!)
229+
const num = parseInt(m[3]!, 10)
230+
const all = getRepo(owner, name).comments.get(num) || []
231+
return paginate(
232+
all,
233+
query,
234+
`/repos/${owner}/${name}/issues/${num}/comments`
235+
)
236+
}
237+
)
230238
addRoute(getRoutes, '/repos/:owner/:repo/pulls/:num', m => {
231239
const owner = decodeURIComponent(m[1]!)
232240
const name = decodeURIComponent(m[2]!)
@@ -423,7 +431,10 @@ export function createFakeGitHubCore(): FakeGitHubCore {
423431
})
424432
}
425433

426-
function consumeFault(method: string, pathname: string): RouteResult | undefined {
434+
function consumeFault(
435+
method: string,
436+
pathname: string
437+
): RouteResult | undefined {
427438
// Match against both the raw and percent-decoded pathname so test regexes
428439
// can be written naturally ('/signatures/cla.json') and still match the
429440
// encoded URLs octokit emits ('/signatures%2Fcla.json').
@@ -439,7 +450,8 @@ export function createFakeGitHubCore(): FakeGitHubCore {
439450
if (f.times <= 0) faults.splice(i, 1)
440451
return {
441452
status: f.status,
442-
body: f.body ?? JSON.stringify({ message: `fault-injected ${f.status}` }),
453+
body:
454+
f.body ?? JSON.stringify({ message: `fault-injected ${f.status}` }),
443455
headers: f.headers
444456
}
445457
}

__tests__/testHelpers/fakeGithubHttp.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@ export interface FakeGitHubHttp {
1717
repo(owner: string, name: string): FakeRepoHandle
1818
recordedLocks: FakeGitHubCore['recordedLocks']
1919
recordedRerunRequests: FakeGitHubCore['recordedRerunRequests']
20-
requestLog: Array<{ method: string; path: string; status: number; body: string }>
20+
requestLog: Array<{
21+
method: string
22+
path: string
23+
status: number
24+
body: string
25+
}>
2126
close(): Promise<void>
2227
}
2328

@@ -30,11 +35,11 @@ export async function startFakeGitHubHttp(): Promise<FakeGitHubHttp> {
3035
req.on('data', c => chunks.push(c))
3136
req.on('end', () => {
3237
const body = Buffer.concat(chunks).toString('utf-8')
33-
const { status, body: out, headers } = core.route(
34-
req.method || 'GET',
35-
req.url || '/',
36-
body
37-
)
38+
const {
39+
status,
40+
body: out,
41+
headers
42+
} = core.route(req.method || 'GET', req.url || '/', body)
3843
requestLog.push({
3944
method: req.method || 'GET',
4045
path: req.url || '/',

__tests__/testHelpers/mockAgent.ts

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,19 +34,6 @@ export function installMockAgent(): MockAgentHarness {
3434
}
3535
}
3636

37-
/** Jest-style beforeEach/afterEach setup. Returns a getter. */
38-
export function useMockAgent(): () => MockAgentHarness {
39-
let harness: MockAgentHarness | undefined
40-
beforeEach(() => {
41-
harness = installMockAgent()
42-
})
43-
afterEach(async () => {
44-
await harness!.close()
45-
harness = undefined
46-
})
47-
return () => harness!
48-
}
49-
5037
/**
5138
* Install an interceptor that captures the request body as JSON and returns
5239
* the given reply. The returned object's `body` is populated after the request

0 commit comments

Comments
 (0)