@intentius/chant-lexicon-github 0.1.5 → 0.1.7

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "xxhash64",
3
3
  "artifacts": {
4
- "manifest.json": "62337ef86c01e6d1",
4
+ "manifest.json": "4f79e2db52795eab",
5
5
  "meta.json": "2d1cccf3c19883a7",
6
6
  "types/index.d.ts": "9d6f903cca5de1e9",
7
7
  "rules/deprecated-action-version.ts": "9ec91b190557f25f",
@@ -24,11 +24,11 @@
24
24
  "rules/gha023.ts": "2d00140d63591c9",
25
25
  "rules/gha027.ts": "6071aedb178c90a8",
26
26
  "rules/yaml-helpers.ts": "df426df288c175c9",
27
- "rules/gha024.ts": "ed75a2900c8bf12d",
27
+ "rules/gha024.ts": "88df51d8e5a01651",
28
28
  "rules/gha025.ts": "d196899f490521ba",
29
29
  "rules/gha006.ts": "baca27402ba18d",
30
30
  "rules/gha028.ts": "9c1ba1eb9a93d8b6",
31
- "rules/gha022.ts": "41038ee697a497d1",
31
+ "rules/gha022.ts": "d1dfc25f1409a8bc",
32
32
  "rules/gha011.ts": "105e2d4faeaa9977",
33
33
  "rules/gha020.ts": "36ef5e141524bab0",
34
34
  "rules/gha009.ts": "df140c0cac573bc4",
@@ -37,5 +37,5 @@
37
37
  "skills/chant-github-patterns.md": "7678ef5c6b4b9bdf",
38
38
  "skills/chant-github-security.md": "f3fcfcd84475b73c"
39
39
  },
