Repository: typesafegithub/github-workflows-kt (kotlin)

Issue:
[Core feature request] Model `outcome` context
**What feature do you need?**
It would be nice if the GitHub contexts are modeled similar to the outputs of actions, so that you can easily access them.
https://docs.github.com/en/actions/learn-github-actions/contexts
For example what I need right now would be `myActionStep.outcome`.

**Do you have an example usage?**
YAML:
```yaml
- run: if '${{ steps.execute_action.outcome }}' NEQ 'failure' exit 1
```
Current kts:
```kts
run(
    command = """
        if '${expr("steps.${executeActionStep.id}.outcome")}' NEQ 'failure' exit 1
    """.trimIndent()
)
```
Intended kts:
```kts
run(
    command = """
        if '${expr(executeActionStep.outcome)}' NEQ 'failure' exit 1
    """.trimIndent()
)
```

Pull request description:
Hello!

As discussed in Slack.

Open to concerns and ideas for `AbstractResult`.

Have a great day!

Patch:
diff --git a/docs/feature-coverage.md b/docs/feature-coverage.md
index fb1bbda99a..2dd864da4f 100644
--- a/docs/feature-coverage.md
+++ b/docs/feature-coverage.md
@@ -19,7 +19,7 @@ Legend:
 | Job containers                              | ✅       |                                                                          |
 | Docker actions                              | ✅       |                                                                          |
 | Local actions                               | ✅       |                                                                          |
