Skip to content

Commit 8661b26

Browse files
authored
feat(library): allow setting custom step ID (#1782)
Closes #1779.
1 parent 721b569 commit 8661b26

File tree

4 files changed

+120
-15
lines changed

4 files changed

+120
-15
lines changed

github-workflows-kt/api/github-workflows-kt.api

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1851,12 +1851,12 @@ public final class io/github/typesafegithub/workflows/dsl/JobBuilder : io/github
18511851
public final fun getStrategyMatrix ()Ljava/util/Map;
18521852
public final fun getTimeoutMinutes ()Ljava/lang/Integer;
18531853
public fun get_customArguments ()Ljava/util/Map;
1854-
public final fun run ([Lkotlin/Unit;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Lio/github/typesafegithub/workflows/domain/Shell;Ljava/lang/String;Ljava/util/Map;)Lio/github/typesafegithub/workflows/domain/CommandStep;
1854+
public final fun run ([Lkotlin/Unit;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Lio/github/typesafegithub/workflows/domain/Shell;Ljava/lang/String;Ljava/util/Map;)Lio/github/typesafegithub/workflows/domain/CommandStep;
18551855
public final fun run ([Lkotlin/Unit;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/Boolean;Ljava/lang/Integer;Lio/github/typesafegithub/workflows/domain/Shell;Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function1;)Lio/github/typesafegithub/workflows/domain/KotlinLogicStep;
1856-
public static synthetic fun run$default (Lio/github/typesafegithub/workflows/dsl/JobBuilder;[Lkotlin/Unit;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Lio/github/typesafegithub/workflows/domain/Shell;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lio/github/typesafegithub/workflows/domain/CommandStep;
1856+
public static synthetic fun run$default (Lio/github/typesafegithub/workflows/dsl/JobBuilder;[Lkotlin/Unit;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Lio/github/typesafegithub/workflows/domain/Shell;Ljava/lang/String;Ljava/util/Map;ILjava/lang/Object;)Lio/github/typesafegithub/workflows/domain/CommandStep;
18571857
public static synthetic fun run$default (Lio/github/typesafegithub/workflows/dsl/JobBuilder;[Lkotlin/Unit;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Lkotlin/jvm/functions/Function1;Ljava/lang/Boolean;Ljava/lang/Integer;Lio/github/typesafegithub/workflows/domain/Shell;Ljava/lang/String;Ljava/util/Map;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lio/github/typesafegithub/workflows/domain/KotlinLogicStep;
1858-
public final fun uses ([Lkotlin/Unit;Lio/github/typesafegithub/workflows/domain/actions/Action;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Ljava/util/Map;)Lio/github/typesafegithub/workflows/domain/ActionStep;
1859-
public static synthetic fun uses$default (Lio/github/typesafegithub/workflows/dsl/JobBuilder;[Lkotlin/Unit;Lio/github/typesafegithub/workflows/domain/actions/Action;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Ljava/util/Map;ILjava/lang/Object;)Lio/github/typesafegithub/workflows/domain/ActionStep;
1858+
public final fun uses ([Lkotlin/Unit;Lio/github/typesafegithub/workflows/domain/actions/Action;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Ljava/util/Map;)Lio/github/typesafegithub/workflows/domain/ActionStep;
1859+
public static synthetic fun uses$default (Lio/github/typesafegithub/workflows/dsl/JobBuilder;[Lkotlin/Unit;Lio/github/typesafegithub/workflows/domain/actions/Action;Ljava/lang/String;Ljava/lang/String;Ljava/util/Map;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Ljava/util/Map;ILjava/lang/Object;)Lio/github/typesafegithub/workflows/domain/ActionStep;
18601860
}
18611861

18621862
public final class io/github/typesafegithub/workflows/dsl/WorkflowBuilder {

github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/dsl/JobBuilder.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ public class JobBuilder<OUTPUT : JobOutputs>(
6565
vararg pleaseUseNamedArguments: Unit,
6666
command: String,
6767
name: String? = null,
68+
/**
69+
* Provide custom step ID. If not given, it will be auto-generated.
70+
*/
71+
id: String? = null,
6872
env: Map<String, String> = mapOf(),
6973
@SuppressWarnings("FunctionParameterNaming")
7074
`if`: String? = null,
@@ -82,7 +86,7 @@ public class JobBuilder<OUTPUT : JobOutputs>(
8286

8387
val newStep =
8488
CommandStep(
85-
id = "step-${job.steps.size}",
89+
id = id ?: "step-${job.steps.size}",
8690
name = name,
8791
command = command,
8892
env = env,
@@ -164,6 +168,10 @@ public class JobBuilder<OUTPUT : JobOutputs>(
164168
vararg pleaseUseNamedArguments: Unit,
165169
action: Action<T>,
166170
name: String? = null,
171+
/**
172+
* Provide custom step ID. If not given, it will be auto-generated.
173+
*/
174+
id: String? = null,
167175
env: Map<String, String> = mapOf(),
168176
@SuppressWarnings("FunctionParameterNaming")
169177
`if`: String? = null,
@@ -173,7 +181,7 @@ public class JobBuilder<OUTPUT : JobOutputs>(
173181
@SuppressWarnings("FunctionParameterNaming")
174182
_customArguments: Map<String, @Contextual Any> = mapOf(),
175183
): ActionStep<T> {
176-
val stepId = "step-${job.steps.size}"
184+
val stepId = id ?: "step-${job.steps.size}"
177185
require(!(`if` != null && condition != null)) {
178186
"Either 'if' or 'condition' have to be set, not both!"
179187
}

github-workflows-kt/src/main/kotlin/io/github/typesafegithub/workflows/dsl/WorkflowBuilder.kt

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ public class WorkflowBuilder(
100100
require(newJob.steps.isNotEmpty()) {
101101
"There are no steps defined!"
102102
}
103+
newJob.requireUniqueStepIds()
103104

104105
workflow = workflow.copy(jobs = workflow.jobs + newJob)
105106
return newJob
@@ -236,17 +237,30 @@ public fun workflow(
236237
}
237238

238239
private fun List<Job<*>>.requireUniqueJobIds() {
239-
val countPerJobName =
240+
val duplicatedJobNames =
240241
this
241242
.map { it.id }
242-
.groupBy { it }
243-
.mapValues { it.value.count() }
244-
245-
require(countPerJobName.none { it.value > 1 }) {
246-
val duplicatedJobNames =
247-
countPerJobName
248-
.filter { it.value > 1 }
249-
.map { it.key }
243+
.findDuplicates()
244+
245+
require(duplicatedJobNames.isEmpty()) {
250246
"Duplicated job names: $duplicatedJobNames"
251247
}
252248
}
249+
250+
private fun Job<*>.requireUniqueStepIds() {
251+
val duplicatedStepIds =
252+
this.steps
253+
.map { it.id }
254+
.findDuplicates()
255+
256+
require(duplicatedStepIds.isEmpty()) {
257+
"Duplicated step IDs for job '${this.id}': $duplicatedStepIds"
258+
}
259+
}
260+
261+
private fun <T> Iterable<T>.findDuplicates(): Set<T> =
262+
this
263+
.groupingBy { it }
264+
.eachCount()
265+
.filter { it.value > 1 }
266+
.keys

github-workflows-kt/src/test/kotlin/io/github/typesafegithub/workflows/dsl/JobBuilderTest.kt

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,87 @@ class JobBuilderTest :
126126
workflow!!.jobs[0].condition shouldBe "b"
127127
}
128128
}
129+
130+
context("step custom IDs") {
131+
test("set custom ID for a 'run' step") {
132+
// When
133+
var workflow: Workflow? = null
134+
workflow(
135+
name = "test",
136+
on = listOf(Push()),
137+
sourceFile = sourceTempFile,
138+
useWorkflow = { workflow = it },
139+
) {
140+
job(id = "test", runsOn = RunnerType.UbuntuLatest) {
141+
run(command = "command with default ID")
142+
run(id = "foobar", command = "command with custom ID")
143+
}
144+
}
145+
146+
// Then
147+
workflow!!.jobs[0].steps[0].id shouldBe "step-0"
148+
workflow.jobs[0].steps[1].id shouldBe "foobar"
149+
}
150+
151+
test("set custom ID for a 'uses' step") {
152+
// When
153+
var workflow: Workflow? = null
154+
workflow(
155+
name = "test",
156+
on = listOf(Push()),
157+
sourceFile = sourceTempFile,
158+
useWorkflow = { workflow = it },
159+
) {
160+
job(id = "test", runsOn = RunnerType.UbuntuLatest) {
161+
uses(
162+
name = "step with default ID",
163+
action = Checkout(),
164+
)
165+
uses(
166+
name = "step with default ID",
167+
id = "foobar",
168+
action = Checkout(),
169+
)
170+
}
171+
}
172+
173+
// Then
174+
workflow!!.jobs[0].steps[0].id shouldBe "step-0"
175+
workflow.jobs[0].steps[1].id shouldBe "foobar"
176+
}
177+
178+
test("custom ID is equal to existing default step ID") {
179+
shouldThrow<IllegalArgumentException> {
180+
workflow(
181+
name = "test",
182+
on = listOf(Push()),
183+
sourceFile = sourceTempFile,
184+
) {
185+
job(id = "test", runsOn = RunnerType.UbuntuLatest) {
186+
run(command = "command with default ID")
187+
run(id = "step-0", command = "command with custom ID")
188+
}
189+
}
190+
}.also {
191+
it.message shouldBe "Duplicated step IDs for job 'test': [step-0]"
192+
}
193+
}
194+
195+
test("two equal custom IDs set") {
196+
shouldThrow<IllegalArgumentException> {
197+
workflow(
198+
name = "test",
199+
on = listOf(Push()),
200+
sourceFile = sourceTempFile,
201+
) {
202+
job(id = "test", runsOn = RunnerType.UbuntuLatest) {
203+
run(id = "foobar", command = "command #1 with custom ID")
204+
run(id = "foobar", command = "command #2 with custom ID")
205+
}
206+
}
207+
}.also {
208+
it.message shouldBe "Duplicated step IDs for job 'test': [foobar]"
209+
}
210+
}
211+
}
129212
})

0 commit comments

Comments
 (0)