Skip to content

Commit 52d7f12

Browse files
committed
pr annotations
1 parent ef60977 commit 52d7f12

File tree

8 files changed

+230
-21
lines changed

8 files changed

+230
-21
lines changed

.github/workflows/validate.yml

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
cache: npm
3636
- name: Install dependencies
3737
run: npm ci
38-
- name: Validate
38+
- name: Lint
3939
run: npm run lint
4040

4141
format:
@@ -52,5 +52,25 @@ jobs:
5252
cache: npm
5353
- name: Install dependencies
5454
run: npm ci
55-
- name: Validate
55+
- name: Check format
5656
run: npm run check-format
57+
58+
type-warnings:
59+
runs-on: ubuntu-latest
60+
steps:
61+
- name: Checkout
62+
uses: actions/checkout@v4
63+
with:
64+
persist-credentials: false
65+
- name: Install Node.js
66+
uses: actions/setup-node@v4
67+
with:
68+
node-version: 22
69+
cache: npm
70+
- name: Install dependencies
71+
run: npm ci
72+
- name: Generate type warnings
73+
run: npm run generate-pr-type-warnings
74+
env:
75+
PR_NUMBER: "${{ github.event.number }}"
76+
GH_REPO: "${{ github.repository }}"

development/ci-interop.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import pathUtil from "node:path";
2+
3+
// https://github.com/actions/toolkit/blob/main/docs/problem-matchers.md#limitations
4+
export const MAX_ANNOTATIONS_PER_TYPE_PER_STEP = 10;
5+
export const MAX_ANNOTATIONS_PER_JOB = 50;
6+
7+
/**
8+
* @returns {boolean} true if running in GitHub Actions, etc.
9+
*/
10+
export const isCI = () => !!process.env.CI;
11+
12+
/**
13+
* @returns {Promise<Set<string>>} List of paths (relative to root without leading / or ./) changed by this PR.
14+
*/
15+
export const getChangedFiles = async () => {
16+
if (!isCI()) {
17+
return [];
18+
}
19+
20+
const prNumber = +process.env.PR_NUMBER;
21+
if (!prNumber) {
22+
throw new Error('Missing PR_NUMBER');
23+
}
24+
25+
const repo = process.env.GH_REPO;
26+
if (typeof repo !== 'string' || !repo.includes('/')) {
27+
throw new Error('Missing GH_REPO');
28+
}
29+
30+
const diffResponse = await fetch(`https://patch-diff.githubusercontent.com/raw/${repo}/pull/${prNumber}.diff`);
31+
const diffText = await diffResponse.text();
32+
const fileMatches = [...diffText.matchAll(/^(?:---|\+\+\+) [ab]\/(.+)$/gm)]
33+
.map(match => match[1]);
34+
35+
return new Set(fileMatches);
36+
};
37+
38+
/**
39+
* @typedef Annotation
40+
* @property {'notice'|'warning'|'error'} type
41+
* @property {string} file Absolute path to file or relative from repository root
42+
* @property {string} title
43+
* @property {string} message
44+
* @property {number} [line] 1-indexed
45+
* @property {number} [col] 1-indexed
46+
* @property {number} [endLine] 1-indexed
47+
* @property {number} [endCol] 1-indexed
48+
*/
49+
50+
/**
51+
* @param {Annotation} annotation
52+
*/
53+
export const createAnnotation = (annotation) => {
54+
const rootDir = pathUtil.join(import.meta.dirname, "..");
55+
const relativeFileName = pathUtil.relative(rootDir, annotation.file);
56+
57+
let output = "";
58+
output += `::${annotation.type} `;
59+
output += `file=${relativeFileName}`;
60+
61+
if (typeof annotation.line === "number") output += `,line=${annotation.line}`;
62+
if (typeof annotation.col === "number") output += `,col=${annotation.col}`;
63+
if (typeof annotation.endLine === "number")
64+
output += `,endLine=${annotation.endLine}`;
65+
if (typeof annotation.endCol === "number")
66+
output += `,endCol=${annotation.endCol}`;
67+
68+
output += `,title=${annotation.title}::`;
69+
output += annotation.message;
70+
71+
console.log(output);
72+
};

