@nitra/cursor 1.8.203 → 1.8.206
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -1
- package/bin/auto-rules.md +2 -0
- package/mdc/rego.mdc +77 -0
- package/package.json +1 -1
- package/policy/ga/{clean-ga-workflows.rego → clean_ga_workflows/clean_ga_workflows.rego} +49 -46
- package/policy/ga/clean_merged_branch/clean_merged_branch.rego +167 -0
- package/policy/ga/git_ai/git_ai.rego +109 -0
- package/policy/ga/lint_ga/lint_ga.rego +144 -0
- package/scripts/auto-rules.mjs +10 -0
- package/scripts/check-adr.mjs +4 -1
- package/scripts/check-ga.mjs +0 -504
- package/scripts/check-hasura.mjs +3 -3
- package/scripts/check-js-run.mjs +1 -4
- package/scripts/check-k8s.mjs +2 -1
- package/scripts/lint-ga.mjs +27 -5
- package/scripts/lint-rego.mjs +67 -21
- package/scripts/run-shellcheck-text.mjs +1 -4
- package/scripts/utils/depcheck-workflow.mjs +2 -6
package/scripts/check-ga.mjs
CHANGED
|
@@ -24,15 +24,11 @@ import { join } from 'node:path'
|
|
|
24
24
|
|
|
25
25
|
import { createCheckReporter } from './utils/check-reporter.mjs'
|
|
26
26
|
import {
|
|
27
|
-
anyRunStepIncludes,
|
|
28
27
|
eventPathsIncludeExact,
|
|
29
28
|
findForbiddenUsesOrRunPatterns,
|
|
30
29
|
findRunStepsWithShellLineContinuationBackslash,
|
|
31
30
|
hasAnyStepUsesContaining,
|
|
32
31
|
hasCheckoutBeforeLocalSetupBunDeps,
|
|
33
|
-
flattenWorkflowSteps,
|
|
34
|
-
getStepRun,
|
|
35
|
-
getStepUses,
|
|
36
32
|
parseWorkflowYaml
|
|
37
33
|
} from './utils/gha-workflow.mjs'
|
|
38
34
|
import { resolveCmd } from './utils/resolve-cmd.mjs'
|
|
@@ -160,370 +156,6 @@ function getObjKey(obj, key) {
|
|
|
160
156
|
: undefined
|
|
161
157
|
}
|
|
162
158
|
|
|
163
|
-
/**
|
|
164
|
-
* Очікує, що значення є рядком рівно `expected`.
|
|
165
|
-
* @param {unknown} v значення
|
|
166
|
-
* @param {string} expected очікуваний рядок
|
|
167
|
-
* @returns {boolean} true, якщо збігається
|
|
168
|
-
*/
|
|
169
|
-
function isExactString(v, expected) {
|
|
170
|
-
return typeof v === 'string' && v === expected
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Перевіряє крок dmvict/clean-workflow-runs@v1 у `clean-ga-workflows.yml`.
|
|
175
|
-
* @param {unknown} step0 перший крок workflow
|
|
176
|
-
* @param {(msg: string) => void} passFn pass
|
|
177
|
-
* @param {(msg: string) => void} failFn fail
|
|
178
|
-
*/
|
|
179
|
-
function validateCleanGaWorkflowsStep0(step0, passFn, failFn) {
|
|
180
|
-
if (!isExactString(getObjKey(step0, 'name'), 'Delete workflow runs')) {
|
|
181
|
-
failFn('clean-ga-workflows.yml: перший крок має мати name: Delete workflow runs (ga.mdc)')
|
|
182
|
-
}
|
|
183
|
-
if (!isExactString(getObjKey(step0, 'uses'), 'dmvict/clean-workflow-runs@v1')) {
|
|
184
|
-
failFn('clean-ga-workflows.yml: перший крок має uses: dmvict/clean-workflow-runs@v1 (ga.mdc)')
|
|
185
|
-
}
|
|
186
|
-
const withObj = getObjKey(step0, 'with')
|
|
187
|
-
const githubToken = ['$', '{{ github.token }}'].join('')
|
|
188
|
-
if (
|
|
189
|
-
getObjKey(withObj, 'token') === githubToken &&
|
|
190
|
-
getObjKey(withObj, 'save_period') === 31 &&
|
|
191
|
-
getObjKey(withObj, 'save_min_runs_number') === 0
|
|
192
|
-
) {
|
|
193
|
-
passFn('clean-ga-workflows.yml: jobs/steps OK')
|
|
194
|
-
} else {
|
|
195
|
-
failFn('clean-ga-workflows.yml: with має містити token/save_period/save_min_runs_number як у ga.mdc')
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Перевіряє структуру workflow `clean-ga-workflows.yml` (ga.mdc).
|
|
201
|
-
* @param {Record<string, unknown> | null} root parsed YAML
|
|
202
|
-
* @param {(msg: string) => void} passFn pass
|
|
203
|
-
* @param {(msg: string) => void} failFn fail
|
|
204
|
-
*/
|
|
205
|
-
function validateCleanGaWorkflows(root, passFn, failFn) {
|
|
206
|
-
if (!root) {
|
|
207
|
-
failFn('clean-ga-workflows.yml: YAML не вдалося розібрати (ga.mdc)')
|
|
208
|
-
return
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
if (isExactString(root.name, 'Clean action for removing completed workflow runs')) {
|
|
212
|
-
passFn('clean-ga-workflows.yml: name OK')
|
|
213
|
-
} else {
|
|
214
|
-
failFn('clean-ga-workflows.yml: name має бути "Clean action for removing completed workflow runs" (ga.mdc)')
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
const on = root.on
|
|
218
|
-
const schedule = getObjKey(on, 'schedule')
|
|
219
|
-
const wfDispatch = getObjKey(on, 'workflow_dispatch')
|
|
220
|
-
|
|
221
|
-
const hasCron =
|
|
222
|
-
Array.isArray(schedule) &&
|
|
223
|
-
schedule.some(v => v && typeof v === 'object' && /** @type {Record<string, unknown>} */ (v).cron === '0 1 16 * *')
|
|
224
|
-
|
|
225
|
-
if (hasCron) {
|
|
226
|
-
passFn('clean-ga-workflows.yml: cron OK')
|
|
227
|
-
} else {
|
|
228
|
-
failFn("clean-ga-workflows.yml: on.schedule має містити cron: '0 1 16 * *' (ga.mdc)")
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
if (!wfDispatch || typeof wfDispatch !== 'object') {
|
|
232
|
-
failFn('clean-ga-workflows.yml: має бути workflow_dispatch: {} (ga.mdc)')
|
|
233
|
-
} else {
|
|
234
|
-
passFn('clean-ga-workflows.yml: workflow_dispatch OK')
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
validateConcurrencyOnRoot('clean-ga-workflows.yml', root, passFn, failFn)
|
|
238
|
-
|
|
239
|
-
const jobs = getObjKey(root, 'jobs')
|
|
240
|
-
const job = getObjKey(jobs, 'cleanup_old_workflows')
|
|
241
|
-
if (!job) {
|
|
242
|
-
failFn('clean-ga-workflows.yml: jobs.cleanup_old_workflows відсутній (ga.mdc)')
|
|
243
|
-
return
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
if (!isExactString(getObjKey(job, 'runs-on'), 'ubuntu-latest')) {
|
|
247
|
-
failFn('clean-ga-workflows.yml: runs-on має бути ubuntu-latest (ga.mdc)')
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const perm = getObjKey(job, 'permissions')
|
|
251
|
-
if (!(getObjKey(perm, 'actions') === 'write' && getObjKey(perm, 'contents') === 'read')) {
|
|
252
|
-
failFn('clean-ga-workflows.yml: permissions мають бути actions: write, contents: read (ga.mdc)')
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const steps = getObjKey(job, 'steps')
|
|
256
|
-
const step0 = Array.isArray(steps) ? steps[0] : null
|
|
257
|
-
if (!step0 || typeof step0 !== 'object') {
|
|
258
|
-
failFn('clean-ga-workflows.yml: steps має містити крок з dmvict/clean-workflow-runs@v1 (ga.mdc)')
|
|
259
|
-
return
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
validateCleanGaWorkflowsStep0(step0, passFn, failFn)
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
/**
|
|
266
|
-
* Перевіряє крок `phpdocker-io/github-actions-delete-abandoned-branches` у `clean-merged-branch.yml`.
|
|
267
|
-
* @param {unknown} step0 перший крок workflow
|
|
268
|
-
* @param {(msg: string) => void} failFn fail
|
|
269
|
-
*/
|
|
270
|
-
function validateCleanMergedBranchStep0(step0, failFn) {
|
|
271
|
-
if (!isExactString(getObjKey(step0, 'id'), 'delete_stuff')) {
|
|
272
|
-
failFn('clean-merged-branch.yml: перший крок має id: delete_stuff (ga.mdc)')
|
|
273
|
-
}
|
|
274
|
-
if (!isExactString(getObjKey(step0, 'uses'), 'phpdocker-io/github-actions-delete-abandoned-branches@v2.0.3')) {
|
|
275
|
-
failFn('clean-merged-branch.yml: перший крок має uses як у ga.mdc')
|
|
276
|
-
}
|
|
277
|
-
const withObj = getObjKey(step0, 'with')
|
|
278
|
-
const ghToken = ['$', '{{ github.token }}'].join('')
|
|
279
|
-
if (getObjKey(withObj, 'github_token') !== ghToken) {
|
|
280
|
-
failFn(['clean-merged-branch.yml: with.github_token має бути $', '{{ github.token }} (ga.mdc)'].join(''))
|
|
281
|
-
}
|
|
282
|
-
if (getObjKey(withObj, 'last_commit_age_days') !== 90) {
|
|
283
|
-
failFn('clean-merged-branch.yml: with.last_commit_age_days має бути 90 (ga.mdc)')
|
|
284
|
-
}
|
|
285
|
-
const ignoreBranches = String(getObjKey(withObj, 'ignore_branches') ?? '')
|
|
286
|
-
if (!(ignoreBranches.includes('main') && ignoreBranches.includes('dev'))) {
|
|
287
|
-
failFn('clean-merged-branch.yml: with.ignore_branches має містити main,dev (ga.mdc)')
|
|
288
|
-
}
|
|
289
|
-
if (getObjKey(withObj, 'dry_run') !== 'no') {
|
|
290
|
-
failFn('clean-merged-branch.yml: with.dry_run має бути no (ga.mdc)')
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Перевіряє крок виводу в `clean-merged-branch.yml`.
|
|
296
|
-
* @param {unknown} step1 другий крок workflow
|
|
297
|
-
* @param {(msg: string) => void} passFn pass
|
|
298
|
-
* @param {(msg: string) => void} failFn fail
|
|
299
|
-
*/
|
|
300
|
-
function validateCleanMergedBranchStep1(step1, passFn, failFn) {
|
|
301
|
-
if (!isExactString(getObjKey(step1, 'name'), 'Get output')) {
|
|
302
|
-
failFn('clean-merged-branch.yml: другий крок має name: Get output (ga.mdc)')
|
|
303
|
-
}
|
|
304
|
-
const env = getObjKey(step1, 'env')
|
|
305
|
-
const deletedBranchesExpr = ['$', '{{ steps.delete_stuff.outputs.deleted_branches }}'].join('')
|
|
306
|
-
if (getObjKey(env, 'DELETED_BRANCHES') !== deletedBranchesExpr) {
|
|
307
|
-
failFn('clean-merged-branch.yml: env.DELETED_BRANCHES має бути як у ga.mdc')
|
|
308
|
-
}
|
|
309
|
-
const echoDeletedBranches = ['echo "Deleted branches: $', '{DELETED_BRANCHES}"'].join('')
|
|
310
|
-
if (String(getObjKey(step1, 'run') ?? '').includes(echoDeletedBranches)) {
|
|
311
|
-
passFn('clean-merged-branch.yml: jobs/steps OK')
|
|
312
|
-
} else {
|
|
313
|
-
failFn('clean-merged-branch.yml: run має echo Deleted branches як у ga.mdc')
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Перевіряє структуру workflow `clean-merged-branch.yml` (ga.mdc).
|
|
319
|
-
* @param {Record<string, unknown> | null} root parsed YAML
|
|
320
|
-
* @param {(msg: string) => void} passFn pass
|
|
321
|
-
* @param {(msg: string) => void} failFn fail
|
|
322
|
-
*/
|
|
323
|
-
function validateCleanMergedBranch(root, passFn, failFn) {
|
|
324
|
-
if (!root) {
|
|
325
|
-
failFn('clean-merged-branch.yml: YAML не вдалося розібрати (ga.mdc)')
|
|
326
|
-
return
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (isExactString(root.name, 'Clean abandoned branches')) {
|
|
330
|
-
passFn('clean-merged-branch.yml: name OK')
|
|
331
|
-
} else {
|
|
332
|
-
failFn('clean-merged-branch.yml: name має бути "Clean abandoned branches" (ga.mdc)')
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
const on = root.on
|
|
336
|
-
const schedule = getObjKey(on, 'schedule')
|
|
337
|
-
const wfDispatch = getObjKey(on, 'workflow_dispatch')
|
|
338
|
-
const hasCron =
|
|
339
|
-
Array.isArray(schedule) &&
|
|
340
|
-
schedule.some(v => v && typeof v === 'object' && /** @type {Record<string, unknown>} */ (v).cron === '0 1 15 * *')
|
|
341
|
-
|
|
342
|
-
if (hasCron) {
|
|
343
|
-
passFn('clean-merged-branch.yml: cron OK')
|
|
344
|
-
} else {
|
|
345
|
-
failFn("clean-merged-branch.yml: on.schedule має містити cron: '0 1 15 * *' (ga.mdc)")
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
if (!wfDispatch || typeof wfDispatch !== 'object') {
|
|
349
|
-
failFn('clean-merged-branch.yml: має бути workflow_dispatch: {} (ga.mdc)')
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
validateConcurrencyOnRoot('clean-merged-branch.yml', root, passFn, failFn)
|
|
353
|
-
|
|
354
|
-
const jobs = getObjKey(root, 'jobs')
|
|
355
|
-
const job = getObjKey(jobs, 'cleanup_old_branches')
|
|
356
|
-
if (!job) {
|
|
357
|
-
failFn('clean-merged-branch.yml: jobs.cleanup_old_branches відсутній (ga.mdc)')
|
|
358
|
-
return
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
const perm = getObjKey(job, 'permissions')
|
|
362
|
-
if (getObjKey(perm, 'contents') !== 'write') {
|
|
363
|
-
failFn('clean-merged-branch.yml: permissions мають бути contents: write (ga.mdc)')
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
const steps = getObjKey(job, 'steps')
|
|
367
|
-
if (!Array.isArray(steps) || steps.length < 2) {
|
|
368
|
-
failFn('clean-merged-branch.yml: steps має містити 2 кроки як у ga.mdc')
|
|
369
|
-
return
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
const step0 = steps[0]
|
|
373
|
-
if (!step0 || typeof step0 !== 'object') {
|
|
374
|
-
failFn('clean-merged-branch.yml: перший крок невалідний (ga.mdc)')
|
|
375
|
-
return
|
|
376
|
-
}
|
|
377
|
-
validateCleanMergedBranchStep0(step0, failFn)
|
|
378
|
-
|
|
379
|
-
const step1 = steps[1]
|
|
380
|
-
if (!step1 || typeof step1 !== 'object') {
|
|
381
|
-
failFn('clean-merged-branch.yml: другий крок невалідний (ga.mdc)')
|
|
382
|
-
return
|
|
383
|
-
}
|
|
384
|
-
validateCleanMergedBranchStep1(step1, passFn, failFn)
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
/**
|
|
388
|
-
* Перевіряє тригери `on.push` / `on.pull_request` у `lint-ga.yml`.
|
|
389
|
-
* @param {unknown} on корінь `on:` з YAML
|
|
390
|
-
* @param {(msg: string) => void} failFn fail
|
|
391
|
-
*/
|
|
392
|
-
function validateLintGaOnTriggers(on, failFn) {
|
|
393
|
-
const push = getObjKey(on, 'push')
|
|
394
|
-
const pr = getObjKey(on, 'pull_request')
|
|
395
|
-
const pushBranches = getObjKey(push, 'branches')
|
|
396
|
-
const pushPaths = getObjKey(push, 'paths')
|
|
397
|
-
const prBranches = getObjKey(pr, 'branches')
|
|
398
|
-
|
|
399
|
-
if (!Array.isArray(pushBranches) || !(pushBranches.includes('dev') && pushBranches.includes('main'))) {
|
|
400
|
-
failFn('lint-ga.yml: on.push.branches має містити dev і main (ga.mdc)')
|
|
401
|
-
}
|
|
402
|
-
if (!Array.isArray(prBranches) || !(prBranches.includes('dev') && prBranches.includes('main'))) {
|
|
403
|
-
failFn('lint-ga.yml: on.pull_request.branches має містити dev і main (ga.mdc)')
|
|
404
|
-
}
|
|
405
|
-
if (
|
|
406
|
-
!Array.isArray(pushPaths) ||
|
|
407
|
-
!(pushPaths.includes('.github/actions/**') && pushPaths.includes('.github/workflows/**'))
|
|
408
|
-
) {
|
|
409
|
-
failFn('lint-ga.yml: on.push.paths має містити .github/actions/** і .github/workflows/** (ga.mdc)')
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/**
|
|
414
|
-
* Перевіряє структуру workflow `lint-ga.yml` (ga.mdc).
|
|
415
|
-
* @param {Record<string, unknown> | null} root parsed YAML
|
|
416
|
-
* @param {(msg: string) => void} passFn pass
|
|
417
|
-
* @param {(msg: string) => void} failFn fail
|
|
418
|
-
*/
|
|
419
|
-
function validateLintGaWorkflowStructure(root, passFn, failFn) {
|
|
420
|
-
if (!root) {
|
|
421
|
-
failFn('lint-ga.yml: YAML не вдалося розібрати (ga.mdc)')
|
|
422
|
-
return
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
if (!isExactString(root.name, 'Lint GA')) {
|
|
426
|
-
failFn('lint-ga.yml: name має бути "Lint GA" (ga.mdc)')
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
validateLintGaOnTriggers(root.on, failFn)
|
|
430
|
-
|
|
431
|
-
validateConcurrencyOnRoot('lint-ga.yml', root, passFn, failFn)
|
|
432
|
-
|
|
433
|
-
const jobs = getObjKey(root, 'jobs')
|
|
434
|
-
const job = getObjKey(jobs, 'lint-ga')
|
|
435
|
-
if (!job) {
|
|
436
|
-
failFn('lint-ga.yml: jobs.lint-ga відсутній (ga.mdc)')
|
|
437
|
-
return
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
if (!isExactString(getObjKey(job, 'runs-on'), 'ubuntu-latest')) {
|
|
441
|
-
failFn('lint-ga.yml: runs-on має бути ubuntu-latest (ga.mdc)')
|
|
442
|
-
}
|
|
443
|
-
const perm = getObjKey(job, 'permissions')
|
|
444
|
-
if (getObjKey(perm, 'contents') !== 'read') {
|
|
445
|
-
failFn('lint-ga.yml: permissions мають бути contents: read (ga.mdc)')
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
const steps = getObjKey(job, 'steps')
|
|
449
|
-
if (!Array.isArray(steps) || steps.length === 0) {
|
|
450
|
-
failFn('lint-ga.yml: jobs.lint-ga.steps відсутні (ga.mdc)')
|
|
451
|
-
return
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const flat = flattenWorkflowSteps(root)
|
|
455
|
-
const usesList = new Set(flat.map(s => getStepUses(s.step)))
|
|
456
|
-
const runBlob = flat.map(s => getStepRun(s.step)).join('\n')
|
|
457
|
-
|
|
458
|
-
if (!usesList.has('actions/checkout@v6')) {
|
|
459
|
-
failFn('lint-ga.yml: має бути uses: actions/checkout@v6 (ga.mdc)')
|
|
460
|
-
}
|
|
461
|
-
if (!usesList.has('./.github/actions/setup-bun-deps')) {
|
|
462
|
-
failFn('lint-ga.yml: має бути uses: ./.github/actions/setup-bun-deps (ga.mdc)')
|
|
463
|
-
}
|
|
464
|
-
if (!usesList.has('astral-sh/setup-uv@v8.0.0')) {
|
|
465
|
-
failFn('lint-ga.yml: має бути uses: astral-sh/setup-uv@v8.0.0 (ga.mdc)')
|
|
466
|
-
}
|
|
467
|
-
if (runBlob.includes('bun run lint-ga')) {
|
|
468
|
-
passFn('lint-ga.yml: структура jobs/steps OK')
|
|
469
|
-
} else {
|
|
470
|
-
failFn('lint-ga.yml: має бути крок run: bun run lint-ga (ga.mdc)')
|
|
471
|
-
}
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
/**
|
|
475
|
-
* Перевіряє структуру workflow `git-ai.yml` (ga.mdc).
|
|
476
|
-
* @param {Record<string, unknown> | null} root parsed YAML
|
|
477
|
-
* @param {(msg: string) => void} passFn pass
|
|
478
|
-
* @param {(msg: string) => void} failFn fail
|
|
479
|
-
*/
|
|
480
|
-
function validateGitAiWorkflowStructure(root, passFn, failFn) {
|
|
481
|
-
if (!root) {
|
|
482
|
-
failFn('git-ai.yml: YAML не вдалося розібрати (ga.mdc)')
|
|
483
|
-
return
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (!isExactString(root.name, 'Git AI')) {
|
|
487
|
-
failFn('git-ai.yml: name має бути "Git AI" (ga.mdc)')
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
const on = root.on
|
|
491
|
-
const pr = getObjKey(on, 'pull_request')
|
|
492
|
-
const types = getObjKey(pr, 'types')
|
|
493
|
-
if (!Array.isArray(types) || !types.includes('closed')) {
|
|
494
|
-
failFn('git-ai.yml: on.pull_request.types має містити closed (ga.mdc)')
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
validateConcurrencyOnRoot('git-ai.yml', root, passFn, failFn)
|
|
498
|
-
|
|
499
|
-
const jobs = getObjKey(root, 'jobs')
|
|
500
|
-
const job = getObjKey(jobs, 'git-ai')
|
|
501
|
-
if (!job) {
|
|
502
|
-
failFn('git-ai.yml: jobs.git-ai відсутній (ga.mdc)')
|
|
503
|
-
return
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
if (!String(getObjKey(job, 'if') ?? '').includes('github.event.pull_request.merged == true')) {
|
|
507
|
-
failFn('git-ai.yml: job має містити if: github.event.pull_request.merged == true (ga.mdc)')
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
const perm = getObjKey(job, 'permissions')
|
|
511
|
-
if (getObjKey(perm, 'contents') !== 'write') {
|
|
512
|
-
failFn('git-ai.yml: permissions мають бути contents: write (ga.mdc)')
|
|
513
|
-
}
|
|
514
|
-
|
|
515
|
-
const flat = flattenWorkflowSteps(root)
|
|
516
|
-
const runBlob = flat.map(s => getStepRun(s.step)).join('\n')
|
|
517
|
-
if (!runBlob.includes('curl -fsSL https://usegitai.com/install.sh | bash')) {
|
|
518
|
-
failFn('git-ai.yml: має встановлювати git-ai через curl | bash (ga.mdc)')
|
|
519
|
-
}
|
|
520
|
-
if (runBlob.includes('git-ai ci github run')) {
|
|
521
|
-
passFn('git-ai.yml: структура jobs/steps OK')
|
|
522
|
-
} else {
|
|
523
|
-
failFn('git-ai.yml: має виконувати git-ai ci github run (ga.mdc)')
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
|
|
527
159
|
/**
|
|
528
160
|
* Перевіряє блок `concurrency` на вже розпарсеному корені workflow (ga.mdc).
|
|
529
161
|
*
|
|
@@ -838,33 +470,6 @@ function checkShellcheckInstalled(passFn, failFn) {
|
|
|
838
470
|
)
|
|
839
471
|
}
|
|
840
472
|
|
|
841
|
-
/**
|
|
842
|
-
* Перевіряє lint-ga.yml workflow.
|
|
843
|
-
* @param {string} wfDir директорія workflows
|
|
844
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
845
|
-
* @param {(msg: string) => void} failFn callback при помилці
|
|
846
|
-
*/
|
|
847
|
-
async function checkLintGaWorkflow(wfDir, passFn, failFn) {
|
|
848
|
-
const lintGaWf = join(wfDir, 'lint-ga.yml')
|
|
849
|
-
if (!existsSync(lintGaWf)) return
|
|
850
|
-
const lgContent = await readFile(lintGaWf, 'utf8')
|
|
851
|
-
const root = parseWorkflowYaml(lgContent)
|
|
852
|
-
const hasBunRun = root ? anyRunStepIncludes(root, 'bun run lint-ga') : lgContent.includes('bun run lint-ga')
|
|
853
|
-
const hasSetupUv = root
|
|
854
|
-
? hasAnyStepUsesContaining(root, ['astral-sh/setup-uv']) || lgContent.includes('astral-sh/setup-uv')
|
|
855
|
-
: lgContent.includes('astral-sh/setup-uv')
|
|
856
|
-
if (hasBunRun) {
|
|
857
|
-
passFn('lint-ga.yml викликає bun run lint-ga')
|
|
858
|
-
} else {
|
|
859
|
-
failFn('lint-ga.yml: крок має містити bun run lint-ga')
|
|
860
|
-
}
|
|
861
|
-
if (hasSetupUv) {
|
|
862
|
-
passFn('lint-ga.yml містить astral-sh/setup-uv')
|
|
863
|
-
} else {
|
|
864
|
-
failFn('lint-ga.yml: додай astral-sh/setup-uv для uvx zizmor (ga.mdc)')
|
|
865
|
-
}
|
|
866
|
-
}
|
|
867
|
-
|
|
868
473
|
/**
|
|
869
474
|
* Перевіряє розширення workflow-файлів і наявність обов'язкових workflow.
|
|
870
475
|
* @param {string} wfDir шлях до директорії workflows
|
|
@@ -898,111 +503,6 @@ function checkGaWorkflowFiles(wfDir, files, pass, fail) {
|
|
|
898
503
|
}
|
|
899
504
|
}
|
|
900
505
|
|
|
901
|
-
/**
|
|
902
|
-
* Перевіряє, чи on.pull_request.types у parsed YAML містить 'closed'.
|
|
903
|
-
* @param {Record<string, unknown>} root розібраний YAML workflow
|
|
904
|
-
* @returns {boolean} true, якщо тригер pull_request має тип closed
|
|
905
|
-
*/
|
|
906
|
-
function hasPullRequestClosedTrigger(root) {
|
|
907
|
-
const on = root.on
|
|
908
|
-
if (!on || typeof on !== 'object') return false
|
|
909
|
-
const pr = /** @type {Record<string, unknown>} */ (on)['pull_request']
|
|
910
|
-
if (!pr || typeof pr !== 'object') return false
|
|
911
|
-
const types = /** @type {Record<string, unknown>} */ (pr).types
|
|
912
|
-
return Array.isArray(types) && types.includes('closed')
|
|
913
|
-
}
|
|
914
|
-
|
|
915
|
-
/**
|
|
916
|
-
* Перевіряє, чи будь-який job у parsed YAML має if-умову з 'merged'.
|
|
917
|
-
* @param {Record<string, unknown>} root розібраний YAML workflow
|
|
918
|
-
* @returns {boolean} true, якщо хоча б один job містить умову merged
|
|
919
|
-
*/
|
|
920
|
-
function hasJobMergedCondition(root) {
|
|
921
|
-
const { jobs } = root
|
|
922
|
-
if (!jobs || typeof jobs !== 'object') return false
|
|
923
|
-
return Object.values(jobs).some(job => {
|
|
924
|
-
if (!job || typeof job !== 'object') return false
|
|
925
|
-
const ifCond = String(/** @type {Record<string, unknown>} */ (job).if ?? '')
|
|
926
|
-
return ifCond.includes('merged')
|
|
927
|
-
})
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
/**
|
|
931
|
-
* Перевіряє parsed YAML git-ai.yml: тригер closed та умова merged.
|
|
932
|
-
* @param {Record<string, unknown>} root розібраний YAML workflow
|
|
933
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
934
|
-
* @param {(msg: string) => void} failFn callback при помилці
|
|
935
|
-
*/
|
|
936
|
-
function validateGitAiParsedYaml(root, passFn, failFn) {
|
|
937
|
-
if (hasPullRequestClosedTrigger(root)) {
|
|
938
|
-
passFn('git-ai.yml: on.pull_request.types містить closed')
|
|
939
|
-
} else {
|
|
940
|
-
failFn('git-ai.yml: on.pull_request.types має містити closed (ga.mdc)')
|
|
941
|
-
}
|
|
942
|
-
|
|
943
|
-
if (hasJobMergedCondition(root)) {
|
|
944
|
-
passFn('git-ai.yml: job має умову merged')
|
|
945
|
-
} else {
|
|
946
|
-
failFn('git-ai.yml: job має містити if: github.event.pull_request.merged == true (ga.mdc)')
|
|
947
|
-
}
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
/**
|
|
951
|
-
* Перевіряє git-ai.yml: тригер pull_request з types: [closed], умова merged у job, виклик git-ai.
|
|
952
|
-
* @param {string} wfDir директорія workflows
|
|
953
|
-
* @param {(msg: string) => void} passFn callback при успішній перевірці
|
|
954
|
-
* @param {(msg: string) => void} failFn callback при помилці
|
|
955
|
-
*/
|
|
956
|
-
async function checkGitAiWorkflow(wfDir, passFn, failFn) {
|
|
957
|
-
const gitAiWf = join(wfDir, 'git-ai.yml')
|
|
958
|
-
if (!existsSync(gitAiWf)) return
|
|
959
|
-
const content = await readFile(gitAiWf, 'utf8')
|
|
960
|
-
const root = parseWorkflowYaml(content)
|
|
961
|
-
|
|
962
|
-
if (root) {
|
|
963
|
-
validateGitAiParsedYaml(root, passFn, failFn)
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
const hasGitAiRun = root ? anyRunStepIncludes(root, 'git-ai ci github run') : content.includes('git-ai ci github run')
|
|
967
|
-
if (hasGitAiRun) {
|
|
968
|
-
passFn('git-ai.yml: крок виконує git-ai ci github run')
|
|
969
|
-
} else {
|
|
970
|
-
failFn('git-ai.yml: крок має містити git-ai ci github run (ga.mdc)')
|
|
971
|
-
}
|
|
972
|
-
}
|
|
973
|
-
|
|
974
|
-
/**
|
|
975
|
-
* Перевіряє, що “канонічні” workflows відповідають ga.mdc (структура і значення).
|
|
976
|
-
* @param {string} wfDir директорія workflows
|
|
977
|
-
* @param {(msg: string) => void} passFn pass
|
|
978
|
-
* @param {(msg: string) => void} failFn fail
|
|
979
|
-
*/
|
|
980
|
-
async function checkCanonicalWorkflowsMatchRule(wfDir, passFn, failFn) {
|
|
981
|
-
const paths = {
|
|
982
|
-
cleanGa: join(wfDir, 'clean-ga-workflows.yml'),
|
|
983
|
-
cleanMerged: join(wfDir, 'clean-merged-branch.yml'),
|
|
984
|
-
lintGa: join(wfDir, 'lint-ga.yml'),
|
|
985
|
-
gitAi: join(wfDir, 'git-ai.yml')
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
if (existsSync(paths.cleanGa)) {
|
|
989
|
-
const c = await readFile(paths.cleanGa, 'utf8')
|
|
990
|
-
validateCleanGaWorkflows(parseWorkflowYaml(c), passFn, failFn)
|
|
991
|
-
}
|
|
992
|
-
if (existsSync(paths.cleanMerged)) {
|
|
993
|
-
const c = await readFile(paths.cleanMerged, 'utf8')
|
|
994
|
-
validateCleanMergedBranch(parseWorkflowYaml(c), passFn, failFn)
|
|
995
|
-
}
|
|
996
|
-
if (existsSync(paths.lintGa)) {
|
|
997
|
-
const c = await readFile(paths.lintGa, 'utf8')
|
|
998
|
-
validateLintGaWorkflowStructure(parseWorkflowYaml(c), passFn, failFn)
|
|
999
|
-
}
|
|
1000
|
-
if (existsSync(paths.gitAi)) {
|
|
1001
|
-
const c = await readFile(paths.gitAi, 'utf8')
|
|
1002
|
-
validateGitAiWorkflowStructure(parseWorkflowYaml(c), passFn, failFn)
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
506
|
/**
|
|
1007
507
|
* Перевіряє відповідність проєкту правилам ga.mdc
|
|
1008
508
|
* @returns {Promise<number>} 0 — все OK, 1 — є проблеми
|
|
@@ -1061,12 +561,8 @@ export async function check() {
|
|
|
1061
561
|
}
|
|
1062
562
|
}
|
|
1063
563
|
|
|
1064
|
-
await checkCanonicalWorkflowsMatchRule(wfDir, pass, fail)
|
|
1065
|
-
|
|
1066
564
|
await checkZizmor(pass, fail)
|
|
1067
565
|
await checkLintGaScript(pass, fail)
|
|
1068
|
-
await checkLintGaWorkflow(wfDir, pass, fail)
|
|
1069
|
-
await checkGitAiWorkflow(wfDir, pass, fail)
|
|
1070
566
|
checkShellcheckInstalled(pass, fail)
|
|
1071
567
|
|
|
1072
568
|
return reporter.getExitCode()
|
package/scripts/check-hasura.mjs
CHANGED
|
@@ -45,8 +45,7 @@ const ENV_FILE_RE = /\.env$/u
|
|
|
45
45
|
const HASURA_ENDPOINT_LINE_RE = /^[ \t]*(?:export[ \t]+)?HASURA_GRAPHQL_ENDPOINT[ \t]*=[ \t]*['"]?([^'"\r\n#]+)/mu
|
|
46
46
|
// Дозволяємо два DNS-суфікси кластера: `<name>.internal` (GKE/GCP) і `cluster.local`
|
|
47
47
|
// (стандартний k8s / Yandex Cloud). У YC namespace.yaml + cluster mode дають коротший суфікс.
|
|
48
|
-
const INTERNAL_HASURA_URL_RE =
|
|
49
|
-
/^http:\/\/([^./]+)\.([^./]+)\.svc\.((?:[^./:]+\.internal)|cluster\.local):(\d+)\/?$/u
|
|
48
|
+
const INTERNAL_HASURA_URL_RE = /^http:\/\/([^./]+)\.([^./]+)\.svc\.((?:[^./:]+\.internal)|cluster\.local):(\d+)\/?$/u
|
|
50
49
|
const CLUSTER_LOCAL_SUFFIX = 'cluster.local'
|
|
51
50
|
const INTERNAL_DNS_SUFFIX = '.internal'
|
|
52
51
|
|
|
@@ -151,7 +150,8 @@ async function checkEnvFile(relPath, expected, reporter) {
|
|
|
151
150
|
const parsed = parseInternalHasuraEndpoint(value)
|
|
152
151
|
if (!parsed.ok) {
|
|
153
152
|
// eslint-disable-next-line @microsoft/sdl/no-insecure-url, sonarjs/no-clear-text-protocols -- hasura.mdc вимагає саме http:// для кластерного URL (TLS не використовується)
|
|
154
|
-
const example =
|
|
153
|
+
const example =
|
|
154
|
+
'http://<service>.<namespace>.svc.<cluster>.internal:<port> або http://<service>.<namespace>.svc.cluster.local:<port>'
|
|
155
155
|
fail(
|
|
156
156
|
`${relPath}: HASURA_GRAPHQL_ENDPOINT="${value}" — потрібен внутрішній кластерний URL виду ${example} (hasura.mdc)`
|
|
157
157
|
)
|
package/scripts/check-js-run.mjs
CHANGED
|
@@ -47,10 +47,7 @@ import {
|
|
|
47
47
|
resolveConnDirFromPackageJson
|
|
48
48
|
} from './utils/conn-imports-scan.mjs'
|
|
49
49
|
import { loadCursorIgnorePaths } from './utils/load-cursor-config.mjs'
|
|
50
|
-
import {
|
|
51
|
-
findPromiseSetTimeoutInText,
|
|
52
|
-
isPromiseSetTimeoutScanSourceFile
|
|
53
|
-
} from './utils/promise-settimeout-scan.mjs'
|
|
50
|
+
import { findPromiseSetTimeoutInText, isPromiseSetTimeoutScanSourceFile } from './utils/promise-settimeout-scan.mjs'
|
|
54
51
|
import { walkDir } from './utils/walkDir.mjs'
|
|
55
52
|
import { getMonorepoPackageRootDirs } from './utils/workspaces.mjs'
|
|
56
53
|
|
package/scripts/check-k8s.mjs
CHANGED
|
@@ -524,7 +524,8 @@ function kustomizationPatchSortKey(patchItem) {
|
|
|
524
524
|
const rec = /** @type {Record<string, unknown>} */ (patchItem)
|
|
525
525
|
const t = rec.target
|
|
526
526
|
/** @type {Record<string, unknown>} */
|
|
527
|
-
const target =
|
|
527
|
+
const target =
|
|
528
|
+
t !== null && typeof t === 'object' && !Array.isArray(t) ? /** @type {Record<string, unknown>} */ (t) : {}
|
|
528
529
|
const kind = typeof target.kind === 'string' ? target.kind : ''
|
|
529
530
|
const name = typeof target.name === 'string' ? target.name : ''
|
|
530
531
|
const ns = typeof target.namespace === 'string' ? target.namespace : ''
|
package/scripts/lint-ga.mjs
CHANGED
|
@@ -27,16 +27,36 @@ import { resolveCmd } from './utils/resolve-cmd.mjs'
|
|
|
27
27
|
/** Каталог пакету `@nitra/cursor`, від якого ресолвимо вшиту директорію policy/. */
|
|
28
28
|
const PACKAGE_ROOT = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
29
29
|
|
|
30
|
-
/** Шлях до Rego-полісі
|
|
30
|
+
/** Шлях до кореня Rego-полісі для GA. У npm-tarball публікується через `files: ["policy"]` у package.json. */
|
|
31
31
|
const GA_POLICY_DIR = join(PACKAGE_ROOT, 'policy', 'ga')
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
|
-
* Workflow-файли, для яких маємо відповідну Rego-полісі.
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
* Workflow-файли, для яких маємо відповідну Rego-полісі. Кожен таргет посилається на під-пакет
|
|
35
|
+
* `ga.<name>` у `policy/ga/<name>/<name>.rego`; conftest викликаємо з `--namespace`, щоб правила
|
|
36
|
+
* іншого workflow не застосовувалися до чужого файлу.
|
|
37
|
+
* @type {Array<{ workflow: string, namespace: string, label: string }>}
|
|
37
38
|
*/
|
|
38
39
|
const CONFTEST_TARGETS = [
|
|
39
|
-
{
|
|
40
|
+
{
|
|
41
|
+
workflow: '.github/workflows/clean-ga-workflows.yml',
|
|
42
|
+
namespace: 'ga.clean_ga_workflows',
|
|
43
|
+
label: 'clean-ga-workflows.yml structure'
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
workflow: '.github/workflows/clean-merged-branch.yml',
|
|
47
|
+
namespace: 'ga.clean_merged_branch',
|
|
48
|
+
label: 'clean-merged-branch.yml structure'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
workflow: '.github/workflows/lint-ga.yml',
|
|
52
|
+
namespace: 'ga.lint_ga',
|
|
53
|
+
label: 'lint-ga.yml structure'
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
workflow: '.github/workflows/git-ai.yml',
|
|
57
|
+
namespace: 'ga.git_ai',
|
|
58
|
+
label: 'git-ai.yml structure'
|
|
59
|
+
}
|
|
40
60
|
]
|
|
41
61
|
|
|
42
62
|
/**
|
|
@@ -218,6 +238,8 @@ function runConftestStep() {
|
|
|
218
238
|
target.workflow,
|
|
219
239
|
'-p',
|
|
220
240
|
GA_POLICY_DIR,
|
|
241
|
+
'--namespace',
|
|
242
|
+
target.namespace,
|
|
221
243
|
'--no-color'
|
|
222
244
|
])
|
|
223
245
|
if (code !== 0) return code
|