Skip to content

Commit 73bae55

Browse files
Copilotpelikhan
andcommitted
Add GitHub workflow transformation module with names field support
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
1 parent 9205aff commit 73bae55

File tree

2 files changed

+298
-0
lines changed

2 files changed

+298
-0
lines changed
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { describe, test } from "node:test"
2+
import assert from "node:assert/strict"
3+
import { processGitHubWorkflow } from "./githubworkflow"
4+
import { YAMLParse } from "./yaml"
5+
6+
describe("GitHubWorkflow", () => {
7+
describe("processGitHubWorkflow", () => {
8+
test("should handle labeled event with names field", () => {
9+
const input = `name: Test Workflow
10+
on:
11+
issues:
12+
types: [labeled]
13+
names: [bug, enhancement]
14+
jobs:
15+
test:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- run: echo "test"
19+
`
20+
const result = processGitHubWorkflow(input)
21+
const workflow = YAMLParse(result)
22+
23+
// Names field should be removed
24+
assert.strictEqual(workflow.on.issues.names, undefined)
25+
26+
// Job should have if condition
27+
assert.ok(workflow.jobs.test.if)
28+
assert.ok(workflow.jobs.test.if.includes('github.event.label.name'))
29+
assert.ok(workflow.jobs.test.if.includes('bug'))
30+
assert.ok(workflow.jobs.test.if.includes('enhancement'))
31+
})
32+
33+
test("should handle unlabeled event with names field", () => {
34+
const input = `name: Test Workflow
35+
on:
36+
issues:
37+
types: [unlabeled]
38+
names: [wontfix]
39+
jobs:
40+
cleanup:
41+
runs-on: ubuntu-latest
42+
steps:
43+
- run: echo "cleanup"
44+
`
45+
const result = processGitHubWorkflow(input)
46+
const workflow = YAMLParse(result)
47+
48+
// Names field should be removed
49+
assert.strictEqual(workflow.on.issues.names, undefined)
50+
51+
// Job should have if condition
52+
assert.ok(workflow.jobs.cleanup.if)
53+
assert.ok(workflow.jobs.cleanup.if.includes('wontfix'))
54+
})
55+
56+
test("should handle both labeled and unlabeled in same trigger", () => {
57+
const input = `name: Test Workflow
58+
on:
59+
issues:
60+
types: [labeled, unlabeled]
61+
names: [bug, feature]
62+
jobs:
63+
process:
64+
runs-on: ubuntu-latest
65+
steps:
66+
- run: echo "process"
67+
`
68+
const result = processGitHubWorkflow(input)
69+
const workflow = YAMLParse(result)
70+
71+
// Names field should be removed
72+
assert.strictEqual(workflow.on.issues.names, undefined)
73+
74+
// Job should have if condition
75+
assert.ok(workflow.jobs.process.if)
76+
assert.ok(workflow.jobs.process.if.includes('bug'))
77+
assert.ok(workflow.jobs.process.if.includes('feature'))
78+
})
79+
80+
test("should preserve existing if conditions", () => {
81+
const input = `name: Test Workflow
82+
on:
83+
issues:
84+
types: [labeled]
85+
names: [critical]
86+
jobs:
87+
alert:
88+
runs-on: ubuntu-latest
89+
if: github.repository == 'owner/repo'
90+
steps:
91+
- run: echo "alert"
92+
`
93+
const result = processGitHubWorkflow(input)
94+
const workflow = YAMLParse(result)
95+
96+
// Job should have combined if condition
97+
assert.ok(workflow.jobs.alert.if)
98+
assert.ok(workflow.jobs.alert.if.includes('github.repository'))
99+
assert.ok(workflow.jobs.alert.if.includes('critical'))
100+
assert.ok(workflow.jobs.alert.if.includes('&&'))
101+
})
102+
103+
test("should not modify workflow without names field", () => {
104+
const input = `name: Test Workflow
105+
on:
106+
issues:
107+
types: [labeled]
108+
jobs:
109+
test:
110+
runs-on: ubuntu-latest
111+
steps:
112+
- run: echo "test"
113+
`
114+
const result = processGitHubWorkflow(input)
115+
const workflow = YAMLParse(result)
116+
117+
// Job should not have if condition added
118+
assert.strictEqual(workflow.jobs.test.if, undefined)
119+
})
120+
121+
test("should not modify workflow without labeled/unlabeled types", () => {
122+
const input = `name: Test Workflow
123+
on:
124+
issues:
125+
types: [opened]
126+
names: [something]
127+
jobs:
128+
test:
129+
runs-on: ubuntu-latest
130+
steps:
131+
- run: echo "test"
132+
`
133+
const result = processGitHubWorkflow(input)
134+
const workflow = YAMLParse(result)
135+
136+
// Names field should still be there (not removed)
137+
// because it's not on a labeled/unlabeled trigger
138+
assert.deepStrictEqual(workflow.on.issues.names, ['something'])
139+
140+
// Job should not have if condition added
141+
assert.strictEqual(workflow.jobs.test.if, undefined)
142+
})
143+
144+
test("should handle multiple jobs", () => {
145+
const input = `name: Test Workflow
146+
on:
147+
issues:
148+
types: [labeled]
149+
names: [urgent]
150+
jobs:
151+
job1:
152+
runs-on: ubuntu-latest
153+
steps:
154+
- run: echo "job1"
155+
job2:
156+
runs-on: ubuntu-latest
157+
steps:
158+
- run: echo "job2"
159+
`
160+
const result = processGitHubWorkflow(input)
161+
const workflow = YAMLParse(result)
162+
163+
// Both jobs should have if condition
164+
assert.ok(workflow.jobs.job1.if)
165+
assert.ok(workflow.jobs.job1.if.includes('urgent'))
166+
assert.ok(workflow.jobs.job2.if)
167+
assert.ok(workflow.jobs.job2.if.includes('urgent'))
168+
})
169+
170+
test("should handle pull_request events", () => {
171+
const input = `name: Test Workflow
172+
on:
173+
pull_request:
174+
types: [labeled]
175+
names: [approved]
176+
jobs:
177+
merge:
178+
runs-on: ubuntu-latest
179+
steps:
180+
- run: echo "merge"
181+
`
182+
const result = processGitHubWorkflow(input)
183+
const workflow = YAMLParse(result)
184+
185+
// Names field should be removed
186+
assert.strictEqual(workflow.on.pull_request.names, undefined)
187+
188+
// Job should have if condition
189+
assert.ok(workflow.jobs.merge.if)
190+
assert.ok(workflow.jobs.merge.if.includes('approved'))
191+
})
192+
})
193+
})
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
/**
2+
* This module provides utilities for processing GitHub Actions workflow files,
3+
* specifically handling the transformation of labeled/unlabeled triggers with
4+
* the "names" field into conditional expressions.
5+
*/
6+
7+
import { YAMLParse, YAMLStringify } from "./yaml"
8+
9+
interface GitHubWorkflowTrigger {
10+
types?: string[]
11+
names?: string[]
12+
[key: string]: any
13+
}
14+
15+
interface GitHubWorkflowOn {
16+
issues?: GitHubWorkflowTrigger
17+
pull_request?: GitHubWorkflowTrigger
18+
pull_request_target?: GitHubWorkflowTrigger
19+
[key: string]: any
20+
}
21+
22+
interface GitHubWorkflowJob {
23+
if?: string
24+
[key: string]: any
25+
}
26+
27+
interface GitHubWorkflow {
28+
on?: GitHubWorkflowOn
29+
jobs?: Record<string, GitHubWorkflowJob>
30+
[key: string]: any
31+
}
32+
33+
/**
34+
* Processes a GitHub Actions workflow to handle the "names" field under
35+
* labeled/unlabeled triggers. The "names" field is removed from the workflow
36+
* and converted into conditional if expressions on the jobs.
37+
*
38+
* @param workflowYaml - The workflow YAML content as a string
39+
* @returns The processed workflow YAML as a string
40+
*/
41+
export function processGitHubWorkflow(workflowYaml: string): string {
42+
const workflow: GitHubWorkflow = YAMLParse(workflowYaml)
43+
44+
if (!workflow.on || !workflow.jobs) {
45+
return workflowYaml
46+
}
47+
48+
// Track which event triggers have names that need to be converted
49+
const labelConditions: string[] = []
50+
51+
// Process each event type in the "on" section
52+
for (const [eventKey, eventConfig] of Object.entries(workflow.on)) {
53+
if (typeof eventConfig === 'object' && eventConfig !== null) {
54+
const trigger = eventConfig as GitHubWorkflowTrigger
55+
56+
// Check if this trigger has types array containing labeled or unlabeled
57+
if (trigger.types && trigger.names) {
58+
const hasLabeledOrUnlabeled = trigger.types.some(
59+
(type: string) => type === 'labeled' || type === 'unlabeled'
60+
)
61+
62+
if (hasLabeledOrUnlabeled) {
63+
// Create the conditional expression
64+
const namesArray = JSON.stringify(trigger.names)
65+
const labelField = 'github.event.label.name'
66+
const condition = `contains(fromJSON('${namesArray}'), ${labelField})`
67+
labelConditions.push(condition)
68+
69+
// Remove the names field from the trigger
70+
delete trigger.names
71+
}
72+
}
73+
}
74+
}
75+
76+
// If we found label conditions, add them to jobs
77+
if (labelConditions.length > 0) {
78+
const combinedCondition = labelConditions.join(' || ')
79+
80+
// Add the condition to all jobs
81+
for (const [jobKey, job] of Object.entries(workflow.jobs)) {
82+
if (job.if) {
83+
// Combine with existing condition
84+
job.if = `(${job.if}) && (${combinedCondition})`
85+
} else {
86+
// Add new condition
87+
job.if = combinedCondition
88+
}
89+
}
90+
}
91+
92+
return YAMLStringify(workflow)
93+
}
94+
95+
/**
96+
* Processes a GitHub Actions workflow file to handle the "names" field.
97+
* This is a convenience wrapper around processGitHubWorkflow that handles
98+
* file input/output.
99+
*
100+
* @param workflowContent - The workflow YAML content
101+
* @returns The processed workflow YAML content
102+
*/
103+
export function transformGitHubWorkflowLabels(workflowContent: string): string {
104+
return processGitHubWorkflow(workflowContent)
105+
}

0 commit comments

Comments
 (0)