Skip to content

Commit df16191

Browse files
authored
feat: add local GitHub Action for testing CI changes (#1104)
Closes #1093 Created a minimalist composite GitHub Action that runs the current `@code-pushup/ci` source code instead of relying on the external `code-pushup/github-action@v0` release.
1 parent 40db7bd commit df16191

File tree

9 files changed

+295
-4
lines changed

9 files changed

+295
-4
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: Code PushUp
2+
description: Minimalist GitHub Action that executes Code PushUp using local @code-pushup/ci source code
3+
4+
inputs:
5+
token:
6+
description: GitHub token for API access
7+
required: true
8+
default: ${{ github.token }}
9+
10+
runs:
11+
using: composite
12+
steps:
13+
- name: Run Node script
14+
run: npx tsx .github/actions/code-pushup/src/runner.ts
15+
shell: bash
16+
env:
17+
TSX_TSCONFIG_PATH: .github/actions/code-pushup/tsconfig.json
18+
GH_TOKEN: ${{ inputs.token }}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "@code-pushup/local-action",
3+
"version": "0.0.0",
4+
"private": true,
5+
"type": "module",
6+
"description": "Minimalist GitHub Action that executes Code PushUp using local @code-pushup/ci source code for testing CI changes"
7+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "local-action",
3+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
4+
"sourceRoot": ".github/actions/code-pushup/src",
5+
"projectType": "application",
6+
"targets": {},
7+
"tags": ["type:app"]
8+
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import * as core from '@actions/core';
2+
import * as github from '@actions/github';
3+
import type { WebhookPayload } from '@actions/github/lib/interfaces';
4+
import type { components } from '@octokit/openapi-types';
5+
import {
6+
type Comment,
7+
type GitBranch,
8+
type Options,
9+
type ProviderAPIClient,
10+
type SourceFileIssue,
11+
runInCI,
12+
} from '@code-pushup/ci';
13+
import { CODE_PUSHUP_UNICODE_LOGO } from '@code-pushup/utils';
14+
15+
type GitHubRefs = {
16+
head: GitBranch;
17+
base?: GitBranch;
18+
};
19+
20+
type PullRequestPayload = NonNullable<WebhookPayload['pull_request']> &
21+
components['schemas']['pull-request-minimal'];
22+
23+
const LOG_PREFIX = '[Code PushUp GitHub action]';
24+
25+
const MAX_COMMENT_CHARS = 65_536;
26+
27+
function convertComment(
28+
comment: Pick<components['schemas']['issue-comment'], 'id' | 'body' | 'url'>,
29+
): Comment {
30+
const { id, body = '', url } = comment;
31+
return { id, body, url };
32+
}
33+
34+
function isPullRequest(
35+
payload: WebhookPayload['pull_request'],
36+
): payload is PullRequestPayload {
37+
return payload != null;
38+
}
39+
40+
function parseBranchRef({ ref, sha }: GitBranch): GitBranch {
41+
return {
42+
ref: ref.split('/').at(-1) ?? ref,
43+
sha,
44+
};
45+
}
46+
47+
function parseGitRefs(): GitHubRefs {
48+
if (isPullRequest(github.context.payload.pull_request)) {
49+
const { head, base } = github.context.payload.pull_request;
50+
return { head: parseBranchRef(head), base: parseBranchRef(base) };
51+
}
52+
return { head: parseBranchRef(github.context) };
53+
}
54+
55+
function createAnnotationsFromIssues(issues: SourceFileIssue[]): void {
56+
if (issues.length > 0) {
57+
core.info(`Creating annotations for ${issues.length} issues:`);
58+
}
59+
// eslint-disable-next-line functional/no-loop-statements
60+
for (const issue of issues) {
61+
const message = issue.message;
62+
const properties: core.AnnotationProperties = {
63+
title: `${CODE_PUSHUP_UNICODE_LOGO} ${issue.plugin.title} | ${issue.audit.title}`,
64+
file: issue.source.file,
65+
startLine: issue.source.position?.startLine,
66+
startColumn: issue.source.position?.startColumn,
67+
endLine: issue.source.position?.endLine,
68+
endColumn: issue.source.position?.endColumn,
69+
};
70+
switch (issue.severity) {
71+
case 'error':
72+
core.error(message, properties);
73+
break;
74+
case 'warning':
75+
core.warning(message, properties);
76+
break;
77+
case 'info':
78+
core.notice(message, properties);
79+
break;
80+
}
81+
}
82+
}
83+
84+
function createGitHubApiClient(): ProviderAPIClient {
85+
const token = process.env.GH_TOKEN;
86+
87+
if (!token) {
88+
throw new Error('No GitHub token found');
89+
}
90+
91+
const octokit = github.getOctokit(token);
92+
93+
return {
94+
maxCommentChars: MAX_COMMENT_CHARS,
95+
96+
listComments: async (): Promise<Comment[]> => {
97+
const comments = await octokit.paginate(
98+
octokit.rest.issues.listComments,
99+
{
100+
...github.context.repo,
101+
issue_number: github.context.issue.number,
102+
},
103+
);
104+
return comments.map(convertComment);
105+
},
106+
107+
createComment: async (body: string): Promise<Comment> => {
108+
const { data } = await octokit.rest.issues.createComment({
109+
...github.context.repo,
110+
issue_number: github.context.issue.number,
111+
body,
112+
});
113+
return convertComment(data);
114+
},
115+
116+
updateComment: async (id: number, body: string): Promise<Comment> => {
117+
const { data } = await octokit.rest.issues.updateComment({
118+
...github.context.repo,
119+
comment_id: id,
120+
body,
121+
});
122+
return convertComment(data);
123+
},
124+
};
125+
}
126+
127+
async function run(): Promise<void> {
128+
try {
129+
const options: Options = {
130+
bin: 'npx nx code-pushup --nx-bail --',
131+
};
132+
133+
const gitRefs = parseGitRefs();
134+
135+
const apiClient = createGitHubApiClient();
136+
137+
const result = await runInCI(gitRefs, apiClient, options);
138+
139+
const issues =
140+
result.mode === 'standalone'
141+
? (result.newIssues ?? [])
142+
: result.projects.flatMap(project => project.newIssues ?? []);
143+
144+
if (issues.length > 0) {
145+
core.info(
146+
`Found ${issues.length} new issues, creating GitHub annotations`,
147+
);
148+
createAnnotationsFromIssues(issues);
149+
}
150+
151+
core.info(`${LOG_PREFIX} Finished running successfully`);
152+
} catch (error) {
153+
const message = error instanceof Error ? error.message : String(error);
154+
core.error(`${LOG_PREFIX} Failed: ${message}`);
155+
core.setFailed(message);
156+
}
157+
}
158+
159+
await run();
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"extends": "../../../tsconfig.base.json",
3+
"include": ["src/**/*"]
4+
}

.github/workflows/code-pushup-fork.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,6 @@ jobs:
3737
- name: Install dependencies
3838
run: npm ci
3939
- name: Run Code PushUp action
40-
uses: code-pushup/github-action@v0
40+
uses: ./.github/actions/code-pushup
4141
with:
42-
bin: npx nx code-pushup --nx-bail --
42+
token: ${{ secrets.GITHUB_TOKEN }}

.github/workflows/code-pushup.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,6 @@ jobs:
3939
- name: Install dependencies
4040
run: npm ci
4141
- name: Run Code PushUp action
42-
uses: code-pushup/github-action@v0
42+
uses: ./.github/actions/code-pushup
4343
with:
44-
bin: npx nx code-pushup --nx-bail --
44+
token: ${{ secrets.GITHUB_TOKEN }}

package-lock.json

Lines changed: 93 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@
4949
"zod": "^4.0.5"
5050
},
5151
"devDependencies": {
52+
"@actions/core": "^1.11.1",
53+
"@actions/github": "^6.0.1",
5254
"@beaussan/nx-knip": "^0.0.5-15",
5355
"@code-pushup/eslint-config": "^0.14.2",
5456
"@commitlint/cli": "^19.5.0",

0 commit comments

Comments
 (0)