Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"ajv": "8.20.0",
"ajv-formats": "3.0.1",
"entities": "8.1.0",
"estree-walker": "3.0.3",
"graphql": "17.0.2",
"knip": "6.34.0",
"lefthook": "2.1.12",
Expand Down
62 changes: 62 additions & 0 deletions test/problem.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,15 @@

import { describe, expect, test } from "bun:test";
import {
badDirectiveIssue,
describeProblem,
type Problem,
type ProblemOf,
quoteList,
type SettingsProblem,
singleDocumentRemovalIssue,
unknownDirectivesIssue,
unknownSectionsIssue,
} from "../src/problem.js";
import { SECTION_KEYS } from "../src/schema.js";

Expand Down Expand Up @@ -495,3 +499,61 @@ describe("quoteList", () => {
expect(quoteList([])).toBe("");
});
});

describe("the document-level issue builders render the lines a user reads", () => {
const DIRECTIVES =
"The underscore marks this action's directives, \"_layering\" (a file's top level or a list " +
"section's {entries} wrapper) and \"_undeclared\" (a file's top level or a wrapper), and " +
"nothing else; there are no private-note keys. Remove the key, or keep the note as a YAML " +
"comment";

test.each<[what: string, line: string, expected: string]>([
[
"one unknown underscore key",
unknownDirectivesIssue(["_notes"]),
`unknown underscore key: _notes. ${DIRECTIVES}`,
],
[
"two unknown underscore keys",
unknownDirectivesIssue(["_notes", "_owner"]),
`unknown underscore keys: _notes, _owner. ${DIRECTIVES}`,
],
[
"a file-wide policy outside the two values",
badDirectiveIssue("sometimes", ["keep", "delete"]),
'_undeclared must be one of "keep", "delete"; got a string that is none of them. Write _undeclared: keep or _undeclared: delete at the top of the file, or remove the key so each list\'s own policy applies',
],
[
"a file-wide policy that is a list",
badDirectiveIssue(["keep"], ["keep", "delete"]),
'_undeclared must be one of "keep", "delete"; got a list. Write _undeclared: keep or _undeclared: delete at the top of the file, or remove the key so each list\'s own policy applies',
],
[
"a file-wide policy that is a mapping",
badDirectiveIssue({ keep: true }, ["keep", "delete"]),
'_undeclared must be one of "keep", "delete"; got a mapping. Write _undeclared: keep or _undeclared: delete at the top of the file, or remove the key so each list\'s own policy applies',
],
[
"a file-wide policy that is not a string",
badDirectiveIssue(null, ["keep", "delete"]),
'_undeclared must be one of "keep", "delete"; got null. Write _undeclared: keep or _undeclared: delete at the top of the file, or remove the key so each list\'s own policy applies',
],
[
"a removal marker in a document that is not a layer",
singleDocumentRemovalIssue("labels[2]"),
"labels[2]: a single document has no lower layer to remove from; _remove: true belongs in a higher layer of a fold (mode: render)",
],
[
"one unknown section",
unknownSectionsIssue(["tags"], ["labels", "teams"]),
'unknown top-level section: tags (known: labels, teams). Fix the typo, or set the "sections" input to limit processing',
],
[
"two unknown sections",
unknownSectionsIssue(["tags", "issues"], ["labels", "teams"]),
'unknown top-level sections: tags, issues (known: labels, teams). Fix the typo, or set the "sections" input to limit processing',
],
])("%s", (_what, line, expected) => {
expect(line).toBe(expected);
});
});
100 changes: 100 additions & 0 deletions test/sections/actions/schema.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* The actions section's parse refusals, each pinned as the problem line a user reads: GitHub's GET-only fields
* (declared, they would re-PUT forever), the OIDC claim-key rules, and an allowlist declared under a policy that
* ignores it. Parsed through the loosened document shape, so a rule that survives here reaches the run.
*/

import { describe, expect, test } from "bun:test";
import { validateSectionShapes } from "../../../src/engine/validate.js";

