Merge pull request #124 from github/joshmgross/reusable-wf-jobs
Add initial support for converting reusable workflow jobs
This commit is contained in:
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
*.md
|
||||||
|
*.js
|
||||||
|
*.json
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
*.md
|
||||||
|
*.js
|
||||||
|
*.json
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import {WorkflowContext} from "@github/actions-languageservice/context/workflow-context";
|
import {WorkflowContext} from "@github/actions-languageservice/context/workflow-context";
|
||||||
import {convertWorkflowTemplate, parseWorkflow, TraceWriter} from "@github/actions-workflow-parser";
|
import {convertWorkflowTemplate, parseWorkflow, TraceWriter} from "@github/actions-workflow-parser";
|
||||||
|
import {isJob} from "@github/actions-workflow-parser/model/type-guards";
|
||||||
|
|
||||||
const nullTrace: TraceWriter = {
|
const nullTrace: TraceWriter = {
|
||||||
info: x => {},
|
info: x => {},
|
||||||
@@ -16,7 +17,14 @@ export function createWorkflowContext(workflow: string, job?: string, stepIndex?
|
|||||||
const context: WorkflowContext = {uri: "test.yaml", template};
|
const context: WorkflowContext = {uri: "test.yaml", template};
|
||||||
|
|
||||||
if (job) {
|
if (job) {
|
||||||
context.job = template.jobs.find(j => j.id.value === job);
|
const workflowJob = template.jobs.find(j => j.id.value === job);
|
||||||
|
if (workflowJob) {
|
||||||
|
if (isJob(workflowJob)) {
|
||||||
|
context.job = workflowJob;
|
||||||
|
} else {
|
||||||
|
context.reusableWorkflowJob = workflowJob;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (stepIndex !== undefined) {
|
if (stepIndex !== undefined) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {data, DescriptionDictionary} from "@github/actions-expressions";
|
import {data, DescriptionDictionary} from "@github/actions-expressions";
|
||||||
import {isScalar, isString} from "@github/actions-workflow-parser";
|
import {isScalar, isString} from "@github/actions-workflow-parser";
|
||||||
import {Job} from "@github/actions-workflow-parser/model/workflow-template";
|
import {isJob} from "@github/actions-workflow-parser/model/type-guards";
|
||||||
|
import {Job, WorkflowJob} from "@github/actions-workflow-parser/model/workflow-template";
|
||||||
import {WorkflowContext} from "../context/workflow-context";
|
import {WorkflowContext} from "../context/workflow-context";
|
||||||
|
|
||||||
export function getNeedsContext(workflowContext: WorkflowContext): DescriptionDictionary {
|
export function getNeedsContext(workflowContext: WorkflowContext): DescriptionDictionary {
|
||||||
@@ -17,11 +18,13 @@ export function getNeedsContext(workflowContext: WorkflowContext): DescriptionDi
|
|||||||
return d;
|
return d;
|
||||||
}
|
}
|
||||||
|
|
||||||
function needsJobContext(job?: Job): DescriptionDictionary {
|
function needsJobContext(job?: WorkflowJob): DescriptionDictionary {
|
||||||
// https://docs.github.com/en/actions/learn-github-actions/contexts#needs-context
|
// https://docs.github.com/en/actions/learn-github-actions/contexts#needs-context
|
||||||
const d = new DescriptionDictionary();
|
const d = new DescriptionDictionary();
|
||||||
|
|
||||||
|
if (job && isJob(job)) {
|
||||||
d.add("outputs", jobOutputs(job));
|
d.add("outputs", jobOutputs(job));
|
||||||
|
}
|
||||||
|
|
||||||
// Can be "success", "failure", "cancelled", or "skipped"
|
// Can be "success", "failure", "cancelled", or "skipped"
|
||||||
d.add("result", new data.Null());
|
d.add("result", new data.Null());
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {isMapping, isSequence, WorkflowTemplate} from "@github/actions-workflow-parser";
|
import {isMapping, isSequence, WorkflowTemplate} from "@github/actions-workflow-parser";
|
||||||
import {Job, Step} from "@github/actions-workflow-parser/model/workflow-template";
|
import {isJob, isReusableWorkflowJob} from "@github/actions-workflow-parser/model/type-guards";
|
||||||
|
import {Step, Job, ReusableWorkflowJob} from "@github/actions-workflow-parser/model/workflow-template";
|
||||||
import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token";
|
import {MappingToken} from "@github/actions-workflow-parser/templates/tokens/mapping-token";
|
||||||
import {SequenceToken} from "@github/actions-workflow-parser/templates/tokens/sequence-token";
|
import {SequenceToken} from "@github/actions-workflow-parser/templates/tokens/sequence-token";
|
||||||
import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token";
|
import {StringToken} from "@github/actions-workflow-parser/templates/tokens/string-token";
|
||||||
@@ -10,9 +11,12 @@ export interface WorkflowContext {
|
|||||||
|
|
||||||
template: WorkflowTemplate | undefined;
|
template: WorkflowTemplate | undefined;
|
||||||
|
|
||||||
/** If the context is for a position within a job, this will be the job */
|
/** If the context is for a position within a regular job, this will be the job */
|
||||||
job?: Job;
|
job?: Job;
|
||||||
|
|
||||||
|
/** If the context is for a position within a reusable workflow job, this will be the reusable workflow job */
|
||||||
|
reusableWorkflowJob?: ReusableWorkflowJob;
|
||||||
|
|
||||||
/** If the context is for a position within a step, this will be the step */
|
/** If the context is for a position within a step, this will be the step */
|
||||||
step?: Step;
|
step?: Step;
|
||||||
}
|
}
|
||||||
@@ -35,7 +39,15 @@ export function getWorkflowContext(
|
|||||||
switch (token.definition?.key) {
|
switch (token.definition?.key) {
|
||||||
case "job": {
|
case "job": {
|
||||||
const jobID = (token as StringToken).value;
|
const jobID = (token as StringToken).value;
|
||||||
context.job = template.jobs.find(job => job.id.value === jobID);
|
const job = template.jobs.find(job => job.id.value === jobID);
|
||||||
|
if (!job) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (isJob(job)) {
|
||||||
|
context.job = job;
|
||||||
|
} else if (isReusableWorkflowJob(job)) {
|
||||||
|
context.reusableWorkflowJob = job;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "steps": {
|
case "steps": {
|
||||||
@@ -54,7 +66,10 @@ export function getWorkflowContext(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
context.step = findStep(context.job?.steps, stepsSequence, stepToken);
|
if (context.job && isJob(context.job)) {
|
||||||
|
context.step = findStep(context.job.steps, stepsSequence, stepToken);
|
||||||
|
}
|
||||||
|
|
||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {convertWorkflowTemplate, parseWorkflow} from "@github/actions-workflow-parser";
|
import {convertWorkflowTemplate, parseWorkflow} from "@github/actions-workflow-parser";
|
||||||
import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert";
|
import {ErrorPolicy} from "@github/actions-workflow-parser/model/convert";
|
||||||
|
import {isJob} from "@github/actions-workflow-parser/model/type-guards";
|
||||||
import {File} from "@github/actions-workflow-parser/workflows/file";
|
import {File} from "@github/actions-workflow-parser/workflows/file";
|
||||||
import {TextDocument} from "vscode-languageserver-textdocument";
|
import {TextDocument} from "vscode-languageserver-textdocument";
|
||||||
import {DocumentLink} from "vscode-languageserver-types";
|
import {DocumentLink} from "vscode-languageserver-types";
|
||||||
@@ -27,7 +28,10 @@ export async function documentLinks(document: TextDocument): Promise<DocumentLin
|
|||||||
const gitHubBaseUri = "https://www.github.com/";
|
const gitHubBaseUri = "https://www.github.com/";
|
||||||
|
|
||||||
for (const job of template?.jobs || []) {
|
for (const job of template?.jobs || []) {
|
||||||
for (const step of job?.steps || []) {
|
if (!job || !isJob(job)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const step of job.steps || []) {
|
||||||
if ("uses" in step) {
|
if ("uses" in step) {
|
||||||
const actionRef = parseActionReference(step.uses.value);
|
const actionRef = parseActionReference(step.uses.value);
|
||||||
if (!actionRef) {
|
if (!actionRef) {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import {File} from "@github/actions-workflow-parser/workflows/file";
|
||||||
|
|
||||||
|
export type WorkflowProvider = {
|
||||||
|
getWorkflow: (name: string) => Promise<File | undefined>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
*.md
|
||||||
|
*.js
|
||||||
|
*.json
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import {TemplateContext} from "../../templates/template-context";
|
||||||
|
import {StringToken, MappingToken, BasicExpressionToken} from "../../templates/tokens";
|
||||||
|
import {isSequence, isString} from "../../templates/tokens/type-guards";
|
||||||
|
import {BaseJob} from "../workflow-template";
|
||||||
|
import {convertConcurrency} from "./concurrency";
|
||||||
|
import {handleTemplateTokenErrors} from "./handle-errors";
|
||||||
|
import {IdBuilder} from "./id-builder";
|
||||||
|
|
||||||
|
export function convertBaseJob(
|
||||||
|
context: TemplateContext,
|
||||||
|
jobKey: StringToken,
|
||||||
|
token: MappingToken
|
||||||
|
): Omit<BaseJob, "type"> {
|
||||||
|
const error = new IdBuilder().tryAddKnownId(jobKey.value);
|
||||||
|
if (error) {
|
||||||
|
context.error(jobKey, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: Omit<BaseJob, "type"> = {
|
||||||
|
id: jobKey,
|
||||||
|
name: undefined,
|
||||||
|
needs: undefined,
|
||||||
|
if: new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined),
|
||||||
|
concurrency: undefined,
|
||||||
|
strategy: undefined
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const item of token) {
|
||||||
|
const propertyName = item.key.assertString("job property name");
|
||||||
|
switch (propertyName.value) {
|
||||||
|
case "concurrency":
|
||||||
|
handleTemplateTokenErrors(item.value, context, undefined, () => convertConcurrency(context, item.value));
|
||||||
|
result.concurrency = item.value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "name":
|
||||||
|
result.name = item.value.assertScalar("job name");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "needs":
|
||||||
|
result.needs = [];
|
||||||
|
if (isString(item.value)) {
|
||||||
|
const jobNeeds = item.value.assertString("job needs id");
|
||||||
|
result.needs.push(jobNeeds);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSequence(item.value)) {
|
||||||
|
for (const seqItem of item.value) {
|
||||||
|
const jobNeeds = seqItem.assertString("job needs id");
|
||||||
|
result.needs.push(jobNeeds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "strategy":
|
||||||
|
result.strategy = item.value;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!result.name) {
|
||||||
|
result.name = result.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import {TemplateContext} from "../../templates/template-context";
|
||||||
|
import {StringToken, MappingToken} from "../../templates/tokens";
|
||||||
|
import {Job} from "../workflow-template";
|
||||||
|
import {convertBaseJob} from "./base-job";
|
||||||
|
import {convertToJobContainer, convertToJobServices} from "./container";
|
||||||
|
import {handleTemplateTokenErrors} from "./handle-errors";
|
||||||
|
import {convertToActionsEnvironmentRef} from "./job/environment";
|
||||||
|
import {convertRunsOn} from "./job/runs-on";
|
||||||
|
import {convertSteps} from "./steps";
|
||||||
|
|
||||||
|
export function convertJob(context: TemplateContext, jobKey: StringToken, token: MappingToken): Job {
|
||||||
|
const base = convertBaseJob(context, jobKey, token);
|
||||||
|
const result: Job = {
|
||||||
|
type: "job",
|
||||||
|
id: base.id,
|
||||||
|
name: base.name,
|
||||||
|
needs: base.needs,
|
||||||
|
if: base.if,
|
||||||
|
env: undefined,
|
||||||
|
concurrency: base.concurrency,
|
||||||
|
environment: undefined,
|
||||||
|
strategy: base.strategy,
|
||||||
|
"runs-on": undefined,
|
||||||
|
container: undefined,
|
||||||
|
services: undefined,
|
||||||
|
outputs: undefined,
|
||||||
|
steps: []
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const item of token) {
|
||||||
|
const propertyName = item.key.assertString("job property name");
|
||||||
|
switch (propertyName.value) {
|
||||||
|
case "container":
|
||||||
|
convertToJobContainer(context, item.value);
|
||||||
|
result.container = item.value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "env":
|
||||||
|
result.env = item.value.assertMapping("job env");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "environment":
|
||||||
|
handleTemplateTokenErrors(item.value, context, undefined, () =>
|
||||||
|
convertToActionsEnvironmentRef(context, item.value)
|
||||||
|
);
|
||||||
|
result.environment = item.value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "outputs":
|
||||||
|
result.outputs = item.value.assertMapping("job outputs");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "runs-on":
|
||||||
|
handleTemplateTokenErrors(item.value, context, undefined, () => convertRunsOn(context, item.value));
|
||||||
|
result["runs-on"] = item.value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "services":
|
||||||
|
convertToJobServices(context, item.value);
|
||||||
|
result.services = item.value;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "steps":
|
||||||
|
result.steps = convertSteps(context, item.value);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import {TemplateContext} from "../../../templates/template-context";
|
||||||
|
import {TemplateToken} from "../../../templates/tokens";
|
||||||
|
import {isMapping, isString, isSequence} from "../../../templates/tokens/type-guards";
|
||||||
|
|
||||||
|
type RunsOn = {
|
||||||
|
labels: Set<string>;
|
||||||
|
group: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function convertRunsOn(context: TemplateContext, token: TemplateToken): RunsOn {
|
||||||
|
const labels = convertRunsOnLabels(token);
|
||||||
|
|
||||||
|
if (!isMapping(token)) {
|
||||||
|
return {
|
||||||
|
labels,
|
||||||
|
group: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let group = "";
|
||||||
|
|
||||||
|
for (const item of token) {
|
||||||
|
const key = item.key.assertString("job runs-on property name");
|
||||||
|
switch (key.value) {
|
||||||
|
case "group": {
|
||||||
|
if (item.value.isExpression) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const groupName = item.value.assertString("job runs-on group name").value;
|
||||||
|
const names = groupName.split("/");
|
||||||
|
switch (names.length) {
|
||||||
|
case 1: {
|
||||||
|
group = groupName;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 2: {
|
||||||
|
if (!["org", "organization", "ent", "enterprise"].includes(names[0])) {
|
||||||
|
context.error(
|
||||||
|
item.value,
|
||||||
|
`Invalid runs-on group name '${groupName}. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.'`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!names[1]) {
|
||||||
|
context.error(item.value, `Invalid runs-on group name '${groupName}'.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
group = groupName;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
context.error(
|
||||||
|
item.value,
|
||||||
|
`Invalid runs-on group name '${groupName}. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.'`
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "labels": {
|
||||||
|
const mapLabels = convertRunsOnLabels(item.value);
|
||||||
|
for (const label of mapLabels) {
|
||||||
|
labels.add(label);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
labels,
|
||||||
|
group
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function convertRunsOnLabels(token: TemplateToken): Set<string> {
|
||||||
|
const labels = new Set<string>();
|
||||||
|
if (token.isExpression) {
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isString(token)) {
|
||||||
|
labels.add(token.value);
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSequence(token)) {
|
||||||
|
for (const item of token) {
|
||||||
|
if (item.isExpression) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = item.assertString("job runs-on label sequence item");
|
||||||
|
labels.add(label.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return labels;
|
||||||
|
}
|
||||||
@@ -1,14 +1,10 @@
|
|||||||
import {TemplateContext} from "../../templates/template-context";
|
import {TemplateContext} from "../../templates/template-context";
|
||||||
import {BasicExpressionToken, MappingToken, StringToken} from "../../templates/tokens";
|
import {StringToken} from "../../templates/tokens";
|
||||||
import {TemplateToken} from "../../templates/tokens/template-token";
|
import {TemplateToken} from "../../templates/tokens/template-token";
|
||||||
import {isMapping, isSequence, isString} from "../../templates/tokens/type-guards";
|
import {isMapping} from "../../templates/tokens/type-guards";
|
||||||
import {Job} from "../workflow-template";
|
import {Job} from "../workflow-template";
|
||||||
import {convertConcurrency} from "./concurrency";
|
|
||||||
import {convertToJobContainer, convertToJobServices} from "./container";
|
|
||||||
import {handleTemplateTokenErrors} from "./handle-errors";
|
import {handleTemplateTokenErrors} from "./handle-errors";
|
||||||
import {IdBuilder} from "./id-builder";
|
import {convertJob} from "./job";
|
||||||
import {convertToActionsEnvironmentRef} from "./job/environment";
|
|
||||||
import {convertSteps} from "./steps";
|
|
||||||
|
|
||||||
type nodeInfo = {
|
type nodeInfo = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -100,200 +96,3 @@ function validateNeeds(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertJob(context: TemplateContext, jobKey: StringToken, token: MappingToken): Job {
|
|
||||||
const error = new IdBuilder().tryAddKnownId(jobKey.value);
|
|
||||||
if (error) {
|
|
||||||
context.error(jobKey, error);
|
|
||||||
}
|
|
||||||
const result: Job = {
|
|
||||||
type: "job",
|
|
||||||
id: jobKey,
|
|
||||||
name: undefined,
|
|
||||||
needs: undefined,
|
|
||||||
if: new BasicExpressionToken(undefined, undefined, "success()", undefined, undefined, undefined),
|
|
||||||
env: undefined,
|
|
||||||
concurrency: undefined,
|
|
||||||
environment: undefined,
|
|
||||||
strategy: undefined,
|
|
||||||
"runs-on": undefined,
|
|
||||||
container: undefined,
|
|
||||||
services: undefined,
|
|
||||||
outputs: undefined,
|
|
||||||
steps: []
|
|
||||||
};
|
|
||||||
|
|
||||||
for (const item of token) {
|
|
||||||
const propertyName = item.key.assertString("job property name");
|
|
||||||
switch (propertyName.value) {
|
|
||||||
case "concurrency":
|
|
||||||
handleTemplateTokenErrors(item.value, context, undefined, () => convertConcurrency(context, item.value));
|
|
||||||
result.concurrency = item.value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "container":
|
|
||||||
// Do early validation, but don't convert
|
|
||||||
convertToJobContainer(context, item.value);
|
|
||||||
result.container = item.value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "env":
|
|
||||||
result.env = item.value.assertMapping("job env");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "environment":
|
|
||||||
handleTemplateTokenErrors(item.value, context, undefined, () =>
|
|
||||||
convertToActionsEnvironmentRef(context, item.value)
|
|
||||||
);
|
|
||||||
result.environment = item.value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "name":
|
|
||||||
result.name = item.value.assertScalar("job name");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "needs":
|
|
||||||
result.needs = [];
|
|
||||||
if (isString(item.value)) {
|
|
||||||
const jobNeeds = item.value.assertString("job needs id");
|
|
||||||
result.needs.push(jobNeeds);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSequence(item.value)) {
|
|
||||||
for (const seqItem of item.value) {
|
|
||||||
const jobNeeds = seqItem.assertString("job needs id");
|
|
||||||
result.needs.push(jobNeeds);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "outputs":
|
|
||||||
result.outputs = item.value.assertMapping("job outputs");
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "runs-on":
|
|
||||||
handleTemplateTokenErrors(item.value, context, undefined, () => convertRunsOn(context, item.value));
|
|
||||||
result["runs-on"] = item.value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "services":
|
|
||||||
// Do early validation, but don't convert
|
|
||||||
convertToJobServices(context, item.value);
|
|
||||||
result.services = item.value;
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "steps":
|
|
||||||
result.steps = convertSteps(context, item.value);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case "strategy":
|
|
||||||
result.strategy = item.value;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!result.name) {
|
|
||||||
result.name = result.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
type RunsOn = {
|
|
||||||
labels: Set<string>;
|
|
||||||
group: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function convertRunsOn(context: TemplateContext, token: TemplateToken): RunsOn {
|
|
||||||
const labels = convertRunsOnLabels(token);
|
|
||||||
|
|
||||||
if (!isMapping(token)) {
|
|
||||||
return {
|
|
||||||
labels,
|
|
||||||
group: ""
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let group = "";
|
|
||||||
|
|
||||||
for (const item of token) {
|
|
||||||
const key = item.key.assertString("job runs-on property name");
|
|
||||||
switch (key.value) {
|
|
||||||
case "group": {
|
|
||||||
if (item.value.isExpression) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const groupName = item.value.assertString("job runs-on group name").value;
|
|
||||||
const names = groupName.split("/");
|
|
||||||
switch (names.length) {
|
|
||||||
case 1: {
|
|
||||||
group = groupName;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 2: {
|
|
||||||
if (!["org", "organization", "ent", "enterprise"].includes(names[0])) {
|
|
||||||
context.error(
|
|
||||||
item.value,
|
|
||||||
`Invalid runs-on group name '${groupName}. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.'`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!names[1]) {
|
|
||||||
context.error(item.value, `Invalid runs-on group name '${groupName}'.`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
group = groupName;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
context.error(
|
|
||||||
item.value,
|
|
||||||
`Invalid runs-on group name '${groupName}. Please use 'organization/' or 'enterprise/' prefix to target a single runner group.'`
|
|
||||||
);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "labels": {
|
|
||||||
const mapLabels = convertRunsOnLabels(item.value);
|
|
||||||
for (const label of mapLabels) {
|
|
||||||
labels.add(label);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
labels,
|
|
||||||
group
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function convertRunsOnLabels(token: TemplateToken): Set<string> {
|
|
||||||
const labels = new Set<string>();
|
|
||||||
if (token.isExpression) {
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isString(token)) {
|
|
||||||
labels.add(token.value);
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSequence(token)) {
|
|
||||||
for (const item of token) {
|
|
||||||
if (item.isExpression) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const label = item.assertString("job runs-on label sequence item");
|
|
||||||
labels.add(label.value);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return labels;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import {TemplateContext} from "../../templates/template-context";
|
||||||
|
import {MappingToken, StringToken} from "../../templates/tokens";
|
||||||
|
import {ReusableWorkflowJob} from "../workflow-template";
|
||||||
|
import {convertBaseJob} from "./base-job";
|
||||||
|
|
||||||
|
export function convertResuableWorkflowJob(
|
||||||
|
context: TemplateContext,
|
||||||
|
jobKey: StringToken,
|
||||||
|
token: MappingToken
|
||||||
|
): ReusableWorkflowJob {
|
||||||
|
const base = convertBaseJob(context, jobKey, token);
|
||||||
|
|
||||||
|
const result: ReusableWorkflowJob = {
|
||||||
|
type: "reusableWorkflowJob",
|
||||||
|
id: base.id,
|
||||||
|
name: base.name,
|
||||||
|
needs: base.needs,
|
||||||
|
if: base.if,
|
||||||
|
concurrency: base.concurrency,
|
||||||
|
strategy: base.strategy,
|
||||||
|
uses: new StringToken(undefined, undefined, "undefined", undefined, undefined)
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const item of token) {
|
||||||
|
const propertyName = item.key.assertString("job property name");
|
||||||
|
switch (propertyName.value) {
|
||||||
|
case "uses":
|
||||||
|
result.uses = item.value.assertString("job item uses");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {ActionStep, RunStep, Step} from "./workflow-template";
|
import {ActionStep, Job, ReusableWorkflowJob, RunStep, Step, WorkflowJob} from "./workflow-template";
|
||||||
|
|
||||||
export function isRunStep(step: Step): step is RunStep {
|
export function isRunStep(step: Step): step is RunStep {
|
||||||
return (step as RunStep).run !== undefined;
|
return (step as RunStep).run !== undefined;
|
||||||
@@ -7,3 +7,11 @@ export function isRunStep(step: Step): step is RunStep {
|
|||||||
export function isActionStep(step: Step): step is ActionStep {
|
export function isActionStep(step: Step): step is ActionStep {
|
||||||
return (step as ActionStep).uses !== undefined;
|
return (step as ActionStep).uses !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isJob(job: WorkflowJob): job is Job {
|
||||||
|
return job.type === "job";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isReusableWorkflowJob(job: WorkflowJob): job is ReusableWorkflowJob {
|
||||||
|
return job.type === "reusableWorkflowJob";
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
|
|
||||||
export type WorkflowTemplate = {
|
export type WorkflowTemplate = {
|
||||||
events: EventsConfig;
|
events: EventsConfig;
|
||||||
jobs: Job[];
|
jobs: WorkflowJob[];
|
||||||
concurrency: TemplateToken;
|
concurrency: TemplateToken;
|
||||||
env: TemplateToken;
|
env: TemplateToken;
|
||||||
|
|
||||||
@@ -28,16 +28,25 @@ export type ActionsEnvironmentReference = {
|
|||||||
url?: TemplateToken;
|
url?: TemplateToken;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Job = {
|
export type WorkflowJob = Job | ReusableWorkflowJob;
|
||||||
type: string;
|
|
||||||
|
export type JobType = "job" | "reusableWorkflowJob";
|
||||||
|
|
||||||
|
export type BaseJob = {
|
||||||
|
type: JobType;
|
||||||
id: StringToken;
|
id: StringToken;
|
||||||
name?: ScalarToken;
|
name?: ScalarToken;
|
||||||
needs?: StringToken[];
|
needs?: StringToken[];
|
||||||
if: BasicExpressionToken;
|
if: BasicExpressionToken;
|
||||||
env?: MappingToken;
|
|
||||||
concurrency?: TemplateToken;
|
concurrency?: TemplateToken;
|
||||||
environment?: TemplateToken;
|
|
||||||
strategy?: TemplateToken;
|
strategy?: TemplateToken;
|
||||||
|
};
|
||||||
|
|
||||||
|
// `job-factory` in the schema
|
||||||
|
export type Job = BaseJob & {
|
||||||
|
type: "job";
|
||||||
|
env?: MappingToken;
|
||||||
|
environment?: TemplateToken;
|
||||||
"runs-on"?: TemplateToken;
|
"runs-on"?: TemplateToken;
|
||||||
container?: TemplateToken;
|
container?: TemplateToken;
|
||||||
services?: TemplateToken;
|
services?: TemplateToken;
|
||||||
@@ -45,6 +54,12 @@ export type Job = {
|
|||||||
steps: Step[];
|
steps: Step[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// `workflow-job` in the schema
|
||||||
|
export type ReusableWorkflowJob = BaseJob & {
|
||||||
|
type: "reusableWorkflowJob";
|
||||||
|
uses: StringToken;
|
||||||
|
};
|
||||||
|
|
||||||
export type Container = {
|
export type Container = {
|
||||||
image: StringToken;
|
image: StringToken;
|
||||||
credentials?: Credential;
|
credentials?: Credential;
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
*.md
|
||||||
|
*.js
|
||||||
|
*.json
|
||||||
Reference in New Issue
Block a user