40
- "composite": "92e041a88a3b5d8d"
40
+ "composite": "a1b4fdb253dcc4a0"
41
41
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "github",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "GitHub",
6
6
  "intrinsics": [
@@ -23,17 +23,20 @@ export const gha022: PostSynthCheck = {
23
23
  const jobsIdx = yaml.search(/^jobs:\s*$/m);
24
24
  if (jobsIdx === -1) continue;
25
25
 
26
- const afterJobs = yaml.slice(jobsIdx + yaml.slice(jobsIdx).indexOf("\n") + 1);
27
- const endMatch = afterJobs.search(/^[a-z]/m);
28
- const jobsContent = endMatch === -1 ? afterJobs : afterJobs.slice(0, endMatch);
26
+ const jobsContent = yaml.slice(jobsIdx + yaml.slice(jobsIdx).indexOf("\n") + 1);
29
27
 
30
28
  for (const [jobName] of jobs) {
31
- // Find this job's section in the raw YAML
32
- const jobPattern = new RegExp(`^ ${jobName}:\\n([\\s\\S]*?)(?=\\n [a-z]|$)`, "m");
33
- const jobSection = jobsContent.match(jobPattern);
34
- const section = jobSection ? jobSection[0] : "";
29
+ const jobHeader = ` ${jobName}:\n`;
30
+ const start = jobsContent.indexOf(jobHeader);
31
+ if (start === -1) continue;
35
32
 
36
- if (!/timeout-minutes:/m.test(section)) {
33
+ // Slice from after the job header to find its content.
34
+ // Use literal \n \w (no m flag) to locate the next sibling job at 2-space indent.
35
+ const rest = jobsContent.slice(start + jobHeader.length);
36
+ const nextJobMatch = rest.search(/\n \w/);
37
+ const section = nextJobMatch === -1 ? rest : rest.slice(0, nextJobMatch);
38
+
39
+ if (!/timeout-minutes:/.test(section)) {
37
40
  diagnostics.push({
38
41
  checkId: "GHA022",
39
42
  severity: "info",
@@ -27,7 +27,7 @@ export const gha024: PostSynthCheck = {
27
27
 
28
28
  if (!isDeployWorkflow) continue;
29
29
 
30
- if (!/^concurrency:/m.test(yaml)) {
30
+ if (!/^\s*concurrency:/m.test(yaml)) {
31
31
  diagnostics.push({
32
32
  checkId: "GHA024",
33
33
  severity: "info",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-github",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "GitHub Actions lexicon for chant — declarative IaC in TypeScript",
5
5
  "license": "Apache-2.0",
6
6
  "homepage": "https://intentius.io/chant",
@@ -34,6 +34,44 @@ jobs:
34
34
  expect(diags[0].message).toContain("build");
35
35
  });
36
36
 
37
+ test("does not flag job that has timeout-minutes as a later property", () => {
38
+ const yaml = `name: CI
39
+ on:
40
+ push:
41
+ jobs:
42
+ build:
43
+ name: Build
44
+ runs-on: ubuntu-latest
45
+ timeout-minutes: 30
46
+ steps:
47
+ - run: echo test
48
+ `;
49
+ const diags = gha022.check(makeCtx(yaml));
50
+ expect(diags).toHaveLength(0);
51
+ });
52
+
53
+ test("does not flag multi-job workflow where all jobs have timeout-minutes", () => {
54
+ const yaml = `name: CI
55
+ on:
56
+ push:
57
+ jobs:
58
+ test:
59
+ runs-on: ubuntu-latest
60
+ timeout-minutes: 20
61
+ steps:
62
+ - run: cargo test
63
+ build:
64
+ runs-on: ubuntu-latest
65
+ timeout-minutes: 30
66
+ needs:
67
+ - test
68
+ steps:
69
+ - run: cargo build
70
+ `;
71
+ const diags = gha022.check(makeCtx(yaml));
72
+ expect(diags).toHaveLength(0);
73
+ });
74
+
37
75
  test("does not flag workflow without jobs", () => {
38
76
  const yaml = `name: CI
39
77
  on:
@@ -23,17 +23,20 @@ export const gha022: PostSynthCheck = {
23
23
  const jobsIdx = yaml.search(/^jobs:\s*$/m);
24
24
  if (jobsIdx === -1) continue;
25
25
 
26
- const afterJobs = yaml.slice(jobsIdx + yaml.slice(jobsIdx).indexOf("\n") + 1);
27
- const endMatch = afterJobs.search(/^[a-z]/m);
28
- const jobsContent = endMatch === -1 ? afterJobs : afterJobs.slice(0, endMatch);
26
+ const jobsContent = yaml.slice(jobsIdx + yaml.slice(jobsIdx).indexOf("\n") + 1);
29
27
 
30
28
  for (const [jobName] of jobs) {
31
- // Find this job's section in the raw YAML
32
- const jobPattern = new RegExp(`^ ${jobName}:\\n([\\s\\S]*?)(?=\\n [a-z]|$)`, "m");
33
- const jobSection = jobsContent.match(jobPattern);
34
- const section = jobSection ? jobSection[0] : "";
29
+ const jobHeader = ` ${jobName}:\n`;
30
+ const start = jobsContent.indexOf(jobHeader);
31
+ if (start === -1) continue;
35
32
 
36
- if (!/timeout-minutes:/m.test(section)) {
33
+ // Slice from after the job header to find its content.
34
+ // Use literal \n \w (no m flag) to locate the next sibling job at 2-space indent.
35
+ const rest = jobsContent.slice(start + jobHeader.length);
36
+ const nextJobMatch = rest.search(/\n \w/);
37
+ const section = nextJobMatch === -1 ? rest : rest.slice(0, nextJobMatch);
38
+
39
+ if (!/timeout-minutes:/.test(section)) {
37
40
  diagnostics.push({
38
41
  checkId: "GHA022",
39
42
  severity: "info",
@@ -51,6 +51,23 @@ jobs:
51
51
  expect(diags).toHaveLength(0);
52
52
  });
53
53
 
54
+ test("does not flag deploy workflow with job-level concurrency", () => {
55
+ const yaml = `name: CI/CD Pipeline
56
+ on:
57
+ push:
58
+ jobs:
59
+ deploy:
60
+ runs-on: ubuntu-latest
61
+ concurrency:
62
+ group: deploy-production
63
+ cancel-in-progress: true
64
+ steps:
65
+ - run: echo deploy
66
+ `;
67
+ const diags = gha024.check(makeCtx(yaml));
68
+ expect(diags).toHaveLength(0);
69
+ });
70
+
54
71
  test("does not flag non-deploy workflow without concurrency", () => {
55
72
  const yaml = `name: CI
56
73
  on:
@@ -27,7 +27,7 @@ export const gha024: PostSynthCheck = {
27
27
 
28
28
  if (!isDeployWorkflow) continue;
29
29
 
30
- if (!/^concurrency:/m.test(yaml)) {
30
+ if (!/^\s*concurrency:/m.test(yaml)) {
31
31
  diagnostics.push({
32
32
  checkId: "GHA024",
33
33
  severity: "info",
@@ -319,7 +319,8 @@ describe("multi-workflow output", () => {
319
319
  expect(typeof output).toBe("object");
320
320
  const result = output as { primary: string; files: Record<string, string> };
321
321
  expect(result.files).toBeDefined();
322
- expect(Object.keys(result.files).length).toBe(2);
322
+ expect(Object.keys(result.files).length).toBe(1);
323
+ expect(result.files["ci.yml"]).toBeUndefined(); // primary is not a secondary file
323
324
  expect(result.primary).toContain("name: CI");
324
325
  expect(result.primary).toContain("build:");
325
326
  expect(result.files["deploy.yml"]).toContain("ship:");
package/src/serializer.ts CHANGED
@@ -367,10 +367,12 @@ function serializeMultiWorkflow(
367
367
  if (jobsSection) doc.jobs = jobsSection;
368
368
 
369
369
  const content = emitYAMLDocument(doc);
370
- const fileName = `${toKebabCase(name)}.yml`;
371
- files[fileName] = content;
372
-
373
- if (i === 0) primary = content;
370
+ if (i === 0) {
371
+ primary = content;
372
+ // primary is written to the -o path by the CLI; don't also add it to files[]
373
+ } else {
374
+ files[`${toKebabCase(name)}.yml`] = content;
375
+ }
374
376
  }
375
377
 
376
378
  return { primary, files };