development/colors.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,8 @@ const color = (i) => (enableColor ? i : "");
33

44
export const RESET = color("\x1b[0m");
55
export const BOLD = color("\x1b[1m");
6+
67
export const RED = color("\x1b[31m");
8+
export const YELLOW = color("\x1b[33m");
79
export const GREEN = color("\x1b[32m");
10+
export const BLUE = color("\x1b[34m");
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import pathUtil from "node:path";
2+
import ts from "typescript";
3+
import { createAnnotation, getChangedFiles, isCI } from "./ci-interop.js";
4+
5+
/**
6+
* @fileoverview
7+
* tsc command output will be parsed by GitHub Actions as errors.
8+
* We want them to be just warnings.
9+
*/
10+
11+
const check = async () => {
12+
const rootDir = pathUtil.join(import.meta.dirname, "..");
13+
14+
const changedFiles = Array.from(await getChangedFiles()).sort();
15+
const changedFilesAbsolute = changedFiles.map(f => pathUtil.join(rootDir, f));
16+
17+
console.log(`${changedFiles.size} changed files:`);
18+
console.log(Array.from(changedFiles).sort().join('\n'));
19+
console.log('');
20+
21+
const tsconfigPath = pathUtil.join(rootDir, "tsconfig.json");
22+
const commandLine = ts.getParsedCommandLineOfConfigFile(
23+
tsconfigPath,
24+
{},
25+
ts.sys
26+
);
27+
28+
const program = ts.createProgram({
29+
rootNames: commandLine.fileNames,
30+
options: commandLine.options,
31+
});
32+
const emitted = program.emit();
33+
34+
const diagnostics = [
35+
...ts.getPreEmitDiagnostics(program),
36+
...emitted.diagnostics,
37+
];
38+
39+
let numWarnings = 0;
40+
41+
for (const diagnostic of diagnostics) {
42+
const startPosition = ts.getLineAndCharacterOfPosition(
43+
diagnostic.file,
44+
diagnostic.start
45+
);
46+
const endPosition = ts.getLineAndCharacterOfPosition(
47+
diagnostic.file,
48+
diagnostic.start
49+
);
50+
const flattened = ts.flattenDiagnosticMessageText(
51+
diagnostic.messageText,
52+
" ",
53+
0
54+
);
55+
56+
if (changedFilesAbsolute.includes(diagnostic.file.fileName)) {
57+
numWarnings++;
58+
59+
createAnnotation({
60+
type: "warning",
61+
file: diagnostic.file.fileName,
62+
title: "Type warning - may indicate a bug - ignore if no bug",
63+
onlyIfChanged: true,
64+
message: flattened,
65+
line: startPosition.line + 1,
66+
col: startPosition.character + 1,
67+
endLine: endPosition.line + 1,
68+
endCol: endPosition.character + 1,
69+
});
70+
}
71+
72+
}
73+
74+
console.log(`${numWarnings} warnings.`);
75+
};
76+
77+
if (isCI()) {
78+
check();
79+
} else {
80+
console.error('Only intended to be run in CI environemnts. Use normal TypeScript CLI instead for development.')
81+
process.exit(1);
82+
}

development/validate.js

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Builder from "./builder.js";
2+
import { createAnnotation, isCI } from "./ci-interop.js";
23
import * as Colors from "./colors.js";
34

