Add uses format validation for step and job-level workflows (#220)
This commit is contained in:
@@ -1268,7 +1268,7 @@ jobs:
|
||||
on: push
|
||||
jobs:
|
||||
a:
|
||||
uses: ./reusable-workflow-with-outputs.yaml
|
||||
uses: ./.github/workflows/reusable-workflow-with-outputs.yaml
|
||||
b:
|
||||
needs: [a]
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
@@ -21,7 +21,7 @@ on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./reusable-workflow-with-inputs.yaml
|
||||
uses: ./.github/workflows/reusable-workflow-with-inputs.yaml
|
||||
with:
|
||||
|
|
||||
`;
|
||||
@@ -49,7 +49,7 @@ on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./reusable-workflow-with-inputs.yaml
|
||||
uses: ./.github/workflows/reusable-workflow-with-inputs.yaml
|
||||
with:
|
||||
username: monalisa
|
||||
|
|
||||
@@ -74,7 +74,7 @@ on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./reusable-workflow-with-inputs.yaml
|
||||
uses: ./.github/workflows/reusable-workflow-with-inputs.yaml
|
||||
secrets:
|
||||
|
|
||||
`;
|
||||
@@ -102,7 +102,7 @@ on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./reusable-workflow-with-inputs.yaml
|
||||
uses: ./.github/workflows/reusable-workflow-with-inputs.yaml
|
||||
secrets: |
|
||||
`;
|
||||
const result = await complete(...getPositionFromCursor(input), {fileProvider: testFileProvider});
|
||||
@@ -117,7 +117,7 @@ on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./reusable-workflow-with-inputs.yaml
|
||||
uses: ./.github/workflows/reusable-workflow-with-inputs.yaml
|
||||
secrets:
|
||||
envPAT: "myPAT"
|
||||
|
|
||||
|
||||
@@ -111,7 +111,7 @@ jobs:
|
||||
on: push
|
||||
jobs:
|
||||
a:
|
||||
uses: ./reusable-workflow-with-outputs.yaml
|
||||
uses: ./.github/workflows/reusable-workflow-with-outputs.yaml
|
||||
|
||||
b:
|
||||
needs: [a]
|
||||
|
||||
@@ -14,7 +14,7 @@ on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./reusable-workflow-with-inputs.yaml
|
||||
uses: ./.github/workflows/reusable-workflow-with-inputs.yaml
|
||||
with:
|
||||
us|ername:
|
||||
`;
|
||||
@@ -31,7 +31,7 @@ on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./reusable-workflow-with-inputs-no-description.yaml
|
||||
uses: ./.github/workflows/reusable-workflow-with-inputs-no-description.yaml
|
||||
with:
|
||||
us|ername:
|
||||
`;
|
||||
@@ -48,7 +48,7 @@ on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./reusable-workflow-with-outputs.yaml
|
||||
uses: ./.github/workflows/reusable-workflow-with-outputs.yaml
|
||||
echo_outputs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
|
||||
@@ -5,9 +5,9 @@ export const testFileProvider: FileProvider = {
|
||||
// eslint-disable-next-line @typescript-eslint/require-await
|
||||
getFileContent: async ref => {
|
||||
switch (fileIdentifier(ref)) {
|
||||
case "monalisa/octocat/workflow.yaml@main":
|
||||
case "monalisa/octocat/.github/workflows/workflow.yaml@main":
|
||||
return {
|
||||
name: "monalisa/octocat/workflow.yaml",
|
||||
name: "monalisa/octocat/.github/workflows/workflow.yaml",
|
||||
content: `
|
||||
on: workflow_call
|
||||
jobs:
|
||||
@@ -31,9 +31,9 @@ jobs:
|
||||
`
|
||||
};
|
||||
|
||||
case "./reusable-workflow.yaml":
|
||||
case "./.github/workflows/reusable-workflow.yaml":
|
||||
return {
|
||||
name: "reusable-workflow.yaml",
|
||||
name: ".github/workflows/reusable-workflow.yaml",
|
||||
content: `
|
||||
on: workflow_call
|
||||
jobs:
|
||||
@@ -44,9 +44,9 @@ jobs:
|
||||
`
|
||||
};
|
||||
|
||||
case "./reusable-workflow-with-inputs.yaml":
|
||||
case "./.github/workflows/reusable-workflow-with-inputs.yaml":
|
||||
return {
|
||||
name: "reusable-workflow-with-inputs.yaml",
|
||||
name: ".github/workflows/reusable-workflow-with-inputs.yaml",
|
||||
content: `
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -76,9 +76,9 @@ jobs:
|
||||
`
|
||||
};
|
||||
|
||||
case "./reusable-workflow-with-inputs-no-description.yaml":
|
||||
case "./.github/workflows/reusable-workflow-with-inputs-no-description.yaml":
|
||||
return {
|
||||
name: "reusable-workflow-with-inputs.yaml",
|
||||
name: ".github/workflows/reusable-workflow-with-inputs.yaml",
|
||||
content: `
|
||||
on:
|
||||
workflow_call:
|
||||
@@ -95,9 +95,9 @@ jobs:
|
||||
`
|
||||
};
|
||||
|
||||
case "./reusable-workflow-with-outputs.yaml":
|
||||
case "./.github/workflows/reusable-workflow-with-outputs.yaml":
|
||||
return {
|
||||
name: "reusable-workflow-with-outputs.yaml",
|
||||
name: ".github/workflows/reusable-workflow-with-outputs.yaml",
|
||||
content: `
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
@@ -635,7 +635,7 @@ jobs:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
node: [14, 16]
|
||||
uses: ./reusable-workflow-with-inputs.yaml
|
||||
uses: ./.github/workflows/reusable-workflow-with-inputs.yaml
|
||||
with:
|
||||
username: User-\${{ strategy.fail-fast }}
|
||||
`;
|
||||
@@ -654,7 +654,7 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
node: [14, 16]
|
||||
uses: ./reusable-workflow-with-inputs.yaml
|
||||
uses: ./.github/workflows/reusable-workflow-with-inputs.yaml
|
||||
with:
|
||||
username: \${{ matrix.node }}
|
||||
`;
|
||||
|
||||
@@ -143,11 +143,29 @@ async function additionalValidations(
|
||||
}
|
||||
}
|
||||
|
||||
// Validate step uses field format
|
||||
if (isString(token) && token.range && validationDefinition?.key === "step-uses") {
|
||||
validateStepUsesFormat(diagnostics, token);
|
||||
}
|
||||
|
||||
// Validate action metadata (inputs, required fields) for regular steps
|
||||
if (token.definition?.key === "regular-step" && token.range) {
|
||||
const context = getProviderContext(documentUri, template, root, token.range);
|
||||
await validateAction(diagnostics, token, context.step, config);
|
||||
}
|
||||
|
||||
// Validate job-level reusable workflow uses field format
|
||||
if (
|
||||
isString(token) &&
|
||||
token.range &&
|
||||
key &&
|
||||
isString(key) &&
|
||||
key.value === "uses" &&
|
||||
parent?.definition?.key === "workflow-job"
|
||||
) {
|
||||
validateWorkflowUsesFormat(diagnostics, token);
|
||||
}
|
||||
|
||||
// Allowed values coming from the schema have already been validated. Only check if
|
||||
// a value provider is defined for a token and if it is, validate the values match.
|
||||
if (token.range && validationDefinition) {
|
||||
@@ -198,6 +216,307 @@ function invalidValue(diagnostics: Diagnostic[], token: StringToken, kind: Value
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the format of a step's `uses` field.
|
||||
*
|
||||
* Valid formats:
|
||||
* - docker://image:tag
|
||||
* - ./local/path
|
||||
* - .\local\path (Windows)
|
||||
* - {owner}/{repo}@{ref}
|
||||
* - {owner}/{repo}/{path}@{ref}
|
||||
*/
|
||||
function validateStepUsesFormat(diagnostics: Diagnostic[], token: StringToken): void {
|
||||
const uses = token.value;
|
||||
|
||||
// Empty uses value
|
||||
if (!uses) {
|
||||
diagnostics.push({
|
||||
message: "`uses' value in action cannot be blank",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: mapRange(token.range),
|
||||
code: "invalid-uses-format"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Docker image reference - always valid format
|
||||
if (uses.startsWith("docker://")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Local action path - always valid format
|
||||
if (uses.startsWith("./") || uses.startsWith(".\\")) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Remote action: must be {owner}/{repo}[/path]@{ref}
|
||||
const atSegments = uses.split("@");
|
||||
|
||||
// Must have exactly one @
|
||||
if (atSegments.length !== 2) {
|
||||
addStepUsesFormatError(diagnostics, token);
|
||||
return;
|
||||
}
|
||||
|
||||
const [repoPath, gitRef] = atSegments;
|
||||
|
||||
// Ref cannot be empty
|
||||
if (!gitRef) {
|
||||
addStepUsesFormatError(diagnostics, token);
|
||||
return;
|
||||
}
|
||||
|
||||
// Split by / or \ to get path segments
|
||||
const pathSegments = repoPath.split(/[\\/]/);
|
||||
|
||||
// Must have at least owner and repo (both non-empty)
|
||||
if (pathSegments.length < 2 || !pathSegments[0] || !pathSegments[1]) {
|
||||
addStepUsesFormatError(diagnostics, token);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if this is a reusable workflow reference (should be at job level, not step)
|
||||
// Path would be like: owner/repo/.github/workflows/file.yml
|
||||
if (pathSegments.length >= 4 && pathSegments[2] === ".github" && pathSegments[3] === "workflows") {
|
||||
diagnostics.push({
|
||||
message: "Reusable workflows should be referenced at the top-level `jobs.<job_id>.uses` key, not within steps",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: mapRange(token.range),
|
||||
code: "invalid-uses-format"
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function addStepUsesFormatError(diagnostics: Diagnostic[], token: StringToken): void {
|
||||
diagnostics.push({
|
||||
message: `Expected format {owner}/{repo}[/path]@{ref}. Actual '${token.value}'`,
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: mapRange(token.range),
|
||||
code: "invalid-uses-format"
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the format of a job's `uses` field (reusable workflow reference).
|
||||
*
|
||||
* Valid formats:
|
||||
* - {owner}/{repo}/.github/workflows/{filename}.yml@{ref}
|
||||
* - {owner}/{repo}/.github/workflows/{filename}.yaml@{ref}
|
||||
* - {owner}/{repo}/.github/workflows-lab/{filename}.yml@{ref}
|
||||
* - {owner}/{repo}/.github/workflows-lab/{filename}.yaml@{ref}
|
||||
* - ./.github/workflows/{filename}.yml
|
||||
* - ./.github/workflows/{filename}.yaml
|
||||
* - ./.github/workflows-lab/{filename}.yml
|
||||
* - ./.github/workflows-lab/{filename}.yaml
|
||||
*/
|
||||
function validateWorkflowUsesFormat(diagnostics: Diagnostic[], token: StringToken): void {
|
||||
const uses = token.value;
|
||||
|
||||
// Local workflow reference
|
||||
if (uses.startsWith("./.github/workflows/") || uses.startsWith("./.github/workflows-lab/")) {
|
||||
// Cannot have @ version for local workflows
|
||||
if (uses.includes("@")) {
|
||||
addWorkflowUsesFormatError(diagnostics, token, "cannot specify version when calling local workflows");
|
||||
return;
|
||||
}
|
||||
|
||||
// Must have .yml or .yaml extension
|
||||
if (!uses.endsWith(".yml") && !uses.endsWith(".yaml")) {
|
||||
addWorkflowUsesFormatError(
|
||||
diagnostics,
|
||||
token,
|
||||
"workflow file should have either a '.yml' or '.yaml' file extension"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Must be at top level of .github/workflows/ or .github/workflows-lab/ (no subdirectories)
|
||||
const pathParts = uses.split("/");
|
||||
if (pathParts.length !== 4) {
|
||||
// Expected: ".", ".github", "workflows" or "workflows-lab", "filename.yml"
|
||||
addWorkflowUsesFormatError(
|
||||
diagnostics,
|
||||
token,
|
||||
"workflows must be defined at the top level of the .github/workflows/ directory"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Filename cannot be just the extension
|
||||
const filename = pathParts[3];
|
||||
if (filename === ".yml" || filename === ".yaml") {
|
||||
addWorkflowUsesFormatError(diagnostics, token, "invalid workflow file name");
|
||||
return;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Remote workflow reference: must have @ for version
|
||||
const atSegments = uses.split("@");
|
||||
if (atSegments.length === 1) {
|
||||
addWorkflowUsesFormatError(diagnostics, token, "no version specified");
|
||||
return;
|
||||
}
|
||||
if (atSegments.length > 2) {
|
||||
addWorkflowUsesFormatError(diagnostics, token, "too many '@' in workflow reference");
|
||||
return;
|
||||
}
|
||||
|
||||
const [pathPart, version] = atSegments;
|
||||
|
||||
// Version cannot be empty
|
||||
if (!version) {
|
||||
addWorkflowUsesFormatError(diagnostics, token, "no version specified");
|
||||
return;
|
||||
}
|
||||
|
||||
// Must contain .github/workflows or .github/workflows-lab path
|
||||
const workflowsMatch = pathPart.match(/\.github\/workflows(-lab)?\//);
|
||||
if (!workflowsMatch || workflowsMatch.index === undefined) {
|
||||
addWorkflowUsesFormatError(diagnostics, token, "references to workflows must be rooted in '.github/workflows'");
|
||||
return;
|
||||
}
|
||||
|
||||
// Split to get owner/repo and path
|
||||
const pathIdx = workflowsMatch.index;
|
||||
const nwoPart = pathPart.substring(0, pathIdx);
|
||||
const workflowPath = pathPart.substring(pathIdx);
|
||||
|
||||
// Validate NWO part: must be owner/repo/
|
||||
const nwoSegments = nwoPart.split("/").filter(s => s.length > 0);
|
||||
if (nwoSegments.length !== 2) {
|
||||
addWorkflowUsesFormatError(
|
||||
diagnostics,
|
||||
token,
|
||||
"references to workflows must be prefixed with format 'owner/repository/' or './' for local workflows"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate owner and repo names
|
||||
const [owner, repo] = nwoSegments;
|
||||
const nwoError = validateNWO(owner, repo);
|
||||
if (nwoError) {
|
||||
addWorkflowUsesFormatError(diagnostics, token, nwoError);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate ref/version format
|
||||
const refError = validateRefName(version);
|
||||
if (refError) {
|
||||
addWorkflowUsesFormatError(diagnostics, token, refError);
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate workflow path is at top level
|
||||
const workflowPathParts = workflowPath.split("/");
|
||||
if (workflowPathParts.length !== 3) {
|
||||
// Expected: ".github", "workflows" or "workflows-lab", "filename.yml"
|
||||
addWorkflowUsesFormatError(
|
||||
diagnostics,
|
||||
token,
|
||||
"workflows must be defined at the top level of the .github/workflows/ directory"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Must have .yml or .yaml extension
|
||||
const filename = workflowPathParts[2];
|
||||
if (!filename.endsWith(".yml") && !filename.endsWith(".yaml")) {
|
||||
addWorkflowUsesFormatError(
|
||||
diagnostics,
|
||||
token,
|
||||
"workflow file should have either a '.yml' or '.yaml' file extension"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Filename cannot be just the extension
|
||||
if (filename === ".yml" || filename === ".yaml") {
|
||||
addWorkflowUsesFormatError(diagnostics, token, "invalid workflow file name");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
function addWorkflowUsesFormatError(diagnostics: Diagnostic[], token: StringToken, reason: string): void {
|
||||
diagnostics.push({
|
||||
message: `Invalid workflow reference '${token.value}': ${reason}`,
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: mapRange(token.range),
|
||||
code: "invalid-workflow-uses-format"
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the git ref/version format.
|
||||
* Based on Launch's ValidateRefName function.
|
||||
*/
|
||||
function validateRefName(refname: string): string | undefined {
|
||||
if (refname.length === 0) {
|
||||
return "no version specified";
|
||||
}
|
||||
|
||||
// Cannot be the single character '@'
|
||||
if (refname === "@") {
|
||||
return "version cannot be the single character '@'";
|
||||
}
|
||||
|
||||
// Cannot have certain invalid characters or sequences
|
||||
const invalidSequences = ["?", "*", "[", "]", "\\", "~", "^", ":", "@{", "..", "//"];
|
||||
for (const seq of invalidSequences) {
|
||||
if (refname.includes(seq)) {
|
||||
return `invalid character '${seq}' in version`;
|
||||
}
|
||||
}
|
||||
|
||||
// Cannot begin or end with a slash '/' or a dot '.'
|
||||
if (refname.startsWith("/") || refname.endsWith("/") || refname.startsWith(".") || refname.endsWith(".")) {
|
||||
return "version cannot begin or end with a slash '/' or a dot '.'";
|
||||
}
|
||||
|
||||
// No slash-separated component can begin with a dot '.' or end with the sequence '.lock'
|
||||
const components = refname.split("/");
|
||||
for (const component of components) {
|
||||
if (component.startsWith(".") || component.endsWith(".lock")) {
|
||||
return `invalid version: ${refname}`;
|
||||
}
|
||||
}
|
||||
|
||||
// No ASCII control characters or whitespace
|
||||
// eslint-disable-next-line no-control-regex
|
||||
if (/[\x00-\x1f\x7f]/.test(refname)) {
|
||||
return "version cannot have ASCII control characters";
|
||||
}
|
||||
|
||||
if (/\s/.test(refname)) {
|
||||
return "version cannot have whitespace";
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates owner and repository names.
|
||||
* Based on Launch's ValidateNWO function.
|
||||
*/
|
||||
function validateNWO(owner: string, repo: string): string | undefined {
|
||||
// Owner name: can have word chars, dots, and hyphens
|
||||
// \w in JS regex is [a-zA-Z0-9_]
|
||||
if (!/^[\w.-]+$/.test(owner)) {
|
||||
return "owner name must be a valid repository owner name";
|
||||
}
|
||||
|
||||
// Repository name: can have word chars, dots, and hyphens
|
||||
if (!/^[\w.-]+$/.test(repo)) {
|
||||
return "repository name is invalid";
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function getProviderContext(
|
||||
documentUri: URI,
|
||||
template: WorkflowTemplate,
|
||||
|
||||
@@ -0,0 +1,872 @@
|
||||
import {DiagnosticSeverity} from "vscode-languageserver-types";
|
||||
import {validate} from "./validate";
|
||||
import {createDocument} from "./test-utils/document";
|
||||
import {clearCache} from "./utils/workflow-cache";
|
||||
|
||||
beforeEach(() => {
|
||||
clearCache();
|
||||
});
|
||||
|
||||
describe("validate uses format", () => {
|
||||
describe("valid formats", () => {
|
||||
it("standard org/repo@ref", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("org/repo with path @ref", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/aws/ec2@main
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("org/repo with deep path @ref", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/aws/nested/deep/path@v1
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("docker image", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: docker://alpine:3.8
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("docker image with registry", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: docker://gcr.io/my-project/my-image:latest
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("local path with ./", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: ./my-action
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("local path with ./ and subdirectories", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: ./.github/actions/my-action
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("local path with .\\ (Windows)", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: .\\my-action
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("SHA ref", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@a5ac7e51b41094c92402da3b24376905380afc29
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("branch ref", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: owner/repo@feature/my-branch
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("invalid formats", () => {
|
||||
it("missing @ref", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Expected format {owner}/{repo}[/path]@{ref}. Actual 'actions/checkout'",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 5, character: 12},
|
||||
end: {line: 5, character: 28}
|
||||
},
|
||||
code: "invalid-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("empty ref", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Expected format {owner}/{repo}[/path]@{ref}. Actual 'actions/checkout@'",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 5, character: 12},
|
||||
end: {line: 5, character: 29}
|
||||
},
|
||||
code: "invalid-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("missing org/owner", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: checkout@v4
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Expected format {owner}/{repo}[/path]@{ref}. Actual 'checkout@v4'",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 5, character: 12},
|
||||
end: {line: 5, character: 23}
|
||||
},
|
||||
code: "invalid-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("empty owner", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: /repo@v4
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Expected format {owner}/{repo}[/path]@{ref}. Actual '/repo@v4'",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 5, character: 12},
|
||||
end: {line: 5, character: 20}
|
||||
},
|
||||
code: "invalid-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("empty repo", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: owner/@v4
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Expected format {owner}/{repo}[/path]@{ref}. Actual 'owner/@v4'",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 5, character: 12},
|
||||
end: {line: 5, character: 21}
|
||||
},
|
||||
code: "invalid-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("multiple @ symbols", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4@extra
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Expected format {owner}/{repo}[/path]@{ref}. Actual 'actions/checkout@v4@extra'",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 5, character: 12},
|
||||
end: {line: 5, character: 37}
|
||||
},
|
||||
code: "invalid-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("just a name with no slash", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: checkout
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Expected format {owner}/{repo}[/path]@{ref}. Actual 'checkout'",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 5, character: 12},
|
||||
end: {line: 5, character: 20}
|
||||
},
|
||||
code: "invalid-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("empty uses value", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: ""
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toContainEqual({
|
||||
message: "`uses' value in action cannot be blank",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 5, character: 12},
|
||||
end: {line: 5, character: 14}
|
||||
},
|
||||
code: "invalid-uses-format"
|
||||
});
|
||||
});
|
||||
|
||||
it("reusable workflow in step", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: owner/repo/.github/workflows/test.yml@main
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message:
|
||||
"Reusable workflows should be referenced at the top-level `jobs.<job_id>.uses` key, not within steps",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 5, character: 12},
|
||||
end: {line: 5, character: 54}
|
||||
},
|
||||
code: "invalid-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("workflow uses format validation", () => {
|
||||
beforeEach(() => {
|
||||
clearCache();
|
||||
});
|
||||
|
||||
describe("valid formats", () => {
|
||||
it("local workflow path", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yml
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("local workflow path with yaml extension", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yaml
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("remote workflow with version", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/.github/workflows/test.yml@v1
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("remote workflow with sha ref", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/.github/workflows/test.yml@abc123
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("remote workflow with branch ref", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/.github/workflows/test.yml@main
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("remote workflow with yaml extension", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/.github/workflows/test.yaml@v1
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("local workflows-lab path", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows-lab/test.yml
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("local workflows-lab path with yaml extension", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows-lab/test.yaml
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it("remote workflows-lab with version", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/.github/workflows-lab/test.yml@v1
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("invalid formats", () => {
|
||||
it("remote workflow missing version", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/.github/workflows/test.yml
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Invalid workflow reference 'owner/repo/.github/workflows/test.yml': no version specified",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 47}
|
||||
},
|
||||
code: "invalid-workflow-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("local workflow with version", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/test.yml@v1
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message:
|
||||
"Invalid workflow reference './.github/workflows/test.yml@v1': cannot specify version when calling local workflows",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 41}
|
||||
},
|
||||
code: "invalid-workflow-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("missing .github/workflows path", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/test.yml@v1
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message:
|
||||
"Invalid workflow reference 'owner/repo/test.yml@v1': references to workflows must be rooted in '.github/workflows'",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 32}
|
||||
},
|
||||
code: "invalid-workflow-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("invalid file extension", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/.github/workflows/test.txt@v1
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message:
|
||||
"Invalid workflow reference 'owner/repo/.github/workflows/test.txt@v1': workflow file should have either a '.yml' or '.yaml' file extension",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 50}
|
||||
},
|
||||
code: "invalid-workflow-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("no extension", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/.github/workflows/test@v1
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message:
|
||||
"Invalid workflow reference 'owner/repo/.github/workflows/test@v1': workflow file should have either a '.yml' or '.yaml' file extension",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 46}
|
||||
},
|
||||
code: "invalid-workflow-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("just a ref", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: test.yml@v1
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message:
|
||||
"Invalid workflow reference 'test.yml@v1': references to workflows must be rooted in '.github/workflows'",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 21}
|
||||
},
|
||||
code: "invalid-workflow-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("local without .github/workflows", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: ./workflows/test.yml
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Invalid workflow reference './workflows/test.yml': no version specified",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 30}
|
||||
},
|
||||
code: "invalid-workflow-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
describe("invalid ref/version format", () => {
|
||||
it("empty version after @", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/.github/workflows/test.yml@
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Invalid workflow reference 'owner/repo/.github/workflows/test.yml@': no version specified",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 48}
|
||||
},
|
||||
code: "invalid-workflow-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("version with invalid character ?", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/.github/workflows/test.yml@v1?
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message:
|
||||
"Invalid workflow reference 'owner/repo/.github/workflows/test.yml@v1?': invalid character '?' in version",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 51}
|
||||
},
|
||||
code: "invalid-workflow-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("version with double dots", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/.github/workflows/test.yml@v1..v2
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message:
|
||||
"Invalid workflow reference 'owner/repo/.github/workflows/test.yml@v1..v2': invalid character '..' in version",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 54}
|
||||
},
|
||||
code: "invalid-workflow-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("version ending with dot", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/.github/workflows/test.yml@v1.
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message:
|
||||
"Invalid workflow reference 'owner/repo/.github/workflows/test.yml@v1.': version cannot begin or end with a slash '/' or a dot '.'",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 51}
|
||||
},
|
||||
code: "invalid-workflow-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("version starting with slash", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/.github/workflows/test.yml@/v1
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message:
|
||||
"Invalid workflow reference 'owner/repo/.github/workflows/test.yml@/v1': version cannot begin or end with a slash '/' or a dot '.'",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 51}
|
||||
},
|
||||
code: "invalid-workflow-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("version ending with .lock", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/.github/workflows/test.yml@refs/heads/main.lock
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message:
|
||||
"Invalid workflow reference 'owner/repo/.github/workflows/test.yml@refs/heads/main.lock': invalid version: refs/heads/main.lock",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 68}
|
||||
},
|
||||
code: "invalid-workflow-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("version with whitespace", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/.github/workflows/test.yml@v1 && rm -rf
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message:
|
||||
"Invalid workflow reference 'owner/repo/.github/workflows/test.yml@v1 && rm -rf': version cannot have whitespace",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 60}
|
||||
},
|
||||
code: "invalid-workflow-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("version with backslash", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/.github/workflows/test.yml@v1\\1
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message:
|
||||
"Invalid workflow reference 'owner/repo/.github/workflows/test.yml@v1\\1': invalid character '\\' in version",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 52}
|
||||
},
|
||||
code: "invalid-workflow-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("invalid owner/repo names", () => {
|
||||
it("owner with invalid characters", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner*/repo/.github/workflows/test.yml@v1
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message:
|
||||
"Invalid workflow reference 'owner*/repo/.github/workflows/test.yml@v1': owner name must be a valid repository owner name",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 51}
|
||||
},
|
||||
code: "invalid-workflow-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("repo with invalid characters", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo!name/.github/workflows/test.yml@v1
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message:
|
||||
"Invalid workflow reference 'owner/repo!name/.github/workflows/test.yml@v1': repository name is invalid",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 55}
|
||||
},
|
||||
code: "invalid-workflow-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("owner with spaces", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner name/repo/.github/workflows/test.yml@v1
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message:
|
||||
"Invalid workflow reference 'owner name/repo/.github/workflows/test.yml@v1': owner name must be a valid repository owner name",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 55}
|
||||
},
|
||||
code: "invalid-workflow-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("invalid workflow filename", () => {
|
||||
it("filename is just .yml", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/.github/workflows/.yml@v1
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Invalid workflow reference 'owner/repo/.github/workflows/.yml@v1': invalid workflow file name",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 46}
|
||||
},
|
||||
code: "invalid-workflow-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("filename is just .yaml", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: owner/repo/.github/workflows/.yaml@v1
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Invalid workflow reference 'owner/repo/.github/workflows/.yaml@v1': invalid workflow file name",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 47}
|
||||
},
|
||||
code: "invalid-workflow-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
it("local workflow filename is just .yml", async () => {
|
||||
const input = `on: push
|
||||
jobs:
|
||||
test:
|
||||
uses: ./.github/workflows/.yml
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input));
|
||||
expect(result).toEqual([
|
||||
{
|
||||
message: "Invalid workflow reference './.github/workflows/.yml': invalid workflow file name",
|
||||
severity: DiagnosticSeverity.Error,
|
||||
range: {
|
||||
start: {line: 3, character: 10},
|
||||
end: {line: 3, character: 34}
|
||||
},
|
||||
code: "invalid-workflow-uses-format"
|
||||
}
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -43,7 +43,7 @@ on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: monalisa/octocat/workflow.yaml@not-a-branch
|
||||
uses: monalisa/octocat/.github/workflows/workflow.yaml@not-a-branch
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input), {
|
||||
fileProvider: testFileProvider
|
||||
@@ -58,7 +58,7 @@ jobs:
|
||||
line: 5
|
||||
},
|
||||
end: {
|
||||
character: 53,
|
||||
character: 71,
|
||||
line: 5
|
||||
}
|
||||
}
|
||||
@@ -72,7 +72,7 @@ on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: monalisa/octocat/workflow.yaml@main
|
||||
uses: monalisa/octocat/.github/workflows/workflow.yaml@main
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input), {
|
||||
fileProvider: testFileProvider
|
||||
@@ -87,7 +87,7 @@ on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./reusable-workflow.yaml
|
||||
uses: ./.github/workflows/reusable-workflow.yaml
|
||||
`;
|
||||
const result = await validate(createDocument("wf.yaml", input), {
|
||||
fileProvider: testFileProvider
|
||||
@@ -102,7 +102,7 @@ on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./reusable-workflow-with-inputs.yaml
|
||||
uses: ./.github/workflows/reusable-workflow-with-inputs.yaml
|
||||
secrets:
|
||||
envPAT: pat
|
||||
`;
|
||||
@@ -119,7 +119,7 @@ jobs:
|
||||
line: 5
|
||||
},
|
||||
end: {
|
||||
character: 46,
|
||||
character: 64,
|
||||
line: 5
|
||||
}
|
||||
}
|
||||
@@ -133,7 +133,7 @@ on: push
|
||||
|
||||
jobs:
|
||||
build:
|
||||
uses: ./reusable-workflow-with-inputs.yaml
|
||||
uses: ./.github/workflows/reusable-workflow-with-inputs.yaml
|
||||
with:
|
||||
username: monalisa
|
||||
secrets:
|
||||
|
||||
Reference in New Issue
Block a user