@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.
@@ -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()
@@ -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 = 'http://<service>.<namespace>.svc.<cluster>.internal:<port> або http://<service>.<namespace>.svc.cluster.local:<port>'
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
  )
@@ -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
 
@@ -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 = t !== null && typeof t === 'object' && !Array.isArray(t) ? /** @type {Record<string, unknown>} */ (t) : {}
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 : ''
@@ -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-полісі (PoC: лише clean-ga-workflows). У npm-tarball публікується через `files` у package.json. */
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-полісі. PoC: один файл; інші підтягуватимемо в міру міграції
35
- * перевірок із `npm/scripts/check-ga.mjs`.
36
- * @type {Array<{ workflow: string, label: string }>}
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
- { workflow: '.github/workflows/clean-ga-workflows.yml', label: 'clean-ga-workflows.yml structure' }
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