45
const builder = new Builder("production");
@@ -15,10 +16,23 @@ if (errors.length === 0) {
1516
errors.length === 1 ? "file" : "files"
1617
} failed validation.${Colors.RESET}`
1718
);
19+
1820
console.error("");
21+
1922
for (const { fileName, error } of errors) {
2023
console.error(`${Colors.BOLD}${fileName}${Colors.RESET}: ${error}`);
24+
25+
if (isCI()) {
26+
const nameWithoutLeadingSlash = fileName.replace(/^\//, "");
27+
createAnnotation({
28+
type: "error",
29+
file: nameWithoutLeadingSlash,
30+
title: "Validation error",
31+
message: error,
32+
});
33+
}
2134
}
22-
console.error(``);
35+
36+
console.error("");
2337
process.exit(1);
2438
}

globals.d.ts

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,15 @@
55

66
interface Element {
77
/**
8-
* requestFullscreen() but available in Safari.
9-
*/
8+
* requestFullscreen() but available in Safari.
9+
*/
1010
webkitRequestFullscreen?(): unknown;
1111
}
1212

1313
interface Document {
1414
/**
15-
* exitFullscreen() but available in Safari.
16-
*/
15+
* exitFullscreen() but available in Safari.
16+
*/
1717
webkitExitFullscreen?(): unknown;
1818
/**
1919
* fullscreenElement but available in Safari.
@@ -26,25 +26,35 @@ interface Document {
2626
*/
2727
declare class NDEFReader extends EventTarget {
2828
constructor();
29-
scan(options?: {signal?: AbortSignal}): Promise<void>;
30-
onreading?(event: Event & {message: NDEFMessage}): void;
29+
scan(options?: { signal?: AbortSignal }): Promise<void>;
30+
onreading?(event: Event & { message: NDEFMessage }): void;
3131
onreadingerror?(event: Event): void;
3232
}
3333

34-
type TypedArray = Uint8Array | Int8Array | Uint16Array | Int16Array | Uint32Array | Int32Array | BigUint64Array | BigInt64Array;
34+
type TypedArray =
35+
| Uint8Array
36+
| Int8Array
37+
| Uint16Array
38+
| Int16Array
39+
| Uint32Array
40+
| Int32Array
41+
| BigUint64Array
42+
| BigInt64Array;
3543

3644
/**
3745
* https://developer.mozilla.org/en-US/docs/Web/API/NDEFMessage/NDEFMessage
3846
*/
3947
declare class NDEFMessage {
40-
constructor(records: Array<{
41-
data?: string | ArrayBuffer | TypedArray | DataView | NDEFRecord[],
42-
encoding?: string;
43-
id?: string;
44-
lang?: string;
45-
mediaType?: string;
46-
recordType?: string;
47-
}>);
48+
constructor(
49+
records: Array<{
50+
data?: string | ArrayBuffer | TypedArray | DataView | NDEFRecord[];
51+
encoding?: string;
52+
id?: string;
53+
lang?: string;
54+
mediaType?: string;
55+
recordType?: string;
56+
}>
57+
);
4858
readonly records: NDEFRecord[];
4959
}
5060

@@ -68,7 +78,15 @@ interface NetworkInformation {
6878
effectiveType: string;
6979
rtt: number;
7080
saveData: boolean;
71-
type: 'bluetooth' | 'cellular' | 'ethernet' | 'none' | 'wifi' | 'wimax' | 'other' | 'unknown';
81+
type:
82+
| "bluetooth"
83+
| "cellular"
84+
| "ethernet"
85+
| "none"
86+
| "wifi"
87+
| "wimax"
88+
| "other"
89+
| "unknown";
7290
}
7391

7492
interface Navigator {

images/CST1229/zip.svg

Lines changed: 1 addition & 1 deletion
Loading

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"check-format": "prettier . --check",
1717
"upload-translations": "node development/upload-translations.js",
1818
"download-translations": "node development/download-translations.js",
19-
"check-types": "tsc"
19+
"generate-pr-type-warnings": "node development/generate-pr-type-warnings.js"
2020
},
2121
"repository": {
2222
"type": "git",

0 commit comments

Comments
 (0)