Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# CHANGELOG

## Version 0.3.0.0

- Changed `runsOn` from `Maybe Text` to `Maybe RunsOn` to support GitHub Actions
runner label lists (e.g. `runs-on: [self-hosted, linux]`), while preserving
the original YAML representation during round-trip serialization

## Version 0.2.0 (2025-07-21)

- Changed `cancelInProgress` from `Maybe Bool` to `Maybe Text` to support GitHub Actions expressions
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import Data.List.NonEmpty (NonEmpty(..))
import Language.Github.Actions.Workflow
import qualified Language.Github.Actions.Job as Job
import qualified Language.Github.Actions.Job.Id as JobId
import qualified Language.Github.Actions.Job.RunsOn as RunsOn
import qualified Language.Github.Actions.Step as Step
import qualified Language.Github.Actions.Workflow.Trigger as Trigger

Expand All @@ -38,7 +39,7 @@ myWorkflow = new
buildJob :: Job.Job
buildJob = Job.new
{ Job.jobName = Just "Build and Test"
, Job.runsOn = Just "ubuntu-latest"
, Job.runsOn = Just (RunsOn.RunsOnString "ubuntu-latest")
, Job.steps = Just $ checkoutStep :| [buildStep, testStep]
}

Expand Down
3 changes: 2 additions & 1 deletion github-actions.cabal
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
cabal-version: 2.2
name: github-actions
version: 0.2.0.0
version: 0.3.0.0
synopsis: Github Actions
description:
This library provides types and instances for serializing and deserializing
Expand Down Expand Up @@ -60,6 +60,7 @@ library
Language.Github.Actions.Job.Environment
Language.Github.Actions.Job.Id
Language.Github.Actions.Job.Needs
Language.Github.Actions.Job.RunsOn
Language.Github.Actions.Job.Strategy
Language.Github.Actions.Permissions
Language.Github.Actions.RunIf
Expand Down
12 changes: 7 additions & 5 deletions src/Language/Github/Actions/Job.hs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ import Language.Github.Actions.Job.Environment (JobEnvironment)
import qualified Language.Github.Actions.Job.Environment as JobEnvironment
import Language.Github.Actions.Job.Needs (JobNeeds)
import qualified Language.Github.Actions.Job.Needs as JobNeeds
import Language.Github.Actions.Job.RunsOn (RunsOn)
import qualified Language.Github.Actions.Job.RunsOn as RunsOn
import Language.Github.Actions.Job.Strategy (JobStrategy)
import qualified Language.Github.Actions.Job.Strategy as JobStrategy
import Language.Github.Actions.Permissions (Permissions)
Expand Down Expand Up @@ -73,7 +75,7 @@ import qualified Language.Github.Actions.Step as Step
-- myJob :: Job
-- myJob = new
-- { jobName = Just "Build and Test"
-- , runsOn = Just "ubuntu-latest"
-- , runsOn = Just (RunsOn.RunsOnString "ubuntu-latest")
-- , steps = Just $ Step.new :| []
-- }
-- @
Expand Down Expand Up @@ -102,8 +104,8 @@ data Job = Job
permissions :: Maybe Permissions,
-- | Condition for running this job
runIf :: Maybe RunIf,
-- | Runner type (e.g., "ubuntu-latest")
runsOn :: Maybe Text,
-- | Runner type (e.g., "ubuntu-latest") or a list of runner labels
runsOn :: Maybe RunsOn,
-- | Secrets available to this job
secrets :: Map Text Text,
-- | Services to run alongside this job
Expand Down Expand Up @@ -185,7 +187,7 @@ gen = do
outputs <- genTextMap
permissions <- Gen.maybe Permissions.gen
runIf <- Gen.maybe RunIf.gen
runsOn <- Gen.maybe genText
runsOn <- Gen.maybe RunsOn.gen
secrets <- genTextMap
services <- Gen.map (Range.linear 1 5) $ liftA2 (,) ServiceId.gen Service.gen
steps <- Gen.maybe (Gen.nonEmpty (Range.linear 1 20) Step.gen)
Expand All @@ -208,7 +210,7 @@ gen = do
-- @
-- buildJob = new
-- { jobName = Just "Build"
-- , runsOn = Just "ubuntu-latest"
-- , runsOn = Just (RunsOn.RunsOnString "ubuntu-latest")
-- , steps = Just $ checkoutStep :| [buildStep]
-- }
-- @
Expand Down
79 changes: 79 additions & 0 deletions src/Language/Github/Actions/Job/RunsOn.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}

