Skip to content

Commit 93020af

Browse files
committed
👷 update CI to add new check on async/await file
Issue: CLDSRV-860
1 parent e20647b commit 93020af

File tree

4 files changed

+250
-3
lines changed

4 files changed

+250
-3
lines changed
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
/**
2+
* Check that all new/modified functions in the current git diff use async/await.
3+
* Fails with exit code 1 if any additions introduce callback-style functions or .then() chains.
4+
*
5+
* Usage: node scripts/check-diff-async.mjs
6+
* In CI: runs against the current PR diff (files changed vs base branch)
7+
*/
8+
import { execSync } from 'node:child_process';
9+
import { Project, SyntaxKind } from 'ts-morph';
10+
11+
const CALLBACK_PARAM_PATTERN = /^(cb|callback|next|done|err)$/i;
12+
13+
function getChangedJsFiles() {
14+
const base = process.env.GITHUB_BASE_REF
15+
? `origin/${process.env.GITHUB_BASE_REF}`
16+
: 'HEAD';
17+
const output = execSync(`git diff --name-only --diff-filter=ACMR ${base} -- '*.js'`, {
18+
encoding: 'utf8',
19+
}).trim();
20+
21+
return output ? output.split('\n').filter(f => f.endsWith('.js')) : [];
22+
}
23+
24+
/**
25+
* Get added line numbers for a file in the current diff.
26+
*/
27+
function getAddedLineNumbers(filePath) {
28+
const base = process.env.GITHUB_BASE_REF
29+
? `origin/${process.env.GITHUB_BASE_REF}`
30+
: 'HEAD';
31+
const diff = execSync(`git diff ${base} -- ${filePath}`, { encoding: 'utf8' });
32+
const addedLines = new Set();
33+
let currentLine = 0;
34+
35+
for (const line of diff.split('\n')) {
36+
const hunkMatch = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,\d+)? @@/);
37+
38+
if (hunkMatch) {
39+
currentLine = parseInt(hunkMatch[1], 10) - 1;
40+
continue;
41+
}
42+
43+
if (line.startsWith('+') && !line.startsWith('+++')) {
44+
currentLine++;
45+
addedLines.add(currentLine);
46+
} else if (!line.startsWith('-')) {
47+
currentLine++;
48+
}
49+
}
50+
51+
return addedLines;
52+
}
53+
54+
const changedFiles = getChangedJsFiles();
55+
if (changedFiles.length === 0) {
56+
console.log('No changed JS files to check.');
57+
process.exit(0);
58+
}
59+
60+
console.log(`Checking ${changedFiles.length} changed JS file(s) for async/await compliance...\n`);
61+
62+
const project = new Project({
63+
compilerOptions: { allowJs: true, noEmit: true },
64+
skipAddingFilesFromTsConfig: true,
65+
});
66+
67+
const filesToCheck = changedFiles.filter(f =>
68+
!f.startsWith('tests/') &&
69+
!f.startsWith('node_modules/') &&
70+
(
71+
f.startsWith('lib/') ||
72+
f.startsWith('bin/') ||
73+
!f.includes('/')
74+
)
75+
);
76+
if (filesToCheck.length === 0) {
77+
console.log('No source JS files in diff (tests and node_modules excluded).');
78+
process.exit(0);
79+
}
80+
81+
project.addSourceFilesAtPaths(filesToCheck);
82+
83+
const violations = [];
84+
85+
for (const sourceFile of project.getSourceFiles()) {
86+
const filePath = sourceFile.getFilePath().replace(process.cwd() + '/', '');
87+
const addedLines = getAddedLineNumbers(filePath);
88+
89+
if (addedLines.size === 0) continue;
90+
91+
const functions = [
92+
...sourceFile.getDescendantsOfKind(SyntaxKind.FunctionDeclaration),
93+
...sourceFile.getDescendantsOfKind(SyntaxKind.FunctionExpression),
94+
...sourceFile.getDescendantsOfKind(SyntaxKind.ArrowFunction),
95+
...sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration),
96+
];
97+
98+
for (const fn of functions) {
99+
if (fn.isAsync()) continue;
100+
101+
const startLine = fn.getStartLineNumber();
102+
if (!addedLines.has(startLine)) continue;
103+
104+
const params = fn.getParameters();
105+
const lastParam = params[params.length - 1];
106+
if (lastParam && CALLBACK_PARAM_PATTERN.test(lastParam.getName())) {
107+
violations.push({
108+
file: filePath,
109+
line: startLine,
110+
type: 'callback',
111+
detail: `function has callback parameter '${lastParam.getName()}'`,
112+
});
113+
}
114+
}
115+
116+
const propertyAccesses = sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression);
117+
for (const access of propertyAccesses) {
118+
if (access.getName() !== 'then') continue;
119+
const line = access.getStartLineNumber();
120+
if (addedLines.has(line)) {
121+
violations.push({
122+
file: filePath,
123+
line,
124+
type: 'then-chain',
125+
detail: 'use await instead of .then()',
126+
});
127+
}
128+
}
129+
}
130+
131+
if (violations.length === 0) {
132+
console.log('✓ All new code in the diff uses async/await.');
133+
process.exit(0);
134+
}
135+
136+
console.error(`✗ Found ${violations.length} async/await violation(s) in the diff:\n`);
137+
for (const v of violations) {
138+
console.error(` ${v.file}:${v.line} [${v.type}] ${v.detail}`);
139+
}
140+
console.error('\nNew code must use async/await instead of callbacks or .then() chains.');
141+
console.error('See the async/await migration guide in CONTRIBUTING.md for help.');
142+
process.exit(1);
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/**
2+
* Count async vs callback-style functions across the codebase using ts-morph.
3+
* Used in CI to track async/await migration progress over time.
4+
*
5+
* Usage: node scripts/count-async-functions.mjs
6+
*/
7+
import { Project, SyntaxKind } from 'ts-morph';
8+
9+
const project = new Project({
10+
compilerOptions: {
11+
allowJs: true,
12+
noEmit: true,
13+
},
14+
skipAddingFilesFromTsConfig: true,
15+
});
16+
17+
project.addSourceFilesAtPaths([
18+
'lib/**/*.js',
19+
'index.js',
20+
'dataserver.js',
21+
'mdserver.js',
22+
'managementAgent.js',
23+
'bin/**/*.js'
24+
]);
25+
26+
let asyncFunctions = 0;
27+
let totalFunctions = 0;
28+
let callbackFunctions = 0;
29+
let thenChains = 0;
30+
31+
const CALLBACK_PARAM_PATTERN = /^(cb|callback|next|done|err)$/i;
32+
33+
for (const sourceFile of project.getSourceFiles()) {
34+
const functions = [
35+
...sourceFile.getDescendantsOfKind(SyntaxKind.FunctionDeclaration),
36+
...sourceFile.getDescendantsOfKind(SyntaxKind.FunctionExpression),
37+
...sourceFile.getDescendantsOfKind(SyntaxKind.ArrowFunction),
38+
...sourceFile.getDescendantsOfKind(SyntaxKind.MethodDeclaration),
39+
];
40+
41+
for (const fn of functions) {
42+
totalFunctions++;
43+
44+
if (fn.isAsync()) {
45+
asyncFunctions++;
46+
continue;
47+
}
48+
49+
const params = fn.getParameters();
50+
const lastParam = params[params.length - 1];
51+
if (lastParam && CALLBACK_PARAM_PATTERN.test(lastParam.getName())) {
52+
callbackFunctions++;
53+
}
54+
}
55+
56+
const propertyAccesses = sourceFile.getDescendantsOfKind(SyntaxKind.PropertyAccessExpression);
57+
for (const access of propertyAccesses) {
58+
if (access.getName() === 'then') {
59+
thenChains++;
60+
}
61+
}
62+
}
63+
64+
const migrationPercent = totalFunctions > 0
65+
? ((asyncFunctions / totalFunctions) * 100).toFixed(1)
66+
: '0.0';
67+
68+
console.log('=== Async/Await Migration Progress ===');
69+
console.log(`Total functions: ${totalFunctions}`);
70+
console.log(`Async functions: ${asyncFunctions} (${migrationPercent}%)`);
71+
console.log(`Callback functions: ${callbackFunctions}`);
72+
console.log(`Remaining .then(): ${thenChains}`);
73+
console.log('');
74+
console.log(`Migration: ${asyncFunctions}/${totalFunctions} functions (${migrationPercent}%)`);
75+
76+
if (process.env.GITHUB_STEP_SUMMARY) {
77+
const { writeFileSync, appendFileSync } = await import('node:fs');
78+
appendFileSync(process.env.GITHUB_STEP_SUMMARY, [
79+
'## Async/Await Migration Progress',
80+
'',
81+
`| Metric | Count |`,
82+
`|--------|-------|`,
83+
`| Total functions | ${totalFunctions} |`,
84+
`| Async functions | ${asyncFunctions} (${migrationPercent}%) |`,
85+
`| Callback-style functions | ${callbackFunctions} |`,
86+
`| Remaining \`.then()\` chains | ${thenChains} |`,
87+
'',
88+
].join('\n'));
89+
}