-| `outcome` context                           | ❌       | [#296](https://github.com/typesafegithub/github-workflows-kt/issues/296) |
+| `outcome` context                           | ✅       |                                                                          |
 | Permissions                                 | ✅       |                                                                          |
 | Public actions                              | ✅       |                                                                          |
 | `runner` context                            | ✅       |                                                                          |
diff --git a/library/api/library.api b/library/api/library.api
index a90f32c53d..858b9ccd19 100644
--- a/library/api/library.api
+++ b/library/api/library.api
@@ -1,3 +1,20 @@
+public abstract class io/github/typesafegithub/workflows/domain/AbstractResult {
+	public final fun eq (Lio/github/typesafegithub/workflows/domain/AbstractResult$Status;)Ljava/lang/String;
+	public final fun neq (Lio/github/typesafegithub/workflows/domain/AbstractResult$Status;)Ljava/lang/String;
+	public fun toString ()Ljava/lang/String;
+}
+
+public final class io/github/typesafegithub/workflows/domain/AbstractResult$Status : java/lang/Enum {
+	public static final field Cancelled Lio/github/typesafegithub/workflows/domain/AbstractResult$Status;
+	public static final field Failure Lio/github/typesafegithub/workflows/domain/AbstractResult$Status;
+	public static final field Skipped Lio/github/typesafegithub/workflows/domain/AbstractResult$Status;
+	public static final field Success Lio/github/typesafegithub/workflows/domain/AbstractResult$Status;
+	public static fun getEntries ()Lkotlin/enums/EnumEntries;
+	public fun toString ()Ljava/lang/String;
+	public static fun valueOf (Ljava/lang/String;)Lio/github/typesafegithub/workflows/domain/AbstractResult$Status;
+	public static fun values ()[Lio/github/typesafegithub/workflows/domain/AbstractResult$Status;
+}
+
 public class io/github/typesafegithub/workflows/domain/ActionStep : io/github/typesafegithub/workflows/domain/Step, io/github/typesafegithub/workflows/domain/WithOutputs {
 	public fun <init> (Ljava/lang/String;Ljava/lang/String;Lio/github/typesafegithub/workflows/domain/actions/Action;Ljava/util/LinkedHashMap;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Lio/github/typesafegithub/workflows/domain/actions/Action$Outputs;Ljava/util/Map;)V
 	public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;Lio/github/typesafegithub/workflows/domain/actions/Action;Ljava/util/LinkedHashMap;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Lio/github/typesafegithub/workflows/domain/actions/Action$Outputs;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
@@ -146,6 +163,7 @@ public final class io/github/typesafegithub/workflows/domain/Job : io/github/typ
 	public final fun getNeeds ()Ljava/util/List;
 	public final fun getOutputs ()Lio/github/typesafegithub/workflows/domain/JobOutputs;
 	public final fun getPermissions ()Ljava/util/Map;
+	public final fun getResult ()Lio/github/typesafegithub/workflows/domain/Job$Result;
 	public final fun getRunsOn ()Lio/github/typesafegithub/workflows/domain/RunnerType;
 	public final fun getServices ()Ljava/util/Map;
 	public final fun getSteps ()Ljava/util/List;
@@ -156,6 +174,10 @@ public final class io/github/typesafegithub/workflows/domain/Job : io/github/typ
 	public fun toString ()Ljava/lang/String;
 }
 
+public final class io/github/typesafegithub/workflows/domain/Job$Result : io/github/typesafegithub/workflows/domain/AbstractResult {
+	public fun <init> (Lio/github/typesafegithub/workflows/domain/Job;)V
+}
+
 public class io/github/typesafegithub/workflows/domain/JobOutputs {
 	public fun <init> ()V
 	public final fun getOutputMapping ()Ljava/util/Map;
@@ -373,14 +395,24 @@ public final class io/github/typesafegithub/workflows/domain/Shell$Sh : io/githu
 public abstract class io/github/typesafegithub/workflows/domain/Step : io/github/typesafegithub/workflows/dsl/HasCustomArguments {
 	public synthetic fun <init> (Ljava/lang/String;Ljava/util/LinkedHashMap;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Ljava/util/Map;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
 	public synthetic fun <init> (Ljava/lang/String;Ljava/util/LinkedHashMap;Ljava/lang/String;Ljava/lang/Boolean;Ljava/lang/Integer;Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
+	public final fun getConclusion ()Lio/github/typesafegithub/workflows/domain/Step$Conclusion;
 	public fun getCondition ()Ljava/lang/String;
 	public fun getContinueOnError ()Ljava/lang/Boolean;
 	public fun getEnv ()Ljava/util/LinkedHashMap;
 	public fun getId ()Ljava/lang/String;
+	public final fun getOutcome ()Lio/github/typesafegithub/workflows/domain/Step$Outcome;
 	public fun getTimeoutMinutes ()Ljava/lang/Integer;
 	public fun get_customArguments ()Ljava/util/Map;
 }
 
+public final class io/github/typesafegithub/workflows/domain/Step$Conclusion : io/github/typesafegithub/workflows/domain/AbstractResult {
+	public fun <init> (Lio/github/typesafegithub/workflows/domain/Step;)V
+}
+
+public final class io/github/typesafegithub/workflows/domain/Step$Outcome : io/github/typesafegithub/workflows/domain/AbstractResult {
+	public fun <init> (Lio/github/typesafegithub/workflows/domain/Step;)V
+}
+
 public final class io/github/typesafegithub/workflows/domain/VolumeMapping {
 	public fun <init> (Ljava/lang/String;Ljava/lang/String;Z)V
 	public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V
diff --git a/library/src/main/kotlin/io/github/typesafegithub/workflows/domain/AbstractResult.kt b/library/src/main/kotlin/io/github/typesafegithub/workflows/domain/AbstractResult.kt
new file mode 100644
index 0000000000..99aee4965a
--- /dev/null
+++ b/library/src/main/kotlin/io/github/typesafegithub/workflows/domain/AbstractResult.kt
@@ -0,0 +1,21 @@
+package io.github.typesafegithub.workflows.domain
+
+public abstract class AbstractResult internal constructor(
+    private val value: String,
+) {
+    public infix fun eq(status: Status): String = "$value == $status"
+
+    public infix fun neq(status: Status): String = "$value != $status"
+
+    override fun toString(): String = value
+
+    public enum class Status {
+        Success,
+        Failure,
+        Cancelled,
+        Skipped,
+        ;
+
+        override fun toString(): String = "'${name.lowercase()}'"
+    }
+}
diff --git a/library/src/main/kotlin/io/github/typesafegithub/workflows/domain/Job.kt b/library/src/main/kotlin/io/github/typesafegithub/workflows/domain/Job.kt
index c33d825c47..553dcc1e01 100644
--- a/library/src/main/kotlin/io/github/typesafegithub/workflows/domain/Job.kt
+++ b/library/src/main/kotlin/io/github/typesafegithub/workflows/domain/Job.kt
@@ -33,4 +33,8 @@ public data class Job<OUTPUT : JobOutputs>(
         }
         outputs.job = this
     }
+
+    public inner class Result : AbstractResult("needs.$id.result")
+
+    public val result: Result get() = Result()
 }
diff --git a/library/src/main/kotlin/io/github/typesafegithub/workflows/domain/Step.kt b/library/src/main/kotlin/io/github/typesafegithub/workflows/domain/Step.kt
index 3e2f63eec9..90711f3213 100644
--- a/library/src/main/kotlin/io/github/typesafegithub/workflows/domain/Step.kt
+++ b/library/src/main/kotlin/io/github/typesafegithub/workflows/domain/Step.kt
@@ -12,7 +12,14 @@ public sealed class Step(
     public open val continueOnError: Boolean? = null,
     public open val timeoutMinutes: Int? = null,
     override val _customArguments: Map<String, @Contextual Any?> = emptyMap(),
-) : HasCustomArguments
+) : HasCustomArguments {
+    public inner class Conclusion : AbstractResult("steps.$id.conclusion")
+
+    public inner class Outcome : AbstractResult("steps.$id.outcome")
+
+    public val conclusion: Conclusion get() = Conclusion()
+    public val outcome: Outcome get() = Outcome()
+}
 
 public interface WithOutputs<out T> {
     public val outputs: T


Test patch:
diff --git a/library/src/test/kotlin/io/github/typesafegithub/workflows/domain/JobTest.kt b/library/src/test/kotlin/io/github/typesafegithub/workflows/domain/JobTest.kt
index 8ffb8b046a..72a736bb00 100644
--- a/library/src/test/kotlin/io/github/typesafegithub/workflows/domain/JobTest.kt
+++ b/library/src/test/kotlin/io/github/typesafegithub/workflows/domain/JobTest.kt
@@ -1,5 +1,6 @@
 package io.github.typesafegithub.workflows.domain
 
+import io.github.typesafegithub.workflows.domain.AbstractResult.Status
 import io.kotest.assertions.throwables.shouldThrow
 import io.kotest.assertions.throwables.shouldThrowAny
 import io.kotest.core.spec.style.FunSpec
@@ -8,6 +9,20 @@ import io.kotest.matchers.shouldBe
 import io.kotest.matchers.throwable.shouldHaveMessage
 
 class JobTest : FunSpec({
+    test("job.result") {
+        val job =
+            Job(
+                id = "some-id",
+                runsOn = RunnerType.UbuntuLatest,
+                steps = listOf(),
+                outputs = JobOutputs.EMPTY,
+            )
+        job.result.toString() shouldBe "needs.some-id.result"
+        job.result eq Status.Failure shouldBe "needs.some-id.result == 'failure'"
+        job.result eq Status.Cancelled shouldBe "needs.some-id.result == 'cancelled'"
+        job.result eq Status.Skipped shouldBe "needs.some-id.result == 'skipped'"
+        job.result eq Status.Success shouldBe "needs.some-id.result == 'success'"
+    }
     context("outputs") {
         test("should include job outputs") {
             val job =
@@ -27,6 +42,12 @@ class JobTest : FunSpec({
 
             job.outputs.output1 shouldBe "needs.some-id.outputs.output1"
             job.outputs.output2 shouldBe "needs.some-id.outputs.output2"
+
+            job.result.toString() shouldBe "needs.some-id.result"
+            job.result eq Status.Failure shouldBe "needs.some-id.result == 'failure'"
+            job.result eq Status.Cancelled shouldBe "needs.some-id.result == 'cancelled'"
+            job.result eq Status.Skipped shouldBe "needs.some-id.result == 'skipped'"
+            job.result eq Status.Success shouldBe "needs.some-id.result == 'success'"
         }
 
         test("should throw if accessing uninitialized output") {
diff --git a/library/src/test/kotlin/io/github/typesafegithub/workflows/domain/StepTest.kt b/library/src/test/kotlin/io/github/typesafegithub/workflows/domain/StepTest.kt
new file mode 100644
index 0000000000..a793a55806
--- /dev/null
+++ b/library/src/test/kotlin/io/github/typesafegithub/workflows/domain/StepTest.kt
@@ -0,0 +1,26 @@
+package io.github.typesafegithub.workflows.domain
+
+import io.github.typesafegithub.workflows.actions.actions.CheckoutV4
+import io.github.typesafegithub.workflows.domain.AbstractResult.Status
+import io.github.typesafegithub.workflows.domain.actions.Action
+import io.kotest.core.spec.style.FunSpec
+import io.kotest.matchers.shouldBe
+
+class StepTest : FunSpec({
+    test("step.outcome") {
+        val step0: Step = CommandStep(id = "step-0", command = "ls")
+        step0.outcome.toString() shouldBe "steps.step-0.outcome"
+        step0.outcome eq Status.Failure shouldBe "steps.step-0.outcome == 'failure'"
+        step0.outcome eq Status.Cancelled shouldBe "steps.step-0.outcome == 'cancelled'"
+        step0.outcome eq Status.Skipped shouldBe "steps.step-0.outcome == 'skipped'"
+        step0.outcome eq Status.Success shouldBe "steps.step-0.outcome == 'success'"
+    }
+    test("step.conclusion") {
+        val someStep: Step = ActionStep(id = "whatever", action = CheckoutV4(), outputs = Action.Outputs("whatever"))
+        someStep.conclusion.toString() shouldBe "steps.whatever.conclusion"
+        someStep.conclusion eq Status.Failure shouldBe "steps.whatever.conclusion == 'failure'"
+        someStep.conclusion eq Status.Cancelled shouldBe "steps.whatever.conclusion == 'cancelled'"
+        someStep.conclusion eq Status.Skipped shouldBe "steps.whatever.conclusion == 'skipped'"
+        someStep.conclusion eq Status.Success shouldBe "steps.whatever.conclusion == 'success'"
+    }
+})