function issues(actions: Record<string, unknown>): readonly string[] | null {
return validateSectionShapes({ actions }, "settings.yml").match(
() => null,
(problem) => problem.issues,
);
}

const REPORTED_ONLY = (field: string) =>
`${field} is a value GitHub reports, not a setting it accepts (the GET returns it, the PUT does not take it), so a declared value could never be applied; remove it from the settings file`;

describe("an actions setting GitHub would ignore or 422 is refused at parse, naming the key and the fix", () => {
test.each<[what: string, actions: Record<string, unknown>, expected: string[]]>([
[
"the GET-only allowlist link at the top level",
{ selected_actions_url: "https://api.github.com/x" },
[`actions.selected_actions_url: ${REPORTED_ONLY("selected_actions_url")}`],
],
[
"the GET-only retention ceiling",
{ artifact_and_log_retention: { days: 30, maximum_allowed_days: 90 } },
[
`actions.artifact_and_log_retention.maximum_allowed_days: ${REPORTED_ONLY("maximum_allowed_days")}`,
],
],
[
"the GET-only subject prefix",
{ oidc_customization_sub: { use_default: true, sub_claim_prefix: "repo:" } },
[`actions.oidc_customization_sub.sub_claim_prefix: ${REPORTED_ONLY("sub_claim_prefix")}`],
],
[
"a claim key with a character GitHub refuses",
{ oidc_customization_sub: { use_default: false, include_claim_keys: ["repo", "job-ref"] } },
[
'actions.oidc_customization_sub.include_claim_keys[1]: a claim key holds only letters, digits, and underscores (such as "repo" or "job_workflow_ref")',
],
],
[
"a repeated claim key",
{ oidc_customization_sub: { use_default: false, include_claim_keys: ["repo", "repo"] } },
[
'actions.oidc_customization_sub.include_claim_keys[1]: "repo" repeats an earlier claim key; GitHub requires the keys to be unique',
],
],
[
"a claim-key list beside the default template, which GitHub ignores",
{ oidc_customization_sub: { use_default: true, include_claim_keys: ["repo"] } },
[
"actions.oidc_customization_sub.include_claim_keys: GitHub ignores include_claim_keys under use_default: true, so the declared list could never take; set use_default: false for a custom template, or remove the list",
],
],
[
"an allowlist under a policy that allows every action",
{ allowed_actions: "all", selected_actions: { github_owned_allowed: true } },
[
'actions.selected_actions: selected_actions is declared together with allowed_actions: "all", but an allowlist only applies under allowed_actions: "selected". Set allowed_actions to "selected", or remove selected_actions',
],
],
[
"an allowlist beside a policy written as a list",
{ allowed_actions: [], selected_actions: { github_owned_allowed: true } },
[
'actions.allowed_actions: Invalid option: expected one of "all"|"local_only"|"selected"',
'actions.selected_actions: selected_actions is declared together with allowed_actions: a list, but an allowlist only applies under allowed_actions: "selected". Set allowed_actions to "selected", or remove selected_actions',
],
],
[
"an allowlist beside a policy written as a mapping",
{ allowed_actions: {}, selected_actions: { github_owned_allowed: true } },
[
'actions.allowed_actions: Invalid option: expected one of "all"|"local_only"|"selected"',
'actions.selected_actions: selected_actions is declared together with allowed_actions: a mapping, but an allowlist only applies under allowed_actions: "selected". Set allowed_actions to "selected", or remove selected_actions',
],
],
])("%s", (_what, actions, expected) => {
expect(issues(actions)).toEqual(expected);
});

test("the same keys in the forms GitHub accepts parse", () => {
expect(
issues({
allowed_actions: "selected",
selected_actions: { github_owned_allowed: true, patterns_allowed: ["actions/*"] },
artifact_and_log_retention: { days: 30 },
oidc_customization_sub: {
use_default: false,
include_claim_keys: ["repo", "job_workflow_ref"],
},
}),
).toBeNull();
});
});
Loading
Loading