Skip to content

Commit d2f52a9

Browse files
authored
Validate implicit if conditions in action.yml files (#317)
## Problem In workflow YAML files, writing `if: foo == bar` shows an error because `foo` and `bar` are not valid contexts. However, the same invalid expression in an action.yml file showed no error. ## Solution Add expression validation for implicit `if` conditions in action.yml files, matching the behavior of workflow YAML validation. ## What's new 1. **Pre-if/post-if validation** (node and docker actions) - `pre-if: foo == bar` now shows error for unknown context - `post-if: unknownFunc()` now shows error for unknown function 2. **Composite step `if` validation** (fix) - Errors from `convertToIfCondition` were being lost due to call ordering - Now captured correctly by calling conversion before retrieving errors ## Why the refactor? The diff includes consolidating multiple validation loops into a single `validateAllTokens()` traversal. This matches the pattern used in workflow YAML validation (`additionalValidations`), making the code consistent between the two validation paths.
1 parent 46b216a commit d2f52a9

File tree

6 files changed

+457
-137
lines changed

6 files changed

+457
-137
lines changed

languageservice/src/validate-action.test.ts

Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,4 +1011,255 @@ runs:
10111011
expect(diagnostics.some(d => d.code === "format-arg-count-mismatch")).toBe(true);
10121012
});
10131013
});
1014+
1015+
describe("if condition context validation", () => {
1016+
it("warns on unknown context in composite step if", async () => {
1017+
const doc = createActionDocument(`
1018+
name: My Action
1019+
description: Unknown context in if
1020+
runs:
1021+
using: composite
1022+
steps:
1023+
- if: foo == bar
1024+
run: echo hi
1025+
shell: bash
1026+
`);
1027+
const diagnostics = await validate(doc);
1028+
expect(diagnostics.some(d => d.message.includes("Unrecognized named-value"))).toBe(true);
1029+
});
1030+
1031+
it("warns on unknown context in pre-if for node action", async () => {
1032+
const doc = createActionDocument(`
1033+
name: My Action
1034+
description: Unknown context in pre-if
1035+
runs:
1036+
using: node20
1037+
main: index.js
1038+
pre: setup.js
1039+
pre-if: foo == bar
1040+
`);
1041+
const diagnostics = await validate(doc);
1042+
expect(diagnostics.some(d => d.message.includes("Unrecognized named-value"))).toBe(true);
1043+
});
1044+
1045+
it("warns on unknown context in post-if for node action", async () => {
1046+
const doc = createActionDocument(`
1047+
name: My Action
1048+
description: Unknown context in post-if
1049+
runs:
1050+
using: node20
1051+
main: index.js
1052+
post: cleanup.js
1053+
post-if: foo == bar
1054+
`);
1055+
const diagnostics = await validate(doc);
1056+
expect(diagnostics.some(d => d.message.includes("Unrecognized named-value"))).toBe(true);
1057+
});
1058+
1059+
it("warns on unknown context in pre-if for docker action", async () => {
1060+
const doc = createActionDocument(`
1061+
name: My Action
1062+
description: Unknown context in pre-if
1063+
runs:
1064+
using: docker
1065+
image: Dockerfile
1066+
pre-entrypoint: /setup.sh
1067+
pre-if: foo == bar
1068+
`);
1069+
const diagnostics = await validate(doc);
1070+
expect(diagnostics.some(d => d.message.includes("Unrecognized named-value"))).toBe(true);
1071+
});
1072+
1073+
it("warns on unknown context in post-if for docker action", async () => {
1074+
const doc = createActionDocument(`
1075+
name: My Action
1076+
description: Unknown context in post-if
1077+
runs:
1078+
using: docker
1079+
image: Dockerfile
1080+
post-entrypoint: /cleanup.sh
1081+
post-if: foo == bar
1082+
`);
1083+
const diagnostics = await validate(doc);
1084+
expect(diagnostics.some(d => d.message.includes("Unrecognized named-value"))).toBe(true);
1085+
});
1086+
1087+
it("allows valid contexts in composite step if", async () => {
1088+
const doc = createActionDocument(`
1089+
name: My Action
1090+
description: Valid context in if
1091+
runs:
1092+
using: composite
1093+
steps:
1094+
- if: github.event_name == 'push'
1095+
run: echo hi
1096+
shell: bash
1097+
`);
1098+
const diagnostics = await validate(doc);
1099+
expect(diagnostics.some(d => d.message.includes("Unrecognized named-value"))).toBe(false);
1100+
});
1101+
1102+
it("allows valid contexts in pre-if", async () => {
1103+
const doc = createActionDocument(`
1104+
name: My Action
1105+
description: Valid context in pre-if
1106+
runs:
1107+
using: node20
1108+
main: index.js
1109+
pre: setup.js
1110+
pre-if: runner.os == 'Linux'
1111+
`);
1112+
const diagnostics = await validate(doc);
1113+
expect(diagnostics.some(d => d.message.includes("Unrecognized named-value"))).toBe(false);
1114+
});
1115+
1116+
it("allows valid contexts in post-if", async () => {
1117+
const doc = createActionDocument(`
1118+
name: My Action
1119+
description: Valid context in post-if
1120+
runs:
1121+
using: node20
1122+
main: index.js
1123+
post: cleanup.js
1124+
post-if: runner.os == 'Linux'
1125+
`);
1126+
const diagnostics = await validate(doc);
1127+
expect(diagnostics.some(d => d.message.includes("Unrecognized named-value"))).toBe(false);
1128+
});
1129+
1130+
it("allows hashFiles function in composite step if", async () => {
1131+
const doc = createActionDocument(`
1132+
name: My Action
1133+
description: hashFiles in if
1134+
runs:
1135+
using: composite
1136+
steps:
1137+
- if: hashFiles('**/package-lock.json') != ''
1138+
run: echo hi
1139+
shell: bash
1140+
`);
1141+
const diagnostics = await validate(doc);
1142+
expect(diagnostics.some(d => d.message.includes("Unrecognized"))).toBe(false);
1143+
});
1144+
1145+
it("allows success, failure, always, cancelled functions in composite step if", async () => {
1146+
const doc = createActionDocument(`
1147+
name: My Action
1148+
description: Status functions in if
1149+
runs:
1150+
using: composite
1151+
steps:
1152+
- if: success() && !cancelled()
1153+
run: echo success
1154+
shell: bash
1155+
- if: failure()
1156+
run: echo failure
1157+
shell: bash
1158+
- if: always()
1159+
run: echo always
1160+
shell: bash
1161+
`);
1162+
const diagnostics = await validate(doc);
1163+
expect(diagnostics.some(d => d.message.includes("Unrecognized"))).toBe(false);
1164+
});
1165+
1166+
it("allows hashFiles function in pre-if", async () => {
1167+
const doc = createActionDocument(`
1168+
name: My Action
1169+
description: hashFiles in pre-if
1170+
runs:
1171+
using: node20
1172+
main: index.js
1173+
pre: setup.js
1174+
pre-if: hashFiles('**/package-lock.json') != ''
1175+
`);
1176+
const diagnostics = await validate(doc);
1177+
expect(diagnostics.some(d => d.message.includes("Unrecognized"))).toBe(false);
1178+
});
1179+
1180+
it("allows status functions in post-if", async () => {
1181+
const doc = createActionDocument(`
1182+
name: My Action
1183+
description: Status functions in post-if
1184+
runs:
1185+
using: node20
1186+
main: index.js
1187+
post: cleanup.js
1188+
post-if: always() || failure()
1189+
`);
1190+
const diagnostics = await validate(doc);
1191+
expect(diagnostics.some(d => d.message.includes("Unrecognized"))).toBe(false);
1192+
});
1193+
1194+
it("errors on unknown function in composite step if", async () => {
1195+
const doc = createActionDocument(`
1196+
name: My Action
1197+
description: Unknown function in if
1198+
runs:
1199+
using: composite
1200+
steps:
1201+
- if: unknownFunc()
1202+
run: echo hi
1203+
shell: bash
1204+
`);
1205+
const diagnostics = await validate(doc);
1206+
expect(diagnostics.some(d => d.message.includes("Unrecognized function"))).toBe(true);
1207+
});
1208+
1209+
it("errors on unknown function in pre-if for node action", async () => {
1210+
const doc = createActionDocument(`
1211+
name: My Action
1212+
description: Unknown function in pre-if
1213+
runs:
1214+
using: node20
1215+
main: index.js
1216+
pre: setup.js
1217+
pre-if: unknownFunc()
1218+
`);
1219+
const diagnostics = await validate(doc);
1220+
expect(diagnostics.some(d => d.message.includes("Unrecognized function"))).toBe(true);
1221+
});
1222+
1223+
it("errors on unknown function in post-if for node action", async () => {
1224+
const doc = createActionDocument(`
1225+
name: My Action
1226+
description: Unknown function in post-if
1227+
runs:
1228+
using: node20
1229+
main: index.js
1230+
post: cleanup.js
1231+
post-if: unknownFunc()
1232+
`);
1233+
const diagnostics = await validate(doc);
1234+
expect(diagnostics.some(d => d.message.includes("Unrecognized function"))).toBe(true);
1235+
});
1236+
1237+
it("errors on unknown function in pre-if for docker action", async () => {
1238+
const doc = createActionDocument(`
1239+
name: My Action
1240+
description: Unknown function in pre-if
1241+
runs:
1242+
using: docker
1243+
image: Dockerfile
1244+
pre-entrypoint: /setup.sh
1245+
pre-if: unknownFunc()
1246+
`);
1247+
const diagnostics = await validate(doc);
1248+
expect(diagnostics.some(d => d.message.includes("Unrecognized function"))).toBe(true);
1249+
});
1250+
1251+
it("errors on unknown function in post-if for docker action", async () => {
1252+
const doc = createActionDocument(`
1253+
name: My Action
1254+
description: Unknown function in post-if
1255+
runs:
1256+
using: docker
1257+
image: Dockerfile
1258+
post-entrypoint: /cleanup.sh
1259+
post-if: unknownFunc()
1260+
`);
1261+
const diagnostics = await validate(doc);
1262+
expect(diagnostics.some(d => d.message.includes("Unrecognized function"))).toBe(true);
1263+
});
1264+
});
10141265
});

0 commit comments

Comments
 (0)