Add initial support for converting reusable wf jobs

This commit is contained in:
Josh Gross
2023-02-03 16:57:28 -05:00
parent 36f879de6c
commit fe16d1a1ed
14 changed files with 335 additions and 224 deletions
@@ -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 {BasicExpressionToken, MappingToken, StringToken} from "../../templates/tokens";
import {StringToken} from "../../templates/tokens";
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 {convertConcurrency} from "./concurrency";
import {convertToJobContainer, convertToJobServices} from "./container";
import {handleTemplateTokenErrors} from "./handle-errors";
import {IdBuilder} from "./id-builder";
import {convertToActionsEnvironmentRef} from "./job/environment";
import {convertSteps} from "./steps";
import {convertJob} from "./job";
type nodeInfo = {
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: "job",
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} from "./workflow-template";
export function isRunStep(step: Step): step is RunStep {
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 {
return (step as ActionStep).uses !== undefined;
}
export function isJob(job: Job | ReusableWorkflowJob): job is Job {
return job.type === "job";
}
export function isReusableWorkflowJob(job: Job | ReusableWorkflowJob): job is ReusableWorkflowJob {
return job.type === "reusableWorkflowJob";
}
@@ -9,7 +9,7 @@ import {
export type WorkflowTemplate = {
events: EventsConfig;
jobs: Job[];
jobs: WorkflowJob[];
concurrency: TemplateToken;
env: TemplateToken;
@@ -28,16 +28,24 @@ export type ActionsEnvironmentReference = {
url?: TemplateToken;
};
export type Job = {
type: string;
export type WorkflowJob = Job | ReusableWorkflowJob;
export type JobType = "job" | "reusableWorkflowJob";
export type BaseJob = {
type: JobType;
id: StringToken;
name?: ScalarToken;
needs?: StringToken[];
if: BasicExpressionToken;
env?: MappingToken;
concurrency?: TemplateToken;
environment?: TemplateToken;
strategy?: TemplateToken;
};
// `job-factory` in the schema
export type Job = BaseJob & {
env?: MappingToken;
environment?: TemplateToken;
"runs-on"?: TemplateToken;
container?: TemplateToken;
services?: TemplateToken;
@@ -45,6 +53,11 @@ export type Job = {
steps: Step[];
};
// `workflow-job` in the schema
export type ReusableWorkflowJob = BaseJob & {
uses: StringToken;
};
export type Container = {
image: StringToken;
credentials?: Credential;