@intentius/chant-lexicon-github 0.124.31 → 0.124.32
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/dist/action-pins.js +53 -0
- package/dist/codegen/docs-cli.js +2 -0
- package/dist/codegen/docs.js +142 -0
- package/dist/codegen/generate-cli.js +26 -0
- package/dist/codegen/generate-lexicon.js +36 -0
- package/dist/codegen/generate-typescript.js +108 -0
- package/dist/codegen/generate.js +111 -0
- package/dist/codegen/naming.js +49 -0
- package/dist/codegen/package.js +40 -0
- package/dist/codegen/parse.js +595 -0
- package/dist/codegen/patches.js +36 -0
- package/dist/components/ci-jobs.js +84 -0
- package/dist/components/generate-op-pipeline.js +440 -0
- package/dist/components/generate-op-waves-pipeline.js +158 -0
- package/dist/components/generate-pipeline.d.ts.map +1 -1
- package/dist/components/generate-pipeline.js +476 -0
- package/dist/composites/cache.js +17 -0
- package/dist/composites/catalog.js +859 -0
- package/dist/composites/checkout.js +25 -0
- package/dist/composites/dependabot.js +43 -0
- package/dist/composites/deploy-environment.js +38 -0
- package/dist/composites/docker-build.js +70 -0
- package/dist/composites/download-artifact.js +18 -0
- package/dist/composites/go-ci.js +62 -0
- package/dist/composites/index.js +15 -0
- package/dist/composites/node-ci.js +44 -0
- package/dist/composites/node-pipeline.js +96 -0
- package/dist/composites/pr-plan-report.js +129 -0
- package/dist/composites/python-ci.js +60 -0
- package/dist/composites/setup-go.js +18 -0
- package/dist/composites/setup-node.js +21 -0
- package/dist/composites/setup-python.js +18 -0
- package/dist/composites/upload-artifact.js +19 -0
- package/dist/coverage.js +32 -0
- package/dist/expression.js +177 -0
- package/dist/generated/index.js +26 -0
- package/dist/generated/runtime.js +4 -0
- package/dist/graph-edges.js +174 -0
- package/dist/import/generator.js +108 -0
- package/dist/import/parser.js +69 -0
- package/dist/index.js +22 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +69 -0
- package/dist/lint/audit-lineage.js +168 -0
- package/dist/lint/post-synth/gha006.js +50 -0
- package/dist/lint/post-synth/gha009.js +35 -0
- package/dist/lint/post-synth/gha011.js +34 -0
- package/dist/lint/post-synth/gha013.js +40 -0
- package/dist/lint/post-synth/gha017.js +25 -0
- package/dist/lint/post-synth/gha018.js +33 -0
- package/dist/lint/post-synth/gha019.js +58 -0
- package/dist/lint/post-synth/gha021.js +42 -0
- package/dist/lint/post-synth/gha022.js +44 -0
- package/dist/lint/post-synth/gha023.js +37 -0
- package/dist/lint/post-synth/gha024.js +32 -0
- package/dist/lint/post-synth/gha025.js +33 -0
- package/dist/lint/post-synth/gha026.js +32 -0
- package/dist/lint/post-synth/gha027.js +46 -0
- package/dist/lint/post-synth/gha028.js +28 -0
- package/dist/lint/post-synth/gha029.js +56 -0
- package/dist/lint/post-synth/gha030.js +35 -0
- package/dist/lint/post-synth/gha031.js +75 -0
- package/dist/lint/post-synth/gha032.js +35 -0
- package/dist/lint/post-synth/gha033.js +39 -0
- package/dist/lint/post-synth/gha034.js +35 -0
- package/dist/lint/post-synth/gha035.js +50 -0
- package/dist/lint/post-synth/gha036.js +37 -0
- package/dist/lint/post-synth/gha037.js +41 -0
- package/dist/lint/post-synth/gha038.js +35 -0
- package/dist/lint/post-synth/gha039.js +34 -0
- package/dist/lint/post-synth/gha040.js +36 -0
- package/dist/lint/post-synth/gha041.js +32 -0
- package/dist/lint/post-synth/gha042.js +33 -0
- package/dist/lint/post-synth/gha043.js +35 -0
- package/dist/lint/post-synth/gha044.js +36 -0
- package/dist/lint/post-synth/gha045.js +33 -0
- package/dist/lint/post-synth/gha046.js +55 -0
- package/dist/lint/post-synth/gha047.js +44 -0
- package/dist/lint/post-synth/gha048.js +34 -0
- package/dist/lint/post-synth/gha049.js +36 -0
- package/dist/lint/post-synth/gha050.js +37 -0
- package/dist/lint/post-synth/gha051.js +38 -0
- package/dist/lint/post-synth/gha052.js +35 -0
- package/dist/lint/post-synth/gha053.js +34 -0
- package/dist/lint/post-synth/gha054.js +35 -0
- package/dist/lint/post-synth/gha055.js +44 -0
- package/dist/lint/post-synth/gha056.js +32 -0
- package/dist/lint/post-synth/gha057.js +35 -0
- package/dist/lint/post-synth/gha058.js +42 -0
- package/dist/lint/post-synth/gha059.js +119 -0
- package/dist/lint/post-synth/gha060.js +129 -0
- package/dist/lint/post-synth/gha061.js +78 -0
- package/dist/lint/post-synth/gha062.js +65 -0
- package/dist/lint/post-synth/gha063.js +56 -0
- package/dist/lint/post-synth/gha064.js +47 -0
- package/dist/lint/post-synth/gha065.js +125 -0
- package/dist/lint/post-synth/gha066.js +40 -0
- package/dist/lint/post-synth/gha067.js +75 -0
- package/dist/lint/post-synth/gha068.js +47 -0
- package/dist/lint/post-synth/gha069.js +99 -0
- package/dist/lint/post-synth/index.js +114 -0
- package/dist/lint/post-synth/yaml-helpers.js +465 -0
- package/dist/lint/rules/data/action-usage-policy.js +16 -0
- package/dist/lint/rules/data/advisory-feed.js +15 -0
- package/dist/lint/rules/data/deprecated-versions.js +14 -0
- package/dist/lint/rules/data/flagged-actions.js +18 -0
- package/dist/lint/rules/data/known-action-slugs.js +38 -0
- package/dist/lint/rules/data/known-actions.js +12 -0
- package/dist/lint/rules/data/preinstalled-tools.js +36 -0
- package/dist/lint/rules/data/recommended-inputs.js +9 -0
- package/dist/lint/rules/data/risky-features.js +22 -0
- package/dist/lint/rules/data/secret-patterns.js +30 -0
- package/dist/lint/rules/data/trusted-action-owners.js +12 -0
- package/dist/lint/rules/data/untrusted-contexts.js +44 -0
- package/dist/lint/rules/deprecated-action-version.js +44 -0
- package/dist/lint/rules/detect-secrets.js +45 -0
- package/dist/lint/rules/extract-inline-structs.js +55 -0
- package/dist/lint/rules/file-job-limit.js +43 -0
- package/dist/lint/rules/index.js +16 -0
- package/dist/lint/rules/job-timeout.js +53 -0
- package/dist/lint/rules/missing-recommended-inputs.js +51 -0
- package/dist/lint/rules/no-hardcoded-secrets.js +39 -0
- package/dist/lint/rules/no-raw-expressions.js +44 -0
- package/dist/lint/rules/suggest-cache.js +60 -0
- package/dist/lint/rules/use-condition-builders.js +37 -0
- package/dist/lint/rules/use-matrix-builder.js +36 -0
- package/dist/lint/rules/use-typed-actions.js +41 -0
- package/dist/lint/rules/validate-concurrency.js +59 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +18 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +33 -0
- package/dist/manifest.json +1 -1
- package/dist/mcp/context-tools.js +152 -0
- package/dist/package-cli.js +20 -0
- package/dist/plugin.js +344 -0
- package/dist/serializer.js +572 -0
- package/dist/spec/fetch.js +46 -0
- package/dist/validate-cli.js +16 -0
- package/dist/validate.js +38 -0
- package/dist/variables.js +41 -0
- package/package.json +6 -6
- package/src/codegen/package.ts +2 -2
- package/src/components/__fixtures__/pr-loop-build.github.golden.yml +6 -2
- package/src/components/__fixtures__/pr-loop-waves.github.golden.yml +178 -0
- package/src/components/__fixtures__/pr-loop.github.golden.yml +6 -2
- package/src/components/generate-pipeline.ts +66 -29
- package/src/components/generate-pr-pipeline.test.ts +44 -20
- package/src/components/generate-pr-waves-pipeline.test.ts +117 -0
- package/src/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regex patterns for detecting hardcoded secrets in source code.
|
|
3
|
+
*/
|
|
4
|
+
export const secretPatterns = [
|
|
5
|
+
{ pattern: /AKIA[0-9A-Z]{16}/, description: "AWS Access Key ID" },
|
|
6
|
+
{ pattern: /-----BEGIN (RSA |EC |DSA )?PRIVATE KEY-----/, description: "Private Key" },
|
|
7
|
+
{ pattern: /sk_live_[a-zA-Z0-9]{20,}/, description: "Stripe Live Secret Key" },
|
|
8
|
+
{ pattern: /xox[bpors]-[a-zA-Z0-9-]{10,}/, description: "Slack Token" },
|
|
9
|
+
{ pattern: /AIza[0-9A-Za-z_-]{35}/, description: "Google API Key" },
|
|
10
|
+
{ pattern: /AC[a-z0-9]{32}/, description: "Twilio Account SID" },
|
|
11
|
+
{ pattern: /SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}/, description: "SendGrid API Key" },
|
|
12
|
+
{ pattern: /key-[a-zA-Z0-9]{32}/, description: "Mailgun API Key" },
|
|
13
|
+
{ pattern: /npm_[a-zA-Z0-9]{36}/, description: "NPM Token" },
|
|
14
|
+
{ pattern: /pypi-[a-zA-Z0-9_-]{50,}/, description: "PyPI Token" },
|
|
15
|
+
{ pattern: /eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}/, description: "JWT Token" },
|
|
16
|
+
{ pattern: /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/, description: "Heroku API Key (UUID)" },
|
|
17
|
+
{ pattern: /dop_v1_[a-f0-9]{64}/, description: "DigitalOcean Personal Access Token" },
|
|
18
|
+
{ pattern: /ghp_[a-zA-Z0-9]{36}/, description: "GitHub Personal Access Token" },
|
|
19
|
+
{ pattern: /ghs_[a-zA-Z0-9]{36}/, description: "GitHub Server-to-Server Token" },
|
|
20
|
+
{ pattern: /ghu_[a-zA-Z0-9]{36}/, description: "GitHub User-to-Server Token" },
|
|
21
|
+
{ pattern: /ghr_[a-zA-Z0-9]{36}/, description: "GitHub Refresh Token" },
|
|
22
|
+
{ pattern: /gho_[a-zA-Z0-9]{36}/, description: "GitHub OAuth Token" },
|
|
23
|
+
{ pattern: /github_pat_[a-zA-Z0-9_]{22,}/, description: "GitHub Fine-grained PAT" },
|
|
24
|
+
{ pattern: /glpat-[a-zA-Z0-9_-]{20,}/, description: "GitLab Personal Access Token" },
|
|
25
|
+
{ pattern: /sk-[a-zA-Z0-9]{48}/, description: "OpenAI API Key" },
|
|
26
|
+
{ pattern: /r8_[a-zA-Z0-9]{37}/, description: "Replicate API Token" },
|
|
27
|
+
{ pattern: /sq0atp-[a-zA-Z0-9_-]{22}/, description: "Square Access Token" },
|
|
28
|
+
{ pattern: /shpat_[a-fA-F0-9]{32}/, description: "Shopify Admin API Token" },
|
|
29
|
+
{ pattern: /shr[a-z]{2}_[a-f0-9]{32}/, description: "Shopify Shared Secret" },
|
|
30
|
+
];
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allowlist of trusted action owners (GitHub orgs) whose `uses:` references are
|
|
3
|
+
* exempt from the SHA-pinning check (GHA029).
|
|
4
|
+
*
|
|
5
|
+
* Ships EMPTY by default: chant recommends pinning every external reference to a
|
|
6
|
+
* full commit SHA, including first-party ones. Populate this set to silence the
|
|
7
|
+
* pinning check for orgs you have decided to trust by tag (e.g. your own org).
|
|
8
|
+
*
|
|
9
|
+
* This is a vendored, code-level allowlist — `PostSynthContext` carries no
|
|
10
|
+
* per-check runtime config channel today. Refresh it by editing this file.
|
|
11
|
+
*/
|
|
12
|
+
export const TRUSTED_ACTION_OWNERS = new Set([]);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendored snapshot of GitHub Actions expression contexts that are
|
|
3
|
+
* attacker-controllable — their values come from event payloads a fork or an
|
|
4
|
+
* outside contributor can set. Interpolating them into a shell (`run:`), into
|
|
5
|
+
* `GITHUB_ENV`/`GITHUB_PATH`, or into another execution sink turns data into
|
|
6
|
+
* code (script injection).
|
|
7
|
+
*
|
|
8
|
+
* Source: GitHub's "Security hardening for GitHub Actions — understanding the
|
|
9
|
+
* risk of script injections" plus the commonly-cited untrusted-context list.
|
|
10
|
+
* Advisory and necessarily incomplete; refresh by editing this list.
|
|
11
|
+
*/
|
|
12
|
+
export const UNTRUSTED_CONTEXTS = [
|
|
13
|
+
"github.event.issue.title",
|
|
14
|
+
"github.event.issue.body",
|
|
15
|
+
"github.event.pull_request.title",
|
|
16
|
+
"github.event.pull_request.body",
|
|
17
|
+
"github.event.pull_request.head.ref",
|
|
18
|
+
"github.event.pull_request.head.label",
|
|
19
|
+
"github.event.pull_request.head.repo.default_branch",
|
|
20
|
+
"github.event.comment.body",
|
|
21
|
+
"github.event.review.body",
|
|
22
|
+
"github.event.review_comment.body",
|
|
23
|
+
"github.event.discussion.title",
|
|
24
|
+
"github.event.discussion.body",
|
|
25
|
+
"github.event.head_commit.message",
|
|
26
|
+
"github.event.head_commit.author.email",
|
|
27
|
+
"github.event.head_commit.author.name",
|
|
28
|
+
"github.event.commits", // .*.message / .*.author.* (array — matched by prefix)
|
|
29
|
+
"github.event.pages", // .*.page_name
|
|
30
|
+
"github.head_ref",
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* Return the first untrusted context referenced inside an expression body, or
|
|
34
|
+
* undefined. Matches by substring so array element accesses
|
|
35
|
+
* (`github.event.commits[0].message`) are caught via their prefix.
|
|
36
|
+
*/
|
|
37
|
+
export function matchUntrustedContext(exprBody) {
|
|
38
|
+
const normalized = exprBody.replace(/\s+/g, "");
|
|
39
|
+
for (const ctx of UNTRUSTED_CONTEXTS) {
|
|
40
|
+
if (normalized.includes(ctx.replace(/\s+/g, "")))
|
|
41
|
+
return ctx;
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA012: Deprecated Action Version
|
|
3
|
+
*
|
|
4
|
+
* Flags `uses:` strings referencing deprecated action versions.
|
|
5
|
+
*/
|
|
6
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
7
|
+
import { deprecatedVersions } from "./data/deprecated-versions.js";
|
|
8
|
+
export const deprecatedActionVersionRule = {
|
|
9
|
+
id: "GHA012",
|
|
10
|
+
severity: "warning",
|
|
11
|
+
category: "correctness",
|
|
12
|
+
description: "Action version is deprecated — upgrade to the recommended version",
|
|
13
|
+
check(context) {
|
|
14
|
+
const { sourceFile } = context;
|
|
15
|
+
const diagnostics = [];
|
|
16
|
+
function visit(node) {
|
|
17
|
+
if (ts.isStringLiteral(node)) {
|
|
18
|
+
const text = node.text;
|
|
19
|
+
const atIndex = text.indexOf("@");
|
|
20
|
+
if (atIndex === -1) {
|
|
21
|
+
ts.forEachChild(node, visit);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const actionName = text.slice(0, atIndex);
|
|
25
|
+
const version = text.slice(atIndex + 1);
|
|
26
|
+
const info = deprecatedVersions[actionName];
|
|
27
|
+
if (info && info.deprecated.includes(version)) {
|
|
28
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
29
|
+
diagnostics.push({
|
|
30
|
+
file: sourceFile.fileName,
|
|
31
|
+
line: line + 1,
|
|
32
|
+
column: character + 1,
|
|
33
|
+
ruleId: "GHA012",
|
|
34
|
+
severity: "warning",
|
|
35
|
+
message: `"${text}" uses deprecated version ${version}. Upgrade to ${actionName}@${info.recommended}.`,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
ts.forEachChild(node, visit);
|
|
40
|
+
}
|
|
41
|
+
visit(sourceFile);
|
|
42
|
+
return diagnostics;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA020: Detect Secrets
|
|
3
|
+
*
|
|
4
|
+
* Scans string literals for patterns matching known secret formats.
|
|
5
|
+
* Skips strings containing "secrets." (proper usage).
|
|
6
|
+
*/
|
|
7
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
8
|
+
import { secretPatterns } from "./data/secret-patterns.js";
|
|
9
|
+
export const detectSecretsRule = {
|
|
10
|
+
id: "GHA020",
|
|
11
|
+
severity: "error",
|
|
12
|
+
category: "security",
|
|
13
|
+
description: "Potential secret detected in source code",
|
|
14
|
+
check(context) {
|
|
15
|
+
const { sourceFile } = context;
|
|
16
|
+
const diagnostics = [];
|
|
17
|
+
function visit(node) {
|
|
18
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
19
|
+
const text = node.text;
|
|
20
|
+
// Skip strings that reference secrets properly
|
|
21
|
+
if (text.includes("secrets.")) {
|
|
22
|
+
ts.forEachChild(node, visit);
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
for (const { pattern, description } of secretPatterns) {
|
|
26
|
+
if (pattern.test(text)) {
|
|
27
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
28
|
+
diagnostics.push({
|
|
29
|
+
file: sourceFile.fileName,
|
|
30
|
+
line: line + 1,
|
|
31
|
+
column: character + 1,
|
|
32
|
+
ruleId: "GHA020",
|
|
33
|
+
severity: "error",
|
|
34
|
+
message: `Potential ${description} detected. Use secrets() to reference secrets securely.`,
|
|
35
|
+
});
|
|
36
|
+
break; // One diagnostic per string
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
ts.forEachChild(node, visit);
|
|
41
|
+
}
|
|
42
|
+
visit(sourceFile);
|
|
43
|
+
return diagnostics;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA005: Extract Inline Structs
|
|
3
|
+
*
|
|
4
|
+
* Flags object literal nesting depth > 2 inside resource constructors.
|
|
5
|
+
*/
|
|
6
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
7
|
+
const RESOURCE_NAMES = new Set(["Job", "Workflow", "ReusableWorkflowCallJob"]);
|
|
8
|
+
const MAX_DEPTH = 2;
|
|
9
|
+
export const extractInlineStructsRule = {
|
|
10
|
+
id: "GHA005",
|
|
11
|
+
severity: "info",
|
|
12
|
+
category: "style",
|
|
13
|
+
description: "Extract deeply nested inline objects to named constants",
|
|
14
|
+
check(context) {
|
|
15
|
+
const { sourceFile } = context;
|
|
16
|
+
const diagnostics = [];
|
|
17
|
+
function isResourceConstructor(node) {
|
|
18
|
+
if (ts.isIdentifier(node.expression))
|
|
19
|
+
return RESOURCE_NAMES.has(node.expression.text);
|
|
20
|
+
if (ts.isPropertyAccessExpression(node.expression))
|
|
21
|
+
return RESOURCE_NAMES.has(node.expression.name.text);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
function checkDepth(node, depth) {
|
|
25
|
+
if (depth > MAX_DEPTH) {
|
|
26
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
27
|
+
diagnostics.push({
|
|
28
|
+
file: sourceFile.fileName,
|
|
29
|
+
line: line + 1,
|
|
30
|
+
column: character + 1,
|
|
31
|
+
ruleId: "GHA005",
|
|
32
|
+
severity: "info",
|
|
33
|
+
message: `Object nesting depth ${depth} exceeds ${MAX_DEPTH}. Consider extracting to a named constant.`,
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
for (const prop of node.properties) {
|
|
38
|
+
if (ts.isPropertyAssignment(prop) && ts.isObjectLiteralExpression(prop.initializer)) {
|
|
39
|
+
checkDepth(prop.initializer, depth + 1);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function visit(node) {
|
|
44
|
+
if (ts.isNewExpression(node) && isResourceConstructor(node) && node.arguments?.[0]) {
|
|
45
|
+
const arg = node.arguments[0];
|
|
46
|
+
if (ts.isObjectLiteralExpression(arg)) {
|
|
47
|
+
checkDepth(arg, 1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
ts.forEachChild(node, visit);
|
|
51
|
+
}
|
|
52
|
+
visit(sourceFile);
|
|
53
|
+
return diagnostics;
|
|
54
|
+
},
|
|
55
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA007: File Job Limit
|
|
3
|
+
*
|
|
4
|
+
* Flags files with more than 10 Job/ReusableWorkflowCallJob constructors.
|
|
5
|
+
*/
|
|
6
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
7
|
+
const JOB_NAMES = new Set(["Job", "ReusableWorkflowCallJob"]);
|
|
8
|
+
const MAX_JOBS = 10;
|
|
9
|
+
export const fileJobLimitRule = {
|
|
10
|
+
id: "GHA007",
|
|
11
|
+
severity: "warning",
|
|
12
|
+
category: "style",
|
|
13
|
+
description: "Too many jobs in a single file",
|
|
14
|
+
check(context) {
|
|
15
|
+
const { sourceFile } = context;
|
|
16
|
+
const diagnostics = [];
|
|
17
|
+
let jobCount = 0;
|
|
18
|
+
function visit(node) {
|
|
19
|
+
if (ts.isNewExpression(node)) {
|
|
20
|
+
let isJob = false;
|
|
21
|
+
if (ts.isIdentifier(node.expression))
|
|
22
|
+
isJob = JOB_NAMES.has(node.expression.text);
|
|
23
|
+
if (ts.isPropertyAccessExpression(node.expression))
|
|
24
|
+
isJob = JOB_NAMES.has(node.expression.name.text);
|
|
25
|
+
if (isJob)
|
|
26
|
+
jobCount++;
|
|
27
|
+
}
|
|
28
|
+
ts.forEachChild(node, visit);
|
|
29
|
+
}
|
|
30
|
+
visit(sourceFile);
|
|
31
|
+
if (jobCount > MAX_JOBS) {
|
|
32
|
+
diagnostics.push({
|
|
33
|
+
file: sourceFile.fileName,
|
|
34
|
+
line: 1,
|
|
35
|
+
column: 1,
|
|
36
|
+
ruleId: "GHA007",
|
|
37
|
+
severity: "warning",
|
|
38
|
+
message: `File has ${jobCount} job constructors (limit: ${MAX_JOBS}). Consider splitting into multiple files.`,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return diagnostics;
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitHub Actions lint rules
|
|
3
|
+
*/
|
|
4
|
+
export { useTypedActionsRule } from "./use-typed-actions.js";
|
|
5
|
+
export { useConditionBuildersRule } from "./use-condition-builders.js";
|
|
6
|
+
export { noHardcodedSecretsRule } from "./no-hardcoded-secrets.js";
|
|
7
|
+
export { useMatrixBuilderRule } from "./use-matrix-builder.js";
|
|
8
|
+
export { extractInlineStructsRule } from "./extract-inline-structs.js";
|
|
9
|
+
export { fileJobLimitRule } from "./file-job-limit.js";
|
|
10
|
+
export { noRawExpressionsRule } from "./no-raw-expressions.js";
|
|
11
|
+
export { missingRecommendedInputsRule } from "./missing-recommended-inputs.js";
|
|
12
|
+
export { deprecatedActionVersionRule } from "./deprecated-action-version.js";
|
|
13
|
+
export { jobTimeoutRule } from "./job-timeout.js";
|
|
14
|
+
export { suggestCacheRule } from "./suggest-cache.js";
|
|
15
|
+
export { validateConcurrencyRule } from "./validate-concurrency.js";
|
|
16
|
+
export { detectSecretsRule } from "./detect-secrets.js";
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA014: Job Timeout
|
|
3
|
+
*
|
|
4
|
+
* Flags `new Job({...})` without `timeoutMinutes` property.
|
|
5
|
+
*/
|
|
6
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
7
|
+
export const jobTimeoutRule = {
|
|
8
|
+
id: "GHA014",
|
|
9
|
+
severity: "warning",
|
|
10
|
+
category: "correctness",
|
|
11
|
+
description: "Job should specify a timeout-minutes value",
|
|
12
|
+
check(context) {
|
|
13
|
+
const { sourceFile } = context;
|
|
14
|
+
const diagnostics = [];
|
|
15
|
+
function visit(node) {
|
|
16
|
+
if (ts.isNewExpression(node)) {
|
|
17
|
+
let isJob = false;
|
|
18
|
+
if (ts.isIdentifier(node.expression) && node.expression.text === "Job")
|
|
19
|
+
isJob = true;
|
|
20
|
+
if (ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === "Job")
|
|
21
|
+
isJob = true;
|
|
22
|
+
if (isJob && node.arguments?.length) {
|
|
23
|
+
const arg = node.arguments[0];
|
|
24
|
+
if (ts.isObjectLiteralExpression(arg)) {
|
|
25
|
+
const hasTimeout = arg.properties.some((prop) => {
|
|
26
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
27
|
+
const name = ts.isIdentifier(prop.name) ? prop.name.text
|
|
28
|
+
: ts.isStringLiteral(prop.name) ? prop.name.text
|
|
29
|
+
: "";
|
|
30
|
+
return name === "timeoutMinutes" || name === "timeout-minutes";
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
});
|
|
34
|
+
if (!hasTimeout) {
|
|
35
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
36
|
+
diagnostics.push({
|
|
37
|
+
file: sourceFile.fileName,
|
|
38
|
+
line: line + 1,
|
|
39
|
+
column: character + 1,
|
|
40
|
+
ruleId: "GHA014",
|
|
41
|
+
severity: "warning",
|
|
42
|
+
message: "Job should specify timeoutMinutes. Default is 360 (6 hours), which may be excessive.",
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
ts.forEachChild(node, visit);
|
|
49
|
+
}
|
|
50
|
+
visit(sourceFile);
|
|
51
|
+
return diagnostics;
|
|
52
|
+
},
|
|
53
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA010: Missing Recommended Inputs
|
|
3
|
+
*
|
|
4
|
+
* Flags setup action composites without version-related inputs.
|
|
5
|
+
*/
|
|
6
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
7
|
+
import { recommendedInputs } from "./data/recommended-inputs.js";
|
|
8
|
+
const SETUP_ACTIONS = new Set(Object.keys(recommendedInputs));
|
|
9
|
+
export const missingRecommendedInputsRule = {
|
|
10
|
+
id: "GHA010",
|
|
11
|
+
severity: "warning",
|
|
12
|
+
category: "correctness",
|
|
13
|
+
description: "Setup action composite should specify a version input",
|
|
14
|
+
check(context) {
|
|
15
|
+
const { sourceFile } = context;
|
|
16
|
+
const diagnostics = [];
|
|
17
|
+
function visit(node) {
|
|
18
|
+
// Match: SetupNode({...}), SetupGo({...}), SetupPython({...})
|
|
19
|
+
if (ts.isCallExpression(node)) {
|
|
20
|
+
let actionName = null;
|
|
21
|
+
if (ts.isIdentifier(node.expression) && SETUP_ACTIONS.has(node.expression.text)) {
|
|
22
|
+
actionName = node.expression.text;
|
|
23
|
+
}
|
|
24
|
+
if (actionName && node.arguments.length > 0) {
|
|
25
|
+
const arg = node.arguments[0];
|
|
26
|
+
if (ts.isObjectLiteralExpression(arg)) {
|
|
27
|
+
const required = recommendedInputs[actionName] ?? [];
|
|
28
|
+
const propNames = arg.properties
|
|
29
|
+
.filter(ts.isPropertyAssignment)
|
|
30
|
+
.map((p) => (ts.isIdentifier(p.name) ? p.name.text : ""));
|
|
31
|
+
const hasAny = required.some((r) => propNames.includes(r));
|
|
32
|
+
if (!hasAny) {
|
|
33
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
34
|
+
diagnostics.push({
|
|
35
|
+
file: sourceFile.fileName,
|
|
36
|
+
line: line + 1,
|
|
37
|
+
column: character + 1,
|
|
38
|
+
ruleId: "GHA010",
|
|
39
|
+
severity: "warning",
|
|
40
|
+
message: `${actionName}() should specify a version input (${required.join(" or ")}).`,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
ts.forEachChild(node, visit);
|
|
47
|
+
}
|
|
48
|
+
visit(sourceFile);
|
|
49
|
+
return diagnostics;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA003: No Hardcoded Secrets
|
|
3
|
+
*
|
|
4
|
+
* Flags string literals matching GitHub token prefixes.
|
|
5
|
+
*/
|
|
6
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
7
|
+
const TOKEN_PREFIXES = ["ghp_", "ghs_", "ghu_", "ghr_", "gho_", "github_pat_"];
|
|
8
|
+
export const noHardcodedSecretsRule = {
|
|
9
|
+
id: "GHA003",
|
|
10
|
+
severity: "error",
|
|
11
|
+
category: "security",
|
|
12
|
+
description: "No hardcoded GitHub tokens in source code",
|
|
13
|
+
check(context) {
|
|
14
|
+
const { sourceFile } = context;
|
|
15
|
+
const diagnostics = [];
|
|
16
|
+
function visit(node) {
|
|
17
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
18
|
+
const text = node.text;
|
|
19
|
+
for (const prefix of TOKEN_PREFIXES) {
|
|
20
|
+
if (text.includes(prefix)) {
|
|
21
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
22
|
+
diagnostics.push({
|
|
23
|
+
file: sourceFile.fileName,
|
|
24
|
+
line: line + 1,
|
|
25
|
+
column: character + 1,
|
|
26
|
+
ruleId: "GHA003",
|
|
27
|
+
severity: "error",
|
|
28
|
+
message: `Hardcoded GitHub token detected (prefix: ${prefix}). Use secrets() instead.`,
|
|
29
|
+
});
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
ts.forEachChild(node, visit);
|
|
35
|
+
}
|
|
36
|
+
visit(sourceFile);
|
|
37
|
+
return diagnostics;
|
|
38
|
+
},
|
|
39
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA008: No Raw Expressions
|
|
3
|
+
*
|
|
4
|
+
* Flags `${{` strings outside valid context paths.
|
|
5
|
+
*/
|
|
6
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
7
|
+
const VALID_CONTEXTS = ["github.", "secrets.", "matrix.", "steps.", "needs.", "inputs.", "env.", "vars.", "runner.", "always()", "failure()", "success()", "cancelled()", "contains(", "startsWith(", "endsWith(", "format(", "join(", "toJSON(", "fromJSON(", "hashFiles("];
|
|
8
|
+
export const noRawExpressionsRule = {
|
|
9
|
+
id: "GHA008",
|
|
10
|
+
severity: "info",
|
|
11
|
+
category: "style",
|
|
12
|
+
description: "Avoid raw ${{ }} expression strings — use typed Expression helpers",
|
|
13
|
+
check(context) {
|
|
14
|
+
const { sourceFile } = context;
|
|
15
|
+
const diagnostics = [];
|
|
16
|
+
function visit(node) {
|
|
17
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
18
|
+
const text = node.text;
|
|
19
|
+
if (text.includes("${{")) {
|
|
20
|
+
// Extract the expression content
|
|
21
|
+
const match = text.match(/\$\{\{\s*(.+?)\s*\}\}/);
|
|
22
|
+
if (match) {
|
|
23
|
+
const expr = match[1];
|
|
24
|
+
const isValid = VALID_CONTEXTS.some((ctx) => expr.startsWith(ctx));
|
|
25
|
+
if (!isValid) {
|
|
26
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
27
|
+
diagnostics.push({
|
|
28
|
+
file: sourceFile.fileName,
|
|
29
|
+
line: line + 1,
|
|
30
|
+
column: character + 1,
|
|
31
|
+
ruleId: "GHA008",
|
|
32
|
+
severity: "info",
|
|
33
|
+
message: `Raw expression "$\{{ ${expr} }}" doesn't match a known context. Use typed Expression helpers.`,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
ts.forEachChild(node, visit);
|
|
40
|
+
}
|
|
41
|
+
visit(sourceFile);
|
|
42
|
+
return diagnostics;
|
|
43
|
+
},
|
|
44
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA015: Suggest Cache
|
|
3
|
+
*
|
|
4
|
+
* Flags setup action composites in steps without a corresponding Cache composite.
|
|
5
|
+
*/
|
|
6
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
7
|
+
const setupActionsNeedingCache = new Set(["SetupNode", "SetupGo", "SetupPython"]);
|
|
8
|
+
export const suggestCacheRule = {
|
|
9
|
+
id: "GHA015",
|
|
10
|
+
severity: "warning",
|
|
11
|
+
category: "performance",
|
|
12
|
+
description: "Setup action should be paired with a Cache composite for faster builds",
|
|
13
|
+
check(context) {
|
|
14
|
+
const { sourceFile } = context;
|
|
15
|
+
const diagnostics = [];
|
|
16
|
+
function visit(node) {
|
|
17
|
+
// Look for steps array with setup actions but no Cache
|
|
18
|
+
if (ts.isPropertyAssignment(node) &&
|
|
19
|
+
ts.isIdentifier(node.name) &&
|
|
20
|
+
node.name.text === "steps" &&
|
|
21
|
+
ts.isArrayLiteralExpression(node.initializer)) {
|
|
22
|
+
const elements = node.initializer.elements;
|
|
23
|
+
let hasSetup = false;
|
|
24
|
+
let setupName = "";
|
|
25
|
+
let hasCache = false;
|
|
26
|
+
for (const el of elements) {
|
|
27
|
+
if (ts.isCallExpression(el)) {
|
|
28
|
+
const name = ts.isIdentifier(el.expression) ? el.expression.text : "";
|
|
29
|
+
if (setupActionsNeedingCache.has(name)) {
|
|
30
|
+
hasSetup = true;
|
|
31
|
+
setupName = name;
|
|
32
|
+
// Check if the setup action already has cache in its props
|
|
33
|
+
if (el.arguments.length > 0 && ts.isObjectLiteralExpression(el.arguments[0])) {
|
|
34
|
+
const hasCacheProp = el.arguments[0].properties.some((p) => ts.isPropertyAssignment(p) && ts.isIdentifier(p.name) && p.name.text === "cache");
|
|
35
|
+
if (hasCacheProp)
|
|
36
|
+
hasCache = true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (name === "Cache")
|
|
40
|
+
hasCache = true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (hasSetup && !hasCache) {
|
|
44
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
45
|
+
diagnostics.push({
|
|
46
|
+
file: sourceFile.fileName,
|
|
47
|
+
line: line + 1,
|
|
48
|
+
column: character + 1,
|
|
49
|
+
ruleId: "GHA015",
|
|
50
|
+
severity: "warning",
|
|
51
|
+
message: `${setupName}() found without Cache. Add a Cache() step or use the built-in cache option for faster builds.`,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
ts.forEachChild(node, visit);
|
|
56
|
+
}
|
|
57
|
+
visit(sourceFile);
|
|
58
|
+
return diagnostics;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA002: Use Condition Builders
|
|
3
|
+
*
|
|
4
|
+
* Flags string literals containing `${{` in `if` property assignments
|
|
5
|
+
* inside Job/Step constructors. Suggests using Expression helpers.
|
|
6
|
+
*/
|
|
7
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
8
|
+
export const useConditionBuildersRule = {
|
|
9
|
+
id: "GHA002",
|
|
10
|
+
severity: "warning",
|
|
11
|
+
category: "style",
|
|
12
|
+
description: "Use Expression helpers instead of raw ${{ }} strings in if conditions",
|
|
13
|
+
check(context) {
|
|
14
|
+
const { sourceFile } = context;
|
|
15
|
+
const diagnostics = [];
|
|
16
|
+
function visit(node) {
|
|
17
|
+
if (ts.isPropertyAssignment(node) &&
|
|
18
|
+
ts.isIdentifier(node.name) &&
|
|
19
|
+
node.name.text === "if" &&
|
|
20
|
+
ts.isStringLiteral(node.initializer) &&
|
|
21
|
+
node.initializer.text.includes("${{")) {
|
|
22
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
23
|
+
diagnostics.push({
|
|
24
|
+
file: sourceFile.fileName,
|
|
25
|
+
line: line + 1,
|
|
26
|
+
column: character + 1,
|
|
27
|
+
ruleId: "GHA002",
|
|
28
|
+
severity: "warning",
|
|
29
|
+
message: "Use typed Expression helpers (e.g., github.ref.eq('refs/heads/main')) instead of raw ${{ }} strings.",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
ts.forEachChild(node, visit);
|
|
33
|
+
}
|
|
34
|
+
visit(sourceFile);
|
|
35
|
+
return diagnostics;
|
|
36
|
+
},
|
|
37
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA004: Use Matrix Builder
|
|
3
|
+
*
|
|
4
|
+
* Flags inline object literals in `matrix` property. Suggests extracting
|
|
5
|
+
* to a named const for reusability and clarity.
|
|
6
|
+
*/
|
|
7
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
8
|
+
export const useMatrixBuilderRule = {
|
|
9
|
+
id: "GHA004",
|
|
10
|
+
severity: "info",
|
|
11
|
+
category: "style",
|
|
12
|
+
description: "Extract inline matrix objects to named constants",
|
|
13
|
+
check(context) {
|
|
14
|
+
const { sourceFile } = context;
|
|
15
|
+
const diagnostics = [];
|
|
16
|
+
function visit(node) {
|
|
17
|
+
if (ts.isPropertyAssignment(node) &&
|
|
18
|
+
ts.isIdentifier(node.name) &&
|
|
19
|
+
node.name.text === "matrix" &&
|
|
20
|
+
ts.isObjectLiteralExpression(node.initializer)) {
|
|
21
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
22
|
+
diagnostics.push({
|
|
23
|
+
file: sourceFile.fileName,
|
|
24
|
+
line: line + 1,
|
|
25
|
+
column: character + 1,
|
|
26
|
+
ruleId: "GHA004",
|
|
27
|
+
severity: "info",
|
|
28
|
+
message: "Consider extracting inline matrix to a named constant for clarity.",
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
ts.forEachChild(node, visit);
|
|
32
|
+
}
|
|
33
|
+
visit(sourceFile);
|
|
34
|
+
return diagnostics;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GHA001: Use Typed Action Composites
|
|
3
|
+
*
|
|
4
|
+
* Flags raw `uses:` string literals when a matching typed composite exists.
|
|
5
|
+
*/
|
|
6
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
7
|
+
import { knownActions } from "./data/known-actions.js";
|
|
8
|
+
export const useTypedActionsRule = {
|
|
9
|
+
id: "GHA001",
|
|
10
|
+
severity: "warning",
|
|
11
|
+
category: "style",
|
|
12
|
+
description: "Use typed action composite instead of raw uses: string",
|
|
13
|
+
check(context) {
|
|
14
|
+
const { sourceFile } = context;
|
|
15
|
+
const diagnostics = [];
|
|
16
|
+
function visit(node) {
|
|
17
|
+
if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name) && node.name.text === "uses") {
|
|
18
|
+
if (ts.isStringLiteral(node.initializer)) {
|
|
19
|
+
const value = node.initializer.text;
|
|
20
|
+
// Extract action name without version: "actions/checkout@v4" → "actions/checkout"
|
|
21
|
+
const actionName = value.split("@")[0];
|
|
22
|
+
const match = knownActions[actionName];
|
|
23
|
+
if (match) {
|
|
24
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
25
|
+
diagnostics.push({
|
|
26
|
+
file: sourceFile.fileName,
|
|
27
|
+
line: line + 1,
|
|
28
|
+
column: character + 1,
|
|
29
|
+
ruleId: "GHA001",
|
|
30
|
+
severity: "warning",
|
|
31
|
+
message: `Use the typed ${match.composite}() composite instead of raw "${value}" string.`,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
ts.forEachChild(node, visit);
|
|
37
|
+
}
|
|
38
|
+
visit(sourceFile);
|
|
39
|
+
return diagnostics;
|
|
40
|
+
},
|
|
41
|
+
};
|