From 26cd6ccc5e20a23a4f52fa5b7370e8d52e036474 Mon Sep 17 00:00:00 2001 From: Michael Webb Date: Mon, 21 Jul 2025 12:36:40 +1000 Subject: [PATCH 1/6] Support different input types as identified in #8 --- github-actions.cabal | 3 + src/Language/Github/Actions/Job.hs | 14 +- src/Language/Github/Actions/JobNeeds.hs | 80 +++++ src/Language/Github/Actions/RunIf.hs | 84 +++++ src/Language/Github/Actions/Step.hs | 6 +- src/Language/Github/Actions/Step/With.hs | 10 +- .../Github/Actions/UnstructuredMap.hs | 137 ++++++++ test/Language/Github/Actions/WorkflowTest.hs | 2 +- test/golden/configuration-main.hs.txt | 303 +++++++++++------- test/golden/issue-8-boolean-if.golden.yml | 13 + test/golden/issue-8-boolean-if.hs.txt | 61 ++++ test/golden/issue-8-boolean-if.yml | 14 + .../issue-8-numeric-retention.golden.yml | 16 + test/golden/issue-8-numeric-retention.hs.txt | 69 ++++ test/golden/issue-8-numeric-retention.yml | 17 + test/golden/issue-8-string-needs.golden.yml | 16 + test/golden/issue-8-string-needs.hs.txt | 99 ++++++ test/golden/issue-8-string-needs.yml | 17 + 18 files changed, 840 insertions(+), 121 deletions(-) create mode 100644 src/Language/Github/Actions/JobNeeds.hs create mode 100644 src/Language/Github/Actions/RunIf.hs create mode 100644 src/Language/Github/Actions/UnstructuredMap.hs create mode 100644 test/golden/issue-8-boolean-if.golden.yml create mode 100644 test/golden/issue-8-boolean-if.hs.txt create mode 100644 test/golden/issue-8-boolean-if.yml create mode 100644 test/golden/issue-8-numeric-retention.golden.yml create mode 100644 test/golden/issue-8-numeric-retention.hs.txt create mode 100644 test/golden/issue-8-numeric-retention.yml create mode 100644 test/golden/issue-8-string-needs.golden.yml create mode 100644 test/golden/issue-8-string-needs.hs.txt create mode 100644 test/golden/issue-8-string-needs.yml diff --git a/github-actions.cabal b/github-actions.cabal index 2a7ad52..e9d0f22 100644 --- a/github-actions.cabal +++ b/github-actions.cabal @@ -60,13 +60,16 @@ library Language.Github.Actions.Job.Environment Language.Github.Actions.Job.Id Language.Github.Actions.Job.Strategy + Language.Github.Actions.JobNeeds Language.Github.Actions.Permissions + Language.Github.Actions.RunIf Language.Github.Actions.Service Language.Github.Actions.Service.Id Language.Github.Actions.Shell Language.Github.Actions.Step Language.Github.Actions.Step.Id Language.Github.Actions.Step.With + Language.Github.Actions.UnstructuredMap Language.Github.Actions.Workflow Language.Github.Actions.Workflow.Trigger diff --git a/src/Language/Github/Actions/Job.hs b/src/Language/Github/Actions/Job.hs index bd54897..5ecb14f 100644 --- a/src/Language/Github/Actions/Job.hs +++ b/src/Language/Github/Actions/Job.hs @@ -43,12 +43,14 @@ import Language.Github.Actions.Job.Container (JobContainer) import qualified Language.Github.Actions.Job.Container as JobContainer import Language.Github.Actions.Job.Environment (JobEnvironment) import qualified Language.Github.Actions.Job.Environment as JobEnvironment -import Language.Github.Actions.Job.Id (JobId) -import qualified Language.Github.Actions.Job.Id as JobId import Language.Github.Actions.Job.Strategy (JobStrategy) import qualified Language.Github.Actions.Job.Strategy as JobStrategy +import Language.Github.Actions.JobNeeds (JobNeeds) +import qualified Language.Github.Actions.JobNeeds as JobNeeds import Language.Github.Actions.Permissions (Permissions) import qualified Language.Github.Actions.Permissions as Permissions +import Language.Github.Actions.RunIf (RunIf) +import qualified Language.Github.Actions.RunIf as RunIf import Language.Github.Actions.Service (Service) import qualified Language.Github.Actions.Service as Service import Language.Github.Actions.Service.Id (ServiceId) @@ -93,13 +95,13 @@ data Job = Job -- | Display name for the job jobName :: Maybe Text, -- | Jobs this job depends on - needs :: Maybe (NonEmpty JobId), + needs :: Maybe JobNeeds, -- | Outputs from this job outputs :: Map Text Text, -- | Permissions for this job permissions :: Maybe Permissions, -- | Condition for running this job - runIf :: Maybe Text, + runIf :: Maybe RunIf, -- | Runner type (e.g., "ubuntu-latest") runsOn :: Maybe Text, -- | Secrets available to this job @@ -179,10 +181,10 @@ gen = do env <- genTextMap environment <- Gen.maybe JobEnvironment.gen jobName <- Gen.maybe genText - needs <- Gen.maybe (Gen.nonEmpty (Range.linear 1 5) JobId.gen) + needs <- Gen.maybe JobNeeds.gen outputs <- genTextMap permissions <- Gen.maybe Permissions.gen - runIf <- Gen.maybe genText + runIf <- Gen.maybe RunIf.gen runsOn <- Gen.maybe genText secrets <- genTextMap services <- Gen.map (Range.linear 1 5) $ liftA2 (,) ServiceId.gen Service.gen diff --git a/src/Language/Github/Actions/JobNeeds.hs b/src/Language/Github/Actions/JobNeeds.hs new file mode 100644 index 0000000..31227dc --- /dev/null +++ b/src/Language/Github/Actions/JobNeeds.hs @@ -0,0 +1,80 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} + +-- | +-- Module : Language.Github.Actions.JobNeeds +-- Description : Job dependency specification for GitHub Actions +-- Copyright : (c) 2025 Bellroy Pty Ltd +-- License : BSD-3-Clause +-- Maintainer : Bellroy Tech Team +-- +-- This module provides the 'JobNeeds' type for representing job dependencies +-- in GitHub Actions workflows. GitHub Actions allows both single job and +-- multiple job dependencies for the 'needs' field. +-- +-- Examples of valid 'needs' specifications: +-- * @needs: build@ - Single job dependency +-- * @needs: [build, test]@ - Multiple job dependencies +-- +-- For more information about GitHub Actions job dependencies, see: +-- +module Language.Github.Actions.JobNeeds + ( JobNeeds (..), + gen, + ) +where + +import Data.Aeson (FromJSON, ToJSON (..), Value (..)) +import qualified Data.Aeson as Aeson +import Data.List.NonEmpty (NonEmpty) +import GHC.Generics (Generic) +import Hedgehog (MonadGen) +import qualified Hedgehog.Gen as Gen +import qualified Hedgehog.Range as Range +import Language.Github.Actions.Job.Id (JobId) +import qualified Language.Github.Actions.Job.Id as JobId + +-- | Job dependency specification that preserves YAML representation. +-- +-- GitHub Actions supports flexible job dependency specification: +-- +-- * 'JobNeedsString' - Single job dependency as string like @needs: build@ +-- * 'JobNeedsArray' - Multiple job dependencies as array like @needs: [build, test]@ +-- +-- Examples: +-- +-- @ +-- -- Single job dependency (string form) +-- stringDep :: JobNeeds +-- stringDep = JobNeedsString (JobId "build") +-- +-- -- Multiple job dependencies (array form) +-- arrayDeps :: JobNeeds +-- arrayDeps = JobNeedsArray (JobId "build" :| [JobId "test", JobId "lint"]) +-- @ +-- +-- 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 JobNeeds + = -- | Single job dependency as string (e.g., @needs: build@) + JobNeedsString JobId + | -- | Multiple job dependencies as array (e.g., @needs: [build, test]@) + JobNeedsArray (NonEmpty JobId) + deriving stock (Eq, Generic, Ord, Show) + +instance FromJSON JobNeeds where + parseJSON v@(Array _) = JobNeedsArray <$> Aeson.parseJSON v + parseJSON v = JobNeedsString <$> Aeson.parseJSON v + +instance ToJSON JobNeeds where + toJSON (JobNeedsString jobId) = toJSON jobId + toJSON (JobNeedsArray jobIds) = toJSON jobIds + +-- | Generate random 'JobNeeds' values for property testing. +gen :: (MonadGen m) => m JobNeeds +gen = + Gen.choice + [ JobNeedsString <$> JobId.gen, + JobNeedsArray <$> Gen.nonEmpty (Range.linear 1 5) JobId.gen + ] diff --git a/src/Language/Github/Actions/RunIf.hs b/src/Language/Github/Actions/RunIf.hs new file mode 100644 index 0000000..3119ff6 --- /dev/null +++ b/src/Language/Github/Actions/RunIf.hs @@ -0,0 +1,84 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE OverloadedStrings #-} + +-- | +-- Module : Language.Github.Actions.RunIf +-- Description : Flexible conditional expressions for GitHub Actions +-- Copyright : (c) 2025 Bellroy Pty Ltd +-- License : BSD-3-Clause +-- Maintainer : Bellroy Tech Team +-- +-- This module provides the 'RunIf' type for representing conditional expressions +-- in GitHub Actions workflows. GitHub Actions allows both boolean and string +-- expressions in 'if' conditions for jobs and steps. +-- +-- Examples of valid 'if' conditions: +-- * @if: false@ - Simple boolean +-- * @if: "github.ref == 'refs/heads/main'"@ - GitHub expression +-- * @if: "\${{ success() && matrix.os == 'ubuntu-latest' }}"@ - Complex expression +-- +-- For more information about GitHub Actions conditional expressions, see: +-- +module Language.Github.Actions.RunIf + ( RunIf (..), + gen, + ) +where + +import Data.Aeson (FromJSON, ToJSON (..), Value (..)) +import qualified Data.Aeson as Aeson +import Data.Text (Text) +import GHC.Generics (Generic) +import Hedgehog (MonadGen) +import qualified Hedgehog.Gen as Gen +import qualified Hedgehog.Range as Range + +-- | A conditional expression that can be either a boolean or string. +-- +-- GitHub Actions supports flexible 'if' conditions: +-- +-- * 'RunIfBool' - Simple boolean values like @true@ or @false@ +-- * 'RunIfString' - GitHub expressions like @"github.ref == 'refs/heads/main'"@ +-- +-- Examples: +-- +-- @ +-- -- Simple boolean condition +-- simpleFalse :: RunIf +-- simpleFalse = RunIfBool False +-- +-- -- GitHub expression condition +-- branchCheck :: RunIf +-- branchCheck = RunIfString "github.ref == 'refs/heads/main'" +-- +-- -- Complex expression with functions +-- complexCheck :: RunIf +-- complexCheck = RunIfString "\${{ success() && matrix.os == 'ubuntu-latest' }}" +-- @ +-- +-- The type preserves the original format during round-trip serialization, +-- so a boolean input remains a boolean in the output YAML. +data RunIf + = -- | Boolean condition (e.g., @false@, @true@) + RunIfBool Bool + | -- | String expression (e.g., @"github.ref == 'refs/heads/main'"@) + RunIfString Text + deriving stock (Eq, Generic, Ord, Show) + +instance FromJSON RunIf where + parseJSON (Bool b) = pure $ RunIfBool b + parseJSON (String s) = pure $ RunIfString s + parseJSON v = fail $ "Expected Bool or String for RunIf, got: " ++ show v + +instance ToJSON RunIf where + toJSON (RunIfBool b) = Bool b + toJSON (RunIfString s) = String s + +-- | Generate random 'RunIf' values for property testing. +gen :: (MonadGen m) => m RunIf +gen = + Gen.choice + [ RunIfBool <$> Gen.bool, + RunIfString <$> Gen.text (Range.linear 5 50) Gen.alphaNum + ] diff --git a/src/Language/Github/Actions/Step.hs b/src/Language/Github/Actions/Step.hs index a6daada..e56e79d 100644 --- a/src/Language/Github/Actions/Step.hs +++ b/src/Language/Github/Actions/Step.hs @@ -35,6 +35,8 @@ import GHC.Generics (Generic) import Hedgehog (MonadGen) import qualified Hedgehog.Gen as Gen import qualified Hedgehog.Range as Range +import Language.Github.Actions.RunIf (RunIf) +import qualified Language.Github.Actions.RunIf as RunIf import Language.Github.Actions.Shell (Shell) import qualified Language.Github.Actions.Shell as Shell import Language.Github.Actions.Step.Id (StepId) @@ -82,7 +84,7 @@ data Step = Step -- | Command or script to run run :: Maybe Text, -- | Condition for running this step - runIf :: Maybe Text, + runIf :: Maybe RunIf, -- | Shell to use for running commands shell :: Maybe Shell, -- | Unique identifier for this step @@ -139,7 +141,7 @@ gen = do env <- genTextMap name <- Gen.maybe genText run <- Gen.maybe genText - runIf <- Gen.maybe genText + runIf <- Gen.maybe RunIf.gen shell <- Gen.maybe Shell.gen stepId <- Gen.maybe StepId.gen timeoutMinutes <- Gen.maybe $ Gen.int (Range.linear 1 120) diff --git a/src/Language/Github/Actions/Step/With.hs b/src/Language/Github/Actions/Step/With.hs index 7241563..af9d765 100644 --- a/src/Language/Github/Actions/Step/With.hs +++ b/src/Language/Github/Actions/Step/With.hs @@ -30,7 +30,6 @@ where import Data.Aeson (FromJSON, ToJSON (..), (.:), (.:?), (.=)) import qualified Data.Aeson as Aeson import qualified Data.Aeson.KeyMap as AesonKeyMap -import Data.Map (Map) import Data.Maybe (catMaybes) import qualified Data.Set as Set import Data.Text (Text) @@ -38,6 +37,8 @@ import GHC.Generics (Generic) import Hedgehog (MonadGen) import qualified Hedgehog.Gen as Gen import qualified Hedgehog.Range as Range +import Language.Github.Actions.UnstructuredMap (UnstructuredMap) +import qualified Language.Github.Actions.UnstructuredMap as UnstructuredMap -- | Docker container arguments for Docker actions. -- @@ -95,14 +96,14 @@ data StepWith = -- | Docker action arguments StepWithDockerArgs StepWithDockerArgsAttrs | -- | Environment variables/general inputs - StepWithEnv (Map Text Text) + StepWithEnv UnstructuredMap deriving stock (Eq, Generic, Ord, Show) instance FromJSON StepWith where parseJSON = Aeson.withObject "StepWith" $ \o -> let objectKeySet = Set.fromList (AesonKeyMap.keys o) dockerKeySet = Set.fromList ["entryPoint", "args"] - in if objectKeySet `Set.isSubsetOf` dockerKeySet + in if not (null objectKeySet) && objectKeySet `Set.isSubsetOf` dockerKeySet then do entryPoint <- o .: "entryPoint" args <- o .:? "args" @@ -127,8 +128,7 @@ gen = entryPoint <- genText args <- Gen.maybe genText pure StepWithDockerArgsAttrs {..}, - StepWithEnv <$> genTextMap + StepWithEnv <$> UnstructuredMap.gen ] where genText = Gen.text (Range.linear 1 5) Gen.alphaNum - genTextMap = Gen.map (Range.linear 1 5) $ liftA2 (,) genText genText diff --git a/src/Language/Github/Actions/UnstructuredMap.hs b/src/Language/Github/Actions/UnstructuredMap.hs new file mode 100644 index 0000000..ec6c0dd --- /dev/null +++ b/src/Language/Github/Actions/UnstructuredMap.hs @@ -0,0 +1,137 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} + +-- | +-- Module : Language.Github.Actions.UnstructuredMap +-- Description : Flexible value types for GitHub Actions YAML parsing +-- Copyright : (c) 2025 Bellroy Pty Ltd +-- License : BSD-3-Clause +-- Maintainer : Bellroy Tech Team +-- +-- This module provides the 'UnstructuredValue' type for representing values +-- that can be strings, numbers, or booleans in GitHub Actions YAML files. +-- This is commonly needed when parsing @Map Text Text@ fields that GitHub +-- Actions allows to have flexible typing. +-- +-- GitHub Actions allows flexible typing in many contexts: +-- * @retention-days: 1@ (number) +-- * @retention-days: "1"@ (string) +-- * @if: false@ (boolean) +-- * @timeout-minutes: 30@ (number) +-- * @working-directory: "./src"@ (string) +-- +-- This type preserves the original YAML type during round-trip parsing, +-- ensuring that numeric values remain numeric and strings remain strings. +module Language.Github.Actions.UnstructuredMap + ( UnstructuredValue (..), + UnstructuredMap (..), + renderUnstructuredValue, + gen, + ) +where + +import Data.Aeson (FromJSON, ToJSON (..), Value (..)) +import qualified Data.Aeson as Aeson +import Data.Map (Map) +import qualified Data.Map as Map +import Data.Text (Text) +import qualified Data.Text as Text +import GHC.Generics (Generic) +import Hedgehog (MonadGen) +import qualified Hedgehog.Gen as Gen +import qualified Hedgehog.Range as Range + +-- | A flexible value that can be a string, number, or boolean. +-- +-- This type is designed to handle the flexible typing that GitHub Actions +-- allows in YAML files, particularly in contexts where we would normally +-- parse @Map Text Text@ but need to support non-string values. +-- +-- Examples: +-- +-- @ +-- -- String value (most common) +-- pathValue :: UnstructuredValue +-- pathValue = UnstructuredValueString "./dist" +-- +-- -- Numeric value (preserves original type) +-- retentionValue :: UnstructuredValue +-- retentionValue = UnstructuredValueNumber 7 +-- +-- -- Boolean value +-- flagValue :: UnstructuredValue +-- flagValue = UnstructuredValueBool False +-- @ +-- +-- The type preserves the original format during round-trip serialization, +-- so numeric inputs remain numeric in the output YAML. +data UnstructuredValue + = -- | String value (e.g., @"./dist"@, @"ubuntu-latest"@) + UnstructuredValueString Text + | -- | Numeric value (e.g., @1@, @30@, @1.5@) + UnstructuredValueNumber Double + | -- | Boolean value (e.g., @true@, @false@) + UnstructuredValueBool Bool + deriving stock (Eq, Generic, Ord, Show) + +instance FromJSON UnstructuredValue where + parseJSON (String s) = pure $ UnstructuredValueString s + parseJSON (Number n) = pure $ UnstructuredValueNumber (realToFrac n) + parseJSON (Bool b) = pure $ UnstructuredValueBool b + parseJSON v = fail $ "Expected String, Number, or Bool for UnstructuredValue, got: " ++ show v + +instance ToJSON UnstructuredValue where + toJSON (UnstructuredValueString s) = String s + toJSON (UnstructuredValueNumber n) = Number (fromRational (toRational n)) + toJSON (UnstructuredValueBool b) = Bool b + +-- | Render an UnstructuredValue as Text. +-- +-- This function is essential for backwards compatibility with existing code +-- that expects @Map Text Text@ values. It allows gradual migration from +-- @Text@ to @UnstructuredValue@ while maintaining the same interface. +-- +-- Examples: +-- +-- @ +-- renderUnstructuredValue (UnstructuredValueString "hello") == "hello" +-- renderUnstructuredValue (UnstructuredValueNumber 42.0) == "42.0" +-- renderUnstructuredValue (UnstructuredValueBool True) == "true" +-- @ +renderUnstructuredValue :: UnstructuredValue -> Text +renderUnstructuredValue (UnstructuredValueString s) = s +renderUnstructuredValue (UnstructuredValueNumber n) = + -- Format numbers nicely, avoiding unnecessary decimal places for integers + if n == fromInteger (round n) + then Text.pack (show (round n :: Integer)) + else Text.pack (show n) +renderUnstructuredValue (UnstructuredValueBool b) = if b then "true" else "false" + +-- | A map of unstructured values, commonly used for flexible YAML parsing. +-- +-- This newtype wraps @Map Text UnstructuredValue@ and provides appropriate +-- JSON instances for parsing GitHub Actions YAML that allows mixed types +-- in key-value mappings. +newtype UnstructuredMap = UnstructuredMap (Map Text UnstructuredValue) + deriving stock (Eq, Generic, Ord, Show) + deriving newtype (FromJSON, ToJSON) + +-- | Generate random 'UnstructuredValue' values for property testing. +genUnstructuredValue :: (MonadGen m) => m UnstructuredValue +genUnstructuredValue = + Gen.choice + [ UnstructuredValueString <$> Gen.text (Range.linear 1 20) Gen.alphaNum, + UnstructuredValueNumber <$> Gen.realFloat (Range.linearFrac 0 1000), + UnstructuredValueBool <$> Gen.bool + ] + +-- | Generate random 'UnstructuredMap' values for property testing. +gen :: (MonadGen m) => m UnstructuredMap +gen = UnstructuredMap <$> Gen.map (Range.linear 0 10) genKeyValue + where + genKeyValue = do + key <- Gen.text (Range.linear 1 20) Gen.alphaNum + value <- genUnstructuredValue + pure (key, value) diff --git a/test/Language/Github/Actions/WorkflowTest.hs b/test/Language/Github/Actions/WorkflowTest.hs index 334c663..e177c97 100644 --- a/test/Language/Github/Actions/WorkflowTest.hs +++ b/test/Language/Github/Actions/WorkflowTest.hs @@ -50,7 +50,7 @@ test_goldenWorkflowFromYaml = do putStrLn $ "roundtrip " <> takeBaseName testYamlFilePath eitherWorkflow <- Yaml.decodeFileEither @Workflow testYamlFilePath either - (BS.writeFile outputFilePath >> (\_ -> fail "YAML decoding failed")) + (BS.writeFile outputFilePath >> (\e -> fail $ "YAML decoding failed: " ++ show e)) (\workflow -> writeOutputFiles outputFilePath haskellOutputFilePath workflow >> pure workflow) $ first (encodeUtf8 . Text.pack . Yaml.prettyPrintParseException) eitherWorkflow writeOutputFiles :: FilePath -> FilePath -> Workflow -> IO () diff --git a/test/golden/configuration-main.hs.txt b/test/golden/configuration-main.hs.txt index cf237b3..4ffa06f 100644 --- a/test/golden/configuration-main.hs.txt +++ b/test/golden/configuration-main.hs.txt @@ -18,7 +18,7 @@ Workflow , env = fromList [] , environment = Nothing , jobName = Nothing - , needs = Just (JobId "validateContent" :| []) + , needs = Just (JobNeedsArray (JobId "validateContent" :| [])) , outputs = fromList [] , permissions = Nothing , runIf = Nothing @@ -34,7 +34,8 @@ Workflow , run = Nothing , runIf = Just - "github.ref == 'refs/heads/master' || github.ref == 'refs/heads/stable'" + (RunIfString + "github.ref == 'refs/heads/master' || github.ref == 'refs/heads/stable'") , shell = Nothing , stepId = Nothing , timeoutMinutes = Nothing @@ -42,10 +43,11 @@ Workflow , with = Just (StepWithEnv - (fromList - [ ( "fetch-depth" , "0" ) - , ( "token" , "${{ env.GH_TOKEN }}" ) - ])) + (UnstructuredMap + (fromList + [ ( "fetch-depth" , UnstructuredValueString "0" ) + , ( "token" , UnstructuredValueString "${{ env.GH_TOKEN }}" ) + ]))) , workingDirectory = Nothing } :| [ Step @@ -55,7 +57,8 @@ Workflow , run = Nothing , runIf = Just - "github.ref != 'refs/heads/master' && github.ref != 'refs/heads/stable'" + (RunIfString + "github.ref != 'refs/heads/master' && github.ref != 'refs/heads/stable'") , shell = Nothing , stepId = Nothing , timeoutMinutes = Nothing @@ -63,10 +66,13 @@ Workflow , with = Just (StepWithEnv - (fromList - [ ( "fetch-depth" , "1" ) - , ( "token" , "${{ env.GH_TOKEN }}" ) - ])) + (UnstructuredMap + (fromList + [ ( "fetch-depth" , UnstructuredValueString "1" ) + , ( "token" + , UnstructuredValueString "${{ env.GH_TOKEN }}" + ) + ]))) , workingDirectory = Nothing } , Step @@ -91,7 +97,7 @@ Workflow , run = Just "echo \"DESTINATION=Staging Environment\" >> $GITHUB_ENV\necho \"ENVIRONMENT=staging\" >> $GITHUB_ENV\n" - , runIf = Just "github.ref == 'refs/heads/master'" + , runIf = Just (RunIfString "github.ref == 'refs/heads/master'") , shell = Nothing , stepId = Nothing , timeoutMinutes = Nothing @@ -106,7 +112,7 @@ Workflow , run = Just "echo \"DESTINATION=Production Environment\" >> $GITHUB_ENV\necho \"ENVIRONMENT=production\" >> $GITHUB_ENV\n" - , runIf = Just "github.ref == 'refs/heads/stable'" + , runIf = Just (RunIfString "github.ref == 'refs/heads/stable'") , shell = Nothing , stepId = Nothing , timeoutMinutes = Nothing @@ -127,12 +133,16 @@ Workflow , with = Just (StepWithEnv - (fromList - [ ( "bellroy-nix-cache-access" , "none" ) - , ( "nix-config-access-tokens" - , "github.com=${{ secrets.READ_HASKELL_REPO_PAT }}" - ) - ])) + (UnstructuredMap + (fromList + [ ( "bellroy-nix-cache-access" + , UnstructuredValueString "none" + ) + , ( "nix-config-access-tokens" + , UnstructuredValueString + "github.com=${{ secrets.READ_HASKELL_REPO_PAT }}" + ) + ]))) , workingDirectory = Nothing } , Step @@ -179,7 +189,13 @@ Workflow , with = Just (StepWithEnv - (fromList [ ( "path" , "${{ env.GIT_REPOSITORY_PATH }}" ) ])) + (UnstructuredMap + (fromList + [ ( "path" + , UnstructuredValueString + "${{ env.GIT_REPOSITORY_PATH }}" + ) + ]))) , workingDirectory = Nothing } , Step @@ -206,7 +222,8 @@ Workflow , run = Just "set +e\nIFS= output=$(./scripts/check_configuration_csvs_sorted 2>&1)\nstatus=$?\n\nif (($status)); then\n echo \"Configuration CSV files are not sorted:\" >>/tmp/pr-comment.txt\n echo \"\" >>/tmp/pr-comment.txt\n echo \"\\`\\`\\`\" >>/tmp/pr-comment.txt\n echo $output >>/tmp/pr-comment.txt\n echo \"\\`\\`\\`\" >>/tmp/pr-comment.txt\n exit $status\nelse\n echo \"All configuration CSV files are sorted.\"\nfi\n" - , runIf = Just "steps.check_changes.outputs.csv_changed == '1'" + , runIf = + Just (RunIfString "steps.check_changes.outputs.csv_changed == '1'") , shell = Nothing , stepId = Nothing , timeoutMinutes = Nothing @@ -227,11 +244,15 @@ Workflow , with = Just (StepWithEnv - (fromList - [ ( "token" , "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}" ) - , ( "tool-name" , "byproduct" ) - , ( "tool-version" , "latest" ) - ])) + (UnstructuredMap + (fromList + [ ( "token" + , UnstructuredValueString + "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}" + ) + , ( "tool-name" , UnstructuredValueString "byproduct" ) + , ( "tool-version" , UnstructuredValueString "latest" ) + ]))) , workingDirectory = Nothing } , Step @@ -247,11 +268,15 @@ Workflow , with = Just (StepWithEnv - (fromList - [ ( "token" , "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}" ) - , ( "tool-name" , "gitapult" ) - , ( "tool-version" , "latest" ) - ])) + (UnstructuredMap + (fromList + [ ( "token" + , UnstructuredValueString + "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}" + ) + , ( "tool-name" , UnstructuredValueString "gitapult" ) + , ( "tool-version" , UnstructuredValueString "latest" ) + ]))) , workingDirectory = Nothing } , Step @@ -267,11 +292,17 @@ Workflow , with = Just (StepWithEnv - (fromList - [ ( "token" , "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}" ) - , ( "tool-name" , "config-rules-cli" ) - , ( "tool-version" , "latest" ) - ])) + (UnstructuredMap + (fromList + [ ( "token" + , UnstructuredValueString + "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}" + ) + , ( "tool-name" + , UnstructuredValueString "config-rules-cli" + ) + , ( "tool-version" , UnstructuredValueString "latest" ) + ]))) , workingDirectory = Nothing } , Step @@ -287,11 +318,15 @@ Workflow , with = Just (StepWithEnv - (fromList - [ ( "token" , "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}" ) - , ( "tool-name" , "powerful-owl" ) - , ( "tool-version" , "latest" ) - ])) + (UnstructuredMap + (fromList + [ ( "token" + , UnstructuredValueString + "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}" + ) + , ( "tool-name" , UnstructuredValueString "powerful-owl" ) + , ( "tool-version" , UnstructuredValueString "latest" ) + ]))) , workingDirectory = Nothing } , Step @@ -376,7 +411,7 @@ Workflow , run = Just "set +e\nIFS= output=$(./gitapult changeset --git_root \"$GITHUB_WORKSPACE\" --from \"${{ env.MERGE_BASE }}\" | ./byproduct validate --markdown 2>&1)\nstatus=$?\n\nif (($status)); then\n echo $output >>/tmp/pr-comment.txt\n exit $status\nelse\n echo \"Byproduct validation succeeded.\"\nfi\n" - , runIf = Just "env.PR_NUMBER != 0" + , runIf = Just (RunIfString "env.PR_NUMBER != 0") , shell = Nothing , stepId = Nothing , timeoutMinutes = Nothing @@ -468,7 +503,8 @@ Workflow "if [ $(git status shipping/tests --porcelain=v1 | wc -l) -gt 0 ]\nthen\n git add shipping/tests\n git commit -m \"Update shipping tests\"\n echo \"GENERATED_ANOTHER_COMMIT=true\" >> $GITHUB_ENV\nfi\n" , runIf = Just - "env.PR_NUMBER != 0 && github.ref != 'refs/heads/stable' && github.ref != 'refs/heads/master'" + (RunIfString + "env.PR_NUMBER != 0 && github.ref != 'refs/heads/stable' && github.ref != 'refs/heads/master'") , shell = Nothing , stepId = Nothing , timeoutMinutes = Nothing @@ -515,7 +551,8 @@ Workflow "if [ $(git status promotions/tests --porcelain=v1 | wc -l) -gt 0 ]\nthen\n git add promotions/tests\n git commit -m \"Update promotion tests\"\n echo \"GENERATED_ANOTHER_COMMIT=true\" >> $GITHUB_ENV\nfi\n" , runIf = Just - "env.PR_NUMBER != 0 && github.ref != 'refs/heads/stable' && github.ref != 'refs/heads/master'" + (RunIfString + "env.PR_NUMBER != 0 && github.ref != 'refs/heads/stable' && github.ref != 'refs/heads/master'") , shell = Nothing , stepId = Nothing , timeoutMinutes = Nothing @@ -531,7 +568,8 @@ Workflow , run = Just "git push --set-upstream origin HEAD\n\ngh run cancel ${{ github.run_id }}\ngh run watch ${{ github.run_id }}\n" - , runIf = Just "env.GENERATED_ANOTHER_COMMIT == 'true'" + , runIf = + Just (RunIfString "env.GENERATED_ANOTHER_COMMIT == 'true'") , shell = Nothing , stepId = Nothing , timeoutMinutes = Nothing @@ -563,7 +601,8 @@ Workflow "echo \"COMMIT_CHANGES<> $GITHUB_ENV\necho \"$(git log --oneline --format=\\\"%s\\\" $(git tag -l [0-9]*_${{ env.ENVIRONMENT }} | tail -n 1)..${{ steps.extract_branch.outputs.branch }} | grep -v 'Merge pull request' | grep -v \"Merge branch 'master' into\" | sed -e 's/^\"//' -e 's/\"$//' | cat)\" >> $GITHUB_ENV\necho \"EOF\" >> $GITHUB_ENV\n" , runIf = Just - "github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/master'" + (RunIfString + "github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/master'") , shell = Just (Bash Nothing) , stepId = Just (StepId "extract_changes") , timeoutMinutes = Nothing @@ -580,7 +619,8 @@ Workflow , run = Nothing , runIf = Just - "github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/master'" + (RunIfString + "github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/master'") , shell = Nothing , stepId = Just (StepId "slack-deploy") , timeoutMinutes = Nothing @@ -590,12 +630,16 @@ Workflow , with = Just (StepWithEnv - (fromList - [ ( "channel-id" , "${{ env.SLACK_CHANNEL_ID }}" ) - , ( "payload" - , "{\n \"text\": \":loudspeaker: *${{ github.actor }}* is deploying *${{ env.SYSTEM_NAME }}* to *${{ env.DESTINATION }}* from *${{ steps.extract_branch.outputs.branch }}*\",\n \"attachments\": [\n {\n \"fallback\": \"Deployment summary\",\n \"color\": \"ffd966\",\n \"fields\": [\n {\n \"title\": \"What is being deployed\",\n \"value\": ${{ toJSON(env.COMMIT_CHANGES) }},\n \"short\": false\n },\n {\n \"title\": \"Status\",\n \"short\": true,\n \"value\": \"In Progress\"\n }\n ]\n }\n ]\n}\n" - ) - ])) + (UnstructuredMap + (fromList + [ ( "channel-id" + , UnstructuredValueString "${{ env.SLACK_CHANNEL_ID }}" + ) + , ( "payload" + , UnstructuredValueString + "{\n \"text\": \":loudspeaker: *${{ github.actor }}* is deploying *${{ env.SYSTEM_NAME }}* to *${{ env.DESTINATION }}* from *${{ steps.extract_branch.outputs.branch }}*\",\n \"attachments\": [\n {\n \"fallback\": \"Deployment summary\",\n \"color\": \"ffd966\",\n \"fields\": [\n {\n \"title\": \"What is being deployed\",\n \"value\": ${{ toJSON(env.COMMIT_CHANGES) }},\n \"short\": false\n },\n {\n \"title\": \"Status\",\n \"short\": true,\n \"value\": \"In Progress\"\n }\n ]\n }\n ]\n}\n" + ) + ]))) , workingDirectory = Nothing } , Step @@ -605,7 +649,7 @@ Workflow , run = Just "set -e\n# XXX: Gitapult cannot distinguish datasets, so we push all data to\n# both endpoints and allow the server to decide what to do.\n# Kafka Webhooks accepts all datasets at once\n./gitapult run \\\n --git_root \"$GITHUB_WORKSPACE\" \\\n --authorization_token \"$KW_STAGING_API_KEY\" \\\n --changeset_url \"$KW_STAGING_BASE_URL/gitapult/configuration/records\" \\\n --revision_url \"$KW_STAGING_BASE_URL/gitapult/configuration/revision\" \\\n --api_key \\\n --compress \\\n --max-payload-bytes 3000000\n\n# XXX: V3 doesn't care about rendering data, but it is too large to send.\n# As at 2024-02-14 gitapult does not support selecting which data to send.\ngit -C \"$GIT_REPOSITORY_PATH\" rm feeds/rendering_data/.gitapult.json\n./gitapult run \\\n --git_root \"$GITHUB_WORKSPACE\" \\\n --authorization_token \"$SF_STAGING_API_KEY\" \\\n --changeset_url \"$SF_STAGING_BASE_URL/api/v1/configuration/shipping\" \\\n --revision_url \"$SF_STAGING_BASE_URL/api/v1/configuration/shipping/revisions\" && \\\ngit -C \"$GIT_REPOSITORY_PATH\" restore --staged feeds/rendering_data/.gitapult.json\ngit -C \"$GIT_REPOSITORY_PATH\" restore feeds/rendering_data/.gitapult.json\n" - , runIf = Just "github.ref == 'refs/heads/master'" + , runIf = Just (RunIfString "github.ref == 'refs/heads/master'") , shell = Nothing , stepId = Nothing , timeoutMinutes = Nothing @@ -620,7 +664,8 @@ Workflow , run = Just "git tag \"${{ join(steps.current-time.outputs.*, '\\n') }}_staging\" && git push --tags\n" - , runIf = Just "success() && github.ref == 'refs/heads/master'" + , runIf = + Just (RunIfString "success() && github.ref == 'refs/heads/master'") , shell = Nothing , stepId = Nothing , timeoutMinutes = Nothing @@ -635,7 +680,7 @@ Workflow , run = Just "curl -X POST -H \"Accept: application/vnd.github.v3+json\" -H \"Authorization: token ${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}\" https://api.github.com/repos/${{ github.repository }}/pulls -d '{\"head\":\"master\",\"base\":\"stable\",\"title\":\"Merge branch master into stable\",\"body\":\"This PR was automatically generated by CI.\"}'\nPR_URL=$(curl -sX GET -H \"Accept: application/vnd.github.v3+json\" -H \"Authorization: token ${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}\" https://api.github.com/repos/${{ github.repository }}/pulls\\?head\\=bellroy:master\\&base\\=stable | jq -r '.[0].html_url | select(length>0)')\necho \"PR_URL=$PR_URL\" >> $GITHUB_ENV\n" - , runIf = Just "github.ref == 'refs/heads/master'" + , runIf = Just (RunIfString "github.ref == 'refs/heads/master'") , shell = Nothing , stepId = Nothing , timeoutMinutes = Nothing @@ -650,7 +695,7 @@ Workflow , run = Just "PR_RESPONSE=$(curl -sX GET -H \"Accept: application/vnd.github.v3+json\" -H \"Authorization: token ${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}\" https://api.github.com/repos/${{ github.repository }}/pulls\\?head\\=bellroy:master\\&base\\=stable | jq -r '.[0]')\nPR_NUM=$(echo \"$PR_RESPONSE\" | jq -r \".number\")\nPR_ASSIGNEE=$(echo \"$PR_RESPONSE\" | jq -r \".assignee\")\nCOMMITS_RESPONSE=$(curl -sX GET -H \"Accept: application/vnd.github.v3+json\" -H \"Authorization: token ${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}\" https://api.github.com/repos/${{ github.repository }}/pulls/$PR_NUM/commits)\nFIRST_COMMITTER=$(curl -sX GET -H \"Accept: application/vnd.github.v3+json\" -H \"Authorization: token ${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}\" https://api.github.com/repos/${{ github.repository }}/pulls/$PR_NUM/commits | jq -r '. | sort_by(.commit.author.date) | .[0] | .author.login')\n# special case for squashed PRs created by trike-deploy e.g. gha-workflows\nif [[ \"$FIRST_COMMITTER\" == \"trike-deploy\" ]]; then\n FIRST_COMMITTER=$(echo \"$COMMITS_RESPONSE\" | jq -r '. | sort_by(.commit.author.date) | .[0]' | sed -En \"s/.*Co-authored-by: (.*) <.*@.*>\\\",/\\1/p\")\nfi\nif [[ \"$PR_ASSIGNEE\" == \"null\" ]]; then\n curl -sX POST -H \"Accept: application/vnd.github.v3+json\" -H \"Authorization: token ${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}\" https://api.github.com/repos/${{ github.repository }}/issues/$PR_NUM/assignees -d \"{\\\"assignees\\\":[\\\"$FIRST_COMMITTER\\\"]}\"\nfi\n" - , runIf = Just "github.ref == 'refs/heads/master'" + , runIf = Just (RunIfString "github.ref == 'refs/heads/master'") , shell = Nothing , stepId = Nothing , timeoutMinutes = Nothing @@ -665,7 +710,7 @@ Workflow , run = Just "set -e\n# XXX: Gitapult cannot distinguish datasets, so we push all data to\n# both endpoints and allow the server to decide what to do.\n# Kafka Webhooks accepts all datasets at once\n./gitapult run \\\n --git_root \"$GITHUB_WORKSPACE\" \\\n --authorization_token \"$KW_PRODUCTION_API_KEY\" \\\n --changeset_url \"$KW_PRODUCTION_BASE_URL/gitapult/configuration/records\" \\\n --revision_url \"$KW_PRODUCTION_BASE_URL/gitapult/configuration/revision\" \\\n --api_key \\\n --compress \\\n --max-payload-bytes 3000000\n\n# XXX: V3 doesn't care about rendering data, but it is too large to send.\n# As at 2024-02-14 gitapult does not support selecting which data to send.\ngit -C \"$GIT_REPOSITORY_PATH\" rm feeds/rendering_data/.gitapult.json\n./gitapult run \\\n --git_root \"$GITHUB_WORKSPACE\" \\\n --authorization_token \"$SF_PRODUCTION_API_KEY\" \\\n --changeset_url \"$SF_PRODUCTION_BASE_URL/api/v1/configuration/shipping\" \\\n --revision_url \"$SF_PRODUCTION_BASE_URL/api/v1/configuration/shipping/revisions\" && \\\ngit -C \"$GIT_REPOSITORY_PATH\" restore --staged feeds/rendering_data/.gitapult.json\ngit -C \"$GIT_REPOSITORY_PATH\" restore feeds/rendering_data/.gitapult.json\n" - , runIf = Just "github.ref == 'refs/heads/stable'" + , runIf = Just (RunIfString "github.ref == 'refs/heads/stable'") , shell = Nothing , stepId = Nothing , timeoutMinutes = Nothing @@ -680,7 +725,8 @@ Workflow , run = Just "git tag \"${{ join(steps.current-time.outputs.*, '\\n') }}_production\" && git push --tags\n" - , runIf = Just "success() && github.ref == 'refs/heads/stable'" + , runIf = + Just (RunIfString "success() && github.ref == 'refs/heads/stable'") , shell = Nothing , stepId = Nothing , timeoutMinutes = Nothing @@ -695,7 +741,7 @@ Workflow , run = Just "echo \"SUCCESS_MESSAGE=:tada: \\nA PR has been opened against the stable branch: ${{ env.PR_URL }}\" >> $GITHUB_ENV\necho \"SUCCESS_MESSAGE_BODY=https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks\\n\\n*PR for Production:* ${{ env.PR_URL }}\" >> $GITHUB_ENV\n" - , runIf = Just "github.ref == 'refs/heads/master'" + , runIf = Just (RunIfString "github.ref == 'refs/heads/master'") , shell = Nothing , stepId = Nothing , timeoutMinutes = Nothing @@ -710,7 +756,7 @@ Workflow , run = Just "echo \"SUCCESS_MESSAGE=:tada:\" >> $GITHUB_ENV\necho \"SUCCESS_MESSAGE_BODY=https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks\" >> $GITHUB_ENV\n" - , runIf = Just "github.ref == 'refs/heads/stable'" + , runIf = Just (RunIfString "github.ref == 'refs/heads/stable'") , shell = Nothing , stepId = Nothing , timeoutMinutes = Nothing @@ -727,7 +773,8 @@ Workflow , run = Nothing , runIf = Just - "success() && (github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/master')" + (RunIfString + "success() && (github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/master')") , shell = Nothing , stepId = Just (StepId "slack-deploy-success") , timeoutMinutes = Nothing @@ -737,13 +784,20 @@ Workflow , with = Just (StepWithEnv - (fromList - [ ( "channel-id" , "${{ env.SLACK_CHANNEL_ID }}" ) - , ( "payload" - , "{\n \"text\": \":loudspeaker: *${{ github.actor }}* has deployed *${{ env.SYSTEM_NAME }}* to *${{ env.DESTINATION }}* from *${{ steps.extract_branch.outputs.branch }}* ${{ env.SUCCESS_MESSAGE }}\",\n \"attachments\": [\n {\n \"fallback\": \"Deployment summary\",\n \"color\": \"00ff00\",\n \"fields\": [\n {\n \"title\": \"What was deployed\",\n \"value\": ${{ toJSON(env.COMMIT_CHANGES) }},\n \"short\": false\n },\n {\n \"title\": \"Status\",\n \"short\": true,\n \"value\": \"Deployed\"\n },\n {\n \"title\": \"Output\",\n \"short\": false,\n \"value\": \"${{ env.SUCCESS_MESSAGE_BODY }}\"\n }\n ]\n }\n ]\n}\n" - ) - , ( "update-ts" , "${{ steps.slack-deploy.outputs.ts }}" ) - ])) + (UnstructuredMap + (fromList + [ ( "channel-id" + , UnstructuredValueString "${{ env.SLACK_CHANNEL_ID }}" + ) + , ( "payload" + , UnstructuredValueString + "{\n \"text\": \":loudspeaker: *${{ github.actor }}* has deployed *${{ env.SYSTEM_NAME }}* to *${{ env.DESTINATION }}* from *${{ steps.extract_branch.outputs.branch }}* ${{ env.SUCCESS_MESSAGE }}\",\n \"attachments\": [\n {\n \"fallback\": \"Deployment summary\",\n \"color\": \"00ff00\",\n \"fields\": [\n {\n \"title\": \"What was deployed\",\n \"value\": ${{ toJSON(env.COMMIT_CHANGES) }},\n \"short\": false\n },\n {\n \"title\": \"Status\",\n \"short\": true,\n \"value\": \"Deployed\"\n },\n {\n \"title\": \"Output\",\n \"short\": false,\n \"value\": \"${{ env.SUCCESS_MESSAGE_BODY }}\"\n }\n ]\n }\n ]\n}\n" + ) + , ( "update-ts" + , UnstructuredValueString + "${{ steps.slack-deploy.outputs.ts }}" + ) + ]))) , workingDirectory = Nothing } , Step @@ -755,7 +809,8 @@ Workflow , run = Nothing , runIf = Just - "failure() && (github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/master')" + (RunIfString + "failure() && (github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/master')") , shell = Nothing , stepId = Just (StepId "slack-deploy-failed") , timeoutMinutes = Nothing @@ -765,13 +820,20 @@ Workflow , with = Just (StepWithEnv - (fromList - [ ( "channel-id" , "${{ env.SLACK_CHANNEL_ID }}" ) - , ( "payload" - , "{ \"text\": \":loudspeaker: *BUILD/DEPLOY FAILURE* of *${{ env.SYSTEM_NAME }}* to *${{ env.DESTINATION }}*\",\n \"attachments\": [\n {\n \"fallback\": \"Deployment summary\",\n \"color\": \"ff0000\",\n \"fields\": [\n {\n \"title\": \"What was being deployed\",\n \"value\": ${{ toJSON(env.COMMIT_CHANGES) }},\n \"short\": false\n },\n {\n \"title\": \"Status\",\n \"short\": true,\n \"value\": \"Failed\"\n },\n {\n \"title\": \"Who broke it\",\n \"value\": \"${{ github.actor }}\"\n },\n {\n \"title\": \"Output\",\n \"short\": false,\n \"value\": \"https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks\"\n }\n ]\n }\n ]\n}\n" - ) - , ( "update-ts" , "${{ steps.slack-deploy.outputs.ts }}" ) - ])) + (UnstructuredMap + (fromList + [ ( "channel-id" + , UnstructuredValueString "${{ env.SLACK_CHANNEL_ID }}" + ) + , ( "payload" + , UnstructuredValueString + "{ \"text\": \":loudspeaker: *BUILD/DEPLOY FAILURE* of *${{ env.SYSTEM_NAME }}* to *${{ env.DESTINATION }}*\",\n \"attachments\": [\n {\n \"fallback\": \"Deployment summary\",\n \"color\": \"ff0000\",\n \"fields\": [\n {\n \"title\": \"What was being deployed\",\n \"value\": ${{ toJSON(env.COMMIT_CHANGES) }},\n \"short\": false\n },\n {\n \"title\": \"Status\",\n \"short\": true,\n \"value\": \"Failed\"\n },\n {\n \"title\": \"Who broke it\",\n \"value\": \"${{ github.actor }}\"\n },\n {\n \"title\": \"Output\",\n \"short\": false,\n \"value\": \"https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks\"\n }\n ]\n }\n ]\n}\n" + ) + , ( "update-ts" + , UnstructuredValueString + "${{ steps.slack-deploy.outputs.ts }}" + ) + ]))) , workingDirectory = Nothing } , Step @@ -781,7 +843,7 @@ Workflow , run = Just "gh pr comment ${{ env.PR_NUMBER }} --body-file /tmp/pr-comment.txt || true" - , runIf = Just "always() && env.PR_NUMBER != 0" + , runIf = Just (RunIfString "always() && env.PR_NUMBER != 0") , shell = Nothing , stepId = Nothing , timeoutMinutes = Nothing @@ -827,11 +889,17 @@ Workflow , with = Just (StepWithEnv - (fromList - [ ( "fetch-depth" , "1" ) - , ( "path" , "${{ env.GITHUB_WORKSPACE }}" ) - , ( "token" , "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}" ) - ])) + (UnstructuredMap + (fromList + [ ( "fetch-depth" , UnstructuredValueString "1" ) + , ( "path" + , UnstructuredValueString "${{ env.GITHUB_WORKSPACE }}" + ) + , ( "token" + , UnstructuredValueString + "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}" + ) + ]))) , workingDirectory = Nothing } :| [ Step @@ -861,7 +929,12 @@ Workflow , with = Just (StepWithEnv - (fromList [ ( "bellroy-nix-cache-access" , "none" ) ])) + (UnstructuredMap + (fromList + [ ( "bellroy-nix-cache-access" + , UnstructuredValueString "none" + ) + ]))) , workingDirectory = Nothing } , Step @@ -877,11 +950,15 @@ Workflow , with = Just (StepWithEnv - (fromList - [ ( "token" , "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}" ) - , ( "tool-name" , "gitapult" ) - , ( "tool-version" , "latest" ) - ])) + (UnstructuredMap + (fromList + [ ( "token" + , UnstructuredValueString + "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}" + ) + , ( "tool-name" , UnstructuredValueString "gitapult" ) + , ( "tool-version" , UnstructuredValueString "latest" ) + ]))) , workingDirectory = Nothing } , Step @@ -897,11 +974,15 @@ Workflow , with = Just (StepWithEnv - (fromList - [ ( "token" , "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}" ) - , ( "tool-name" , "powerful-owl" ) - , ( "tool-version" , "latest" ) - ])) + (UnstructuredMap + (fromList + [ ( "token" + , UnstructuredValueString + "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}" + ) + , ( "tool-name" , UnstructuredValueString "powerful-owl" ) + , ( "tool-version" , UnstructuredValueString "latest" ) + ]))) , workingDirectory = Nothing } , Step @@ -968,7 +1049,7 @@ Workflow , run = Just "gh pr comment ${{ env.PR_NUMBER }} --body-file /tmp/pr-comment.txt || true" - , runIf = Just "always() && env.PR_NUMBER != 0" + , runIf = Just (RunIfString "always() && env.PR_NUMBER != 0") , shell = Nothing , stepId = Nothing , timeoutMinutes = Nothing @@ -983,7 +1064,8 @@ Workflow , run = Nothing , runIf = Just - "failure() && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/stable')" + (RunIfString + "failure() && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/stable')") , shell = Nothing , stepId = Nothing , timeoutMinutes = Nothing @@ -991,17 +1073,24 @@ Workflow , with = Just (StepWithEnv - (fromList - [ ( "branch" - , "${{ steps.checkout_branch.outputs.branch }}" - ) - , ( "channel-id" , "${{ env.SLACK_CHANNEL_ID }}" ) - , ( "message-type" , "build-fail" ) - , ( "show-authors" , "true" ) - , ( "slack-bot-token" - , "${{ secrets.BELLROY_SLACK_TOKEN }}" - ) - ])) + (UnstructuredMap + (fromList + [ ( "branch" + , UnstructuredValueString + "${{ steps.checkout_branch.outputs.branch }}" + ) + , ( "channel-id" + , UnstructuredValueString "${{ env.SLACK_CHANNEL_ID }}" + ) + , ( "message-type" + , UnstructuredValueString "build-fail" + ) + , ( "show-authors" , UnstructuredValueString "true" ) + , ( "slack-bot-token" + , UnstructuredValueString + "${{ secrets.BELLROY_SLACK_TOKEN }}" + ) + ]))) , workingDirectory = Nothing } ]) diff --git a/test/golden/issue-8-boolean-if.golden.yml b/test/golden/issue-8-boolean-if.golden.yml new file mode 100644 index 0000000..0b9ee6e --- /dev/null +++ b/test/golden/issue-8-boolean-if.golden.yml @@ -0,0 +1,13 @@ +jobs: + test: + runs-on: ubuntu-latest + steps: + - if: false + name: Test step + run: echo "test" +name: Test Boolean If +'on': + push: + branches-ignore: + - refs/tags/*_staging + - refs/tags/*_production diff --git a/test/golden/issue-8-boolean-if.hs.txt b/test/golden/issue-8-boolean-if.hs.txt new file mode 100644 index 0000000..40097cb --- /dev/null +++ b/test/golden/issue-8-boolean-if.hs.txt @@ -0,0 +1,61 @@ +Workflow + { concurrency = Nothing + , defaults = Nothing + , env = fromList [] + , jobs = + fromList + [ ( JobId "test" + , 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 "ubuntu-latest" + , secrets = fromList [] + , services = fromList [] + , steps = + Just + (Step + { continueOnError = False + , env = fromList [] + , name = Just "Test step" + , run = Just "echo \"test\"" + , runIf = Just (RunIfBool False) + , 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 Boolean If" + } \ No newline at end of file diff --git a/test/golden/issue-8-boolean-if.yml b/test/golden/issue-8-boolean-if.yml new file mode 100644 index 0000000..1b01c03 --- /dev/null +++ b/test/golden/issue-8-boolean-if.yml @@ -0,0 +1,14 @@ +name: Test Boolean If +on: + push: + branches-ignore: + - "refs/tags/*_staging" + - "refs/tags/*_production" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Test step + if: false + run: echo "test" diff --git a/test/golden/issue-8-numeric-retention.golden.yml b/test/golden/issue-8-numeric-retention.golden.yml new file mode 100644 index 0000000..dc4bb8e --- /dev/null +++ b/test/golden/issue-8-numeric-retention.golden.yml @@ -0,0 +1,16 @@ +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Upload artifact + uses: actions/upload-artifact@v4.2.0 + with: + name: test-artifact + path: ./test + retention-days: 1 +name: Test Numeric Retention +'on': + push: + branches-ignore: + - refs/tags/*_staging + - refs/tags/*_production diff --git a/test/golden/issue-8-numeric-retention.hs.txt b/test/golden/issue-8-numeric-retention.hs.txt new file mode 100644 index 0000000..84287d7 --- /dev/null +++ b/test/golden/issue-8-numeric-retention.hs.txt @@ -0,0 +1,69 @@ +Workflow + { concurrency = Nothing + , defaults = Nothing + , env = fromList [] + , jobs = + fromList + [ ( JobId "test" + , 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 "ubuntu-latest" + , secrets = fromList [] + , services = fromList [] + , steps = + Just + (Step + { continueOnError = False + , env = fromList [] + , name = Just "Upload artifact" + , run = Nothing + , runIf = Nothing + , shell = Nothing + , stepId = Nothing + , timeoutMinutes = Nothing + , uses = Just "actions/upload-artifact@v4.2.0" + , with = + Just + (StepWithEnv + (UnstructuredMap + (fromList + [ ( "name" , UnstructuredValueString "test-artifact" ) + , ( "path" , UnstructuredValueString "./test" ) + , ( "retention-days" , UnstructuredValueNumber 1.0 ) + ]))) + , 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 Numeric Retention" + } \ No newline at end of file diff --git a/test/golden/issue-8-numeric-retention.yml b/test/golden/issue-8-numeric-retention.yml new file mode 100644 index 0000000..c49aa6d --- /dev/null +++ b/test/golden/issue-8-numeric-retention.yml @@ -0,0 +1,17 @@ +name: Test Numeric Retention +on: + push: + branches-ignore: + - "refs/tags/*_staging" + - "refs/tags/*_production" + +jobs: + test: + runs-on: ubuntu-latest + steps: + - name: Upload artifact + uses: actions/upload-artifact@v4.2.0 + with: + name: test-artifact + path: ./test + retention-days: 1 diff --git a/test/golden/issue-8-string-needs.golden.yml b/test/golden/issue-8-string-needs.golden.yml new file mode 100644 index 0000000..13949c5 --- /dev/null +++ b/test/golden/issue-8-string-needs.golden.yml @@ -0,0 +1,16 @@ +jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo "build" + test: + needs: build + runs-on: ubuntu-latest + steps: + - run: echo "test" +name: Test String Needs +'on': + push: + branches-ignore: + - refs/tags/*_staging + - refs/tags/*_production diff --git a/test/golden/issue-8-string-needs.hs.txt b/test/golden/issue-8-string-needs.hs.txt new file mode 100644 index 0000000..73e5bb4 --- /dev/null +++ b/test/golden/issue-8-string-needs.hs.txt @@ -0,0 +1,99 @@ +Workflow + { concurrency = Nothing + , defaults = Nothing + , env = fromList [] + , jobs = + fromList + [ ( JobId "build" + , 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 "ubuntu-latest" + , secrets = fromList [] + , services = fromList [] + , steps = + Just + (Step + { continueOnError = False + , env = fromList [] + , name = Nothing + , run = Just "echo \"build\"" + , runIf = Nothing + , shell = Nothing + , stepId = Nothing + , timeoutMinutes = Nothing + , uses = Nothing + , with = Nothing + , workingDirectory = Nothing + } :| + []) + , strategy = Nothing + , timeoutMinutes = Nothing + , uses = Nothing + , with = fromList [] + } + ) + , ( JobId "test" + , Job + { concurrency = Nothing + , container = Nothing + , continueOnError = Nothing + , defaults = Nothing + , env = fromList [] + , environment = Nothing + , jobName = Nothing + , needs = Just (JobNeedsString (JobId "build")) + , outputs = fromList [] + , permissions = Nothing + , runIf = Nothing + , runsOn = Just "ubuntu-latest" + , secrets = fromList [] + , services = fromList [] + , steps = + Just + (Step + { continueOnError = False + , env = fromList [] + , name = Nothing + , run = Just "echo \"test\"" + , 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 String Needs" + } \ No newline at end of file diff --git a/test/golden/issue-8-string-needs.yml b/test/golden/issue-8-string-needs.yml new file mode 100644 index 0000000..c7dd5e8 --- /dev/null +++ b/test/golden/issue-8-string-needs.yml @@ -0,0 +1,17 @@ +name: Test String Needs +on: + push: + branches-ignore: + - "refs/tags/*_staging" + - "refs/tags/*_production" + +jobs: + build: + runs-on: ubuntu-latest + steps: + - run: echo "build" + test: + needs: build + runs-on: ubuntu-latest + steps: + - run: echo "test" From 15432ad2b7fd813ebf98df954f110fbc1893dc28 Mon Sep 17 00:00:00 2001 From: Michael Webb Date: Mon, 21 Jul 2025 12:37:56 +1000 Subject: [PATCH 2/6] Rename module --- github-actions.cabal | 2 +- src/Language/Github/Actions/Job.hs | 4 ++-- src/Language/Github/Actions/{JobNeeds.hs => Job/Needs.hs} | 4 ++-- src/Language/Github/Actions/UnstructuredMap.hs | 1 - 4 files changed, 5 insertions(+), 6 deletions(-) rename src/Language/Github/Actions/{JobNeeds.hs => Job/Needs.hs} (96%) diff --git a/github-actions.cabal b/github-actions.cabal index e9d0f22..1bebe54 100644 --- a/github-actions.cabal +++ b/github-actions.cabal @@ -59,8 +59,8 @@ library Language.Github.Actions.Job.Container Language.Github.Actions.Job.Environment Language.Github.Actions.Job.Id + Language.Github.Actions.Job.Needs Language.Github.Actions.Job.Strategy - Language.Github.Actions.JobNeeds Language.Github.Actions.Permissions Language.Github.Actions.RunIf Language.Github.Actions.Service diff --git a/src/Language/Github/Actions/Job.hs b/src/Language/Github/Actions/Job.hs index 5ecb14f..9960981 100644 --- a/src/Language/Github/Actions/Job.hs +++ b/src/Language/Github/Actions/Job.hs @@ -43,10 +43,10 @@ import Language.Github.Actions.Job.Container (JobContainer) import qualified Language.Github.Actions.Job.Container as JobContainer 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.Strategy (JobStrategy) import qualified Language.Github.Actions.Job.Strategy as JobStrategy -import Language.Github.Actions.JobNeeds (JobNeeds) -import qualified Language.Github.Actions.JobNeeds as JobNeeds import Language.Github.Actions.Permissions (Permissions) import qualified Language.Github.Actions.Permissions as Permissions import Language.Github.Actions.RunIf (RunIf) diff --git a/src/Language/Github/Actions/JobNeeds.hs b/src/Language/Github/Actions/Job/Needs.hs similarity index 96% rename from src/Language/Github/Actions/JobNeeds.hs rename to src/Language/Github/Actions/Job/Needs.hs index 31227dc..685016d 100644 --- a/src/Language/Github/Actions/JobNeeds.hs +++ b/src/Language/Github/Actions/Job/Needs.hs @@ -2,7 +2,7 @@ {-# LANGUAGE DerivingStrategies #-} -- | --- Module : Language.Github.Actions.JobNeeds +-- Module : Language.Github.Actions.Job.Needs -- Description : Job dependency specification for GitHub Actions -- Copyright : (c) 2025 Bellroy Pty Ltd -- License : BSD-3-Clause @@ -18,7 +18,7 @@ -- -- For more information about GitHub Actions job dependencies, see: -- -module Language.Github.Actions.JobNeeds +module Language.Github.Actions.Job.Needs ( JobNeeds (..), gen, ) diff --git a/src/Language/Github/Actions/UnstructuredMap.hs b/src/Language/Github/Actions/UnstructuredMap.hs index ec6c0dd..38de141 100644 --- a/src/Language/Github/Actions/UnstructuredMap.hs +++ b/src/Language/Github/Actions/UnstructuredMap.hs @@ -35,7 +35,6 @@ where import Data.Aeson (FromJSON, ToJSON (..), Value (..)) import qualified Data.Aeson as Aeson import Data.Map (Map) -import qualified Data.Map as Map import Data.Text (Text) import qualified Data.Text as Text import GHC.Generics (Generic) From 32dca66a9145fe917671a7f755acff0f0491601d Mon Sep 17 00:00:00 2001 From: Michael Webb Date: Mon, 21 Jul 2025 12:40:09 +1000 Subject: [PATCH 3/6] Update docs in Job.Needs --- src/Language/Github/Actions/Job/Needs.hs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Language/Github/Actions/Job/Needs.hs b/src/Language/Github/Actions/Job/Needs.hs index 685016d..3682dd5 100644 --- a/src/Language/Github/Actions/Job/Needs.hs +++ b/src/Language/Github/Actions/Job/Needs.hs @@ -9,12 +9,13 @@ -- Maintainer : Bellroy Tech Team -- -- This module provides the 'JobNeeds' type for representing job dependencies --- in GitHub Actions workflows. GitHub Actions allows both single job and --- multiple job dependencies for the 'needs' field. +-- in GitHub Actions workflows. GitHub Actions allows both strings and +-- lists of strings for the 'needs' field. -- -- Examples of valid 'needs' specifications: --- * @needs: build@ - Single job dependency --- * @needs: [build, test]@ - Multiple job dependencies +-- * @needs: build@ - Single job specified as a string +-- * @needs: [build]@ - Single job specified as a list of strings +-- * @needs: [build, test]@ - Multiple job dependencies specified as a list of strings -- -- For more information about GitHub Actions job dependencies, see: -- @@ -57,10 +58,8 @@ import qualified Language.Github.Actions.Job.Id as JobId -- A string input will serialize back to a string, and an array input will -- serialize back to an array, preventing information loss. data JobNeeds - = -- | Single job dependency as string (e.g., @needs: build@) - JobNeedsString JobId - | -- | Multiple job dependencies as array (e.g., @needs: [build, test]@) - JobNeedsArray (NonEmpty JobId) + = JobNeedsString JobId + | JobNeedsArray (NonEmpty JobId) deriving stock (Eq, Generic, Ord, Show) instance FromJSON JobNeeds where @@ -71,7 +70,6 @@ instance ToJSON JobNeeds where toJSON (JobNeedsString jobId) = toJSON jobId toJSON (JobNeedsArray jobIds) = toJSON jobIds --- | Generate random 'JobNeeds' values for property testing. gen :: (MonadGen m) => m JobNeeds gen = Gen.choice From 66044bf85103d66b5a0c9ad337d2a46dd4818480 Mon Sep 17 00:00:00 2001 From: Michael Webb Date: Mon, 21 Jul 2025 12:41:27 +1000 Subject: [PATCH 4/6] Update docs in RunIf --- src/Language/Github/Actions/RunIf.hs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/Language/Github/Actions/RunIf.hs b/src/Language/Github/Actions/RunIf.hs index 3119ff6..3486c9d 100644 --- a/src/Language/Github/Actions/RunIf.hs +++ b/src/Language/Github/Actions/RunIf.hs @@ -14,9 +14,8 @@ -- expressions in 'if' conditions for jobs and steps. -- -- Examples of valid 'if' conditions: --- * @if: false@ - Simple boolean --- * @if: "github.ref == 'refs/heads/main'"@ - GitHub expression --- * @if: "\${{ success() && matrix.os == 'ubuntu-latest' }}"@ - Complex expression +-- * @if: false@ - Boolean +-- * @if: "github.ref == 'refs/heads/main'"@ - String -- -- For more information about GitHub Actions conditional expressions, see: -- @@ -51,19 +50,13 @@ import qualified Hedgehog.Range as Range -- -- GitHub expression condition -- branchCheck :: RunIf -- branchCheck = RunIfString "github.ref == 'refs/heads/main'" --- --- -- Complex expression with functions --- complexCheck :: RunIf --- complexCheck = RunIfString "\${{ success() && matrix.os == 'ubuntu-latest' }}" -- @ -- -- The type preserves the original format during round-trip serialization, -- so a boolean input remains a boolean in the output YAML. data RunIf - = -- | Boolean condition (e.g., @false@, @true@) - RunIfBool Bool - | -- | String expression (e.g., @"github.ref == 'refs/heads/main'"@) - RunIfString Text + = RunIfBool Bool + | RunIfString Text deriving stock (Eq, Generic, Ord, Show) instance FromJSON RunIf where @@ -75,7 +68,6 @@ instance ToJSON RunIf where toJSON (RunIfBool b) = Bool b toJSON (RunIfString s) = String s --- | Generate random 'RunIf' values for property testing. gen :: (MonadGen m) => m RunIf gen = Gen.choice From 53f6b8ba152c7cc1515ef43e4b364c120c791d3b Mon Sep 17 00:00:00 2001 From: Michael Webb Date: Mon, 21 Jul 2025 12:43:32 +1000 Subject: [PATCH 5/6] Update docs in UnstructuredMap --- .../Github/Actions/UnstructuredMap.hs | 50 ++----------------- 1 file changed, 5 insertions(+), 45 deletions(-) diff --git a/src/Language/Github/Actions/UnstructuredMap.hs b/src/Language/Github/Actions/UnstructuredMap.hs index 38de141..209b979 100644 --- a/src/Language/Github/Actions/UnstructuredMap.hs +++ b/src/Language/Github/Actions/UnstructuredMap.hs @@ -42,37 +42,17 @@ import Hedgehog (MonadGen) import qualified Hedgehog.Gen as Gen import qualified Hedgehog.Range as Range --- | A flexible value that can be a string, number, or boolean. +-- | A map that can have values of string, number, or boolean. -- -- This type is designed to handle the flexible typing that GitHub Actions --- allows in YAML files, particularly in contexts where we would normally --- parse @Map Text Text@ but need to support non-string values. --- --- Examples: --- --- @ --- -- String value (most common) --- pathValue :: UnstructuredValue --- pathValue = UnstructuredValueString "./dist" --- --- -- Numeric value (preserves original type) --- retentionValue :: UnstructuredValue --- retentionValue = UnstructuredValueNumber 7 --- --- -- Boolean value --- flagValue :: UnstructuredValue --- flagValue = UnstructuredValueBool False --- @ +-- allows in YAML files. -- -- The type preserves the original format during round-trip serialization, -- so numeric inputs remain numeric in the output YAML. data UnstructuredValue - = -- | String value (e.g., @"./dist"@, @"ubuntu-latest"@) - UnstructuredValueString Text - | -- | Numeric value (e.g., @1@, @30@, @1.5@) - UnstructuredValueNumber Double - | -- | Boolean value (e.g., @true@, @false@) - UnstructuredValueBool Bool + = UnstructuredValueString Text + | UnstructuredValueNumber Double + | UnstructuredValueBool Bool deriving stock (Eq, Generic, Ord, Show) instance FromJSON UnstructuredValue where @@ -86,19 +66,6 @@ instance ToJSON UnstructuredValue where toJSON (UnstructuredValueNumber n) = Number (fromRational (toRational n)) toJSON (UnstructuredValueBool b) = Bool b --- | Render an UnstructuredValue as Text. --- --- This function is essential for backwards compatibility with existing code --- that expects @Map Text Text@ values. It allows gradual migration from --- @Text@ to @UnstructuredValue@ while maintaining the same interface. --- --- Examples: --- --- @ --- renderUnstructuredValue (UnstructuredValueString "hello") == "hello" --- renderUnstructuredValue (UnstructuredValueNumber 42.0) == "42.0" --- renderUnstructuredValue (UnstructuredValueBool True) == "true" --- @ renderUnstructuredValue :: UnstructuredValue -> Text renderUnstructuredValue (UnstructuredValueString s) = s renderUnstructuredValue (UnstructuredValueNumber n) = @@ -108,16 +75,10 @@ renderUnstructuredValue (UnstructuredValueNumber n) = else Text.pack (show n) renderUnstructuredValue (UnstructuredValueBool b) = if b then "true" else "false" --- | A map of unstructured values, commonly used for flexible YAML parsing. --- --- This newtype wraps @Map Text UnstructuredValue@ and provides appropriate --- JSON instances for parsing GitHub Actions YAML that allows mixed types --- in key-value mappings. newtype UnstructuredMap = UnstructuredMap (Map Text UnstructuredValue) deriving stock (Eq, Generic, Ord, Show) deriving newtype (FromJSON, ToJSON) --- | Generate random 'UnstructuredValue' values for property testing. genUnstructuredValue :: (MonadGen m) => m UnstructuredValue genUnstructuredValue = Gen.choice @@ -126,7 +87,6 @@ genUnstructuredValue = UnstructuredValueBool <$> Gen.bool ] --- | Generate random 'UnstructuredMap' values for property testing. gen :: (MonadGen m) => m UnstructuredMap gen = UnstructuredMap <$> Gen.map (Range.linear 0 10) genKeyValue where From e5684d3d1f5635f427293071f0bf4335a25a03c1 Mon Sep 17 00:00:00 2001 From: Michael Webb Date: Mon, 21 Jul 2025 12:48:53 +1000 Subject: [PATCH 6/6] Update docs in UnstructuredMap --- src/Language/Github/Actions/UnstructuredMap.hs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Language/Github/Actions/UnstructuredMap.hs b/src/Language/Github/Actions/UnstructuredMap.hs index 209b979..5abb4ab 100644 --- a/src/Language/Github/Actions/UnstructuredMap.hs +++ b/src/Language/Github/Actions/UnstructuredMap.hs @@ -10,17 +10,13 @@ -- License : BSD-3-Clause -- Maintainer : Bellroy Tech Team -- --- This module provides the 'UnstructuredValue' type for representing values --- that can be strings, numbers, or booleans in GitHub Actions YAML files. --- This is commonly needed when parsing @Map Text Text@ fields that GitHub --- Actions allows to have flexible typing. +-- This module provides the 'UnstructuredMap' type for representing a map of +-- values that can be strings, numbers, or booleans in GitHub Actions YAML files. -- -- GitHub Actions allows flexible typing in many contexts: -- * @retention-days: 1@ (number) -- * @retention-days: "1"@ (string) --- * @if: false@ (boolean) --- * @timeout-minutes: 30@ (number) --- * @working-directory: "./src"@ (string) +-- * @should-retain: false@ (boolean) -- -- This type preserves the original YAML type during round-trip parsing, -- ensuring that numeric values remain numeric and strings remain strings.