.github/workflows/tests.yaml

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,29 @@ jobs:
9696
- name: Install python deps
9797
run: pip install flake8
9898
- name: Lint Javascript
99-
run: yarn run --silent lint -- --max-warnings 0
99+
run: yarn run --silent lint
100100
- name: Lint Markdown
101101
run: yarn run --silent lint_md
102102
- name: Lint python
103103
run: flake8 $(git ls-files "*.py")
104104
- name: Lint Yaml
105105
run: yamllint -c yamllint.yml $(git ls-files "*.yml")
106+
- name: Check async/await compliance in diff
107+
run: yarn run check-diff-async
108+
109+
async-migration-report:
110+
runs-on: ubuntu-24.04
111+
steps:
112+
- name: Checkout
113+
uses: actions/checkout@v4
114+
- uses: actions/setup-node@v4
115+
with:
116+
node-version: '22'
117+
cache: yarn
118+
- name: install dependencies
119+
run: yarn install --frozen-lockfile --network-concurrency 1
120+
- name: Count async/await migration progress
121+
run: yarn run count-async
106122

107123
unit-tests:
108124
runs-on: ubuntu-24.04

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@
144144
"multiple_backend_test": "CI=true S3BACKEND=mem S3METADATA=mem S3DATA=multiple mocha --reporter mocha-multi-reporters --reporter-options configFile=$INIT_CWD/tests/reporter-config.json -t 20000 --recursive tests/multipleBackend --exit",
145145
"cover": "nyc --clean --silent yarn run",
146146
"postcover": "nyc report --report-dir ./coverage/test --reporter=lcov",
147-
"count-async": "node scripts/count-async-functions.mjs",
148-
"check-diff-async": "node scripts/check-diff-async.mjs"
147+
"count-async": "node .github/scripts/count-async-functions.mjs",
148+
"check-diff-async": "node .github/scripts/check-diff-async.mjs"
149149
}
150150
}

0 commit comments

Comments
 (0)