-- |
-- Module : Language.Github.Actions.Job.RunsOn
-- Description : Runner specification for GitHub Actions jobs
-- Copyright : (c) 2025 Bellroy Pty Ltd
-- License : BSD-3-Clause
-- Maintainer : Bellroy Tech Team <haskell@bellroy.com>
--
-- This module provides the 'RunsOn' type for representing the runner
-- configuration of a GitHub Actions job. GitHub Actions allows both strings
-- and lists of strings for the 'runs-on' field.
--
-- Examples of valid 'runs-on' specifications:
-- * @runs-on: ubuntu-latest@ - A single runner specified as a string
-- * @runs-on: [self-hosted, linux]@ - A runner specified as a list of labels
-- * @runs-on: [self-hosted, linux, x64]@ - A runner specified as a list of labels
--
-- For more information about GitHub Actions runner selection, see:
-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idruns-on>
module Language.Github.Actions.Job.RunsOn
( RunsOn (..),
gen,
)
where

import Data.Aeson (FromJSON, ToJSON (..), Value (..))
import qualified Data.Aeson as Aeson
import Data.List.NonEmpty (NonEmpty)
import Data.Text (Text)
import GHC.Generics (Generic)
import Hedgehog (MonadGen)
import qualified Hedgehog.Gen as Gen
import qualified Hedgehog.Range as Range

-- | Runner specification that preserves YAML representation.
--
-- GitHub Actions supports flexible runner specification:
--
-- * 'RunsOnString' - A single runner label as a string like @runs-on: ubuntu-latest@
-- * 'RunsOnArray' - A list of runner labels like @runs-on: [self-hosted, linux]@
--
-- Examples:
--
-- @
-- -- Single runner label (string form)
-- stringRunner :: RunsOn
-- stringRunner = RunsOnString "ubuntu-latest"
--
-- -- Multiple runner labels (array form)
-- arrayRunner :: RunsOn
-- arrayRunner = RunsOnArray ("self-hosted" :| ["linux", "x64"])
-- @
--
-- The type preserves the original YAML format during round-trip serialization.
-- A string input will serialize back to a string, and an array input will
-- serialize back to an array, preventing information loss.
data RunsOn
= RunsOnString Text
| RunsOnArray (NonEmpty Text)
deriving stock (Eq, Generic, Ord, Show)

instance FromJSON RunsOn where
parseJSON v@(Array _) = RunsOnArray <$> Aeson.parseJSON v
parseJSON v = RunsOnString <$> Aeson.parseJSON v

instance ToJSON RunsOn where
toJSON (RunsOnString label) = toJSON label
toJSON (RunsOnArray labels) = toJSON labels

gen :: (MonadGen m) => m RunsOn
gen =
Gen.choice
[ RunsOnString <$> genText,
RunsOnArray <$> Gen.nonEmpty (Range.linear 1 5) genText
]
where
genText = Gen.text (Range.linear 1 5) Gen.alphaNum
4 changes: 2 additions & 2 deletions test/golden/configuration-main.hs.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Workflow
, outputs = fromList []
, permissions = Nothing
, runIf = Nothing
, runsOn = Just "ubuntu-20.04"
, runsOn = Just (RunsOnString "ubuntu-20.04")
, secrets = fromList []
, services = fromList []
, steps =
Expand Down Expand Up @@ -871,7 +871,7 @@ Workflow
, outputs = fromList []
, permissions = Nothing
, runIf = Nothing
, runsOn = Just "ubuntu-20.04"
, runsOn = Just (RunsOnString "ubuntu-20.04")
, secrets = fromList []
, services = fromList []
, steps =
Expand Down
2 changes: 1 addition & 1 deletion test/golden/issue-8-boolean-if.hs.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Workflow
, outputs = fromList []
, permissions = Nothing
, runIf = Nothing
, runsOn = Just "ubuntu-latest"
, runsOn = Just (RunsOnString "ubuntu-latest")
, secrets = fromList []
, services = fromList []
, steps =
Expand Down
2 changes: 1 addition & 1 deletion test/golden/issue-8-numeric-retention.hs.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Workflow
, outputs = fromList []
, permissions = Nothing
, runIf = Nothing
, runsOn = Just "ubuntu-latest"
, runsOn = Just (RunsOnString "ubuntu-latest")
, secrets = fromList []
, services = fromList []
, steps =
Expand Down
18 changes: 18 additions & 0 deletions test/golden/issue-8-runs-on-list.golden.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
jobs:
array-runner:
runs-on:
- self-hosted
- linux
- x64
steps:
- run: echo "array"
string-runner:
runs-on: ubuntu-latest
steps:
- run: echo "string"
name: Test RunsOn
'on':
push:
branches-ignore:
- refs/tags/*_staging
- refs/tags/*_production
100 changes: 100 additions & 0 deletions test/golden/issue-8-runs-on-list.hs.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
Workflow
{ concurrency = Nothing
, defaults = Nothing
, env = fromList []
, jobs =
fromList
[ ( JobId "array-runner"
, Job
{ concurrency = Nothing
, container = Nothing
, continueOnError = Nothing
, defaults = Nothing
, env = fromList []
, environment = Nothing
, jobName = Nothing
, needs = Nothing
, outputs = fromList []
, permissions = Nothing
, runIf = Nothing
, runsOn =
Just (RunsOnArray ("self-hosted" :| [ "linux" , "x64" ]))
, secrets = fromList []
, services = fromList []
, steps =
Just
(Step
{ continueOnError = False
, env = fromList []
, name = Nothing
, run = Just "echo \"array\""
, runIf = Nothing
, shell = Nothing
, stepId = Nothing
, timeoutMinutes = Nothing
, uses = Nothing
, with = Nothing
, workingDirectory = Nothing
} :|
[])
, strategy = Nothing
, timeoutMinutes = Nothing
, uses = Nothing
, with = fromList []
}
)
, ( JobId "string-runner"
, Job
{ concurrency = Nothing
, container = Nothing
, continueOnError = Nothing
, defaults = Nothing
, env = fromList []
, environment = Nothing
, jobName = Nothing
, needs = Nothing
, outputs = fromList []
, permissions = Nothing
, runIf = Nothing
, runsOn = Just (RunsOnString "ubuntu-latest")
, secrets = fromList []
, services = fromList []
, steps =
Just
(Step
{ continueOnError = False
, env = fromList []
, name = Nothing
, run = Just "echo \"string\""
, runIf = Nothing
, shell = Nothing
, stepId = Nothing
, timeoutMinutes = Nothing
, uses = Nothing
, with = Nothing
, workingDirectory = Nothing
} :|
[])
, strategy = Nothing
, timeoutMinutes = Nothing
, uses = Nothing
, with = fromList []
}
)
]
, on =
fromList
[ PushTrigger
PushTriggerAttributes
{ branches = Nothing
, branchesIgnore =
Just ("refs/tags/*_staging" :| [ "refs/tags/*_production" ])
, paths = Nothing
, pathsIgnore = Nothing
, tags = Nothing
}
]
, permissions = Nothing
, runName = Nothing
, workflowName = Just "Test RunsOn"
}
16 changes: 16 additions & 0 deletions test/golden/issue-8-runs-on-list.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Test RunsOn
on:
push:
branches-ignore:
- "refs/tags/*_staging"
- "refs/tags/*_production"

jobs:
string-runner:
runs-on: ubuntu-latest
steps:
- run: echo "string"
array-runner:
runs-on: [self-hosted, linux, x64]
steps:
- run: echo "array"
4 changes: 2 additions & 2 deletions test/golden/issue-8-string-needs.hs.txt
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Workflow
, outputs = fromList []
, permissions = Nothing
, runIf = Nothing
, runsOn = Just "ubuntu-latest"
, runsOn = Just (RunsOnString "ubuntu-latest")
, secrets = fromList []
, services = fromList []
, steps =
Expand Down Expand Up @@ -55,7 +55,7 @@ Workflow
, outputs = fromList []
, permissions = Nothing
, runIf = Nothing
, runsOn = Just "ubuntu-latest"
, runsOn = Just (RunsOnString "ubuntu-latest")
, secrets = fromList []
, services = fromList []
, steps =
Expand Down