packages feed

github-actions (empty) → 0.1.0.0

raw patch · 26 files changed

+5937/−0 lines, 26 filesdep +aesondep +basedep +bytestringsetup-changed

Dependencies added: aeson, base, bytestring, containers, filepath, github-actions, hedgehog, hoist-error, pretty-show, string-interpolate, tasty, tasty-discover, tasty-golden, tasty-golden-extra, tasty-hedgehog, tasty-hunit, text, vector, yaml

Files

+ LICENSE view
@@ -0,0 +1,29 @@+Copyright (C) 2024 Bellroy Pty Ltd++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are+met:++1. Redistributions of source code must retain the above copyright+   notice, this list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright+   notice, this list of conditions and the following disclaimer in the+   documentation and/or other materials provided with the+   distribution.++3. Neither the name of the copyright holder nor the names of its+   contributors may be used to endorse or promote products derived+   from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,97 @@+# Github Actions++[![Haskell-CI](https://github.com/bellroy/github-actions/actions/workflows/haskell-ci.yml/badge.svg)](https://github.com/bellroy/github-actions/actions/workflows/haskell-ci.yml)++This library provides types and instances for serializing and deserializing+GitHub Actions YAML, so that workflows can be built and maintained in Haskell.++As specified here:+https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions++## Usage Examples++### 1. Exporting a Workflow to YAML++You can create a workflow in Haskell and export it to YAML format:++```haskell+{-# LANGUAGE OverloadedStrings #-}++import qualified Data.Yaml as Yaml+import qualified Data.Map as Map+import qualified Data.Set as Set+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.Step as Step+import qualified Language.Github.Actions.Workflow.Trigger as Trigger++-- Create a simple CI workflow+myWorkflow :: Workflow+myWorkflow = new+  { workflowName = Just "CI"+  , on = Set.singleton (Trigger.PushTrigger Trigger.pushTriggerDefaults)+  , jobs = Map.singleton (JobId.JobId "build") buildJob+  }++buildJob :: Job.Job+buildJob = Job.new+  { Job.jobName = Just "Build and Test"+  , Job.runsOn = Just "ubuntu-latest"+  , Job.steps = Just $ checkoutStep :| [buildStep, testStep]+  }++checkoutStep :: Step.Step+checkoutStep = Step.new+  { Step.name = Just "Checkout repository"+  , Step.uses = Just "actions/checkout@v4"+  }++buildStep :: Step.Step+buildStep = Step.new+  { Step.name = Just "Build project"+  , Step.run = Just "npm install && npm run build"+  }++testStep :: Step.Step+testStep = Step.new+  { Step.name = Just "Run tests"+  , Step.run = Just "npm test"+  }++-- Export to YAML+exportWorkflow :: IO ()+exportWorkflow = Yaml.encodeFile "workflow.yml" myWorkflow+```++### 2. Importing a YAML file into a Workflow representation++You can load an existing GitHub Actions YAML file into a Haskell `Workflow` type:++```haskell+{-# LANGUAGE TypeApplications #-}++import qualified Data.Yaml as Yaml+import Language.Github.Actions.Workflow (Workflow)++-- Import from YAML file+importWorkflow :: FilePath -> IO (Either String Workflow)+importWorkflow yamlFilePath = do+  result <- Yaml.decodeFileEither @Workflow yamlFilePath+  case result of+    Left parseException ->+      return $ Left $ Yaml.prettyPrintParseException parseException+    Right workflow ->+      return $ Right workflow++-- Example usage+main :: IO ()+main = do+  result <- importWorkflow ".github/workflows/ci.yml"+  case result of+    Left errorMsg -> putStrLn $ "Failed to parse workflow: " ++ errorMsg+    Right workflow -> do+      putStrLn "Successfully parsed workflow!"+      print workflow+```
+ Setup.hs view
@@ -0,0 +1,3 @@+import Distribution.Simple++main = defaultMain
+ github-actions.cabal view
@@ -0,0 +1,93 @@+cabal-version:      2.2+name:               github-actions+version:            0.1.0.0+synopsis:           Github Actions+description:+  This library provides types and instances for serializing and deserializing+  Github Actions YAML, so that flows can be built and maintained in Haskell.++homepage:           http://github.com/bellroy/github-actions+bug-reports:        http://github.com/bellroy/github-actions/issues+license:            BSD-3-Clause+license-file:       LICENSE+author:             Bellroy Tech Team <haskell@bellroy.com>+maintainer:         Bellroy Tech Team <haskell@bellroy.com>+copyright:          Copyright (C) 2025 Bellroy Pty Ltd+category:           Language+build-type:         Simple+extra-source-files:+  README.md+  test/golden/configuration-main.golden.yml+  test/golden/configuration-main.hs.txt+  test/golden/configuration-main.yml++tested-with:        GHC ==9.6.6 || ==9.8.2 || ==9.10.1++common opts+  default-language: Haskell2010+  ghc-options:+    -Wall -Wcompat -Widentities -Wincomplete-record-updates+    -Wincomplete-uni-patterns -Werror=incomplete-patterns+    -Wredundant-constraints -Wpartial-fields -Wtabs+    -Wmissing-local-signatures -fhelpful-errors+    -fprint-expanded-synonyms -fwarn-unused-do-bind++common deps+  build-depends:+    , aeson               ^>=2.2.3.0+    , base                >=4.14      && <4.22+    , containers          ==0.6.7     || ==0.6.8 || ==0.7   || ==0.8+    , hedgehog            ^>=1.5+    , hoist-error         ^>=0.3+    , string-interpolate  ^>=0.3.3+    , text                ==1.2.4.1   || ==2.0.2 || ==2.1.1+    , vector              ^>=0.13.0.0++library+  import:          opts, deps+  hs-source-dirs:  src++  -- cabal-fmt: expand src/+  exposed-modules:+    Language.Github.Actions.Concurrency+    Language.Github.Actions.Defaults+    Language.Github.Actions.Internal+    Language.Github.Actions.Job+    Language.Github.Actions.Job.Container+    Language.Github.Actions.Job.Environment+    Language.Github.Actions.Job.Id+    Language.Github.Actions.Job.Strategy+    Language.Github.Actions.Permissions+    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.Workflow+    Language.Github.Actions.Workflow.Trigger++test-suite spec+  import:             deps, opts+  type:               exitcode-stdio-1.0+  hs-source-dirs:     test+  main-is:            Main.hs+  ghc-options:        -threaded+  other-modules:      Language.Github.Actions.WorkflowTest+  build-tool-depends: tasty-discover:tasty-discover ^>=4.2.2+  build-depends:+    , bytestring          ==0.11.5.3  || ==0.12.1.0+    , filepath            ^>=1.4      || ^>=1.5+    , github-actions+    , pretty-show         ^>=1.10+    , tasty               ^>=1.5+    , tasty-discover      ^>=5.0.0+    , tasty-golden        ^>=2.3.5+    , tasty-golden-extra  ^>=0.1.0+    , tasty-hedgehog      ^>=1.4.0.0+    , tasty-hunit         ^>=0.10.0.3+    , yaml                ^>=0.11.11++source-repository head+  type:     git+  location: https://github.com/bellroy/github-actions.git
+ src/Language/Github/Actions/Concurrency.hs view
@@ -0,0 +1,81 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++-- |+-- Module      : Language.Github.Actions.Concurrency+-- Description : GitHub Actions concurrency settings+-- Copyright   : (c) 2025 Bellroy Pty Ltd+-- License     : BSD-3-Clause+-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>+--+-- This module provides the 'Concurrency' type for controlling concurrent execution+-- of GitHub Actions workflows and jobs.+--+-- Concurrency settings allow you to control how many workflow runs or job executions+-- can happen simultaneously.+--+-- For more information about GitHub Actions concurrency, see:+-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#concurrency>+module Language.Github.Actions.Concurrency+  ( Concurrency (..),+    gen,+  )+where++import Data.Aeson (FromJSON, ToJSON (..), (.:?), (.=))+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++-- | Concurrency settings for workflows and jobs.+--+-- Concurrency allows you to control whether multiple workflow runs or job executions+-- can happen simultaneously. This is useful for preventing conflicts when deploying+-- or when you want to ensure only one workflow processes a particular resource at a time.+--+-- Example usage:+--+-- @+-- import Language.Github.Actions.Concurrency+--+-- -- Only allow one deployment per branch+-- deploymentConcurrency :: Concurrency+-- deploymentConcurrency = Concurrency+--  { group = Just "${{ github.ref }}"+--  , cancelInProgress = Just True+--  }+-- @+--+-- For more details, see: <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#concurrency>+data Concurrency = Concurrency+  { -- | Concurrency group identifier+    group :: Maybe Text,+    -- | Whether to cancel in-progress runs+    cancelInProgress :: Maybe Bool+  }+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON Concurrency where+  parseJSON = Aeson.withObject "Concurrency" $ \o -> do+    group <- o .:? "group"+    cancelInProgress <- o .:? "cancel-in-progress"+    pure Concurrency {..}++instance ToJSON Concurrency where+  toJSON Concurrency {..} =+    Aeson.object+      [ "group" .= group,+        "cancel-in-progress" .= cancelInProgress+      ]++gen :: (MonadGen m) => m Concurrency+gen = do+  group <- Gen.maybe (Gen.text (Range.linear 1 5) Gen.alphaNum)+  cancelInProgress <- Gen.maybe Gen.bool+  pure Concurrency {..}
+ src/Language/Github/Actions/Defaults.hs view
@@ -0,0 +1,97 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++-- |+-- Module      : Language.Github.Actions.Defaults+-- Description : Default settings for GitHub Actions workflows and jobs+-- Copyright   : (c) 2025 Bellroy Pty Ltd+-- License     : BSD-3-Clause+-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>+--+-- This module provides the 'Defaults' type for setting default values that apply+-- to all steps within a job or all jobs within a workflow.+--+-- Defaults allow you to specify common settings like shell type and working directory+-- that will be inherited by all steps unless explicitly overridden at the step level.+--+-- For more information about GitHub Actions defaults, see:+-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#defaults>+module Language.Github.Actions.Defaults+  ( Defaults (..),+    gen,+  )+where++import Data.Aeson (FromJSON, ToJSON, (.:), (.:?), (.=))+import qualified Data.Aeson as Aeson+import Data.Maybe (catMaybes)+import Data.Text (Text)+import GHC.Generics (Generic)+import Hedgehog (MonadGen)+import qualified Hedgehog.Gen as Gen+import qualified Hedgehog.Range as Range+import Language.Github.Actions.Shell (Shell)+import qualified Language.Github.Actions.Shell as Shell++-- | Default settings for steps within a job or jobs within a workflow.+--+-- Defaults provide a convenient way to set common configuration that applies to+-- both Jobs and Workflows without having to repeat the same settings everywhere.+--+--+-- Example usage:+--+-- @+-- import Language.Github.Actions.Defaults+-- import Language.Github.Actions.Shell+--+-- -- Set bash as default shell for all steps+-- bashDefaults :: Defaults+-- bashDefaults = Defaults+--  { runShell = Just (Bash Nothing)+--  , runWorkingDirectory = Nothing+--  }+--+-- -- Set working directory for all steps+-- workdirDefaults :: Defaults+-- workdirDefaults = Defaults+--  { runShell = Nothing+--  , runWorkingDirectory = Just "/src"+--  }+-- @+--+-- For more details, see: <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#defaults>+data Defaults = Defaults+  { -- | Default shell for run commands+    runShell :: Maybe Shell,+    -- | Default working directory for run commands+    runWorkingDirectory :: Maybe Text+  }+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON Defaults where+  parseJSON = Aeson.withObject "Defaults" $ \o -> do+    run <- o .: "run"+    runShell <- run .:? "shell"+    runWorkingDirectory <- run .:? "working-directory"+    pure Defaults {..}++instance ToJSON Defaults where+  toJSON Defaults {..} =+    Aeson.object+      [ "run"+          .= Aeson.object+            ( catMaybes+                [ ("shell" .=) <$> runShell,+                  ("working-directory" .=) <$> runWorkingDirectory+                ]+            )+      ]++gen :: (MonadGen m) => m Defaults+gen = do+  runShell <- Gen.maybe Shell.gen+  runWorkingDirectory <- Gen.maybe (Gen.text (Range.linear 1 5) Gen.alphaNum)+  pure Defaults {..}
+ src/Language/Github/Actions/Internal.hs view
@@ -0,0 +1,16 @@+{-# LANGUAGE ScopedTypeVariables #-}++module Language.Github.Actions.Internal (inverseMap) where++import qualified Data.Map as Map++-- | Based on: <https://hackage.haskell.org/package/relude-1.2.2.0/docs/src/Relude.Enum.html#inverseMap>+inverseMap ::+  forall a k.+  (Bounded a, Enum a, Ord k) =>+  (a -> k) ->+  (k -> Maybe a)+inverseMap f = (`Map.lookup` dict)+  where+    dict :: Map.Map k a+    dict = Map.fromList ((\a -> (f a, a)) <$> [minBound .. maxBound])
+ src/Language/Github/Actions/Job.hs view
@@ -0,0 +1,235 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++-- |+-- Module      : Language.Github.Actions.Job+-- Description : GitHub Actions job definition and serialization+-- Copyright   : (c) 2025 Bellroy Pty Ltd+-- License     : BSD-3-Clause+-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>+--+-- This module provides the 'Job' type for representing individual jobs within GitHub Actions workflows.+-- Jobs are collections of steps that execute on the same runner.+--+-- A job defines the environment, dependencies, and steps that should be executed as part of a workflow.+-- Jobs can run in parallel or be configured to depend on other jobs.+--+-- For more information about GitHub Actions job syntax, see:+-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobs>+module Language.Github.Actions.Job+  ( Job (..),+    gen,+    new,+  )+where++import Data.Aeson (FromJSON, ToJSON (..), (.!=), (.:?), (.=))+import qualified Data.Aeson as Aeson+import Data.List.NonEmpty (NonEmpty (..))+import Data.Map (Map)+import Data.Maybe (catMaybes)+import Data.Text (Text)+import GHC.Generics (Generic)+import Hedgehog (MonadGen)+import qualified Hedgehog.Gen as Gen+import qualified Hedgehog.Range as Range+import Language.Github.Actions.Concurrency (Concurrency)+import qualified Language.Github.Actions.Concurrency as Concurrency+import Language.Github.Actions.Defaults (Defaults)+import qualified Language.Github.Actions.Defaults as Defaults+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.Permissions (Permissions)+import qualified Language.Github.Actions.Permissions as Permissions+import Language.Github.Actions.Service (Service)+import qualified Language.Github.Actions.Service as Service+import Language.Github.Actions.Service.Id (ServiceId)+import qualified Language.Github.Actions.Service.Id as ServiceId+import Language.Github.Actions.Step (Step)+import qualified Language.Github.Actions.Step as Step++-- | A job within a GitHub Actions workflow.+--+-- A job is a set of steps that execute on the same runner. Jobs can run in parallel+-- or sequentially depending on their dependencies. Each job runs in its own virtual+-- environment specified by the runner.+--+-- Example usage:+--+-- @+-- import Language.Github.Actions.Job+-- import qualified Language.Github.Actions.Step as Step+--+-- myJob :: Job+-- myJob = new+--  { jobName = Just "Build and Test"+--  , runsOn = Just "ubuntu-latest"+--  , steps = Just $ Step.new :| []+--  }+-- @+--+-- For more details, see: <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobs>+data Job = Job+  { -- | Concurrency settings for this job+    concurrency :: Maybe Concurrency,+    -- | Container to run the job in+    container :: Maybe JobContainer,+    -- | Whether to continue on step failure+    continueOnError :: Maybe Bool,+    -- | Default settings for steps in this job+    defaults :: Maybe Defaults,+    -- | Environment variables for this job+    env :: Map Text Text,+    -- | Deployment environment settings+    environment :: Maybe JobEnvironment,+    -- | Display name for the job+    jobName :: Maybe Text,+    -- | Jobs this job depends on+    needs :: Maybe (NonEmpty JobId),+    -- | Outputs from this job+    outputs :: Map Text Text,+    -- | Permissions for this job+    permissions :: Maybe Permissions,+    -- | Condition for running this job+    runIf :: Maybe Text,+    -- | Runner type (e.g., "ubuntu-latest")+    runsOn :: Maybe Text,+    -- | Secrets available to this job+    secrets :: Map Text Text,+    -- | Services to run alongside this job+    services :: Map ServiceId Service,+    -- | Steps to execute in this job+    steps :: Maybe (NonEmpty Step),+    -- | Matrix strategy for this job+    strategy :: Maybe JobStrategy,+    -- | Timeout for the job in minutes+    timeoutMinutes :: Maybe Int,+    -- | Reusable workflow to call+    uses :: Maybe Text,+    -- | Inputs for reusable workflows+    with :: Map Text Text+  }+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON Job where+  parseJSON = Aeson.withObject "Job" $ \o -> do+    concurrency <- o .:? "concurrency"+    container <- o .:? "container"+    continueOnError <- o .:? "continue-on-error"+    defaults <- o .:? "defaults"+    env <- o .:? "env" .!= mempty+    environment <- o .:? "environment"+    jobName <- o .:? "name"+    needs <- o .:? "needs"+    outputs <- o .:? "outputs" .!= mempty+    permissions <- o .:? "permissions"+    runIf <- o .:? "if"+    runsOn <- o .:? "runs-on"+    secrets <- o .:? "secrets" .!= mempty+    services <- o .:? "services" .!= mempty+    steps <- o .:? "steps"+    strategy <- o .:? "strategy"+    timeoutMinutes <- o .:? "timeout-minutes"+    uses <- o .:? "uses"+    with <- o .:? "with" .!= mempty+    pure Job {..}++instance ToJSON Job where+  toJSON Job {..} =+    Aeson.object $+      catMaybes+        [ ("concurrency" .=) <$> concurrency,+          ("container" .=) <$> container,+          ("continue-on-error" .=) <$> continueOnError,+          ("defaults" .=) <$> defaults,+          ("env" .=) <$> monoidToMaybe env,+          ("environment" .=) <$> environment,+          ("if" .=) <$> runIf,+          ("name" .=) <$> jobName,+          ("needs" .=) <$> needs,+          ("outputs" .=) <$> monoidToMaybe outputs,+          ("permissions" .=) <$> permissions,+          ("runs-on" .=) <$> runsOn,+          ("secrets" .=) <$> monoidToMaybe secrets,+          ("services" .=) <$> monoidToMaybe services,+          ("steps" .=) <$> steps,+          ("strategy" .=) <$> strategy,+          ("timeout-minutes" .=) <$> timeoutMinutes,+          ("uses" .=) <$> uses,+          ("with" .=) <$> monoidToMaybe with+        ]+    where+      monoidToMaybe :: (Eq a, Monoid a) => a -> Maybe a+      monoidToMaybe a = if a == mempty then Nothing else Just a++gen :: (MonadGen m) => m Job+gen = do+  concurrency <- Gen.maybe Concurrency.gen+  container <- Gen.maybe JobContainer.gen+  continueOnError <- Gen.maybe Gen.bool+  defaults <- Gen.maybe Defaults.gen+  env <- genTextMap+  environment <- Gen.maybe JobEnvironment.gen+  jobName <- Gen.maybe genText+  needs <- Gen.maybe (Gen.nonEmpty (Range.linear 1 5) JobId.gen)+  outputs <- genTextMap+  permissions <- Gen.maybe Permissions.gen+  runIf <- Gen.maybe genText+  runsOn <- Gen.maybe genText+  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)+  strategy <- Gen.maybe JobStrategy.gen+  timeoutMinutes <- Gen.maybe $ Gen.int (Range.linear 1 120)+  uses <- Gen.maybe genText+  with <- genTextMap+  pure Job {..}+  where+    genText = Gen.text (Range.linear 1 5) Gen.alphaNum+    genTextMap = Gen.map (Range.linear 1 5) $ liftA2 (,) genText genText++-- | Create a new empty 'Job' with default values.+--+-- This provides a minimal job that can be extended with specific steps,+-- runner configuration, and other settings.+--+-- Example:+--+-- @+-- buildJob = new+--   { jobName = Just "Build"+--   , runsOn = Just "ubuntu-latest"+--   , steps = Just $ checkoutStep :| [buildStep]+--   }+-- @+new :: Job+new =+  Job+    { concurrency = Nothing,+      container = Nothing,+      continueOnError = Nothing,+      defaults = Nothing,+      env = mempty,+      environment = Nothing,+      jobName = Nothing,+      needs = Nothing,+      outputs = mempty,+      permissions = Nothing,+      runIf = Nothing,+      runsOn = Nothing,+      secrets = mempty,+      services = mempty,+      steps = Nothing,+      strategy = Nothing,+      timeoutMinutes = Nothing,+      uses = Nothing,+      with = mempty+    }
+ src/Language/Github/Actions/Job/Container.hs view
@@ -0,0 +1,125 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++-- |+-- Module      : Language.Github.Actions.Job.Container+-- Description : Container configuration 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 'JobContainer' type for configuring Docker containers+-- that jobs run inside of in GitHub Actions workflows.+--+-- Job containers allow you to run job steps inside a Docker container with a specific+-- environment, dependencies, and configuration. This provides consistency across+-- different runner environments and enables the use of custom tooling.+--+-- For more information about GitHub Actions job containers, see:+-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idcontainer>+module Language.Github.Actions.Job.Container+  ( JobContainer (..),+    gen,+  )+where++import Data.Aeson (FromJSON, ToJSON, (.:?), (.=))+import qualified Data.Aeson as Aeson+import Data.Map (Map)+import Data.Maybe (catMaybes)+import Data.Text (Text)+import GHC.Generics (Generic)+import Hedgehog (MonadGen)+import qualified Hedgehog.Gen as Gen+import qualified Hedgehog.Range as Range++-- | Container configuration for running a job inside a Docker container.+--+-- Job containers provide an isolated, consistent environment for job execution.+-- This is useful for ensuring specific tool versions, operating system environments,+-- or complex dependency setups.+--+-- Example usage:+--+-- @+-- import Language.Github.Actions.Job.Container+-- import qualified Data.Map as Map+--+-- -- Node.js development container+-- nodeContainer :: JobContainer+-- nodeContainer = JobContainer+--  { image = Just "node:18"+--  , env = Just $ Map.fromList [("NODE_ENV", "test")]+--  , credentials = Nothing+--  , options = Nothing+--  , ports = Nothing+--  , volumes = Nothing+--  }+--+-- -- Database testing container with services+-- dbTestContainer :: JobContainer+-- dbTestContainer = JobContainer+--  { image = Just "ubuntu:22.04"+--  , env = Just $ Map.fromList [("DEBIAN_FRONTEND", "noninteractive")]+--  , credentials = Nothing+--  , options = Just "--network postgres"+--  , ports = Just ["8080:8080"]+--  , volumes = Just ["\${{ github.workspace }}:\/workspace"]+--  }+-- @+--+-- For more details, see: <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idcontainer>+data JobContainer = JobContainer+  { -- | Registry credentials for private images+    credentials :: Maybe (Map Text Text),+    -- | Environment variables for the container+    env :: Maybe (Map Text Text),+    -- | Docker image to use for the container+    image :: Maybe Text,+    -- | Additional Docker run options+    options :: Maybe Text,+    -- | Ports to expose from the container+    ports :: Maybe [Text],+    -- | Volumes to mount in the container+    volumes :: Maybe [Text]+  }+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON JobContainer where+  parseJSON = Aeson.withObject "JobContainer" $ \o -> do+    credentials <- o .:? "credentials"+    env <- o .:? "env"+    image <- o .:? "image"+    options <- o .:? "options"+    ports <- o .:? "ports"+    volumes <- o .:? "volumes"+    pure JobContainer {..}++instance ToJSON JobContainer where+  toJSON JobContainer {..} =+    Aeson.object $+      catMaybes+        [ ("credentials" .=) <$> credentials,+          ("env" .=) <$> env,+          ("image" .=) <$> image,+          ("options" .=) <$> options,+          ("ports" .=) <$> ports,+          ("volumes" .=) <$> volumes+        ]++gen :: (MonadGen m) => m JobContainer+gen = do+  credentials <- Gen.maybe genTextMap+  env <- Gen.maybe genTextMap+  image <- Gen.maybe genText+  options <- Gen.maybe genText+  ports <-+    Gen.maybe . Gen.list (Range.linear 0 10) $+      Gen.text (Range.linear 1 5) Gen.digit+  volumes <- Gen.maybe $ Gen.list (Range.linear 0 10) genText+  pure JobContainer {..}+  where+    genText = Gen.text (Range.linear 1 5) Gen.alphaNum+    genTextMap = Gen.map (Range.linear 1 5) $ liftA2 (,) genText genText
+ src/Language/Github/Actions/Job/Environment.hs view
@@ -0,0 +1,97 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}++-- |+-- Module      : Language.Github.Actions.Job.Environment+-- Description : Deployment environment configuration 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 'JobEnvironment' type for configuring deployment+-- environments that jobs target in GitHub Actions workflows.+--+-- Job environments allow you to:+-- * Target specific deployment environments (production, staging, etc.)+-- * Control access through environment protection rules+-- * Use environment-specific secrets and variables+-- * Track deployment history and status+--+-- For more information about GitHub Actions environments, see:+-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idenvironment>+module Language.Github.Actions.Job.Environment+  ( JobEnvironment (..),+    gen,+  )+where++import Data.Aeson (FromJSON, ToJSON)+import qualified Data.Aeson as Aeson+import Data.Map (Map)+import Data.Text (Text)+import GHC.Generics (Generic)+import Hedgehog (MonadGen)+import qualified Hedgehog.Gen as Gen+import qualified Hedgehog.Range as Range++-- | Environment configuration for deployment jobs.+--+-- Job environments can be specified in two ways:+--+-- * 'NamedJobEnvironment' - Reference a named environment by string+-- * 'CustomJobEnvironment' - Configure environment with custom URL and other properties+--+-- Named environments are the most common and reference environments configured+-- in your repository settings. Custom environments allow inline configuration+-- for specific deployment scenarios.+--+-- Example usage:+--+-- @+-- import Language.Github.Actions.Job.Environment+-- import qualified Data.Map as Map+--+-- -- Reference a named environment+-- prodEnvironment :: JobEnvironment+-- prodEnvironment = NamedJobEnvironment "production"+--+-- stagingEnvironment :: JobEnvironment+-- stagingEnvironment = NamedJobEnvironment "staging"+--+-- -- Custom environment with URL+-- customEnvironment :: JobEnvironment+-- customEnvironment = CustomJobEnvironment $ Map.fromList+--  [ ("name", "review-pr-123")+--  , ("url", "https:\/\/pr-123.preview.example.com")+--  ]+-- @+--+-- For more details, see: <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idenvironment>+data JobEnvironment+  = -- | Reference a named environment+    NamedJobEnvironment Text+  | -- | Custom environment configuration+    CustomJobEnvironment (Map Text Text)+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON JobEnvironment where+  parseJSON v = case v of+    Aeson.String s ->+      pure $ NamedJobEnvironment s+    Aeson.Object o -> CustomJobEnvironment <$> Aeson.parseJSON (Aeson.Object o)+    _ -> fail "JobEnvironment must be a string or an object"++instance ToJSON JobEnvironment where+  toJSON (NamedJobEnvironment s) = Aeson.String s+  toJSON (CustomJobEnvironment m) = Aeson.toJSON m++gen :: (MonadGen m) => m JobEnvironment+gen =+  Gen.choice+    [ NamedJobEnvironment <$> genText,+      CustomJobEnvironment <$> genTextMap+    ]+  where+    genText = Gen.text (Range.linear 1 5) Gen.alphaNum+    genTextMap = Gen.map (Range.linear 1 5) $ liftA2 (,) genText genText
+ src/Language/Github/Actions/Job/Id.hs view
@@ -0,0 +1,73 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++-- |+-- Module      : Language.Github.Actions.Job.Id+-- Description : Job identifiers for GitHub Actions workflows+-- Copyright   : (c) 2025 Bellroy Pty Ltd+-- License     : BSD-3-Clause+-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>+--+-- This module provides the 'JobId' type for uniquely identifying jobs within+-- GitHub Actions workflows.+--+-- Job IDs are used to reference jobs in dependency declarations (needs), outputs,+-- and other cross-job references. They must be unique within a workflow and follow+-- specific naming conventions.+--+-- For more information about GitHub Actions job IDs, see:+-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_id>+module Language.Github.Actions.Job.Id+  ( JobId (..),+    gen,+    render,+  )+where++import Data.Aeson (FromJSON, FromJSONKey, ToJSON (..), ToJSONKey)+import Data.Text (Text)+import GHC.Generics (Generic)+import Hedgehog (MonadGen)+import qualified Hedgehog.Gen as Gen+import qualified Hedgehog.Range as Range++-- | A unique identifier for a job within a GitHub Actions workflow.+--+-- Job IDs are used to:+-- * Reference jobs in dependency declarations (needs)+-- * Access job outputs from other jobs+-- * Identify jobs in workflow run logs and API responses+--+-- Job IDs must be unique within a workflow and should follow these conventions:+-- * Start with a letter or underscore+-- * Contain only alphanumeric characters, hyphens, and underscores+-- * Be descriptive of the job's purpose+--+-- Example usage:+--+-- @+-- import Language.Github.Actions.Job.Id+--+-- -- Simple job IDs+-- buildJobId :: JobId+-- buildJobId = JobId "build"+--+-- testJobId :: JobId+-- testJobId = JobId "test"+--+-- -- More descriptive job IDs+-- deployProdJobId :: JobId+-- deployProdJobId = JobId "deploy-production"+-- @+--+-- For more details, see: <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_id>+newtype JobId = JobId Text+  deriving stock (Eq, Generic, Ord, Show)+  deriving newtype (FromJSON, FromJSONKey, ToJSON, ToJSONKey)++gen :: (MonadGen m) => m JobId+gen = JobId <$> Gen.text (Range.linear 1 5) Gen.alphaNum++render :: JobId -> Text+render (JobId t) = t
+ src/Language/Github/Actions/Job/Strategy.hs view
@@ -0,0 +1,158 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- |+-- Module      : Language.Github.Actions.Job.Strategy+-- Description : Matrix strategies 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 'JobStrategy' type for configuring matrix strategies+-- that allow jobs to run multiple times with different configurations.+--+-- Matrix strategies are useful for testing across multiple:+-- * Operating systems (Ubuntu, Windows, macOS)+-- * Language versions (Node 16, 18, 20)+-- * Database versions (PostgreSQL 12, 13, 14)+-- * Or any other configurable parameters+--+-- For more information about GitHub Actions matrix strategies, see:+-- <https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/running-variations-of-jobs-in-a-workflow>+module Language.Github.Actions.Job.Strategy+  ( JobStrategy (..),+    gen,+  )+where++import Data.Aeson (FromJSON, ToJSON, (.!=), (.:?), (.=))+import qualified Data.Aeson as Aeson+import qualified Data.Aeson.Types as Aeson+import Data.Map (Map)+import qualified Data.Map as Map+import Data.Maybe (catMaybes)+import Data.String (fromString)+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++-- | Matrix strategy configuration for running job variations.+--+-- Matrix strategies define how to run a job multiple times with different+-- configurations. The matrix creates a cross-product of all variable combinations,+-- with options to include additional combinations or exclude specific ones.+--+-- Example usage:+--+-- @+-- import Language.Github.Actions.Job.Strategy+-- import qualified Data.Map as Map+-- import qualified Data.Aeson as Aeson+--+-- -- Simple OS and Node version matrix+-- nodeMatrix :: JobStrategy+-- nodeMatrix = JobStrategy+--  { exclude = Nothing+--  , failFast = Just False+--  , include = Nothing+--  , maxParallel = Just 3+--  , otherVariables = Just $ Map.fromList+--      [ ("os", Aeson.toJSON ["ubuntu-latest", "windows-latest", "macos-latest"])+--      , ("node-version", Aeson.toJSON ["16", "18", "20"])+--      ]+--  }+--+-- -- Matrix with exclusions and includes+-- complexMatrix :: JobStrategy+-- complexMatrix = JobStrategy+--  { exclude = Just ["{ \"os\": \"windows-latest\", \"node-version\": \"16\" }"]+--  , failFast = Just True+--  , include = Just ["{ \"os\": \"ubuntu-latest\", \"node-version\": \"21\", \"experimental\": true }"]+--  , maxParallel = Nothing+--  , otherVariables = Just $ Map.fromList+--      [ ("os", Aeson.toJSON ["ubuntu-latest", "windows-latest"])+--      , ("node-version", Aeson.toJSON ["18", "20"])+--      ]+--  }+-- @+--+-- For more details, see: <https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/running-variations-of-jobs-in-a-workflow>+data JobStrategy = JobStrategy+  { -- | Matrix combinations to exclude+    exclude :: Maybe [Text],+    -- | Whether to cancel all jobs when one fails+    failFast :: Maybe Bool,+    -- | Additional matrix combinations to include+    include :: Maybe [Text],+    -- | Maximum number of parallel jobs+    maxParallel :: Maybe Int,+    -- | Matrix variables (os, version, etc.)+    otherVariables :: Maybe (Map Text Aeson.Value)+  }+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON JobStrategy where+  parseJSON = Aeson.withObject "JobStrategy" $ \o -> do+    matrix <- o .:? "matrix" .!= mempty+    exclude <- matrix .:? "exclude"+    failFast <- o .:? "fail-fast"+    include <- matrix .:? "include"+    maxParallel <- o .:? "max-parallel"+    rawMatrix :: Map Text Aeson.Value <- o .:? "matrix" .!= mempty+    let excludeKey :: Text = "exclude"+        includeKey :: Text = "include"+        filteredRawMatrix =+          Map.filterWithKey+            (\k _ -> k /= excludeKey && k /= includeKey)+            rawMatrix+        otherVariables =+          if null filteredRawMatrix+            then Nothing+            else Just filteredRawMatrix+    pure JobStrategy {..}++instance ToJSON JobStrategy where+  toJSON JobStrategy {..} =+    Aeson.object $+      catMaybes+        [ ("fail-fast" .=) <$> failFast,+          ("matrix" .=) <$> maybeMatrixObject,+          ("max-parallel" .=) <$> maxParallel+        ]+    where+      maybeMatrixObject :: Maybe Aeson.Value+      maybeMatrixObject =+        let pairs =+              maybe [] otherVariableMapToAesonPair otherVariables+                ++ catMaybes+                  [ ("exclude" .=) <$> exclude,+                    ("include" .=) <$> include+                  ]+         in if null pairs+              then Nothing+              else Just $ Aeson.object pairs++      otherVariableMapToAesonPair :: Map Text Aeson.Value -> [Aeson.Pair]+      otherVariableMapToAesonPair =+        Map.foldMapWithKey+          ( \k v ->+              [fromString (Text.unpack k) .= v]+          )++gen :: (MonadGen m) => m JobStrategy+gen = do+  exclude <- Gen.maybe $ Gen.list (Range.linear 1 3) genText+  failFast <- Gen.maybe Gen.bool+  include <- Gen.maybe $ Gen.list (Range.linear 1 3) genText+  maxParallel <- Gen.maybe $ Gen.int (Range.linear 1 10)+  otherVariables <- Gen.maybe $ (fmap . fmap) Aeson.String genTextMap+  pure JobStrategy {..}+  where+    genText = Gen.text (Range.linear 1 5) Gen.alphaNum+    genTextMap = Gen.map (Range.linear 1 5) $ liftA2 (,) genText genText
+ src/Language/Github/Actions/Permissions.hs view
@@ -0,0 +1,202 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}++-- |+-- Module      : Language.Github.Actions.Permissions+-- Description : GitHub Actions permissions and access control+-- Copyright   : (c) 2025 Bellroy Pty Ltd+-- License     : BSD-3-Clause+-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>+--+-- This module provides types for managing GitHub Actions permissions and access control.+-- Permissions control what GitHub APIs and resources workflows and jobs can access.+--+-- You can set permissions at the workflow level (affecting all jobs) or at individual+-- job levels. This follows the principle of least privilege by allowing you to grant+-- only the specific permissions needed.+--+-- For more information about GitHub Actions permissions, see:+-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#permissions>+module Language.Github.Actions.Permissions+  ( Permissions (..),+    PermissionType (..),+    Permission (..),+    gen,+  )+where++import Control.Monad.Fail.Hoist (hoistFail')+import Data.Aeson (FromJSON, FromJSONKey, ToJSON, ToJSONKey)+import qualified Data.Aeson as Aeson+import qualified Data.Aeson.Types as Aeson+import Data.Map (Map)+import Data.String.Interpolate (i)+import Data.Text (Text)+import GHC.Generics (Generic)+import Hedgehog (MonadGen)+import qualified Hedgehog.Gen as Gen+import qualified Hedgehog.Range as Range+import Language.Github.Actions.Internal (inverseMap)++-- | Types of permissions that can be granted to GitHub Actions workflows.+--+-- Each permission type corresponds to a specific area of GitHub functionality+-- that workflows might need to access.+--+-- For more details about each permission type, see: <https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token>+data PermissionType+  = -- | Manage GitHub Actions (e.g., cancel workflow runs)+    Actions+  | -- | Create and verify attestations+    Attestations+  | -- | Create and update check runs and suites+    Checks+  | -- | Read and write repository contents+    Contents+  | -- | Create and manage deployments+    Deployments+  | -- | Request OIDC JWT ID tokens+    IdToken+  | -- | Create and manage issues+    Issues+  | -- | Create and manage discussions+    Discussions+  | -- | Publish and manage packages+    Packages+  | -- | Deploy to GitHub Pages+    Pages+  | -- | Create and manage pull requests+    PullRequests+  | -- | Manage repository projects+    RepositoryProjects+  | -- | Read and write security events+    SecurityEvents+  | -- | Create commit status checks+    Statuses+  deriving stock (Bounded, Enum, Eq, Generic, Ord, Show)++instance FromJSON PermissionType where+  parseJSON =+    Aeson.withText "PermissionType" $+      hoistFail' . parsePermissionType++instance FromJSONKey PermissionType where+  fromJSONKey = Aeson.FromJSONKeyTextParser (Aeson.parseJSON . Aeson.String)++instance ToJSON PermissionType where+  toJSON = Aeson.String . renderPermissionType++instance ToJSONKey PermissionType where+  toJSONKey = Aeson.toJSONKeyText renderPermissionType++renderPermissionType :: PermissionType -> Text+renderPermissionType = \case+  Actions -> "actions"+  Attestations -> "attestations"+  Checks -> "checks"+  Contents -> "contents"+  Deployments -> "deployments"+  IdToken -> "id-token"+  Issues -> "issues"+  Discussions -> "discussions"+  Packages -> "packages"+  Pages -> "pages"+  PullRequests -> "pull-requests"+  RepositoryProjects -> "repository-projects"+  SecurityEvents -> "security-events"+  Statuses -> "statuses"++parsePermissionType :: Text -> Either String PermissionType+parsePermissionType t =+  maybe (Left [i|Unknown PermissionType: #{t}|]) Right $+    inverseMap renderPermissionType t++-- | Permission levels that can be granted for each permission type.+data Permission+  = -- | No access granted+    None+  | -- | Read-only access+    Read+  | -- | Read and write access+    Write+  deriving stock (Bounded, Enum, Eq, Generic, Ord, Show)++instance FromJSON Permission where+  parseJSON =+    Aeson.withText "Permission" $+      hoistFail' . parsePermission++instance ToJSON Permission where+  toJSON = Aeson.String . renderPermission++renderPermission :: Permission -> Text+renderPermission = \case+  None -> "none"+  Read -> "read"+  Write -> "write"++parsePermission :: Text -> Either String Permission+parsePermission t =+  maybe (Left [i|Unknown Permission: #{t}|]) Right $+    inverseMap renderPermission t++-- | Overall permissions configuration for a workflow or job.+--+-- Permissions can be set globally (affecting all permission types) or+-- individually for specific permission types.+--+-- Example usage:+--+-- @+-- import Language.Github.Actions.Permissions+--+-- -- Grant read access to everything+-- readOnlyPerms :: Permissions+-- readOnlyPerms = ReadAll+--+-- -- Grant specific permissions only+-- customPerms :: Permissions+-- customPerms = Custom $ Map.fromList+--  [ (Contents, Read)+--  , (PullRequests, Write)+--  ]+-- @+data Permissions+  = -- | No permissions granted (empty object)+    NoPermissions+  | -- | Read access to all permission types+    ReadAll+  | -- | Write access to all permission types+    WriteAll+  | -- | Custom permission mapping+    Custom (Map PermissionType Permission)+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON Permissions where+  parseJSON v = case v of+    Aeson.String "{}" -> pure NoPermissions+    Aeson.String "read-all" -> pure ReadAll+    Aeson.String "write-all" -> pure WriteAll+    Aeson.String s -> fail [i|Invalid Permissions: #{s}|]+    Aeson.Object o -> Custom <$> Aeson.parseJSON (Aeson.Object o)+    _ -> fail "Permissions must be a string or an object"++instance ToJSON Permissions where+  toJSON = \case+    NoPermissions -> Aeson.String "{}"+    ReadAll -> Aeson.String "read-all"+    WriteAll -> Aeson.String "write-all"+    Custom m -> Aeson.toJSON m++gen :: (MonadGen m) => m Permissions+gen =+  Gen.choice+    [ pure NoPermissions,+      pure ReadAll,+      pure WriteAll,+      fmap Custom . Gen.map (Range.linear 1 5) $+        liftA2 (,) Gen.enumBounded Gen.enumBounded+    ]
+ src/Language/Github/Actions/Service.hs view
@@ -0,0 +1,147 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++-- |+-- Module      : Language.Github.Actions.Service+-- Description : GitHub Actions service containers for jobs+-- Copyright   : (c) 2025 Bellroy Pty Ltd+-- License     : BSD-3-Clause+-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>+--+-- This module provides the 'Service' type for defining service containers that run+-- alongside job steps in GitHub Actions workflows.+--+-- Service containers are Docker containers that provide services like databases,+-- message queues, or other dependencies that your job steps might need during execution.+-- They run in parallel with your job and are accessible via hostname.+--+-- For more information about GitHub Actions service containers, see:+-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idservices>+module Language.Github.Actions.Service+  ( Service (..),+    gen,+    new,+  )+where++import Data.Aeson (FromJSON, ToJSON (..), (.:?), (.=))+import qualified Data.Aeson as Aeson+import Data.Map (Map)+import Data.Maybe (catMaybes)+import Data.Text (Text)+import GHC.Generics (Generic)+import Hedgehog (MonadGen)+import qualified Hedgehog.Gen as Gen+import qualified Hedgehog.Range as Range++-- | A service container definition for GitHub Actions jobs.+--+-- Service containers run Docker images that provide services your job steps can use.+-- Common examples include databases, caches, and message queues.+--+-- Service containers are automatically started before job steps run and stopped after+-- the job completes. They can be accessed by job steps using their service ID as hostname.+--+-- Example usage:+--+-- @+-- import Language.Github.Actions.Service+-- import qualified Data.Map as Map+--+-- -- PostgreSQL database service+-- postgresService :: Service+-- postgresService = new+--  { image = Just "postgres:13"+--  , env = Just $ Map.fromList+--      [ ("POSTGRES_PASSWORD", "postgres")+--      , ("POSTGRES_DB", "testdb")+--      ]+--  , ports = Just ["5432:5432"]+--  }+--+-- -- Redis cache service+-- redisService :: Service+-- redisService = new+--  { image = Just "redis:6-alpine"+--  , ports = Just ["6379:6379"]+--  }+-- @+--+-- For more details, see: <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idservices>+data Service = Service+  { -- | Registry credentials for private images+    credentials :: Maybe (Map Text Text),+    -- | Environment variables for the service+    env :: Maybe (Map Text Text),+    -- | Docker image to use for the service+    image :: Maybe Text,+    -- | Additional Docker options+    options :: Maybe Text,+    -- | Ports to expose from the service+    ports :: Maybe [Text],+    -- | Volumes to mount in the service+    volumes :: Maybe [Text]+  }+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON Service where+  parseJSON = Aeson.withObject "Service" $ \o -> do+    credentials <- o .:? "credentials"+    env <- o .:? "env"+    image <- o .:? "image"+    options <- o .:? "options"+    ports <- o .:? "ports"+    volumes <- o .:? "volumes"+    pure Service {..}++instance ToJSON Service where+  toJSON Service {..} =+    Aeson.object $+      catMaybes+        [ ("credentials" .=) <$> credentials,+          ("env" .=) <$> env,+          ("image" .=) <$> image,+          ("options" .=) <$> options,+          ("ports" .=) <$> ports,+          ("volumes" .=) <$> volumes+        ]++gen :: (MonadGen m) => m Service+gen = do+  credentials <- Gen.maybe genTextMap+  env <- Gen.maybe genTextMap+  image <- Gen.maybe genText+  options <- Gen.maybe genText+  ports <- Gen.maybe . Gen.list (Range.linear 1 3) $ Gen.text (Range.linear 1 5) Gen.digit+  volumes <- Gen.maybe . Gen.list (Range.linear 1 3) $ genText+  pure Service {..}+  where+    genText = Gen.text (Range.linear 1 5) Gen.alphaNum+    genTextMap = Gen.map (Range.linear 1 5) $ liftA2 (,) genText genText++-- | Create a new empty 'Service' with default values.+--+-- This provides a minimal service definition that can be extended with specific+-- image, ports, environment variables, and other configuration.+--+-- Example:+--+-- @+-- databaseService = new+--   { image = Just "postgres:13"+--   , env = Just $ Map.singleton "POSTGRES_PASSWORD" "secret"+--   , ports = Just ["5432:5432"]+--   }+-- @+new :: Service+new =+  Service+    { credentials = Nothing,+      env = Nothing,+      image = Nothing,+      options = Nothing,+      ports = Nothing,+      volumes = Nothing+    }
+ src/Language/Github/Actions/Service/Id.hs view
@@ -0,0 +1,72 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++-- |+-- Module      : Language.Github.Actions.Service.Id+-- Description : Service identifiers for GitHub Actions workflows+-- Copyright   : (c) 2025 Bellroy Pty Ltd+-- License     : BSD-3-Clause+-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>+--+-- This module provides the 'ServiceId' type for uniquely identifying service+-- containers within GitHub Actions jobs.+--+-- Service IDs are used to reference service containers from job steps via hostname.+-- They must be unique within a job and follow specific naming conventions.+--+-- For more information about GitHub Actions service containers, see:+-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idservices>+module Language.Github.Actions.Service.Id+  ( ServiceId (..),+    gen,+    render,+  )+where++import Data.Aeson (FromJSON, FromJSONKey, ToJSON (..), ToJSONKey)+import Data.Text (Text)+import GHC.Generics (Generic)+import Hedgehog (MonadGen)+import qualified Hedgehog.Gen as Gen+import qualified Hedgehog.Range as Range++-- | A unique identifier for a service container within a GitHub Actions job.+--+-- Service IDs are used to:+-- * Reference the service container from job steps using the service ID as hostname+-- * Configure service-specific settings like ports, environment variables, and health checks+-- * Identify services in workflow run logs and container networks+--+-- Service IDs must be unique within a job and should follow these conventions:+-- * Start with a letter or underscore+-- * Contain only alphanumeric characters, hyphens, and underscores+-- * Be descriptive of the service's purpose+--+-- Example usage:+--+-- @+-- import Language.Github.Actions.Service.Id+--+-- -- Database service IDs+-- postgresServiceId :: ServiceId+-- postgresServiceId = ServiceId "postgres"+--+-- redisServiceId :: ServiceId+-- redisServiceId = ServiceId "redis"+--+-- -- More descriptive service IDs+-- testDatabaseServiceId :: ServiceId+-- testDatabaseServiceId = ServiceId "test-database"+-- @+--+-- For more details, see: <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idservices>+newtype ServiceId = ServiceId Text+  deriving stock (Eq, Generic, Ord, Show)+  deriving newtype (FromJSON, FromJSONKey, ToJSON, ToJSONKey)++gen :: (MonadGen m) => m ServiceId+gen = ServiceId <$> Gen.text (Range.linear 1 5) Gen.alphaNum++render :: ServiceId -> Text+render (ServiceId t) = t
+ src/Language/Github/Actions/Shell.hs view
@@ -0,0 +1,158 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RankNTypes #-}++-- |+-- Module      : Language.Github.Actions.Shell+-- Description : GitHub Actions shell configuration for steps+-- Copyright   : (c) 2025 Bellroy Pty Ltd+-- License     : BSD-3-Clause+-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>+--+-- This module provides the 'Shell' type for specifying which shell to use when+-- running commands in GitHub Actions steps.+--+-- Different shells provide different capabilities and are available on different+-- operating systems. The shell can be specified at the workflow, job, or step level.+--+-- For more information about GitHub Actions shell configuration, see:+-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell>+module Language.Github.Actions.Shell+  ( Platform (..),+    Shell (..),+    arguments,+    gen,+    supportedPlatforms,+  )+where++import Control.Monad.Fail.Hoist (hoistFail')+import Data.Aeson (FromJSON, ToJSON)+import qualified Data.Aeson as Aeson+import Data.List.NonEmpty (NonEmpty (..))+import Data.String.Interpolate (i)+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++-- | Shell types available for running commands in GitHub Actions steps.+--+-- Each shell has different capabilities and platform availability:+--+-- * 'Bash' - Available on all platforms, most commonly used+-- * 'Sh' - POSIX sh, available on Linux and macOS only+-- * 'Python' - Executes commands as Python code+-- * 'Cmd' - Windows Command Prompt, Windows only+-- * 'Powershell' - Windows PowerShell 5.1, Windows only+-- * 'Pwsh' - PowerShell Core, available on all platforms+--+-- Example usage:+--+-- @+-- import Language.Github.Actions.Shell+--+-- -- Use bash with default options+-- bashShell :: Shell+-- bashShell = Bash Nothing+--+-- -- Use bash with specific options+-- bashWithOptions :: Shell+-- bashWithOptions = Bash (Just "--noprofile --norc")+--+-- -- Use PowerShell Core+-- pwshShell :: Shell+-- pwshShell = Pwsh Nothing+-- @+--+-- For more details, see: <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsshell>+data Shell+  = -- | Bash shell with optional arguments+    Bash (Maybe Text)+  | -- | POSIX sh shell (Linux/macOS only) with optional arguments+    Sh (Maybe Text)+  | -- | Python interpreter with optional arguments+    Python (Maybe Text)+  | -- | Windows cmd.exe with optional arguments+    Cmd (Maybe Text)+  | -- | Windows PowerShell 5.1 with optional arguments+    Powershell (Maybe Text)+  | -- | PowerShell Core with optional arguments+    Pwsh (Maybe Text)+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON Shell where+  parseJSON =+    Aeson.withText "Shell" $+      hoistFail' . parseShell++instance ToJSON Shell where+  toJSON = Aeson.String . renderShell++renderShell :: Shell -> Text+renderShell = \case+  Bash args -> [i|bash#{maybe "" ((" " <>)) args}|]+  Sh args -> [i|sh#{maybe "" ((" " <>)) args}|]+  Python args -> [i|python#{maybe "" ((" " <>)) args}|]+  Cmd args -> [i|cmd#{maybe "" ((" " <>)) args}|]+  Powershell args -> [i|powershell#{maybe "" ((" " <>)) args}|]+  Pwsh args -> [i|pwsh#{maybe "" ((" " <>)) args}|]++parseShell :: Text -> Either String Shell+parseShell t =+  case tokens of+    "bash" : args -> Right . Bash $ maybeArgs args+    "cmd" : args -> Right . Cmd $ maybeArgs args+    "powershell" : args -> Right . Powershell $ maybeArgs args+    "pwsh" : args -> Right . Pwsh $ maybeArgs args+    "python" : args -> Right . Python $ maybeArgs args+    "sh" : args -> Right . Sh $ maybeArgs args+    _ -> Left [i|Unknown shell: #{t}|]+  where+    tokens = Text.words t+    maybeArgs = \case+      [] -> Nothing+      args -> Just $ Text.unwords args++gen :: (MonadGen m) => m Shell+gen =+  Gen.choice+    [ Bash <$> Gen.maybe genText,+      Sh <$> Gen.maybe genText,+      Python <$> Gen.maybe genText,+      Cmd <$> Gen.maybe genText,+      Powershell <$> Gen.maybe genText,+      Pwsh <$> Gen.maybe genText+    ]+  where+    genText = Gen.text (Range.linear 1 5) Gen.alphaNum++data Platform = Windows | MacOS | Linux++supportedPlatforms :: Shell -> NonEmpty Platform+supportedPlatforms = \case+  Bash _ -> Linux :| [MacOS, Windows]+  Sh _ -> MacOS :| []+  Python _ -> Linux :| [MacOS, Windows]+  Cmd _ -> Windows :| []+  Powershell _ -> Windows :| []+  Pwsh _ -> Windows :| []++-- | A lens into the arguments for a shell, compatible with the "lens" package+--+-- @+-- arguments :: Lens' Shell (Maybe Text)+-- @+arguments :: forall f. (Functor f) => (Maybe Text -> f (Maybe Text)) -> Shell -> f Shell+arguments f = \case+  Bash args -> Bash <$> f args+  Sh args -> Sh <$> f args+  Python args -> Python <$> f args+  Cmd args -> Cmd <$> f args+  Powershell args -> Powershell <$> f args+  Pwsh args -> Pwsh <$> f args
+ src/Language/Github/Actions/Step.hs view
@@ -0,0 +1,186 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++-- |+-- Module      : Language.Github.Actions.Step+-- Description : GitHub Actions step definition and serialization+-- Copyright   : (c) 2025 Bellroy Pty Ltd+-- License     : BSD-3-Clause+-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>+--+-- This module provides the 'Step' type for representing individual steps within GitHub Actions jobs.+-- Steps are the individual tasks that make up a job, such as checking out code, running commands,+-- or using pre-built actions.+--+-- A step can either run a command/script or use a pre-built action from the GitHub marketplace+-- or a custom action. Steps run sequentially within a job.+--+-- For more information about GitHub Actions step syntax, see:+-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idsteps>+module Language.Github.Actions.Step+  ( Step (..),+    gen,+    new,+  )+where++import Data.Aeson (FromJSON, ToJSON (..), (.!=), (.:?), (.=))+import qualified Data.Aeson as Aeson+import Data.Map (Map)+import Data.Maybe (catMaybes)+import Data.Text (Text)+import GHC.Generics (Generic)+import Hedgehog (MonadGen)+import qualified Hedgehog.Gen as Gen+import qualified Hedgehog.Range as Range+import Language.Github.Actions.Shell (Shell)+import qualified Language.Github.Actions.Shell as Shell+import Language.Github.Actions.Step.Id (StepId)+import qualified Language.Github.Actions.Step.Id as StepId+import Language.Github.Actions.Step.With (StepWith)+import qualified Language.Github.Actions.Step.With as StepWith++-- | A step within a GitHub Actions job.+--+-- A step is an individual task within a job. Steps can run commands, scripts, or actions.+-- Each step runs in the runner environment and can use outputs from previous steps.+--+-- There are two main types of steps:+-- * Command steps that use 'run' to execute shell commands+-- * Action steps that use 'uses' to run a pre-built action+--+-- Example usage:+--+-- @+-- import Language.Github.Actions.Step+--+-- -- A command step+-- commandStep :: Step+-- commandStep = new+--  { name = Just "Run tests"+--  , run = Just "npm test"+--  }+--+-- -- An action step+-- actionStep :: Step+-- actionStep = new+--  { name = Just "Checkout code"+--  , uses = Just "actions/checkout\@v4"+--  }+-- @+--+-- For more details, see: <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idsteps>+data Step = Step+  { -- | Whether to continue job execution if this step fails+    continueOnError :: Bool,+    -- | Environment variables for this step+    env :: Map Text Text,+    -- | Display name for the step+    name :: Maybe Text,+    -- | Command or script to run+    run :: Maybe Text,+    -- | Condition for running this step+    runIf :: Maybe Text,+    -- | Shell to use for running commands+    shell :: Maybe Shell,+    -- | Unique identifier for this step+    stepId :: Maybe StepId,+    -- | Timeout for the step in minutes+    timeoutMinutes :: Maybe Int,+    -- | Action to run (e.g., "actions/checkout@v4")+    uses :: Maybe Text,+    -- | Inputs for the action+    with :: Maybe StepWith,+    -- | Working directory for the step+    workingDirectory :: Maybe Text+  }+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON Step where+  parseJSON = Aeson.withObject "Step" $ \o -> do+    continueOnError <- o .:? "continue-on-error" .!= False+    env <- o .:? "env" .!= mempty+    name <- o .:? "name"+    run <- o .:? "run"+    runIf <- o .:? "if"+    uses <- o .:? "uses"+    shell <- o .:? "shell"+    stepId <- o .:? "id"+    timeoutMinutes <- o .:? "timeout-minutes"+    with <- o .:? "with"+    workingDirectory <- o .:? "working-directory"+    pure Step {..}++instance ToJSON Step where+  toJSON Step {..} =+    Aeson.object $+      catMaybes+        [ if continueOnError then Just ("continue-on-error" .= True) else Nothing,+          ("env" .=) <$> monoidToMaybe env,+          ("id" .=) <$> stepId,+          ("if" .=) <$> runIf,+          ("name" .=) <$> name,+          ("run" .=) <$> run,+          ("shell" .=) <$> shell,+          ("timeout-minutes" .=) <$> timeoutMinutes,+          ("uses" .=) <$> uses,+          ("with" .=) <$> with,+          ("working-directory" .=) <$> workingDirectory+        ]+    where+      monoidToMaybe :: (Eq a, Monoid a) => a -> Maybe a+      monoidToMaybe a = if a == mempty then Nothing else Just a++gen :: (MonadGen m) => m Step+gen = do+  continueOnError <- Gen.bool+  env <- genTextMap+  name <- Gen.maybe genText+  run <- Gen.maybe genText+  runIf <- Gen.maybe genText+  shell <- Gen.maybe Shell.gen+  stepId <- Gen.maybe StepId.gen+  timeoutMinutes <- Gen.maybe $ Gen.int (Range.linear 1 120)+  uses <- Gen.maybe genText+  with <- Gen.maybe StepWith.gen+  workingDirectory <- Gen.maybe genText+  pure Step {..}+  where+    genText = Gen.text (Range.linear 1 5) Gen.alphaNum+    genTextMap = Gen.map (Range.linear 3 20) $ liftA2 (,) genText genText++-- | Create a new empty 'Step' with default values.+--+-- This provides a minimal step that can be extended with specific commands,+-- actions, or other configuration.+--+-- Example:+--+-- @+-- checkoutStep = new+--   { name = Just "Checkout repository"+--   , uses = Just "actions/checkout\@v4"+--   }+--+-- commandStep = new+--   { name = Just "Build project"+--   , run = Just "make build"+--   }+-- @+new :: Step+new =+  Step+    { continueOnError = False,+      env = mempty,+      name = Nothing,+      run = Nothing,+      runIf = Nothing,+      shell = Nothing,+      stepId = Nothing,+      timeoutMinutes = Nothing,+      uses = Nothing,+      with = Nothing,+      workingDirectory = Nothing+    }
+ src/Language/Github/Actions/Step/Id.hs view
@@ -0,0 +1,73 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}++-- |+-- Module      : Language.Github.Actions.Step.Id+-- Description : Step identifiers for GitHub Actions workflows+-- Copyright   : (c) 2025 Bellroy Pty Ltd+-- License     : BSD-3-Clause+-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>+--+-- This module provides the 'StepId' type for uniquely identifying steps within+-- GitHub Actions jobs.+--+-- Step IDs are used to reference step outputs from later steps in the same job+-- or from other jobs. They must be unique within a job and follow specific+-- naming conventions.+--+-- For more information about GitHub Actions step IDs, see:+-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsid>+module Language.Github.Actions.Step.Id+  ( StepId (..),+    gen,+    render,+  )+where++import Data.Aeson (FromJSON, FromJSONKey, ToJSON (..), ToJSONKey)+import Data.Text (Text)+import GHC.Generics (Generic)+import Hedgehog (MonadGen)+import qualified Hedgehog.Gen as Gen+import qualified Hedgehog.Range as Range++-- | A unique identifier for a step within a GitHub Actions job.+--+-- Step IDs are used to:+-- * Reference step outputs from later steps using @steps.<step_id>.outputs.<output_name>@+-- * Reference step outcomes using @steps.<step_id>.outcome@ or @steps.<step_id>.conclusion@+-- * Create dependencies between steps (though steps run sequentially by default)+--+-- Step IDs must be unique within a job and should follow these conventions:+-- * Start with a letter or underscore+-- * Contain only alphanumeric characters, hyphens, and underscores+-- * Be descriptive of the step's purpose+--+-- Example usage:+--+-- @+-- import Language.Github.Actions.Step.Id+--+-- -- Simple step IDs+-- checkoutStepId :: StepId+-- checkoutStepId = StepId "checkout"+--+-- buildStepId :: StepId+-- buildStepId = StepId "build"+--+-- -- More descriptive step IDs+-- uploadArtifactsStepId :: StepId+-- uploadArtifactsStepId = StepId "upload-build-artifacts"+-- @+--+-- For more details, see: <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsid>+newtype StepId = StepId Text+  deriving stock (Eq, Generic, Ord, Show)+  deriving newtype (FromJSON, FromJSONKey, ToJSON, ToJSONKey)++gen :: (MonadGen m) => m StepId+gen = StepId <$> Gen.text (Range.linear 1 5) Gen.alphaNum++render :: StepId -> Text+render (StepId t) = t
+ src/Language/Github/Actions/Step/With.hs view
@@ -0,0 +1,134 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++-- |+-- Module      : Language.Github.Actions.Step.With+-- Description : Input parameters for GitHub Actions steps+-- Copyright   : (c) 2025 Bellroy Pty Ltd+-- License     : BSD-3-Clause+-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>+--+-- This module provides the 'StepWith' type for specifying input parameters+-- to GitHub Actions steps that use pre-built actions.+--+-- The 'with' keyword allows you to pass inputs to actions, which can be:+-- * Environment variables for the action+-- * Docker container arguments (for Docker actions)+-- * Configuration parameters specific to the action+--+-- For more information about GitHub Actions step inputs, see:+-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepswith>+module Language.Github.Actions.Step.With+  ( StepWith (..),+    gen,+  )+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)+import GHC.Generics (Generic)+import Hedgehog (MonadGen)+import qualified Hedgehog.Gen as Gen+import qualified Hedgehog.Range as Range++-- | Docker container arguments for Docker actions.+--+-- Specifies the entry point and optional arguments for Docker actions.+data StepWithDockerArgsAttrs = StepWithDockerArgsAttrs+  { -- | Docker container entry point+    entryPoint :: Text,+    -- | Optional arguments to pass to the entry point+    args :: Maybe Text+  }+  deriving stock (Eq, Generic, Ord, Show)++-- | Input parameters for GitHub Actions steps.+--+-- Step inputs can be specified in two main ways:+--+-- * 'StepWithDockerArgs' - For Docker actions that need entry point and arguments+-- * 'StepWithEnv' - For actions that accept environment variables or general inputs+--+-- Most actions use the environment variable format, where inputs are passed+-- as key-value pairs.+--+-- Example usage:+--+-- @+-- import Language.Github.Actions.Step.With+-- import qualified Data.Map as Map+--+-- -- Environment inputs for actions/checkout+-- checkoutInputs :: StepWith+-- checkoutInputs = StepWithEnv $ Map.fromList+--  [ ("repository", "owner/repo")+--  , ("ref", "main")+--  , ("token", "\${{ secrets.GITHUB_TOKEN }}")+--  ]+--+-- -- Docker action inputs+-- dockerInputs :: StepWith+-- dockerInputs = StepWithDockerArgs $ StepWithDockerArgsAttrs+--  { entryPoint = "/entrypoint.sh"+--  , args = Just "arg1 arg2"+--  }+--+-- -- Action inputs for actions/upload-artifact+-- uploadInputs :: StepWith+-- uploadInputs = StepWithEnv $ Map.fromList+--  [ ("name", "build-artifacts")+--  , ("path", "dist/")+--  , ("retention-days", "30")+--  ]+-- @+--+-- For more details, see: <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#jobsjob_idstepswith>+data StepWith+  = -- | Docker action arguments+    StepWithDockerArgs StepWithDockerArgsAttrs+  | -- | Environment variables/general inputs+    StepWithEnv (Map Text Text)+  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+          then do+            entryPoint <- o .: "entryPoint"+            args <- o .:? "args"+            pure . StepWithDockerArgs $ StepWithDockerArgsAttrs {..}+          else StepWithEnv <$> Aeson.parseJSON (Aeson.Object o)++instance ToJSON StepWith where+  toJSON = \case+    StepWithDockerArgs StepWithDockerArgsAttrs {..} ->+      Aeson.object $+        catMaybes+          [ Just $ "entryPoint" .= entryPoint,+            ("args" .=) <$> args+          ]+    StepWithEnv env ->+      toJSON env++gen :: (MonadGen m) => m StepWith+gen =+  Gen.choice+    [ StepWithDockerArgs <$> do+        entryPoint <- genText+        args <- Gen.maybe genText+        pure StepWithDockerArgsAttrs {..},+      StepWithEnv <$> genTextMap+    ]+  where+    genText = Gen.text (Range.linear 1 5) Gen.alphaNum+    genTextMap = Gen.map (Range.linear 1 5) $ liftA2 (,) genText genText
+ src/Language/Github/Actions/Workflow.hs view
@@ -0,0 +1,204 @@+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards #-}++-- |+-- Module      : Language.Github.Actions.Workflow+-- Description : GitHub Actions workflow definition and serialization+-- Copyright   : (c) 2025 Bellroy Pty Ltd+-- License     : BSD-3-Clause+-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>+--+-- This module provides the core 'Workflow' type for representing GitHub Actions workflows,+-- along with JSON serialization/deserialization and generation utilities.+--+-- GitHub Actions workflows are defined in YAML files that specify automated processes+-- triggered by events in a GitHub repository. This module provides a type-safe way to+-- construct and serialize these workflows.+--+-- For more information about GitHub Actions workflow syntax, see:+-- <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions>+module Language.Github.Actions.Workflow+  ( Workflow (..),+    gen,+    new,+  )+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 Data.Set (Set)+import qualified Data.Set as Set+import Data.Text (Text)+import GHC.Generics (Generic)+import Hedgehog (MonadGen)+import qualified Hedgehog.Gen as Gen+import qualified Hedgehog.Range as Range+import Language.Github.Actions.Concurrency (Concurrency)+import qualified Language.Github.Actions.Concurrency as Concurrency+import Language.Github.Actions.Defaults (Defaults)+import qualified Language.Github.Actions.Defaults as Defaults+import Language.Github.Actions.Job (Job)+import qualified Language.Github.Actions.Job as Job+import Language.Github.Actions.Job.Id (JobId)+import qualified Language.Github.Actions.Job.Id as JobId+import Language.Github.Actions.Permissions (Permissions)+import qualified Language.Github.Actions.Permissions as Permissions+import Language.Github.Actions.Workflow.Trigger (WorkflowTrigger)+import qualified Language.Github.Actions.Workflow.Trigger as WorkflowTrigger++-- | Internal type for workflow job steps (used in JSON parsing)+data WorkflowJobStep = WorkflowJobStep+  { -- | Optional step name+    name :: Maybe Text,+    -- | Optional run command+    run :: Maybe Text+  }+  deriving stock (Eq, Generic, Ord, Show)++-- | A GitHub Actions workflow definition.+--+-- A workflow defines automated processes that run in response to events in your repository.+-- Workflows are defined in YAML files stored in the @.github\/workflows@ directory.+--+-- Example usage:+--+-- @+-- import Language.Github.Actions.Workflow+-- import qualified Language.Github.Actions.Job as Job+-- import qualified Language.Github.Actions.Job.Id as JobId+--+-- myWorkflow :: Workflow+-- myWorkflow = new+--  { workflowName = Just "CI"+--  , jobs = Map.singleton (JobId "build") Job.new+--  , on = Set.singleton pushTrigger+--  }+-- @+--+-- For more details, see: <https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions>+data Workflow = Workflow+  { -- | Concurrency settings to limit workflow runs+    concurrency :: Maybe Concurrency,+    -- | Default settings for all jobs in the workflow+    defaults :: Maybe Defaults,+    -- | Environment variables available to all jobs+    env :: Map Text Text,+    -- | The jobs that make up the workflow+    jobs :: Map JobId Job,+    -- | Events that trigger the workflow+    on :: Set WorkflowTrigger,+    -- | Permissions granted to the workflow+    permissions :: Maybe Permissions,+    -- | Name for workflow runs+    runName :: Maybe Text,+    -- | Name of the workflow+    workflowName :: Maybe Text+  }+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON Workflow where+  parseJSON = Aeson.withObject "Workflow" $ \o -> do+    concurrency <- o .:? "concurrency"+    defaults <- o .:? "defaults"+    env <- o .:? "env" .!= mempty+    jobs <- o .: "jobs"+    Aeson.Object onObject <- o .: "on"+    triggers <-+      traverse+        ( Aeson.parseJSON+            . Aeson.Object+            . uncurry AesonKeyMap.singleton+        )+        $ AesonKeyMap.toList onObject+    let on = Set.fromList triggers+    permissions <- o .:? "permissions"+    runName <- o .:? "run-name"+    workflowName <- o .:? "name"+    pure Workflow {..}++instance ToJSON Workflow where+  toJSON Workflow {..} =+    Aeson.object $+      catMaybes+        [ ("concurrency" .=) <$> concurrency,+          ("defaults" .=) <$> defaults,+          ("env" .=) <$> monoidToMaybe env,+          Just $ "jobs" .= jobs,+          ("name" .=) <$> workflowName,+          Just $ "on" .= Aeson.Object (mconcat (forceToJSONObject . Aeson.toJSON <$> Set.toList on)),+          ("permissions" .=) <$> permissions,+          ("run-name" .=) <$> runName+        ]+    where+      forceToJSONObject :: Aeson.Value -> Aeson.Object+      forceToJSONObject (Aeson.Object o) = o+      forceToJSONObject _ = error "Expected Aeson.Object"++      monoidToMaybe :: (Eq a, Monoid a) => a -> Maybe a+      monoidToMaybe a = if a == mempty then Nothing else Just a++gen :: (MonadGen m) => m Workflow+gen = do+  concurrency <- Gen.maybe Concurrency.gen+  defaults <- Gen.maybe Defaults.gen+  env <- genTextMap+  jobs <-+    Gen.map (Range.linear 1 5) (liftA2 (,) JobId.gen Job.gen)+  on <-+    Set.fromList+      . snd+      . foldr onlyAddIfJsonKeyNotAlreadyPresent ([], [])+      <$> Gen.list (Range.linear 1 5) WorkflowTrigger.gen++  permissions <- Gen.maybe Permissions.gen+  runName <- Gen.maybe genText+  workflowName <- Gen.maybe genText+  pure Workflow {..}+  where+    onlyAddIfJsonKeyNotAlreadyPresent ::+      WorkflowTrigger ->+      ([AesonKeyMap.Key], [WorkflowTrigger]) ->+      ([AesonKeyMap.Key], [WorkflowTrigger])+    onlyAddIfJsonKeyNotAlreadyPresent trigger (keys, triggers) =+      let key = case Aeson.toJSONKey of+            Aeson.ToJSONKeyText f _ -> f trigger+            _ -> error "Expected Aeson.ToJSONKeyText"+       in if key `elem` keys+            then (keys, triggers)+            else (key : keys, trigger : triggers)++    genText = Gen.text (Range.linear 1 5) Gen.alphaNum+    genTextMap = Gen.map (Range.linear 1 5) $ liftA2 (,) genText genText++-- | Create a new empty 'Workflow' with default values.+--+-- This provides a minimal workflow that can be extended with specific jobs,+-- triggers, and other configuration.+--+-- Example:+--+-- @+-- myWorkflow = new+--   { workflowName = Just "My Workflow"+--   , jobs = Map.singleton (JobId "test") Job.new+--   , on = Set.singleton (PushTrigger pushTriggerDefaults)+--   }+-- @+new :: Workflow+new =+  Workflow+    { concurrency = Nothing,+      defaults = Nothing,+      env = mempty,+      jobs = mempty,+      on = mempty,+      permissions = Nothing,+      runName = Nothing,+      workflowName = Nothing+    }
+ src/Language/Github/Actions/Workflow/Trigger.hs view
@@ -0,0 +1,1459 @@+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE DerivingStrategies #-}+{-# LANGUAGE DuplicateRecordFields #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE ScopedTypeVariables #-}++-- |+-- Module      : Language.Github.Actions.Workflow.Trigger+-- Description : GitHub Actions workflow trigger events and configurations+-- Copyright   : (c) 2025 Bellroy Pty Ltd+-- License     : BSD-3-Clause+-- Maintainer  : Bellroy Tech Team <haskell@bellroy.com>+--+-- This module provides comprehensive support for GitHub Actions workflow triggers,+-- which determine when workflows should run.+--+-- Workflow triggers respond to various GitHub events such as pushes, pull requests,+-- issue updates, scheduled times, and external repository dispatch events. Each trigger+-- type can be configured with specific activity types and filtering criteria.+--+-- The main types include:+--+-- * 'WorkflowTrigger' - The primary trigger type supporting all GitHub event types+-- * Activity type enums for each event (e.g., 'PullRequestActivityType', 'IssuesActivityType')+-- * Attribute types for triggers with complex configuration (e.g., 'PushTriggerAttributes')+--+-- Example usage:+--+-- @+-- import Language.Github.Actions.Workflow.Trigger+--+-- -- Trigger on push to main branch+-- pushTrigger :: WorkflowTrigger+-- pushTrigger = PushTrigger $ PushTriggerAttributes+--   { branches = Just ("main" :| [])+--   , branchesIgnore = Nothing+--   , paths = Nothing+--   , pathsIgnore = Nothing+--   , tags = Nothing+--   }+--+-- -- Trigger on pull request opened or synchronized+-- prTrigger :: WorkflowTrigger+-- prTrigger = PullRequestTrigger $ PullRequestTriggerAttributes+--   { activityTypes = Just (PullRequestOpened :| [PullRequestSynchronize])+--   , branches = Nothing+--   , branchesIgnore = Nothing+--   , paths = Nothing+--   , pathsIgnore = Nothing+--   }+-- @+--+-- For more information about GitHub Actions events and triggers, see:+-- <https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows>+module Language.Github.Actions.Workflow.Trigger+  ( BranchProtectionRuleActivityType (..),+    CheckRunActivityType (..),+    DiscussionActivityType (..),+    DiscussionCommentActivityType (..),+    IssueCommentActivityType (..),+    IssuesActivityType (..),+    LabelActivityType (..),+    MilestoneActivityType (..),+    PullRequestActivityType (..),+    PullRequestReviewActivityType (..),+    PullRequestReviewCommentActivityType (..),+    PullRequestTargetActivityType (..),+    PullRequestTargetTriggerAttributes (..),+    PullRequestTriggerAttributes (..),+    PushTriggerAttributes (..),+    RegistryPackageActivityType (..),+    ReleaseActivityType (..),+    WorkflowCallAttributes (..),+    WorkflowDispatchAttributes (..),+    WorkflowRunActivityType (..),+    WorkflowRunTriggerAttributes (..),+    WorkflowTrigger (..),+    gen,+  )+where++import Control.Applicative ((<|>))+import Control.Monad (guard)+import Control.Monad.Fail.Hoist (hoistFail')+import Data.Aeson (FromJSON, ToJSON, ToJSONKey, (.:), (.:?), (.=))+import qualified Data.Aeson as Aeson+import qualified Data.Aeson.KeyMap as AesonKeyMap+import qualified Data.Aeson.Types as Aeson+import Data.List.NonEmpty (NonEmpty (..), nonEmpty)+import Data.Map (Map)+import qualified Data.Map as Map+import Data.Maybe (catMaybes, fromMaybe, isJust)+import Data.String.Interpolate (i)+import Data.Text (Text)+import Data.Traversable (for)+import qualified Data.Vector as Vector+import GHC.Generics (Generic)+import Hedgehog (MonadGen)+import qualified Hedgehog.Gen as Gen+import qualified Hedgehog.Range as Range+import Language.Github.Actions.Internal (inverseMap)++data BranchProtectionRuleActivityType+  = BranchProtectionRuleCreated+  | BranchProtectionRuleDeleted+  | BranchProtectionRuleEdited+  deriving stock (Bounded, Enum, Eq, Generic, Ord, Show)++instance FromJSON BranchProtectionRuleActivityType where+  parseJSON =+    Aeson.withText "BranchProtectionRuleActivityType" $+      hoistFail' . parseBranchProtectionRuleActivityType++instance ToJSON BranchProtectionRuleActivityType where+  toJSON = Aeson.String . renderBranchProtectionRuleActivityType++renderBranchProtectionRuleActivityType :: BranchProtectionRuleActivityType -> Text+renderBranchProtectionRuleActivityType = \case+  BranchProtectionRuleCreated -> "created"+  BranchProtectionRuleDeleted -> "deleted"+  BranchProtectionRuleEdited -> "edited"++parseBranchProtectionRuleActivityType :: Text -> Either String BranchProtectionRuleActivityType+parseBranchProtectionRuleActivityType t =+  maybe (Left [i|Unknown BranchProtectionRuleActivityType: #{t}|]) Right $+    inverseMap renderBranchProtectionRuleActivityType t++data CheckRunActivityType+  = CheckRunCompleted+  | CheckRunCreated+  | CheckRunRequestedAction+  | CheckRunRerequested+  deriving stock (Bounded, Enum, Eq, Generic, Ord, Show)++instance FromJSON CheckRunActivityType where+  parseJSON =+    Aeson.withText "CheckRunActivityType" $+      hoistFail' . parseCheckRunActivityType++instance ToJSON CheckRunActivityType where+  toJSON = Aeson.String . renderCheckRunActivityType++renderCheckRunActivityType :: CheckRunActivityType -> Text+renderCheckRunActivityType = \case+  CheckRunCompleted -> "completed"+  CheckRunCreated -> "created"+  CheckRunRequestedAction -> "requested_action"+  CheckRunRerequested -> "rerequested"++parseCheckRunActivityType :: Text -> Either String CheckRunActivityType+parseCheckRunActivityType t =+  maybe (Left [i|Unknown CheckRunActivityType: #{t}|]) Right $+    inverseMap renderCheckRunActivityType t++data DiscussionActivityType+  = DiscussionAnswered+  | DiscussionCategoryChanged+  | DiscussionCreated+  | DiscussionDeleted+  | DiscussionEdited+  | DiscussionLabeled+  | DiscussionLocked+  | DiscussionPinned+  | DiscussionTransferred+  | DiscussionUnanswered+  | DiscussionUnlabeled+  | DiscussionUnlocked+  | DiscussionUnpinned+  deriving stock (Bounded, Enum, Eq, Generic, Ord, Show)++instance FromJSON DiscussionActivityType where+  parseJSON =+    Aeson.withText "DiscussionActivityType" $+      hoistFail' . parseDiscussionActivityType++instance ToJSON DiscussionActivityType where+  toJSON = Aeson.String . renderDiscussionActivityType++renderDiscussionActivityType :: DiscussionActivityType -> Text+renderDiscussionActivityType = \case+  DiscussionAnswered -> "answered"+  DiscussionCategoryChanged -> "category_changed"+  DiscussionCreated -> "created"+  DiscussionDeleted -> "deleted"+  DiscussionEdited -> "edited"+  DiscussionLabeled -> "labeled"+  DiscussionLocked -> "locked"+  DiscussionPinned -> "pinned"+  DiscussionTransferred -> "transferred"+  DiscussionUnanswered -> "unanswered"+  DiscussionUnlabeled -> "unlabeled"+  DiscussionUnlocked -> "unlocked"+  DiscussionUnpinned -> "unpinned"++parseDiscussionActivityType :: Text -> Either String DiscussionActivityType+parseDiscussionActivityType t =+  maybe (Left [i|Unknown DiscussionActivityType: #{t}|]) Right $+    inverseMap renderDiscussionActivityType t++data DiscussionCommentActivityType+  = DiscussionCommentCreated+  | DiscussionCommentDeleted+  | DiscussionCommentEdited+  deriving stock (Bounded, Enum, Eq, Generic, Ord, Show)++instance FromJSON DiscussionCommentActivityType where+  parseJSON =+    Aeson.withText "DiscussionCommentActivityType" $+      hoistFail' . parseDiscussionCommentActivityType++instance ToJSON DiscussionCommentActivityType where+  toJSON = Aeson.String . renderDiscussionCommentActivityType++renderDiscussionCommentActivityType :: DiscussionCommentActivityType -> Text+renderDiscussionCommentActivityType = \case+  DiscussionCommentCreated -> "created"+  DiscussionCommentDeleted -> "deleted"+  DiscussionCommentEdited -> "edited"++parseDiscussionCommentActivityType :: Text -> Either String DiscussionCommentActivityType+parseDiscussionCommentActivityType t =+  maybe (Left [i|Unknown DiscussionCommentActivityType: #{t}|]) Right $+    inverseMap renderDiscussionCommentActivityType t++data IssueCommentActivityType+  = IssueCommentCreated+  | IssueCommentDeleted+  | IssueCommentEdited+  deriving stock (Bounded, Enum, Eq, Generic, Ord, Show)++instance FromJSON IssueCommentActivityType where+  parseJSON =+    Aeson.withText "IssueCommentActivityType" $+      hoistFail' . parseIssueCommentActivityType++instance ToJSON IssueCommentActivityType where+  toJSON = Aeson.String . renderIssueCommentActivityType++renderIssueCommentActivityType :: IssueCommentActivityType -> Text+renderIssueCommentActivityType = \case+  IssueCommentCreated -> "created"+  IssueCommentDeleted -> "deleted"+  IssueCommentEdited -> "edited"++parseIssueCommentActivityType :: Text -> Either String IssueCommentActivityType+parseIssueCommentActivityType t =+  maybe (Left [i|Unknown IssueCommentActivityType: #{t}|]) Right $+    inverseMap renderIssueCommentActivityType t++data IssuesActivityType+  = IssuesAssigned+  | IssuesClosed+  | IssuesDeleted+  | IssuesDemilestoned+  | IssuesEdited+  | IssuesLabeled+  | IssuesLocked+  | IssuesMilestoned+  | IssuesOpened+  | IssuesPinned+  | IssuesReopened+  | IssuesTransferred+  | IssuesUnassigned+  | IssuesUnlabeled+  | IssuesUnlocked+  | IssuesUnpinned+  deriving stock (Bounded, Enum, Eq, Generic, Ord, Show)++instance FromJSON IssuesActivityType where+  parseJSON =+    Aeson.withText "IssuesActivityType" $+      hoistFail' . parseIssuesActivityType++instance ToJSON IssuesActivityType where+  toJSON = Aeson.String . renderIssuesActivityType++renderIssuesActivityType :: IssuesActivityType -> Text+renderIssuesActivityType = \case+  IssuesAssigned -> "assigned"+  IssuesClosed -> "closed"+  IssuesDeleted -> "deleted"+  IssuesDemilestoned -> "demilestoned"+  IssuesEdited -> "edited"+  IssuesLabeled -> "labeled"+  IssuesLocked -> "locked"+  IssuesMilestoned -> "milestoned"+  IssuesOpened -> "opened"+  IssuesPinned -> "pinned"+  IssuesReopened -> "reopened"+  IssuesTransferred -> "transferred"+  IssuesUnassigned -> "unassigned"+  IssuesUnlabeled -> "unlabeled"+  IssuesUnlocked -> "unlocked"+  IssuesUnpinned -> "unpinned"++parseIssuesActivityType :: Text -> Either String IssuesActivityType+parseIssuesActivityType t =+  maybe (Left [i|Unknown IssuesActivityType: #{t}|]) Right $+    inverseMap renderIssuesActivityType t++data LabelActivityType+  = LabelCreated+  | LabelDeleted+  | LabelEdited+  deriving stock (Bounded, Enum, Eq, Generic, Ord, Show)++instance FromJSON LabelActivityType where+  parseJSON =+    Aeson.withText "LabelActivityType" $+      hoistFail' . parseLabelActivityType++instance ToJSON LabelActivityType where+  toJSON = Aeson.String . renderLabelActivityType++renderLabelActivityType :: LabelActivityType -> Text+renderLabelActivityType = \case+  LabelCreated -> "created"+  LabelDeleted -> "deleted"+  LabelEdited -> "edited"++parseLabelActivityType :: Text -> Either String LabelActivityType+parseLabelActivityType t =+  maybe (Left [i|Unknown LabelActivityType: #{t}|]) Right $+    inverseMap renderLabelActivityType t++data MilestoneActivityType+  = MilestoneCreated+  | MilestoneClosed+  | MilestoneDeleted+  | MilestoneEdited+  | MilestoneOpened+  deriving stock (Bounded, Enum, Eq, Generic, Ord, Show)++instance FromJSON MilestoneActivityType where+  parseJSON =+    Aeson.withText "MilestoneActivityType" $+      hoistFail' . parseMilestoneActivityType++instance ToJSON MilestoneActivityType where+  toJSON = Aeson.String . renderMilestoneActivityType++renderMilestoneActivityType :: MilestoneActivityType -> Text+renderMilestoneActivityType = \case+  MilestoneCreated -> "created"+  MilestoneClosed -> "closed"+  MilestoneDeleted -> "deleted"+  MilestoneEdited -> "edited"+  MilestoneOpened -> "opened"++parseMilestoneActivityType :: Text -> Either String MilestoneActivityType+parseMilestoneActivityType t =+  maybe (Left [i|Unknown MilestoneActivityType: #{t}|]) Right $+    inverseMap renderMilestoneActivityType t++-- | Activity types for pull request trigger events.+--+-- These specify which pull request activities should trigger the workflow.+-- Common combinations include opened/synchronized for CI workflows, or+-- closed for deployment workflows.+--+-- Example usage:+--+-- @+-- -- Trigger on pull request creation and updates+-- ciTrigger :: PullRequestActivityType+-- ciTrigger = PullRequestTrigger $ PullRequestTriggerAttributes+--  { activityTypes = Just (PullRequestOpened :| [PullRequestSynchronize])+--  , branches = Nothing+--  , branchesIgnore = Nothing+--  , paths = Nothing+--  , pathsIgnore = Nothing+--  }+-- @+--+-- For more details, see: <https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request>+data PullRequestActivityType+  = -- | Pull request was assigned to someone+    PullRequestAssigned+  | -- | Auto-merge was disabled+    PullRequestAutoMergeDisabled+  | -- | Auto-merge was enabled+    PullRequestAutoMergeEnabled+  | -- | Pull request was closed+    PullRequestClosed+  | -- | Pull request was converted to draft+    PullRequestConvertedToDraft+  | -- | Milestone was removed from pull request+    PullRequestDemilestoned+  | -- | Pull request was removed from merge queue+    PullRequestDequeued+  | -- | Pull request title or body was edited+    PullRequestEdited+  | -- | Pull request was added to merge queue+    PullRequestEnqueued+  | -- | Label was added to pull request+    PullRequestLabeled+  | -- | Pull request conversation was locked+    PullRequestLocked+  | -- | Milestone was added to pull request+    PullRequestMilestoned+  | -- | Pull request was opened+    PullRequestOpened+  | -- | Draft pull request was marked ready for review+    PullRequestReadyForReview+  | -- | Pull request was reopened+    PullRequestReopened+  | -- | Review request was removed+    PullRequestReviewRequestRemoved+  | -- | Review was requested+    PullRequestReviewRequested+  | -- | Pull request's head branch was updated+    PullRequestSynchronize+  | -- | Pull request was unassigned+    PullRequestUnassigned+  | -- | Label was removed from pull request+    PullRequestUnlabeled+  | -- | Pull request conversation was unlocked+    PullRequestUnlocked+  deriving stock (Bounded, Enum, Eq, Generic, Ord, Show)++instance FromJSON PullRequestActivityType where+  parseJSON =+    Aeson.withText "PullRequestActivityType" $+      hoistFail' . parsePullRequestActivityType++instance ToJSON PullRequestActivityType where+  toJSON = Aeson.String . renderPullRequestActivityType++renderPullRequestActivityType :: PullRequestActivityType -> Text+renderPullRequestActivityType = \case+  PullRequestAssigned -> "assigned"+  PullRequestAutoMergeDisabled -> "auto_merge_disabled"+  PullRequestAutoMergeEnabled -> "auto_merge_enabled"+  PullRequestClosed -> "closed"+  PullRequestConvertedToDraft -> "converted_to_draft"+  PullRequestDemilestoned -> "demilestoned"+  PullRequestDequeued -> "dequeued"+  PullRequestEdited -> "edited"+  PullRequestEnqueued -> "enqueued"+  PullRequestLabeled -> "labeled"+  PullRequestLocked -> "locked"+  PullRequestMilestoned -> "milestoned"+  PullRequestOpened -> "opened"+  PullRequestReadyForReview -> "ready_for_review"+  PullRequestReopened -> "reopened"+  PullRequestReviewRequestRemoved -> "review_request_removed"+  PullRequestReviewRequested -> "review_requested"+  PullRequestSynchronize -> "synchronize"+  PullRequestUnassigned -> "unassigned"+  PullRequestUnlabeled -> "unlabeled"+  PullRequestUnlocked -> "unlocked"++parsePullRequestActivityType :: Text -> Either String PullRequestActivityType+parsePullRequestActivityType t =+  maybe (Left [i|Unknown PullRequestActivityType: #{t}|]) Right $+    inverseMap renderPullRequestActivityType t++data PullRequestTriggerAttributes = PullRequestTriggerAttributes+  { activityTypes :: Maybe (NonEmpty PullRequestActivityType),+    branches :: Maybe (NonEmpty Text),+    branchesIgnore :: Maybe (NonEmpty Text),+    paths :: Maybe (NonEmpty Text),+    pathsIgnore :: Maybe (NonEmpty Text)+  }+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON PullRequestTriggerAttributes where+  parseJSON = Aeson.withObject "PullRequestTriggerAttributes" $ \o -> do+    activityTypes <- o .:? "types"+    branches <- o .:? "branches"+    branchesIgnore <- o .:? "branches-ignore"+    paths <- o .:? "paths"+    pathsIgnore <- o .:? "paths-ignore"+    pure PullRequestTriggerAttributes {..}++instance ToJSON PullRequestTriggerAttributes where+  toJSON PullRequestTriggerAttributes {..} =+    Aeson.object $+      catMaybes+        [ ("branches" .=) <$> branches,+          ("branches-ignore" .=) <$> branchesIgnore,+          ("paths" .=) <$> paths,+          ("paths-ignore" .=) <$> pathsIgnore,+          ("types" .=) <$> activityTypes+        ]++data PullRequestReviewActivityType+  = PullRequestReviewDismissed+  | PullRequestReviewEdited+  | PullRequestReviewSubmitted+  deriving stock (Bounded, Enum, Eq, Generic, Ord, Show)++instance FromJSON PullRequestReviewActivityType where+  parseJSON =+    Aeson.withText "PullRequestReviewActivityType" $+      hoistFail' . parsePullRequestReviewActivityType++instance ToJSON PullRequestReviewActivityType where+  toJSON = Aeson.String . renderPullRequestReviewActivityType++renderPullRequestReviewActivityType :: PullRequestReviewActivityType -> Text+renderPullRequestReviewActivityType = \case+  PullRequestReviewDismissed -> "dismissed"+  PullRequestReviewEdited -> "edited"+  PullRequestReviewSubmitted -> "submitted"++parsePullRequestReviewActivityType :: Text -> Either String PullRequestReviewActivityType+parsePullRequestReviewActivityType t =+  maybe (Left [i|Unknown PullRequestReviewActivityType: #{t}|]) Right $+    inverseMap renderPullRequestReviewActivityType t++data PullRequestReviewCommentActivityType+  = PullRequestReviewCommentCreated+  | PullRequestReviewCommentDeleted+  | PullRequestReviewCommentEdited+  deriving stock (Bounded, Enum, Eq, Generic, Ord, Show)++instance FromJSON PullRequestReviewCommentActivityType where+  parseJSON =+    Aeson.withText "PullRequestReviewCommentActivityType" $+      hoistFail' . parsePullRequestReviewCommentActivityType++instance ToJSON PullRequestReviewCommentActivityType where+  toJSON = Aeson.String . renderPullRequestReviewCommentActivityType++renderPullRequestReviewCommentActivityType :: PullRequestReviewCommentActivityType -> Text+renderPullRequestReviewCommentActivityType = \case+  PullRequestReviewCommentCreated -> "created"+  PullRequestReviewCommentDeleted -> "deleted"+  PullRequestReviewCommentEdited -> "edited"++parsePullRequestReviewCommentActivityType :: Text -> Either String PullRequestReviewCommentActivityType+parsePullRequestReviewCommentActivityType t =+  maybe (Left [i|Unknown PullRequestReviewCommentActivityType: #{t}|]) Right $+    inverseMap renderPullRequestReviewCommentActivityType t++data PullRequestTargetActivityType+  = PullRequestTargetAssigned+  | PullRequestTargetAutoMergeDisabled+  | PullRequestTargetAutoMergeEnabled+  | PullRequestTargetClosed+  | PullRequestTargetConvertedToDraft+  | PullRequestTargetEdited+  | PullRequestTargetLabeled+  | PullRequestTargetLocked+  | PullRequestTargetOpened+  | PullRequestTargetReadyForReview+  | PullRequestTargetReopened+  | PullRequestTargetReviewRequestRemoved+  | PullRequestTargetReviewRequested+  | PullRequestTargetSynchronize+  | PullRequestTargetUnassigned+  | PullRequestTargetUnlabeled+  | PullRequestTargetUnlocked+  deriving stock (Bounded, Enum, Eq, Generic, Ord, Show)++instance FromJSON PullRequestTargetActivityType where+  parseJSON =+    Aeson.withText "PullRequestTargetActivityType" $+      hoistFail' . parsePullRequestTargetActivityType++instance ToJSON PullRequestTargetActivityType where+  toJSON = Aeson.String . renderPullRequestTargetActivityType++renderPullRequestTargetActivityType :: PullRequestTargetActivityType -> Text+renderPullRequestTargetActivityType = \case+  PullRequestTargetAssigned -> "assigned"+  PullRequestTargetAutoMergeDisabled -> "auto_merge_disabled"+  PullRequestTargetAutoMergeEnabled -> "auto_merge_enabled"+  PullRequestTargetClosed -> "closed"+  PullRequestTargetConvertedToDraft -> "converted_to_draft"+  PullRequestTargetEdited -> "edited"+  PullRequestTargetLabeled -> "labeled"+  PullRequestTargetLocked -> "locked"+  PullRequestTargetOpened -> "opened"+  PullRequestTargetReadyForReview -> "ready_for_review"+  PullRequestTargetReopened -> "reopened"+  PullRequestTargetReviewRequestRemoved -> "review_request_removed"+  PullRequestTargetReviewRequested -> "review_requested"+  PullRequestTargetSynchronize -> "synchronize"+  PullRequestTargetUnassigned -> "unassigned"+  PullRequestTargetUnlabeled -> "unlabeled"+  PullRequestTargetUnlocked -> "unlocked"++parsePullRequestTargetActivityType :: Text -> Either String PullRequestTargetActivityType+parsePullRequestTargetActivityType t =+  maybe (Left [i|Unknown PullRequestTargetActivityType: #{t}|]) Right $+    inverseMap renderPullRequestTargetActivityType t++data PullRequestTargetTriggerAttributes = PullRequestTargetTriggerAttributes+  { activityTypes :: Maybe (NonEmpty PullRequestTargetActivityType),+    branches :: Maybe (NonEmpty Text),+    branchesIgnore :: Maybe (NonEmpty Text),+    paths :: Maybe (NonEmpty Text),+    pathsIgnore :: Maybe (NonEmpty Text)+  }+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON PullRequestTargetTriggerAttributes where+  parseJSON = Aeson.withObject "PullRequestTargetTriggerAttributes" $ \o -> do+    activityTypes <- o .:? "types"+    branches <- o .:? "branches"+    branchesIgnore <- o .:? "branches-ignore"+    paths <- o .:? "paths"+    pathsIgnore <- o .:? "paths-ignore"+    pure PullRequestTargetTriggerAttributes {..}++instance ToJSON PullRequestTargetTriggerAttributes where+  toJSON PullRequestTargetTriggerAttributes {..} =+    Aeson.object $+      catMaybes+        [ ("branches" .=) <$> branches,+          ("branches-ignore" .=) <$> branchesIgnore,+          ("paths" .=) <$> paths,+          ("paths-ignore" .=) <$> pathsIgnore,+          ("types" .=) <$> activityTypes+        ]++-- | Configuration attributes for push trigger events.+--+-- Push triggers can be filtered by branches, paths, and tags to control exactly+-- when the workflow should run. This allows for fine-grained control over which+-- repository changes should initiate workflow execution.+--+-- Example usage:+--+-- @+-- -- Trigger on pushes to main or develop branches+-- pushMainDevelop :: PushTriggerAttributes+-- pushMainDevelop = PushTriggerAttributes+--  { branches = Just ("main" :| ["develop"])+--  , branchesIgnore = Nothing+--  , paths = Nothing+--  , pathsIgnore = Nothing+--  , tags = Nothing+--  }+--+-- -- Trigger on pushes to docs directory, ignoring gh-pages+-- pushDocsOnly :: PushTriggerAttributes+-- pushDocsOnly = PushTriggerAttributes+--  { branches = Nothing+--  , branchesIgnore = Just ("gh-pages" :| [])+--  , paths = Just ("docs\/**" :| [])+--  , pathsIgnore = Nothing+--  , tags = Nothing+--  }+-- @+--+-- For more details, see: <https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#push>+data PushTriggerAttributes = PushTriggerAttributes+  { -- | Branches to include (default: all)+    branches :: Maybe (NonEmpty Text),+    -- | Branches to exclude+    branchesIgnore :: Maybe (NonEmpty Text),+    -- | File paths to include (default: all)+    paths :: Maybe (NonEmpty Text),+    -- | File paths to exclude+    pathsIgnore :: Maybe (NonEmpty Text),+    -- | Tags to include+    tags :: Maybe (NonEmpty Text)+  }+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON PushTriggerAttributes where+  parseJSON = Aeson.withObject "PushTriggerAttributes" $ \o -> do+    branches <- o .:? "branches"+    branchesIgnore <- o .:? "branches-ignore"+    paths <- o .:? "paths"+    pathsIgnore <- o .:? "paths-ignore"+    tags <- o .:? "tags"+    pure PushTriggerAttributes {..}++instance ToJSON PushTriggerAttributes where+  toJSON PushTriggerAttributes {..} =+    Aeson.object $+      catMaybes+        [ ("branches" .=) <$> branches,+          ("branches-ignore" .=) <$> branchesIgnore,+          ("paths" .=) <$> paths,+          ("paths-ignore" .=) <$> pathsIgnore,+          ("tags" .=) <$> tags+        ]++data RegistryPackageActivityType+  = RegistryPackagePublished+  | RegistryPackageUpdated+  deriving stock (Bounded, Enum, Eq, Generic, Ord, Show)++instance FromJSON RegistryPackageActivityType where+  parseJSON =+    Aeson.withText "RegistryPackageActivityType" $+      hoistFail' . parseRegistryPackageActivityType++instance ToJSON RegistryPackageActivityType where+  toJSON = Aeson.String . renderRegistryPackageActivityType++renderRegistryPackageActivityType :: RegistryPackageActivityType -> Text+renderRegistryPackageActivityType = \case+  RegistryPackagePublished -> "published"+  RegistryPackageUpdated -> "updated"++parseRegistryPackageActivityType :: Text -> Either String RegistryPackageActivityType+parseRegistryPackageActivityType t =+  maybe (Left [i|Unknown RegistryPackageActivityType: #{t}|]) Right $+    inverseMap renderRegistryPackageActivityType t++data ReleaseActivityType+  = ReleaseCreated+  | ReleaseDeleted+  | ReleaseEdited+  | ReleasePrereleased+  | ReleasePublished+  | ReleaseReleased+  | ReleaseUnpublished+  deriving stock (Bounded, Enum, Eq, Generic, Ord, Show)++instance FromJSON ReleaseActivityType where+  parseJSON =+    Aeson.withText "ReleaseActivityType" $ hoistFail' . parseReleaseActivityType++instance ToJSON ReleaseActivityType where+  toJSON = Aeson.String . renderReleaseActivityType++renderReleaseActivityType :: ReleaseActivityType -> Text+renderReleaseActivityType = \case+  ReleaseCreated -> "created"+  ReleaseDeleted -> "deleted"+  ReleaseEdited -> "edited"+  ReleasePrereleased -> "prereleased"+  ReleasePublished -> "published"+  ReleaseReleased -> "released"+  ReleaseUnpublished -> "unpublished"++parseReleaseActivityType :: Text -> Either String ReleaseActivityType+parseReleaseActivityType t =+  maybe (Left [i|Unknown ReleaseActivityType: #{t}|]) Right $+    inverseMap renderReleaseActivityType t++data WorkflowCallInputType+  = WorkflowCallInputTypeBoolean+  | WorkflowCallInputTypeNumber+  | WorkflowCallInputTypeString+  deriving stock (Bounded, Enum, Eq, Generic, Ord, Show)++instance FromJSON WorkflowCallInputType where+  parseJSON =+    Aeson.withText "WorkflowCallInputType" $ hoistFail' . parseWorkflowCallInputType++instance ToJSON WorkflowCallInputType where+  toJSON = Aeson.String . renderWorkflowCallInputType++renderWorkflowCallInputType :: WorkflowCallInputType -> Text+renderWorkflowCallInputType = \case+  WorkflowCallInputTypeBoolean -> "boolean"+  WorkflowCallInputTypeNumber -> "number"+  WorkflowCallInputTypeString -> "string"++parseWorkflowCallInputType :: Text -> Either String WorkflowCallInputType+parseWorkflowCallInputType t =+  maybe (Left [i|Unknown WorkflowCallInputType: #{t}|]) Right $+    inverseMap renderWorkflowCallInputType t++data WorkflowCallInput = WorkflowCallInput+  { description :: Maybe Text,+    default_ :: Maybe Aeson.Value,+    required :: Maybe Bool,+    type_ :: WorkflowCallInputType+  }+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON WorkflowCallInput where+  parseJSON = Aeson.withObject "WorkflowCallInput" $ \o -> do+    description <- o .:? "description"+    default_ <- o .:? "default"+    required <- o .:? "required"+    type_ <- o .: "type"+    pure WorkflowCallInput {..}++instance ToJSON WorkflowCallInput where+  toJSON WorkflowCallInput {..} =+    Aeson.object $+      catMaybes+        [ ("description" .=) <$> description,+          ("default" .=) <$> default_,+          ("required" .=) <$> required,+          Just $ "type" .= type_+        ]++data WorkflowCallOutput = WorkflowCallOutput+  { description :: Maybe Text,+    value :: Text+  }+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON WorkflowCallOutput where+  parseJSON = Aeson.withObject "WorkflowCallOutput" $ \o -> do+    description <- o .:? "description"+    value <- o .: "value"+    pure WorkflowCallOutput {..}++instance ToJSON WorkflowCallOutput where+  toJSON WorkflowCallOutput {..} =+    Aeson.object $+      catMaybes+        [ ("description" .=) <$> description,+          Just $ "value" .= value+        ]++data WorkflowCallSecret = WorkflowCallSecret+  { description :: Maybe Text,+    required :: Maybe Bool+  }+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON WorkflowCallSecret where+  parseJSON = Aeson.withObject "WorkflowCallSecret" $ \o -> do+    description <- o .:? "description"+    required <- o .:? "required"+    pure WorkflowCallSecret {..}++instance ToJSON WorkflowCallSecret where+  toJSON WorkflowCallSecret {..} =+    Aeson.object $+      catMaybes+        [ ("description" .=) <$> description,+          ("required" .=) <$> required+        ]++data WorkflowCallAttributes = WorkflowCallAttributes+  { inputs :: Maybe (Map Text WorkflowCallInput),+    outputs :: Maybe (Map Text WorkflowCallOutput),+    secrets :: Maybe (Map Text WorkflowCallSecret)+  }+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON WorkflowCallAttributes where+  parseJSON = Aeson.withObject "WorkflowCallAttributes" $ \o -> do+    inputs <- o .:? "inputs"+    outputs <- o .:? "outputs"+    secrets <- o .:? "secrets"+    pure WorkflowCallAttributes {..}++instance ToJSON WorkflowCallAttributes where+  toJSON WorkflowCallAttributes {..} =+    Aeson.object $+      catMaybes+        [ ("inputs" .=) <$> inputs,+          ("outputs" .=) <$> outputs,+          ("secrets" .=) <$> secrets+        ]++data WorkflowDispatchInputType+  = WorkflowDispatchInputTypeBoolean+  | WorkflowDispatchInputTypeChoice (NonEmpty Text)+  | WorkflowDispatchInputTypeEnvironment+  | WorkflowDispatchInputTypeNumber+  | WorkflowDispatchInputTypeString+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON WorkflowDispatchInputType where+  parseJSON = Aeson.withObject "WorkflowDispatchInputType" $ \o -> do+    t :: Text <- o .: "type"+    maybeOptions <- o .:? "options"+    case t of+      "boolean" -> pure WorkflowDispatchInputTypeBoolean+      "choice" -> case maybeOptions of+        Just (op :| ops) -> pure $ WorkflowDispatchInputTypeChoice $ op :| ops+        Nothing -> fail "Expected a non-empty list of options"+      "environment" -> pure WorkflowDispatchInputTypeEnvironment+      "number" -> pure WorkflowDispatchInputTypeNumber+      "string" -> pure WorkflowDispatchInputTypeString+      _ -> fail [i|Unknown WorkflowDispatchInputType: #{t}|]++data WorkflowDispatchInput = WorkflowDispatchInput+  { description :: Maybe Text,+    default_ :: Maybe Aeson.Value,+    required :: Maybe Bool,+    type_ :: Maybe WorkflowDispatchInputType+  }+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON WorkflowDispatchInput where+  parseJSON = Aeson.withObject "WorkflowDispatchInput" $ \o -> do+    description <- o .:? "description"+    default_ <- o .:? "default"+    required <- o .:? "required"+    workflowDispatchInputTypeText :: Maybe Text <- o .:? "type"+    type_ <-+      maybe+        (pure Nothing)+        (const . Aeson.parseJSON $ Aeson.Object o)+        workflowDispatchInputTypeText+    pure WorkflowDispatchInput {..}++instance ToJSON WorkflowDispatchInput where+  toJSON WorkflowDispatchInput {..} =+    Aeson.object $+      catMaybes+        [ ("description" .=) <$> description,+          ("default" .=) <$> default_,+          ("required" .=) <$> required,+          ("type" .=)+            . ( \case+                  WorkflowDispatchInputTypeBoolean ->+                    "boolean" :: Text+                  WorkflowDispatchInputTypeChoice _ ->+                    "choice"+                  WorkflowDispatchInputTypeEnvironment ->+                    "environment"+                  WorkflowDispatchInputTypeNumber ->+                    "number"+                  WorkflowDispatchInputTypeString ->+                    "string"+              )+            <$> type_,+          type_+            >>= ( \case+                    WorkflowDispatchInputTypeChoice ops ->+                      Just $ "options" .= ops+                    _ ->+                      Nothing+                )+        ]++newtype WorkflowDispatchAttributes = WorkflowDispatchAttributes+  { inputs :: Maybe (Map Text WorkflowDispatchInput)+  }+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON WorkflowDispatchAttributes where+  parseJSON = Aeson.withObject "WorkflowDispatchAttributes" $ \o -> do+    inputs <- o .:? "inputs"+    pure WorkflowDispatchAttributes {..}++instance ToJSON WorkflowDispatchAttributes where+  toJSON WorkflowDispatchAttributes {..} =+    Aeson.object $+      catMaybes+        [ ("inputs" .=) <$> inputs+        ]++data WorkflowRunActivityType+  = WorkflowRunCompleted+  | WorkflowRunInProgress+  | WorkflowRunRequested+  deriving stock (Bounded, Enum, Eq, Generic, Ord, Show)++instance FromJSON WorkflowRunActivityType where+  parseJSON =+    Aeson.withText "WorkflowRunActivityType" $ hoistFail' . parseWorkflowRunActivityType++instance ToJSON WorkflowRunActivityType where+  toJSON = Aeson.String . renderWorkflowRunActivityType++renderWorkflowRunActivityType :: WorkflowRunActivityType -> Text+renderWorkflowRunActivityType = \case+  WorkflowRunCompleted -> "completed"+  WorkflowRunInProgress -> "in_progress"+  WorkflowRunRequested -> "requested"++parseWorkflowRunActivityType :: Text -> Either String WorkflowRunActivityType+parseWorkflowRunActivityType t =+  maybe (Left [i|Unknown WorkflowRunActivityType: #{t}|]) Right $+    inverseMap renderWorkflowRunActivityType t++data WorkflowRunTriggerAttributes = WorkflowRunTriggerAttributes+  { activityTypes :: NonEmpty WorkflowRunActivityType,+    workflows :: Maybe (NonEmpty Text),+    branches :: Maybe (NonEmpty Text),+    branchesIgnore :: Maybe (NonEmpty Text)+  }+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON WorkflowRunTriggerAttributes where+  parseJSON = Aeson.withObject "WorkflowRunTriggerAttributes" $ \o -> do+    activityTypes <- o .: "types"+    workflows <- o .:? "workflows"+    branches <- o .:? "branches"+    branchesIgnore <- o .:? "branches-ignore"+    pure WorkflowRunTriggerAttributes {..}++instance ToJSON WorkflowRunTriggerAttributes where+  toJSON WorkflowRunTriggerAttributes {..} =+    Aeson.object $+      catMaybes+        [ Just $ "types" .= activityTypes,+          ("workflows" .=) <$> workflows,+          ("branches" .=) <$> branches,+          ("branches-ignore" .=) <$> branchesIgnore+        ]++-- | Comprehensive enumeration of all GitHub Actions workflow trigger events.+--+-- Each trigger corresponds to a specific GitHub event that can initiate a workflow run.+-- Many triggers can be configured with activity types to specify exactly which sub-events+-- should cause the workflow to run.+--+-- Common trigger examples:+--+-- * 'PushTrigger' - Runs on pushes to repository branches or tags+-- * 'PullRequestTrigger' - Runs on pull request events (open, sync, close, etc.)+-- * 'ScheduleTrigger' - Runs on a schedule using cron syntax+-- * 'WorkflowDispatchTrigger' - Allows manual workflow execution+-- * 'IssuesTrigger' - Runs on issue events (open, close, label, etc.)+--+-- For triggers with complex configuration, attribute types provide filtering options:+--+-- @+-- -- Push trigger with branch filtering+-- pushToMain :: WorkflowTrigger+-- pushToMain = PushTrigger $ PushTriggerAttributes+--  { branches = Just ("main" :| [])+--  , branchesIgnore = Nothing+--  , paths = Nothing+--  , pathsIgnore = Nothing+--  , tags = Nothing+--  }+--+-- -- Issue trigger for specific activity types+-- issueEvents :: WorkflowTrigger+-- issueEvents = IssuesTrigger (IssuesOpened :| [IssuesClosed, IssuesLabeled])+-- @+--+-- For complete event documentation, see: <https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows>+data WorkflowTrigger+  = -- | Branch protection rule events+    BranchProtectionRuleTrigger (NonEmpty BranchProtectionRuleActivityType)+  | -- | Check run events+    CheckRunTrigger (NonEmpty CheckRunActivityType)+  | -- | Check suite completion+    CheckSuiteCompletedTrigger+  | -- | Branch or tag creation+    CreateTrigger+  | -- | Branch or tag deletion+    DeleteTrigger+  | -- | Deployment events+    DeploymentTrigger+  | -- | Deployment status changes+    DeploymentStatusTrigger+  | -- | Discussion events+    DiscussionTrigger (NonEmpty DiscussionActivityType)+  | -- | Discussion comment events+    DiscussionCommentTrigger (NonEmpty DiscussionCommentActivityType)+  | -- | Repository fork events+    ForkTrigger+  | -- | Wiki page events+    GollumTrigger+  | -- | Issue comment events+    IssueCommentTrigger (NonEmpty IssueCommentActivityType)+  | -- | Issue events+    IssuesTrigger (NonEmpty IssuesActivityType)+  | -- | Label events+    LabelTrigger (NonEmpty LabelActivityType)+  | -- | Merge group check requests+    MergeGroupChecksRequestedTrigger+  | -- | Milestone events+    MilestoneTrigger (NonEmpty MilestoneActivityType)+  | -- | GitHub Pages build events+    PageBuildTrigger+  | -- | Repository publicity changes+    PublicTrigger+  | -- | Pull request events with filtering+    PullRequestTrigger PullRequestTriggerAttributes+  | -- | Pull request review events+    PullRequestReviewTrigger (NonEmpty PullRequestReviewActivityType)+  | -- | PR review comment events+    PullRequestReviewCommentTrigger (NonEmpty PullRequestReviewCommentActivityType)+  | -- | Pull request target events with filtering+    PullRequestTargetTrigger PullRequestTargetTriggerAttributes+  | -- | Push events with filtering+    PushTrigger PushTriggerAttributes+  | -- | Package registry events+    RegistryPackageTrigger (NonEmpty RegistryPackageActivityType)+  | -- | Release events+    ReleaseTrigger (NonEmpty ReleaseActivityType)+  | -- | External repository dispatch events+    RepositoryDispatchTrigger (NonEmpty Text)+  | -- | Scheduled events (cron expressions)+    ScheduleTrigger (NonEmpty Text)+  | -- | Commit status events+    StatusTrigger+  | -- | Repository watch events+    WatchStartedTrigger+  | -- | Reusable workflow calls+    WorkflowCallTrigger WorkflowCallAttributes+  | -- | Manual workflow dispatch+    WorkflowDispatchTrigger WorkflowDispatchAttributes+  | -- | Workflow run events with filtering+    WorkflowRunTrigger WorkflowRunTriggerAttributes+  deriving stock (Eq, Generic, Ord, Show)++instance FromJSON WorkflowTrigger where+  parseJSON = Aeson.withObject "WorkflowTrigger" $ \o -> do+    maybeBranchProtectionRuleActivityTypes <-+      maybeActivityTypesInNestedObject o "branch_protection_rule"+    maybeCheckRunActivityTypes <- maybeActivityTypesInNestedObject o "check_run"+    maybeCheckSuiteActivityTypes :: Maybe (NonEmpty Text) <-+      maybeActivityTypesInNestedObject o "check_suite"+    maybeCreate :: Maybe Aeson.Value <- o .:? "create"+    maybeDelete :: Maybe Aeson.Value <- o .:? "delete"+    maybeDeployment :: Maybe Aeson.Value <- o .:? "deployment"+    maybeDeploymentStatus :: Maybe Aeson.Value <- o .:? "deployment_status"+    maybeDiscussionActivityTypes <-+      maybeActivityTypesInNestedObject o "discussion"+    maybeDiscussionCommentActivityTypes <-+      maybeActivityTypesInNestedObject o "discussion_comment"+    maybeFork :: Maybe Aeson.Value <- o .:? "fork"+    maybeGollum :: Maybe Aeson.Value <- o .:? "gollum"+    maybeIssueCommentActivityTypes <-+      maybeActivityTypesInNestedObject o "issue_comment"+    maybeIssuesActivityTypes <- maybeActivityTypesInNestedObject o "issues"+    maybeLabelActivityTypes <- maybeActivityTypesInNestedObject o "label"+    maybeMergeGroupChecksRequestedActivityTypes :: Maybe (NonEmpty Text) <-+      maybeActivityTypesInNestedObject o "merge_group"+    maybeMilestoneActivityTypes <- maybeActivityTypesInNestedObject o "milestone"+    maybePageBuild :: Maybe Aeson.Value <- o .:? "page_build"+    maybePublic :: Maybe Aeson.Value <- o .:? "public"+    maybePullRequestTriggerAttributes <- o .:? "pull_request"+    maybePullRequestReviewActivityTypes <-+      maybeActivityTypesInNestedObject o "pull_request_review"+    maybePullRequestReviewCommentActivityTypes <-+      maybeActivityTypesInNestedObject o "pull_request_review_comment"+    maybePullRequestTargetTriggerAttributes <- o .:? "pull_request_target"+    maybePushTriggerAttributes <- o .:? "push"+    maybeRegistryPackageActivityTypes <- maybeActivityTypesInNestedObject o "registry_package"+    maybeReleaseActivityTypes <- maybeActivityTypesInNestedObject o "release"+    maybeRepositoryDispatchActivityTypes :: Maybe (NonEmpty Text) <-+      maybeActivityTypesInNestedObject o "repository_dispatch"+    maybeScheduleCrons <- maybeScheduleCronsParser o+    maybeStatus :: Maybe Aeson.Value <- o .:? "status"+    maybeWatchStartedActivityTypes :: Maybe (NonEmpty Text) <-+      maybeActivityTypesInNestedObject o "watch"+    maybeWorkflowCall <- o .:? "workflow_call"+    maybeWorkflowDispatch <- o .:? "workflow_dispatch"+    maybeWorkflowRun <- o .:? "workflow_run"+    hoistFail'+      . fromMaybe (Left [i|Invalid workflow trigger (`on` property):\n#{AesonKeyMap.keys o}|])+      $ (Right . BranchProtectionRuleTrigger <$> maybeBranchProtectionRuleActivityTypes)+        <|> (Right . CheckRunTrigger <$> maybeCheckRunActivityTypes)+        <|> ( Right CheckSuiteCompletedTrigger+                <$ guard+                  (maybeCheckSuiteActivityTypes == Just ("completed" :| []))+            )+        <|> (Right CreateTrigger <$ guard (isJust maybeCreate))+        <|> (Right DeleteTrigger <$ guard (isJust maybeDelete))+        <|> (Right DeploymentTrigger <$ guard (isJust maybeDeployment))+        <|> (Right DeploymentStatusTrigger <$ guard (isJust maybeDeploymentStatus))+        <|> (Right . DiscussionTrigger <$> maybeDiscussionActivityTypes)+        <|> (Right . DiscussionCommentTrigger <$> maybeDiscussionCommentActivityTypes)+        <|> (Right ForkTrigger <$ guard (isJust maybeFork))+        <|> (Right GollumTrigger <$ guard (isJust maybeGollum))+        <|> (Right . IssueCommentTrigger <$> maybeIssueCommentActivityTypes)+        <|> (Right . IssuesTrigger <$> maybeIssuesActivityTypes)+        <|> (Right . LabelTrigger <$> maybeLabelActivityTypes)+        <|> ( Right MergeGroupChecksRequestedTrigger+                <$ guard+                  (maybeMergeGroupChecksRequestedActivityTypes == Just ("checks_requested" :| []))+            )+        <|> (Right . MilestoneTrigger <$> maybeMilestoneActivityTypes)+        <|> (Right PageBuildTrigger <$ guard (isJust maybePageBuild))+        <|> (Right PublicTrigger <$ guard (isJust maybePublic))+        <|> (Right . PullRequestTrigger <$> maybePullRequestTriggerAttributes)+        <|> (Right . PullRequestReviewTrigger <$> maybePullRequestReviewActivityTypes)+        <|> (Right . PullRequestReviewCommentTrigger <$> maybePullRequestReviewCommentActivityTypes)+        <|> (Right . PullRequestTargetTrigger <$> maybePullRequestTargetTriggerAttributes)+        <|> (Right . PushTrigger <$> maybePushTriggerAttributes)+        <|> (Right . RegistryPackageTrigger <$> maybeRegistryPackageActivityTypes)+        <|> (Right . ReleaseTrigger <$> maybeReleaseActivityTypes)+        <|> (Right . RepositoryDispatchTrigger <$> maybeRepositoryDispatchActivityTypes)+        <|> (Right . ScheduleTrigger <$> maybeScheduleCrons)+        <|> (Right StatusTrigger <$ guard (isJust maybeStatus))+        <|> ( Right WatchStartedTrigger+                <$ guard+                  (maybeWatchStartedActivityTypes == Just ("started" :| []))+            )+        <|> (Right . WorkflowCallTrigger <$> maybeWorkflowCall)+        <|> (Right . WorkflowDispatchTrigger <$> maybeWorkflowDispatch)+        <|> (Right . WorkflowRunTrigger <$> maybeWorkflowRun)+    where+      maybeActivityTypesInNestedObject ::+        (FromJSON a) =>+        Aeson.Object ->+        Aeson.Key ->+        Aeson.Parser (Maybe (NonEmpty a))+      maybeActivityTypesInNestedObject o attributeName =+        o+          .:? attributeName+          >>= maybe+            (pure Nothing)+            (Aeson.withObject "ActivityTypes" (.: "types"))++      maybeScheduleCronsParser :: Aeson.Object -> Aeson.Parser (Maybe (NonEmpty Text))+      maybeScheduleCronsParser o =+        o+          .:? "schedule"+          >>= maybe+            (pure Nothing)+            ( Aeson.withArray "Crons" $ \a ->+                fmap nonEmpty . for (Vector.toList a) $+                  Aeson.withObject "Cron" (.: "cron")+            )++instance ToJSONKey WorkflowTrigger where+  toJSONKey =+    Aeson.toJSONKeyText $ \case+      BranchProtectionRuleTrigger _ -> "branch_protection_rule"+      CheckRunTrigger _ -> "check_run"+      CheckSuiteCompletedTrigger -> "check_suite"+      CreateTrigger -> "create"+      DeleteTrigger -> "delete"+      DeploymentTrigger -> "deployment"+      DeploymentStatusTrigger -> "deployment_status"+      DiscussionTrigger _ -> "discussion"+      DiscussionCommentTrigger _ -> "discussion_comment"+      ForkTrigger -> "fork"+      GollumTrigger -> "gollum"+      IssueCommentTrigger _ -> "issue_comment"+      IssuesTrigger _ -> "issues"+      LabelTrigger _ -> "label"+      MergeGroupChecksRequestedTrigger -> "merge_group"+      MilestoneTrigger _ -> "milestone"+      PageBuildTrigger -> "page_build"+      PublicTrigger -> "public"+      PullRequestTrigger _ -> "pull_request"+      PullRequestReviewTrigger _ -> "pull_request_review"+      PullRequestReviewCommentTrigger _ -> "pull_request_review_comment"+      PullRequestTargetTrigger _ -> "pull_request_target"+      PushTrigger _ -> "push"+      RegistryPackageTrigger _ -> "registry_package"+      ReleaseTrigger _ -> "release"+      RepositoryDispatchTrigger _ -> "repository_dispatch"+      ScheduleTrigger _ -> "schedule"+      StatusTrigger -> "status"+      WatchStartedTrigger -> "watch"+      WorkflowCallTrigger _ -> "workflow_call"+      WorkflowDispatchTrigger _ -> "workflow_dispatch"+      WorkflowRunTrigger _ -> "workflow_run"++instance ToJSON WorkflowTrigger where+  toJSON trigger =+    Aeson.toJSON . Map.singleton trigger $ case trigger of+      BranchProtectionRuleTrigger activityTypes ->+        Aeson.object ["types" .= activityTypes]+      CheckRunTrigger activityTypes ->+        Aeson.object ["types" .= activityTypes]+      CheckSuiteCompletedTrigger ->+        Aeson.object ["types" .= [Aeson.String "completed"]]+      CreateTrigger ->+        Aeson.object []+      DeleteTrigger ->+        Aeson.object []+      DeploymentTrigger ->+        Aeson.object []+      DeploymentStatusTrigger ->+        Aeson.object []+      DiscussionTrigger activityTypes ->+        Aeson.object ["types" .= activityTypes]+      DiscussionCommentTrigger activityTypes ->+        Aeson.object ["types" .= activityTypes]+      ForkTrigger ->+        Aeson.object []+      GollumTrigger ->+        Aeson.object []+      IssueCommentTrigger activityTypes ->+        Aeson.object ["types" .= activityTypes]+      IssuesTrigger activityTypes ->+        Aeson.object ["types" .= activityTypes]+      LabelTrigger activityTypes ->+        Aeson.object ["types" .= activityTypes]+      MergeGroupChecksRequestedTrigger ->+        Aeson.object ["types" .= [Aeson.String "checks_requested"]]+      MilestoneTrigger activityTypes ->+        Aeson.object ["types" .= activityTypes]+      PageBuildTrigger ->+        Aeson.object []+      PublicTrigger ->+        Aeson.object []+      PullRequestTrigger attrs ->+        Aeson.toJSON attrs+      PullRequestReviewTrigger activityTypes ->+        Aeson.object ["types" .= activityTypes]+      PullRequestReviewCommentTrigger activityTypes ->+        Aeson.object ["types" .= activityTypes]+      PullRequestTargetTrigger attrs ->+        Aeson.toJSON attrs+      PushTrigger attrs ->+        Aeson.toJSON attrs+      RegistryPackageTrigger activityTypes ->+        Aeson.object ["types" .= activityTypes]+      ReleaseTrigger activityTypes ->+        Aeson.object ["types" .= activityTypes]+      RepositoryDispatchTrigger activityTypes ->+        Aeson.object ["types" .= activityTypes]+      ScheduleTrigger crons ->+        Aeson.toJSON $ (\cron -> Aeson.object ["cron" .= cron]) <$> crons+      StatusTrigger ->+        Aeson.object []+      WatchStartedTrigger ->+        Aeson.object ["types" .= [Aeson.String "started"]]+      WorkflowCallTrigger attrs ->+        Aeson.toJSON attrs+      WorkflowDispatchTrigger attrs ->+        Aeson.toJSON attrs+      WorkflowRunTrigger attrs ->+        Aeson.toJSON attrs++gen :: (MonadGen m) => m WorkflowTrigger+gen =+  Gen.choice+    [ BranchProtectionRuleTrigger <$> Gen.nonEmpty (Range.linear 1 3) Gen.enumBounded,+      CheckRunTrigger <$> Gen.nonEmpty (Range.linear 1 3) Gen.enumBounded,+      pure CheckSuiteCompletedTrigger,+      pure CreateTrigger,+      pure DeleteTrigger,+      pure DeploymentTrigger,+      pure DeploymentStatusTrigger,+      DiscussionTrigger <$> Gen.nonEmpty (Range.linear 1 3) Gen.enumBounded,+      DiscussionCommentTrigger <$> Gen.nonEmpty (Range.linear 1 3) Gen.enumBounded,+      pure ForkTrigger,+      pure GollumTrigger,+      IssueCommentTrigger <$> Gen.nonEmpty (Range.linear 1 3) Gen.enumBounded,+      IssuesTrigger <$> Gen.nonEmpty (Range.linear 1 3) Gen.enumBounded,+      LabelTrigger <$> Gen.nonEmpty (Range.linear 1 3) Gen.enumBounded,+      pure MergeGroupChecksRequestedTrigger,+      MilestoneTrigger <$> Gen.nonEmpty (Range.linear 1 3) Gen.enumBounded,+      pure PageBuildTrigger,+      pure PublicTrigger,+      PullRequestTrigger <$> genPullRequestTriggerAttributes,+      PullRequestReviewTrigger <$> Gen.nonEmpty (Range.linear 1 3) Gen.enumBounded,+      PullRequestReviewCommentTrigger <$> Gen.nonEmpty (Range.linear 1 3) Gen.enumBounded,+      PullRequestTargetTrigger <$> genPullRequestTargetAttributes,+      PushTrigger <$> genPushTriggerAttributes,+      RegistryPackageTrigger <$> Gen.nonEmpty (Range.linear 1 3) Gen.enumBounded,+      ReleaseTrigger <$> Gen.nonEmpty (Range.linear 1 3) Gen.enumBounded,+      RepositoryDispatchTrigger <$> Gen.nonEmpty (Range.linear 1 3) genText,+      ScheduleTrigger <$> Gen.nonEmpty (Range.linear 1 5) genText,+      pure StatusTrigger,+      pure WatchStartedTrigger,+      WorkflowCallTrigger <$> genWorkflowCallAttributes,+      WorkflowDispatchTrigger <$> genWorkflowDispatchAttributes,+      WorkflowRunTrigger <$> genWorkflowRunTriggerAttributes+    ]++genText :: (MonadGen m) => m Text+genText = Gen.text (Range.linear 1 5) Gen.alphaNum++genMap :: (MonadGen m) => m a -> m (Map Text a)+genMap ga = Gen.map (Range.linear 1 5) $ liftA2 (,) genText ga++genPullRequestTargetAttributes :: (MonadGen m) => m PullRequestTargetTriggerAttributes+genPullRequestTargetAttributes = do+  activityTypes <-+    Gen.maybe $+      Gen.nonEmpty (Range.linear 1 3) Gen.enumBounded+  branches <-+    Gen.maybe $ Gen.nonEmpty (Range.linear 1 5) genText+  branchesIgnore <-+    Gen.maybe $ Gen.nonEmpty (Range.linear 1 5) genText+  paths <-+    Gen.maybe $ Gen.nonEmpty (Range.linear 1 5) genText+  pathsIgnore <-+    Gen.maybe $ Gen.nonEmpty (Range.linear 1 5) genText+  pure PullRequestTargetTriggerAttributes {..}++genPullRequestTriggerAttributes :: (MonadGen m) => m PullRequestTriggerAttributes+genPullRequestTriggerAttributes = do+  activityTypes <-+    Gen.maybe $+      Gen.nonEmpty (Range.linear 1 3) Gen.enumBounded+  branches <-+    Gen.maybe $ Gen.nonEmpty (Range.linear 1 5) genText+  branchesIgnore <-+    Gen.maybe $ Gen.nonEmpty (Range.linear 1 5) genText+  paths <-+    Gen.maybe $ Gen.nonEmpty (Range.linear 1 5) genText+  pathsIgnore <-+    Gen.maybe $ Gen.nonEmpty (Range.linear 1 5) genText+  pure PullRequestTriggerAttributes {..}++genPushTriggerAttributes :: (MonadGen m) => m PushTriggerAttributes+genPushTriggerAttributes = do+  branches <-+    Gen.maybe $ Gen.nonEmpty (Range.linear 1 5) genText+  branchesIgnore <-+    Gen.maybe $ Gen.nonEmpty (Range.linear 1 5) genText+  paths <-+    Gen.maybe $ Gen.nonEmpty (Range.linear 1 5) genText+  pathsIgnore <-+    Gen.maybe $ Gen.nonEmpty (Range.linear 1 5) genText+  tags <-+    Gen.maybe $ Gen.nonEmpty (Range.linear 1 5) genText+  pure PushTriggerAttributes {..}++genWorkflowCallAttributes :: (MonadGen m) => m WorkflowCallAttributes+genWorkflowCallAttributes = do+  inputs <- Gen.maybe $ genMap genWorkflowCallInput+  outputs <- Gen.maybe $ genMap genWorkflowCallOutput+  secrets <- Gen.maybe $ genMap genWorkflowCallSecret+  pure WorkflowCallAttributes {..}++genWorkflowCallInput :: (MonadGen m) => m WorkflowCallInput+genWorkflowCallInput = do+  description <- Gen.maybe genText+  default_ <-+    Gen.maybe $+      Aeson.String+        <$> Gen.text (Range.linear 3 20) Gen.alphaNum+  required <- Gen.maybe Gen.bool+  type_ <- Gen.enumBounded+  pure WorkflowCallInput {..}++genWorkflowCallOutput :: (MonadGen m) => m WorkflowCallOutput+genWorkflowCallOutput = do+  description <- Gen.maybe genText+  value <- genText+  pure WorkflowCallOutput {..}++genWorkflowCallSecret :: (MonadGen m) => m WorkflowCallSecret+genWorkflowCallSecret = do+  description <- Gen.maybe genText+  required <- Gen.maybe Gen.bool+  pure WorkflowCallSecret {..}++genWorkflowDispatchAttributes :: (MonadGen m) => m WorkflowDispatchAttributes+genWorkflowDispatchAttributes = do+  inputs <- Gen.maybe $ genMap genWorkflowDispatchInput+  pure WorkflowDispatchAttributes {..}++genWorkflowDispatchInput :: (MonadGen m) => m WorkflowDispatchInput+genWorkflowDispatchInput = do+  description <- Gen.maybe genText+  default_ <-+    Gen.maybe $+      Aeson.String+        <$> Gen.text (Range.linear 3 20) Gen.alphaNum+  required <- Gen.maybe Gen.bool+  type_ <- Gen.maybe genWorkflowDispatchInputType+  pure WorkflowDispatchInput {..}++genWorkflowDispatchInputType :: (MonadGen m) => m WorkflowDispatchInputType+genWorkflowDispatchInputType = do+  Gen.choice+    [ pure WorkflowDispatchInputTypeBoolean,+      WorkflowDispatchInputTypeChoice <$> Gen.nonEmpty (Range.linear 1 5) genText,+      pure WorkflowDispatchInputTypeEnvironment,+      pure WorkflowDispatchInputTypeNumber,+      pure WorkflowDispatchInputTypeString+    ]++genWorkflowRunTriggerAttributes :: (MonadGen m) => m WorkflowRunTriggerAttributes+genWorkflowRunTriggerAttributes = do+  activityTypes <- Gen.nonEmpty (Range.linear 1 3) Gen.enumBounded+  workflows <- Gen.maybe $ Gen.nonEmpty (Range.linear 1 5) genText+  branches <- Gen.maybe $ Gen.nonEmpty (Range.linear 1 5) genText+  branchesIgnore <- Gen.maybe $ Gen.nonEmpty (Range.linear 1 5) genText+  pure WorkflowRunTriggerAttributes {..}
+ test/Language/Github/Actions/WorkflowTest.hs view
@@ -0,0 +1,68 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}+{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}++module Language.Github.Actions.WorkflowTest where++import qualified Data.Aeson as Aeson+import Data.Bifunctor (first)+import qualified Data.ByteString as BS+import Data.List (isPrefixOf)+import qualified Data.Text as Text+import Data.Text.Encoding (encodeUtf8)+import qualified Data.Yaml as Yaml+import Hedgehog (Gen, Property, forAll, property, tripping)+import Language.Github.Actions.Workflow (Workflow)+import qualified Language.Github.Actions.Workflow as Workflow+import System.FilePath (takeBaseName)+import Test.Tasty (TestTree, testGroup)+import Test.Tasty.Golden (findByExtension)+import Test.Tasty.Golden.Extra.GoldenVsToYAML (goldenVsToYaml)+import Text.Show.Pretty (ppShow)++test_goldenWorkflowFromYaml :: IO TestTree+test_goldenWorkflowFromYaml = do+  testYamlFiles <-+    filter (not . (reverse ".golden.yml" `isPrefixOf`) . reverse)+      <$> findByExtension [".yml"] "test/golden"+  pure $+    testGroup+      "Yaml Roundtrip"+      [ runGoldenVsToYamlFileTest testYamlFilePath+        | testYamlFilePath <- testYamlFiles+      ]+  where+    runGoldenVsToYamlFileTest testYamlFilePath =+      let outputFilePath =+            (<> ".golden.yml")+              . reverse+              . drop 4+              $ reverse testYamlFilePath+          haskellOutputFilePath =+            (<> ".hs.txt")+              . reverse+              . drop 4+              $ reverse testYamlFilePath+       in goldenVsToYaml+            ("roundtrip " <> takeBaseName testYamlFilePath)+            testYamlFilePath+            $ do+              putStrLn $ "roundtrip " <> takeBaseName testYamlFilePath+              eitherWorkflow <- Yaml.decodeFileEither @Workflow testYamlFilePath+              either+                (BS.writeFile outputFilePath >> (\_ -> fail "YAML decoding failed"))+                (\workflow -> writeOutputFiles outputFilePath haskellOutputFilePath workflow >> pure workflow)+                $ first (encodeUtf8 . Text.pack . Yaml.prettyPrintParseException) eitherWorkflow+    writeOutputFiles :: FilePath -> FilePath -> Workflow -> IO ()+    writeOutputFiles outputFilePath haskellOutputFilePath workflow = do+      BS.writeFile outputFilePath (Yaml.encode workflow)+        >> BS.writeFile haskellOutputFilePath (encodeUtf8 . Text.pack $ ppShow workflow)++hprop_WorkflowRoundTrip :: Property+hprop_WorkflowRoundTrip =+  trippingJSON Workflow.gen++trippingJSON :: (Eq a, Show a, Aeson.FromJSON a, Aeson.ToJSON a) => Gen a -> Property+trippingJSON gen = property $ do+  a <- forAll gen+  tripping a Aeson.encode Aeson.eitherDecode
+ test/Main.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF tasty-discover -optF --tree-display #-}
+ test/golden/configuration-main.golden.yml view
@@ -0,0 +1,550 @@+concurrency:+  cancel-in-progress: false+  group: ${{ github.ref }}+env:+  SYSTEM_NAME: Product Configuration+jobs:+  build:+    needs:+    - validateContent+    runs-on: ubuntu-20.04+    steps:+    - if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/stable'+      name: checkout branch+      uses: bellroy/gha-workflows/composite/checkout@master+      with:+        fetch-depth: '0'+        token: ${{ env.GH_TOKEN }}+    - if: github.ref != 'refs/heads/master' && github.ref != 'refs/heads/stable'+      name: checkout branch+      uses: bellroy/gha-workflows/composite/checkout@master+      with:+        fetch-depth: '1'+        token: ${{ env.GH_TOKEN }}+    - id: extract_branch+      name: Extract branch name+      run: |+        echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT+      shell: bash+    - if: github.ref == 'refs/heads/master'+      name: Set env variables for staging+      run: |+        echo "DESTINATION=Staging Environment" >> $GITHUB_ENV+        echo "ENVIRONMENT=staging" >> $GITHUB_ENV+    - if: github.ref == 'refs/heads/stable'+      name: Set env variables for production+      run: |+        echo "DESTINATION=Production Environment" >> $GITHUB_ENV+        echo "ENVIRONMENT=production" >> $GITHUB_ENV+    - name: Install and setup nix+      uses: bellroy/gha-workflows/composite/install-nix@master+      with:+        bellroy-nix-cache-access: none+        nix-config-access-tokens: github.com=${{ secrets.READ_HASKELL_REPO_PAT }}+    - name: Install script packages+      run: |+        sudo apt-get update+        sudo apt-get install -y parallel+    - name: Get Pull Request number+      run: |+        echo "PR_NUMBER=$(gh pr view --json number -q .number)" >> $GITHUB_ENV+      working-directory: ${{ env.GITHUB_WORKSPACE }}+    - id: changed-files+      name: Get changed files+      uses: tj-actions/changed-files@48d8f15b2aaa3d255ca5af3eba4870f807ce6b3c+      with:+        path: ${{ env.GIT_REPOSITORY_PATH }}+    - id: check_changes+      name: Check for CSV changes+      run: echo "csv_changed=${{ steps.changed-files.outputs.all_changed_files ==+        '' && '0' || contains(steps.changed-files.outputs.all_changed_files, '.csv')+        && '1' || '0' }}" >> $GITHUB_OUTPUT+    - if: steps.check_changes.outputs.csv_changed == '1'+      name: Check that all configuration CSV files are sorted alphabetically+      run: |+        set +e+        IFS= output=$(./scripts/check_configuration_csvs_sorted 2>&1)+        status=$?++        if (($status)); then+          echo "Configuration CSV files are not sorted:" >>/tmp/pr-comment.txt+          echo "" >>/tmp/pr-comment.txt+          echo "\`\`\`" >>/tmp/pr-comment.txt+          echo $output >>/tmp/pr-comment.txt+          echo "\`\`\`" >>/tmp/pr-comment.txt+          exit $status+        else+          echo "All configuration CSV files are sorted."+        fi+    - id: fetch_byproduct+      name: Fetch byproduct+      uses: bellroy/gha-workflows/composite/get-tool@master+      with:+        token: ${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}+        tool-name: byproduct+        tool-version: latest+    - id: fetch_gitapult+      name: Fetch gitapult+      uses: bellroy/gha-workflows/composite/get-tool@master+      with:+        token: ${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}+        tool-name: gitapult+        tool-version: latest+    - id: fetch_config-rules-cli+      name: Fetch config-rules-cli+      uses: bellroy/gha-workflows/composite/get-tool@master+      with:+        token: ${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}+        tool-name: config-rules-cli+        tool-version: latest+    - id: fetch_powerful-owl+      name: Fetch powerful-owl+      uses: bellroy/gha-workflows/composite/get-tool@master+      with:+        token: ${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}+        tool-name: powerful-owl+        tool-version: latest+    - name: Run byproduct once+      run: |+        find products/autogenerated/ -type f -name '*.json' | grep -v './\.' | xargs rm+        ./byproduct execute --once --in products/configuration --out products/autogenerated+    - name: Configure git+      run: |+        git config --global user.name "${{ github.actor }}"+        git config --global user.email "${{ github.actor }}@users.noreply.github.com"+    - name: Update autogenerated product files+      run: |+        git add products/autogenerated+        if git commit -m "Update autogenerated product files"; then+          echo "GENERATED_ANOTHER_COMMIT=true" >> $GITHUB_ENV+        else+          echo "No changes detected"+        fi+    - name: Update products autogenerated content blocks+      run: |+        scripts/update_products_autogenerated_content_blocks+        git add products/content++        if git commit -m "Update autogenerated products content blocks"; then+          echo "GENERATED_ANOTHER_COMMIT=true" >> $GITHUB_ENV+        else+          echo "No changes detected"+        fi+    - name: Set merge base+      run: |+        if [ "${{ github.ref }}" = "refs/heads/master" ]; then+          echo "MERGE_BASE=origin/stable" >> $GITHUB_ENV+        else+          echo "MERGE_BASE=$(git show-branch --merge-base origin/master ${{ steps.extract_branch.outputs.branch }})" >> $GITHUB_ENV+        fi+    - if: env.PR_NUMBER != 0+      name: Write byproduct validate output to env variable+      run: |+        set +e+        IFS= output=$(./gitapult changeset --git_root "$GITHUB_WORKSPACE" --from "${{ env.MERGE_BASE }}" | ./byproduct validate --markdown 2>&1)+        status=$?++        if (($status)); then+          echo $output >>/tmp/pr-comment.txt+          exit $status+        else+          echo "Byproduct validation succeeded."+        fi+    - name: Update feed autogenerated content blocks+      run: |+        nix run .#update_feeds_autogenerated_content_blocks+        git add feeds/content++        if git commit -m "Update autogenerated feed content blocks"; then+          echo "GENERATED_ANOTHER_COMMIT=true" >> $GITHUB_ENV+        else+          echo "No changes detected"+        fi+    - name: Update feed autogenerated rendering data+      run: |+        POWERFUL_OWL_COMMAND=$GITHUB_WORKSPACE/powerful-owl POWERFUL_OWL_FOLDER=./feeds/ ./scripts/generate-rendering-data+        git add ./feeds/page_rendering_data+        git add ./feeds/rendering_data++        if git commit -m "Update feed autogenerated rendering data"; then+          echo "GENERATED_ANOTHER_COMMIT=true" >> $GITHUB_ENV+        else+          echo "No changes detected"+        fi+      working-directory: ${{ env.GIT_REPOSITORY_PATH }}+    - name: Update feed golden tests+      run: |+        scripts/update_feeds_golden_tests+        git add feeds/tests/golden++        if git commit -m "Update autogenerated feeds golden test outputs"; then+          echo "GENERATED_ANOTHER_COMMIT=true" >> $GITHUB_ENV+        else+          echo "No changes detected"+        fi+    - name: Update autogenerated shipping configuration files+      run: |+        set +e+        IFS= output=$(./config-rules-cli shipping-options-transform --input-csv-dir shipping/configuration --output-json-dir shipping/autogenerated)+        status=$?++        if (($status)); then+          echo "Failed to transform shipping data" >>/tmp/pr-comment.txt+          echo "" >>/tmp/pr-comment.txt+          echo "\`\`\`" >>/tmp/pr-comment.txt+          echo $output >>/tmp/pr-comment.txt+          echo "\`\`\`" >>/tmp/pr-comment.txt+          exit $status+        else+          git add shipping/autogenerated+          if git commit -m "Update autogenerated shipping configuration files"; then+            echo "GENERATED_ANOTHER_COMMIT=true" >> $GITHUB_ENV+          else+            echo "No changes detected"+          fi+        fi+    - name: Generate new shipping tests+      run: |+        SHIPPING_TEST_DIRECTORY=shipping/tests+          find $SHIPPING_TEST_DIRECTORY -type f -name '*.result.json' -exec rm {} \;+            ./config-rules-cli -- shipping-options-check --repo-dir .+    - if: env.PR_NUMBER != 0 && github.ref != 'refs/heads/stable' && github.ref !=+        'refs/heads/master'+      name: Commit new shipping tests+      run: |+        if [ $(git status shipping/tests --porcelain=v1 | wc -l) -gt 0 ]+        then+          git add shipping/tests+          git commit -m "Update shipping tests"+          echo "GENERATED_ANOTHER_COMMIT=true" >> $GITHUB_ENV+        fi+    - name: Update autogenerated promotion configuration files+      run: |+        set +e+        IFS= output=$(./config-rules-cli promotion-transform --input-csv-dir promotions/configuration --output-json-dir promotions/autogenerated)+        status=$?++        if (($status)); then+          echo "Failed to transform promotions data" >>/tmp/pr-comment.txt+          echo "" >>/tmp/pr-comment.txt+          echo "\`\`\`" >>/tmp/pr-comment.txt+          echo $output >>/tmp/pr-comment.txt+          echo "\`\`\`" >>/tmp/pr-comment.txt+          exit $status+        else+          git add promotions/autogenerated+          if git commit -m "Update autogenerated promotions configuration files"; then+            echo "GENERATED_ANOTHER_COMMIT=true" >> $GITHUB_ENV+          else+            echo "No changes detected"+          fi+        fi+    - name: Generate new promotion rule tests+      run: |+        PROMOTIONS_TEST_DIRECTORY=promotions/tests+          find $PROMOTIONS_TEST_DIRECTORY -type f -name '*.result.json' -exec rm {} \;+            ./config-rules-cli -- promotion-check --repo-dir .+    - if: env.PR_NUMBER != 0 && github.ref != 'refs/heads/stable' && github.ref !=+        'refs/heads/master'+      name: Commit new promotion tests+      run: |+        if [ $(git status promotions/tests --porcelain=v1 | wc -l) -gt 0 ]+        then+          git add promotions/tests+          git commit -m "Update promotion tests"+          echo "GENERATED_ANOTHER_COMMIT=true" >> $GITHUB_ENV+        fi+    - if: env.GENERATED_ANOTHER_COMMIT == 'true'+      name: Push any new commits and exit workflow if commits pushed+      run: |+        git push --set-upstream origin HEAD++        gh run cancel ${{ github.run_id }}+        gh run watch ${{ github.run_id }}+    - id: current-time+      name: Get current time+      run: echo "timestamp=$(date -u +"%y%m%d%H%M%S")" >> $GITHUB_OUTPUT+    - id: extract_changes+      if: github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/master'+      name: Set changes variable+      run: |+        echo "COMMIT_CHANGES<<EOF" >> $GITHUB_ENV+        echo "$(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+        echo "EOF" >> $GITHUB_ENV+      shell: bash+      working-directory: ${{ env.GITHUB_WORKSPACE }}+    - env:+        SLACK_BOT_TOKEN: ${{ secrets.BELLROY_SLACK_TOKEN }}+      id: slack-deploy+      if: github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/master'+      name: Slack - Notifications - Post to slack+      uses: slackapi/slack-github-action@70cd7be8e40a46e8b0eced40b0de447bdb42f68e+      with:+        channel-id: ${{ env.SLACK_CHANNEL_ID }}+        payload: |+          {+            "text": ":loudspeaker: *${{ github.actor }}* is deploying *${{ env.SYSTEM_NAME }}* to *${{ env.DESTINATION }}* from *${{ steps.extract_branch.outputs.branch }}*",+            "attachments": [+              {+                "fallback": "Deployment summary",+                "color": "ffd966",+                "fields": [+                  {+                    "title": "What is being deployed",+                    "value": ${{ toJSON(env.COMMIT_CHANGES) }},+                    "short": false+                  },+                  {+                    "title": "Status",+                    "short": true,+                    "value": "In Progress"+                  }+                ]+              }+            ]+          }+    - if: github.ref == 'refs/heads/master'+      name: Push content to staging+      run: |+        set -e+        # XXX: Gitapult cannot distinguish datasets, so we push all data to+        # both endpoints and allow the server to decide what to do.+        # Kafka Webhooks accepts all datasets at once+        ./gitapult run \+          --git_root "$GITHUB_WORKSPACE" \+          --authorization_token "$KW_STAGING_API_KEY" \+          --changeset_url "$KW_STAGING_BASE_URL/gitapult/configuration/records" \+          --revision_url "$KW_STAGING_BASE_URL/gitapult/configuration/revision" \+          --api_key \+          --compress \+          --max-payload-bytes 3000000++        # XXX: V3 doesn't care about rendering data, but it is too large to send.+        # As at 2024-02-14 gitapult does not support selecting which data to send.+        git -C "$GIT_REPOSITORY_PATH" rm feeds/rendering_data/.gitapult.json+        ./gitapult run \+          --git_root "$GITHUB_WORKSPACE" \+          --authorization_token "$SF_STAGING_API_KEY" \+          --changeset_url "$SF_STAGING_BASE_URL/api/v1/configuration/shipping" \+          --revision_url "$SF_STAGING_BASE_URL/api/v1/configuration/shipping/revisions" && \+        git -C "$GIT_REPOSITORY_PATH" restore --staged feeds/rendering_data/.gitapult.json+        git -C "$GIT_REPOSITORY_PATH" restore feeds/rendering_data/.gitapult.json+    - if: success() && github.ref == 'refs/heads/master'+      name: Tag published staging content+      run: |+        git tag "${{ join(steps.current-time.outputs.*, '\n') }}_staging" && git push --tags+    - if: github.ref == 'refs/heads/master'+      name: Open PR to stable+      run: |+        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."}'+        PR_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)')+        echo "PR_URL=$PR_URL" >> $GITHUB_ENV+    - continue-on-error: true+      if: github.ref == 'refs/heads/master'+      name: Set assignee+      run: |+        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]')+        PR_NUM=$(echo "$PR_RESPONSE" | jq -r ".number")+        PR_ASSIGNEE=$(echo "$PR_RESPONSE" | jq -r ".assignee")+        COMMITS_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)+        FIRST_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')+        # special case for squashed PRs created by trike-deploy e.g. gha-workflows+        if [[ "$FIRST_COMMITTER" == "trike-deploy" ]]; then+            FIRST_COMMITTER=$(echo "$COMMITS_RESPONSE" | jq -r '. | sort_by(.commit.author.date) | .[0]' | sed -En "s/.*Co-authored-by: (.*) <.*@.*>\",/\1/p")+        fi+        if [[ "$PR_ASSIGNEE" == "null" ]]; then+            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\"]}"+        fi+    - if: github.ref == 'refs/heads/stable'+      name: Push content to production+      run: |+        set -e+        # XXX: Gitapult cannot distinguish datasets, so we push all data to+        # both endpoints and allow the server to decide what to do.+        # Kafka Webhooks accepts all datasets at once+        ./gitapult run \+          --git_root "$GITHUB_WORKSPACE" \+          --authorization_token "$KW_PRODUCTION_API_KEY" \+          --changeset_url "$KW_PRODUCTION_BASE_URL/gitapult/configuration/records" \+          --revision_url "$KW_PRODUCTION_BASE_URL/gitapult/configuration/revision" \+          --api_key \+          --compress \+          --max-payload-bytes 3000000++        # XXX: V3 doesn't care about rendering data, but it is too large to send.+        # As at 2024-02-14 gitapult does not support selecting which data to send.+        git -C "$GIT_REPOSITORY_PATH" rm feeds/rendering_data/.gitapult.json+        ./gitapult run \+          --git_root "$GITHUB_WORKSPACE" \+          --authorization_token "$SF_PRODUCTION_API_KEY" \+          --changeset_url "$SF_PRODUCTION_BASE_URL/api/v1/configuration/shipping" \+          --revision_url "$SF_PRODUCTION_BASE_URL/api/v1/configuration/shipping/revisions" && \+        git -C "$GIT_REPOSITORY_PATH" restore --staged feeds/rendering_data/.gitapult.json+        git -C "$GIT_REPOSITORY_PATH" restore feeds/rendering_data/.gitapult.json+    - if: success() && github.ref == 'refs/heads/stable'+      name: Tag published production content+      run: |+        git tag "${{ join(steps.current-time.outputs.*, '\n') }}_production" && git push --tags+    - if: github.ref == 'refs/heads/master'+      name: Set success message variable for staging+      run: |+        echo "SUCCESS_MESSAGE=:tada: \nA PR has been opened against the stable branch: ${{ env.PR_URL }}" >> $GITHUB_ENV+        echo "SUCCESS_MESSAGE_BODY=https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks\n\n*PR for Production:* ${{ env.PR_URL }}" >> $GITHUB_ENV+    - if: github.ref == 'refs/heads/stable'+      name: Set success message variable for production+      run: |+        echo "SUCCESS_MESSAGE=:tada:" >> $GITHUB_ENV+        echo "SUCCESS_MESSAGE_BODY=https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks" >> $GITHUB_ENV+    - env:+        SLACK_BOT_TOKEN: ${{ secrets.BELLROY_SLACK_TOKEN }}+      id: slack-deploy-success+      if: success() && (github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/master')+      name: Post to Slack if published successfully+      uses: slackapi/slack-github-action@70cd7be8e40a46e8b0eced40b0de447bdb42f68e+      with:+        channel-id: ${{ env.SLACK_CHANNEL_ID }}+        payload: |+          {+            "text": ":loudspeaker: *${{ github.actor }}* has deployed *${{ env.SYSTEM_NAME }}* to *${{ env.DESTINATION }}* from *${{ steps.extract_branch.outputs.branch }}* ${{ env.SUCCESS_MESSAGE }}",+            "attachments": [+              {+                "fallback": "Deployment summary",+                "color": "00ff00",+                "fields": [+                  {+                    "title": "What was deployed",+                    "value": ${{ toJSON(env.COMMIT_CHANGES) }},+                    "short": false+                  },+                  {+                    "title": "Status",+                    "short": true,+                    "value": "Deployed"+                  },+                  {+                    "title": "Output",+                    "short": false,+                    "value": "${{ env.SUCCESS_MESSAGE_BODY }}"+                  }+                ]+              }+            ]+          }+        update-ts: ${{ steps.slack-deploy.outputs.ts }}+    - env:+        SLACK_BOT_TOKEN: ${{ secrets.BELLROY_SLACK_TOKEN }}+      id: slack-deploy-failed+      if: failure() && (github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/master')+      name: Post to Slack if build/deploy fails+      uses: slackapi/slack-github-action@70cd7be8e40a46e8b0eced40b0de447bdb42f68e+      with:+        channel-id: ${{ env.SLACK_CHANNEL_ID }}+        payload: |+          { "text": ":loudspeaker: *BUILD/DEPLOY FAILURE* of *${{ env.SYSTEM_NAME }}* to *${{ env.DESTINATION }}*",+            "attachments": [+              {+                "fallback": "Deployment summary",+                "color": "ff0000",+                "fields": [+                  {+                    "title": "What was being deployed",+                    "value": ${{ toJSON(env.COMMIT_CHANGES) }},+                    "short": false+                  },+                  {+                    "title": "Status",+                    "short": true,+                    "value": "Failed"+                  },+                  {+                    "title": "Who broke it",+                    "value": "${{ github.actor }}"+                  },+                  {+                    "title": "Output",+                    "short": false,+                    "value": "https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks"+                  }+                ]+              }+            ]+          }+        update-ts: ${{ steps.slack-deploy.outputs.ts }}+    - if: always() && env.PR_NUMBER != 0+      name: Add comment to PR+      run: gh pr comment ${{ env.PR_NUMBER }} --body-file /tmp/pr-comment.txt || true+      working-directory: ${{ env.GITHUB_WORKSPACE }}+  validateContent:+    runs-on: ubuntu-20.04+    steps:+    - id: checkout_branch+      name: checkout branch+      uses: bellroy/gha-workflows/composite/checkout@master+      with:+        fetch-depth: '1'+        path: ${{ env.GITHUB_WORKSPACE }}+        token: ${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}+    - id: extract_branch+      name: Extract branch name+      run: |+        echo "branch=${GITHUB_REF#refs/heads/}" >>$GITHUB_OUTPUT+      shell: bash+    - name: Install and setup nix+      uses: bellroy/gha-workflows/composite/install-nix@master+      with:+        bellroy-nix-cache-access: none+    - id: fetch_gitapult+      name: Fetch gitapult+      uses: bellroy/gha-workflows/composite/get-tool@master+      with:+        token: ${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}+        tool-name: gitapult+        tool-version: latest+    - id: fetch_powerful-owl+      name: Fetch powerful-owl+      uses: bellroy/gha-workflows/composite/get-tool@master+      with:+        token: ${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}+        tool-name: powerful-owl+        tool-version: latest+    - name: Get Pull Request number+      run: |+        echo "PR_NUMBER=$(gh pr view --json number -q .number)" >> $GITHUB_ENV+      working-directory: ${{ env.GITHUB_WORKSPACE }}+    - name: Ensure tmp directory exists+      run: mkdir -p tmp+      working-directory: ${{ env.GITHUB_WORKSPACE }}+    - name: Validate with gitapult+      run: |+        ./gitapult validate --git_root "$GITHUB_WORKSPACE"+    - name: Validate content with powerful-owl+      run: |+        for CONTENT_DIR in feeds products+        do+          if [[ "$PR_NUMBER" ]]+          then+            ./powerful-owl validate --markdown --content_path "$GITHUB_WORKSPACE/$CONTENT_DIR" &>>/tmp/pr-comment.txt+          else+            ./powerful-owl validate --content_path "$GITHUB_WORKSPACE/$CONTENT_DIR"+          fi+        done+    - if: always() && env.PR_NUMBER != 0+      name: Add comment to PR+      run: gh pr comment ${{ env.PR_NUMBER }} --body-file /tmp/pr-comment.txt || true+      working-directory: ${{ env.GITHUB_WORKSPACE }}+    - if: failure() && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/stable')+      name: Post to slack if build fails+      uses: bellroy/gha-workflows/composite/slack-update@master+      with:+        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 }}+    timeout-minutes: 30+name: Publish+'on':+  push:+    branches-ignore:+    - refs/tags/*_staging+    - refs/tags/*_production
+ test/golden/configuration-main.hs.txt view
@@ -0,0 +1,1030 @@+Workflow+  { concurrency =+      Just+        Concurrency+          { group = Just "${{ github.ref }}"+          , cancelInProgress = Just False+          }+  , defaults = Nothing+  , env = fromList [ ( "SYSTEM_NAME" , "Product Configuration" ) ]+  , jobs =+      fromList+        [ ( JobId "build"+          , Job+              { concurrency = Nothing+              , container = Nothing+              , continueOnError = Nothing+              , defaults = Nothing+              , env = fromList []+              , environment = Nothing+              , jobName = Nothing+              , needs = Just (JobId "validateContent" :| [])+              , outputs = fromList []+              , permissions = Nothing+              , runIf = Nothing+              , runsOn = Just "ubuntu-20.04"+              , secrets = fromList []+              , services = fromList []+              , steps =+                  Just+                    (Step+                       { continueOnError = False+                       , env = fromList []+                       , name = Just "checkout branch"+                       , run = Nothing+                       , runIf =+                           Just+                             "github.ref == 'refs/heads/master' || github.ref == 'refs/heads/stable'"+                       , shell = Nothing+                       , stepId = Nothing+                       , timeoutMinutes = Nothing+                       , uses = Just "bellroy/gha-workflows/composite/checkout@master"+                       , with =+                           Just+                             (StepWithEnv+                                (fromList+                                   [ ( "fetch-depth" , "0" )+                                   , ( "token" , "${{ env.GH_TOKEN }}" )+                                   ]))+                       , workingDirectory = Nothing+                       } :|+                       [ Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "checkout branch"+                           , run = Nothing+                           , runIf =+                               Just+                                 "github.ref != 'refs/heads/master' && github.ref != 'refs/heads/stable'"+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Just "bellroy/gha-workflows/composite/checkout@master"+                           , with =+                               Just+                                 (StepWithEnv+                                    (fromList+                                       [ ( "fetch-depth" , "1" )+                                       , ( "token" , "${{ env.GH_TOKEN }}" )+                                       ]))+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Extract branch name"+                           , run =+                               Just+                                 "echo \"branch=${GITHUB_REF#refs/heads/}\" >> $GITHUB_OUTPUT\n"+                           , runIf = Nothing+                           , shell = Just (Bash Nothing)+                           , stepId = Just (StepId "extract_branch")+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Set env variables for staging"+                           , run =+                               Just+                                 "echo \"DESTINATION=Staging Environment\" >> $GITHUB_ENV\necho \"ENVIRONMENT=staging\" >> $GITHUB_ENV\n"+                           , runIf = Just "github.ref == 'refs/heads/master'"+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Set env variables for production"+                           , run =+                               Just+                                 "echo \"DESTINATION=Production Environment\" >> $GITHUB_ENV\necho \"ENVIRONMENT=production\" >> $GITHUB_ENV\n"+                           , runIf = Just "github.ref == 'refs/heads/stable'"+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Install and setup nix"+                           , run = Nothing+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Just "bellroy/gha-workflows/composite/install-nix@master"+                           , with =+                               Just+                                 (StepWithEnv+                                    (fromList+                                       [ ( "bellroy-nix-cache-access" , "none" )+                                       , ( "nix-config-access-tokens"+                                         , "github.com=${{ secrets.READ_HASKELL_REPO_PAT }}"+                                         )+                                       ]))+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Install script packages"+                           , run =+                               Just "sudo apt-get update\nsudo apt-get install -y parallel\n"+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Get Pull Request number"+                           , run =+                               Just+                                 "echo \"PR_NUMBER=$(gh pr view --json number -q .number)\" >> $GITHUB_ENV\n"+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Just "${{ env.GITHUB_WORKSPACE }}"+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Get changed files"+                           , run = Nothing+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Just (StepId "changed-files")+                           , timeoutMinutes = Nothing+                           , uses =+                               Just+                                 "tj-actions/changed-files@48d8f15b2aaa3d255ca5af3eba4870f807ce6b3c"+                           , with =+                               Just+                                 (StepWithEnv+                                    (fromList [ ( "path" , "${{ env.GIT_REPOSITORY_PATH }}" ) ]))+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Check for CSV changes"+                           , run =+                               Just+                                 "echo \"csv_changed=${{ steps.changed-files.outputs.all_changed_files == '' && '0' || contains(steps.changed-files.outputs.all_changed_files, '.csv') && '1' || '0' }}\" >> $GITHUB_OUTPUT"+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Just (StepId "check_changes")+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name =+                               Just+                                 "Check that all configuration CSV files are sorted alphabetically"+                           , 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'"+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Fetch byproduct"+                           , run = Nothing+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Just (StepId "fetch_byproduct")+                           , timeoutMinutes = Nothing+                           , uses = Just "bellroy/gha-workflows/composite/get-tool@master"+                           , with =+                               Just+                                 (StepWithEnv+                                    (fromList+                                       [ ( "token" , "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}" )+                                       , ( "tool-name" , "byproduct" )+                                       , ( "tool-version" , "latest" )+                                       ]))+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Fetch gitapult"+                           , run = Nothing+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Just (StepId "fetch_gitapult")+                           , timeoutMinutes = Nothing+                           , uses = Just "bellroy/gha-workflows/composite/get-tool@master"+                           , with =+                               Just+                                 (StepWithEnv+                                    (fromList+                                       [ ( "token" , "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}" )+                                       , ( "tool-name" , "gitapult" )+                                       , ( "tool-version" , "latest" )+                                       ]))+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Fetch config-rules-cli"+                           , run = Nothing+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Just (StepId "fetch_config-rules-cli")+                           , timeoutMinutes = Nothing+                           , uses = Just "bellroy/gha-workflows/composite/get-tool@master"+                           , with =+                               Just+                                 (StepWithEnv+                                    (fromList+                                       [ ( "token" , "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}" )+                                       , ( "tool-name" , "config-rules-cli" )+                                       , ( "tool-version" , "latest" )+                                       ]))+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Fetch powerful-owl"+                           , run = Nothing+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Just (StepId "fetch_powerful-owl")+                           , timeoutMinutes = Nothing+                           , uses = Just "bellroy/gha-workflows/composite/get-tool@master"+                           , with =+                               Just+                                 (StepWithEnv+                                    (fromList+                                       [ ( "token" , "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}" )+                                       , ( "tool-name" , "powerful-owl" )+                                       , ( "tool-version" , "latest" )+                                       ]))+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Run byproduct once"+                           , run =+                               Just+                                 "find products/autogenerated/ -type f -name '*.json' | grep -v './\\.' | xargs rm\n./byproduct execute --once --in products/configuration --out products/autogenerated\n"+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Configure git"+                           , run =+                               Just+                                 "git config --global user.name \"${{ github.actor }}\"\ngit config --global user.email \"${{ github.actor }}@users.noreply.github.com\"\n"+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Update autogenerated product files"+                           , run =+                               Just+                                 "git add products/autogenerated\nif git commit -m \"Update autogenerated product files\"; then\n  echo \"GENERATED_ANOTHER_COMMIT=true\" >> $GITHUB_ENV\nelse\n  echo \"No changes detected\"\nfi\n"+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Update products autogenerated content blocks"+                           , run =+                               Just+                                 "scripts/update_products_autogenerated_content_blocks\ngit add products/content\n\nif git commit -m \"Update autogenerated products content blocks\"; then\n  echo \"GENERATED_ANOTHER_COMMIT=true\" >> $GITHUB_ENV\nelse\n  echo \"No changes detected\"\nfi\n"+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Set merge base"+                           , run =+                               Just+                                 "if [ \"${{ github.ref }}\" = \"refs/heads/master\" ]; then\n  echo \"MERGE_BASE=origin/stable\" >> $GITHUB_ENV\nelse\n  echo \"MERGE_BASE=$(git show-branch --merge-base origin/master ${{ steps.extract_branch.outputs.branch }})\" >> $GITHUB_ENV\nfi\n"+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Write byproduct validate output to env variable"+                           , 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"+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Update feed autogenerated content blocks"+                           , run =+                               Just+                                 "nix run .#update_feeds_autogenerated_content_blocks\ngit add feeds/content\n\nif git commit -m \"Update autogenerated feed content blocks\"; then\n  echo \"GENERATED_ANOTHER_COMMIT=true\" >> $GITHUB_ENV\nelse\n  echo \"No changes detected\"\nfi\n"+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Update feed autogenerated rendering data"+                           , run =+                               Just+                                 "POWERFUL_OWL_COMMAND=$GITHUB_WORKSPACE/powerful-owl POWERFUL_OWL_FOLDER=./feeds/ ./scripts/generate-rendering-data\ngit add ./feeds/page_rendering_data\ngit add ./feeds/rendering_data\n\nif git commit -m \"Update feed autogenerated rendering data\"; then\n  echo \"GENERATED_ANOTHER_COMMIT=true\" >> $GITHUB_ENV\nelse\n  echo \"No changes detected\"\nfi\n"+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Just "${{ env.GIT_REPOSITORY_PATH }}"+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Update feed golden tests"+                           , run =+                               Just+                                 "scripts/update_feeds_golden_tests\ngit add feeds/tests/golden\n\nif git commit -m \"Update autogenerated feeds golden test outputs\"; then\n  echo \"GENERATED_ANOTHER_COMMIT=true\" >> $GITHUB_ENV\nelse\n  echo \"No changes detected\"\nfi\n"+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Update autogenerated shipping configuration files"+                           , run =+                               Just+                                 "set +e\nIFS= output=$(./config-rules-cli shipping-options-transform --input-csv-dir shipping/configuration --output-json-dir shipping/autogenerated)\nstatus=$?\n\nif (($status)); then\n  echo \"Failed to transform shipping data\" >>/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  git add shipping/autogenerated\n  if git commit -m \"Update autogenerated shipping configuration files\"; then\n    echo \"GENERATED_ANOTHER_COMMIT=true\" >> $GITHUB_ENV\n  else\n    echo \"No changes detected\"\n  fi\nfi\n"+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Generate new shipping tests"+                           , run =+                               Just+                                 "SHIPPING_TEST_DIRECTORY=shipping/tests\n  find $SHIPPING_TEST_DIRECTORY -type f -name '*.result.json' -exec rm {} \\;\n    ./config-rules-cli -- shipping-options-check --repo-dir .\n"+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Commit new shipping tests"+                           , run =+                               Just+                                 "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'"+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Update autogenerated promotion configuration files"+                           , run =+                               Just+                                 "set +e\nIFS= output=$(./config-rules-cli promotion-transform --input-csv-dir promotions/configuration --output-json-dir promotions/autogenerated)\nstatus=$?\n\nif (($status)); then\n  echo \"Failed to transform promotions data\" >>/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  git add promotions/autogenerated\n  if git commit -m \"Update autogenerated promotions configuration files\"; then\n    echo \"GENERATED_ANOTHER_COMMIT=true\" >> $GITHUB_ENV\n  else\n    echo \"No changes detected\"\n  fi\nfi\n"+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Generate new promotion rule tests"+                           , run =+                               Just+                                 "PROMOTIONS_TEST_DIRECTORY=promotions/tests\n  find $PROMOTIONS_TEST_DIRECTORY -type f -name '*.result.json' -exec rm {} \\;\n    ./config-rules-cli -- promotion-check --repo-dir .\n"+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Commit new promotion tests"+                           , run =+                               Just+                                 "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'"+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name =+                               Just "Push any new commits and exit workflow if commits pushed"+                           , 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'"+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Get current time"+                           , run =+                               Just+                                 "echo \"timestamp=$(date -u +\"%y%m%d%H%M%S\")\" >> $GITHUB_OUTPUT"+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Just (StepId "current-time")+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Set changes variable"+                           , run =+                               Just+                                 "echo \"COMMIT_CHANGES<<EOF\" >> $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'"+                           , shell = Just (Bash Nothing)+                           , stepId = Just (StepId "extract_changes")+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Just "${{ env.GITHUB_WORKSPACE }}"+                           }+                       , Step+                           { continueOnError = False+                           , env =+                               fromList+                                 [ ( "SLACK_BOT_TOKEN" , "${{ secrets.BELLROY_SLACK_TOKEN }}" ) ]+                           , name = Just "Slack - Notifications - Post to slack"+                           , run = Nothing+                           , runIf =+                               Just+                                 "github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/master'"+                           , shell = Nothing+                           , stepId = Just (StepId "slack-deploy")+                           , timeoutMinutes = Nothing+                           , uses =+                               Just+                                 "slackapi/slack-github-action@70cd7be8e40a46e8b0eced40b0de447bdb42f68e"+                           , 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"+                                         )+                                       ]))+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Push content to staging"+                           , 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'"+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Tag published staging content"+                           , run =+                               Just+                                 "git tag \"${{ join(steps.current-time.outputs.*, '\\n') }}_staging\" && git push --tags\n"+                           , runIf = Just "success() && github.ref == 'refs/heads/master'"+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Open PR to stable"+                           , 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'"+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = True+                           , env = fromList []+                           , name = Just "Set assignee"+                           , 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'"+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Push content to production"+                           , 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'"+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Tag published production content"+                           , run =+                               Just+                                 "git tag \"${{ join(steps.current-time.outputs.*, '\\n') }}_production\" && git push --tags\n"+                           , runIf = Just "success() && github.ref == 'refs/heads/stable'"+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Set success message variable for staging"+                           , 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'"+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Set success message variable for production"+                           , 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'"+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env =+                               fromList+                                 [ ( "SLACK_BOT_TOKEN" , "${{ secrets.BELLROY_SLACK_TOKEN }}" ) ]+                           , name = Just "Post to Slack if published successfully"+                           , run = Nothing+                           , runIf =+                               Just+                                 "success() && (github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/master')"+                           , shell = Nothing+                           , stepId = Just (StepId "slack-deploy-success")+                           , timeoutMinutes = Nothing+                           , uses =+                               Just+                                 "slackapi/slack-github-action@70cd7be8e40a46e8b0eced40b0de447bdb42f68e"+                           , 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 }}" )+                                       ]))+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env =+                               fromList+                                 [ ( "SLACK_BOT_TOKEN" , "${{ secrets.BELLROY_SLACK_TOKEN }}" ) ]+                           , name = Just "Post to Slack if build/deploy fails"+                           , run = Nothing+                           , runIf =+                               Just+                                 "failure() && (github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/master')"+                           , shell = Nothing+                           , stepId = Just (StepId "slack-deploy-failed")+                           , timeoutMinutes = Nothing+                           , uses =+                               Just+                                 "slackapi/slack-github-action@70cd7be8e40a46e8b0eced40b0de447bdb42f68e"+                           , 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 }}" )+                                       ]))+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Add comment to PR"+                           , run =+                               Just+                                 "gh pr comment ${{ env.PR_NUMBER }} --body-file /tmp/pr-comment.txt || true"+                           , runIf = Just "always() && env.PR_NUMBER != 0"+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Just "${{ env.GITHUB_WORKSPACE }}"+                           }+                       ])+              , strategy = Nothing+              , timeoutMinutes = Nothing+              , uses = Nothing+              , with = fromList []+              }+          )+        , ( JobId "validateContent"+          , 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-20.04"+              , secrets = fromList []+              , services = fromList []+              , steps =+                  Just+                    (Step+                       { continueOnError = False+                       , env = fromList []+                       , name = Just "checkout branch"+                       , run = Nothing+                       , runIf = Nothing+                       , shell = Nothing+                       , stepId = Just (StepId "checkout_branch")+                       , timeoutMinutes = Nothing+                       , uses = Just "bellroy/gha-workflows/composite/checkout@master"+                       , with =+                           Just+                             (StepWithEnv+                                (fromList+                                   [ ( "fetch-depth" , "1" )+                                   , ( "path" , "${{ env.GITHUB_WORKSPACE }}" )+                                   , ( "token" , "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}" )+                                   ]))+                       , workingDirectory = Nothing+                       } :|+                       [ Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Extract branch name"+                           , run =+                               Just "echo \"branch=${GITHUB_REF#refs/heads/}\" >>$GITHUB_OUTPUT\n"+                           , runIf = Nothing+                           , shell = Just (Bash Nothing)+                           , stepId = Just (StepId "extract_branch")+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Install and setup nix"+                           , run = Nothing+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Just "bellroy/gha-workflows/composite/install-nix@master"+                           , with =+                               Just+                                 (StepWithEnv+                                    (fromList [ ( "bellroy-nix-cache-access" , "none" ) ]))+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Fetch gitapult"+                           , run = Nothing+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Just (StepId "fetch_gitapult")+                           , timeoutMinutes = Nothing+                           , uses = Just "bellroy/gha-workflows/composite/get-tool@master"+                           , with =+                               Just+                                 (StepWithEnv+                                    (fromList+                                       [ ( "token" , "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}" )+                                       , ( "tool-name" , "gitapult" )+                                       , ( "tool-version" , "latest" )+                                       ]))+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Fetch powerful-owl"+                           , run = Nothing+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Just (StepId "fetch_powerful-owl")+                           , timeoutMinutes = Nothing+                           , uses = Just "bellroy/gha-workflows/composite/get-tool@master"+                           , with =+                               Just+                                 (StepWithEnv+                                    (fromList+                                       [ ( "token" , "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}" )+                                       , ( "tool-name" , "powerful-owl" )+                                       , ( "tool-version" , "latest" )+                                       ]))+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Get Pull Request number"+                           , run =+                               Just+                                 "echo \"PR_NUMBER=$(gh pr view --json number -q .number)\" >> $GITHUB_ENV\n"+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Just "${{ env.GITHUB_WORKSPACE }}"+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Ensure tmp directory exists"+                           , run = Just "mkdir -p tmp"+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Just "${{ env.GITHUB_WORKSPACE }}"+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Validate with gitapult"+                           , run =+                               Just "./gitapult validate --git_root \"$GITHUB_WORKSPACE\"\n"+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Validate content with powerful-owl"+                           , run =+                               Just+                                 "for CONTENT_DIR in feeds products\ndo\n  if [[ \"$PR_NUMBER\" ]]\n  then\n    ./powerful-owl validate --markdown --content_path \"$GITHUB_WORKSPACE/$CONTENT_DIR\" &>>/tmp/pr-comment.txt\n  else\n    ./powerful-owl validate --content_path \"$GITHUB_WORKSPACE/$CONTENT_DIR\"\n  fi\ndone\n"+                           , runIf = Nothing+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Nothing+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Add comment to PR"+                           , run =+                               Just+                                 "gh pr comment ${{ env.PR_NUMBER }} --body-file /tmp/pr-comment.txt || true"+                           , runIf = Just "always() && env.PR_NUMBER != 0"+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Nothing+                           , with = Nothing+                           , workingDirectory = Just "${{ env.GITHUB_WORKSPACE }}"+                           }+                       , Step+                           { continueOnError = False+                           , env = fromList []+                           , name = Just "Post to slack if build fails"+                           , run = Nothing+                           , runIf =+                               Just+                                 "failure() && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/stable')"+                           , shell = Nothing+                           , stepId = Nothing+                           , timeoutMinutes = Nothing+                           , uses = Just "bellroy/gha-workflows/composite/slack-update@master"+                           , 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 }}"+                                         )+                                       ]))+                           , workingDirectory = Nothing+                           }+                       ])+              , strategy = Nothing+              , timeoutMinutes = Just 30+              , 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 "Publish"+  }
+ test/golden/configuration-main.yml view
@@ -0,0 +1,549 @@+# This file is generated by `generate.sh`.+# Generated from https://github.com/bellroy/gha-workflows/tree/master/dhall/workflows/configuration/main.dhall+# Think twice before editing it directly.+concurrency:+  cancel-in-progress: false+  group: "${{ github.ref }}"+env:+  SYSTEM_NAME: Product Configuration+jobs:+  build:+    needs:+      - validateContent+    runs-on: ubuntu-20.04+    steps:+      - if: "github.ref == 'refs/heads/master' || github.ref == 'refs/heads/stable'"+        name: checkout branch+        uses: "bellroy/gha-workflows/composite/checkout@master"+        with:+          fetch-depth: "0"+          token: "${{ env.GH_TOKEN }}"+      - if: "github.ref != 'refs/heads/master' && github.ref != 'refs/heads/stable'"+        name: checkout branch+        uses: "bellroy/gha-workflows/composite/checkout@master"+        with:+          fetch-depth: "1"+          token: "${{ env.GH_TOKEN }}"+      - id: extract_branch+        name: Extract branch name+        run: |+          echo "branch=${GITHUB_REF#refs/heads/}" >> $GITHUB_OUTPUT+        shell: bash+      - if: "github.ref == 'refs/heads/master'"+        name: Set env variables for staging+        run: |+          echo "DESTINATION=Staging Environment" >> $GITHUB_ENV+          echo "ENVIRONMENT=staging" >> $GITHUB_ENV+      - if: "github.ref == 'refs/heads/stable'"+        name: Set env variables for production+        run: |+          echo "DESTINATION=Production Environment" >> $GITHUB_ENV+          echo "ENVIRONMENT=production" >> $GITHUB_ENV+      - name: Install and setup nix+        uses: "bellroy/gha-workflows/composite/install-nix@master"+        with:+          bellroy-nix-cache-access: none+          nix-config-access-tokens: "github.com=${{ secrets.READ_HASKELL_REPO_PAT }}"+      - name: Install script packages+        run: |+          sudo apt-get update+          sudo apt-get install -y parallel+      - name: Get Pull Request number+        run: |+          echo "PR_NUMBER=$(gh pr view --json number -q .number)" >> $GITHUB_ENV+        working-directory: "${{ env.GITHUB_WORKSPACE }}"+      - id: changed-files+        name: Get changed files+        uses: "tj-actions/changed-files@48d8f15b2aaa3d255ca5af3eba4870f807ce6b3c"+        with:+          path: "${{ env.GIT_REPOSITORY_PATH }}"+      - id: check_changes+        name: Check for CSV changes+        run: 'echo "csv_changed=${{ steps.changed-files.outputs.all_changed_files == '''' && ''0'' || contains(steps.changed-files.outputs.all_changed_files, ''.csv'') && ''1'' || ''0'' }}" >> $GITHUB_OUTPUT'+      - if: "steps.check_changes.outputs.csv_changed == '1'"+        name: Check that all configuration CSV files are sorted alphabetically+        run: |+          set +e+          IFS= output=$(./scripts/check_configuration_csvs_sorted 2>&1)+          status=$?++          if (($status)); then+            echo "Configuration CSV files are not sorted:" >>/tmp/pr-comment.txt+            echo "" >>/tmp/pr-comment.txt+            echo "\`\`\`" >>/tmp/pr-comment.txt+            echo $output >>/tmp/pr-comment.txt+            echo "\`\`\`" >>/tmp/pr-comment.txt+            exit $status+          else+            echo "All configuration CSV files are sorted."+          fi+      - id: fetch_byproduct+        name: Fetch byproduct+        uses: "bellroy/gha-workflows/composite/get-tool@master"+        with:+          token: "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}"+          tool-name: byproduct+          tool-version: latest+      - id: fetch_gitapult+        name: Fetch gitapult+        uses: "bellroy/gha-workflows/composite/get-tool@master"+        with:+          token: "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}"+          tool-name: gitapult+          tool-version: latest+      - id: fetch_config-rules-cli+        name: Fetch config-rules-cli+        uses: "bellroy/gha-workflows/composite/get-tool@master"+        with:+          token: "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}"+          tool-name: config-rules-cli+          tool-version: latest+      - id: fetch_powerful-owl+        name: Fetch powerful-owl+        uses: "bellroy/gha-workflows/composite/get-tool@master"+        with:+          token: "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}"+          tool-name: powerful-owl+          tool-version: latest+      - name: Run byproduct once+        run: |+          find products/autogenerated/ -type f -name '*.json' | grep -v './\.' | xargs rm+          ./byproduct execute --once --in products/configuration --out products/autogenerated+      - name: Configure git+        run: |+          git config --global user.name "${{ github.actor }}"+          git config --global user.email "${{ github.actor }}@users.noreply.github.com"+      - name: Update autogenerated product files+        run: |+          git add products/autogenerated+          if git commit -m "Update autogenerated product files"; then+            echo "GENERATED_ANOTHER_COMMIT=true" >> $GITHUB_ENV+          else+            echo "No changes detected"+          fi+      - name: Update products autogenerated content blocks+        run: |+          scripts/update_products_autogenerated_content_blocks+          git add products/content++          if git commit -m "Update autogenerated products content blocks"; then+            echo "GENERATED_ANOTHER_COMMIT=true" >> $GITHUB_ENV+          else+            echo "No changes detected"+          fi+      - name: Set merge base+        run: |+          if [ "${{ github.ref }}" = "refs/heads/master" ]; then+            echo "MERGE_BASE=origin/stable" >> $GITHUB_ENV+          else+            echo "MERGE_BASE=$(git show-branch --merge-base origin/master ${{ steps.extract_branch.outputs.branch }})" >> $GITHUB_ENV+          fi+      - if: "env.PR_NUMBER != 0"+        name: Write byproduct validate output to env variable+        run: |+          set +e+          IFS= output=$(./gitapult changeset --git_root "$GITHUB_WORKSPACE" --from "${{ env.MERGE_BASE }}" | ./byproduct validate --markdown 2>&1)+          status=$?++          if (($status)); then+            echo $output >>/tmp/pr-comment.txt+            exit $status+          else+            echo "Byproduct validation succeeded."+          fi+      - name: Update feed autogenerated content blocks+        run: |+          nix run .#update_feeds_autogenerated_content_blocks+          git add feeds/content++          if git commit -m "Update autogenerated feed content blocks"; then+            echo "GENERATED_ANOTHER_COMMIT=true" >> $GITHUB_ENV+          else+            echo "No changes detected"+          fi+      - name: Update feed autogenerated rendering data+        run: |+          POWERFUL_OWL_COMMAND=$GITHUB_WORKSPACE/powerful-owl POWERFUL_OWL_FOLDER=./feeds/ ./scripts/generate-rendering-data+          git add ./feeds/page_rendering_data+          git add ./feeds/rendering_data++          if git commit -m "Update feed autogenerated rendering data"; then+            echo "GENERATED_ANOTHER_COMMIT=true" >> $GITHUB_ENV+          else+            echo "No changes detected"+          fi+        working-directory: "${{ env.GIT_REPOSITORY_PATH }}"+      - name: Update feed golden tests+        run: |+          scripts/update_feeds_golden_tests+          git add feeds/tests/golden++          if git commit -m "Update autogenerated feeds golden test outputs"; then+            echo "GENERATED_ANOTHER_COMMIT=true" >> $GITHUB_ENV+          else+            echo "No changes detected"+          fi+      - name: Update autogenerated shipping configuration files+        run: |+          set +e+          IFS= output=$(./config-rules-cli shipping-options-transform --input-csv-dir shipping/configuration --output-json-dir shipping/autogenerated)+          status=$?++          if (($status)); then+            echo "Failed to transform shipping data" >>/tmp/pr-comment.txt+            echo "" >>/tmp/pr-comment.txt+            echo "\`\`\`" >>/tmp/pr-comment.txt+            echo $output >>/tmp/pr-comment.txt+            echo "\`\`\`" >>/tmp/pr-comment.txt+            exit $status+          else+            git add shipping/autogenerated+            if git commit -m "Update autogenerated shipping configuration files"; then+              echo "GENERATED_ANOTHER_COMMIT=true" >> $GITHUB_ENV+            else+              echo "No changes detected"+            fi+          fi+      - name: Generate new shipping tests+        run: |+          SHIPPING_TEST_DIRECTORY=shipping/tests+            find $SHIPPING_TEST_DIRECTORY -type f -name '*.result.json' -exec rm {} \;+              ./config-rules-cli -- shipping-options-check --repo-dir .+      - if: "env.PR_NUMBER != 0 && github.ref != 'refs/heads/stable' && github.ref != 'refs/heads/master'"+        name: Commit new shipping tests+        run: |+          if [ $(git status shipping/tests --porcelain=v1 | wc -l) -gt 0 ]+          then+            git add shipping/tests+            git commit -m "Update shipping tests"+            echo "GENERATED_ANOTHER_COMMIT=true" >> $GITHUB_ENV+          fi+      - name: Update autogenerated promotion configuration files+        run: |+          set +e+          IFS= output=$(./config-rules-cli promotion-transform --input-csv-dir promotions/configuration --output-json-dir promotions/autogenerated)+          status=$?++          if (($status)); then+            echo "Failed to transform promotions data" >>/tmp/pr-comment.txt+            echo "" >>/tmp/pr-comment.txt+            echo "\`\`\`" >>/tmp/pr-comment.txt+            echo $output >>/tmp/pr-comment.txt+            echo "\`\`\`" >>/tmp/pr-comment.txt+            exit $status+          else+            git add promotions/autogenerated+            if git commit -m "Update autogenerated promotions configuration files"; then+              echo "GENERATED_ANOTHER_COMMIT=true" >> $GITHUB_ENV+            else+              echo "No changes detected"+            fi+          fi+      - name: Generate new promotion rule tests+        run: |+          PROMOTIONS_TEST_DIRECTORY=promotions/tests+            find $PROMOTIONS_TEST_DIRECTORY -type f -name '*.result.json' -exec rm {} \;+              ./config-rules-cli -- promotion-check --repo-dir .+      - if: "env.PR_NUMBER != 0 && github.ref != 'refs/heads/stable' && github.ref != 'refs/heads/master'"+        name: Commit new promotion tests+        run: |+          if [ $(git status promotions/tests --porcelain=v1 | wc -l) -gt 0 ]+          then+            git add promotions/tests+            git commit -m "Update promotion tests"+            echo "GENERATED_ANOTHER_COMMIT=true" >> $GITHUB_ENV+          fi+      - if: "env.GENERATED_ANOTHER_COMMIT == 'true'"+        name: Push any new commits and exit workflow if commits pushed+        run: |+          git push --set-upstream origin HEAD++          gh run cancel ${{ github.run_id }}+          gh run watch ${{ github.run_id }}+      - id: current-time+        name: Get current time+        run: 'echo "timestamp=$(date -u +"%y%m%d%H%M%S")" >> $GITHUB_OUTPUT'+      - id: extract_changes+        if: "github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/master'"+        name: Set changes variable+        run: |+          echo "COMMIT_CHANGES<<EOF" >> $GITHUB_ENV+          echo "$(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+          echo "EOF" >> $GITHUB_ENV+        shell: bash+        working-directory: "${{ env.GITHUB_WORKSPACE }}"+      - env:+          SLACK_BOT_TOKEN: "${{ secrets.BELLROY_SLACK_TOKEN }}"+        id: slack-deploy+        if: "github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/master'"+        name: Slack - Notifications - Post to slack+        uses: "slackapi/slack-github-action@70cd7be8e40a46e8b0eced40b0de447bdb42f68e"+        with:+          channel-id: "${{ env.SLACK_CHANNEL_ID }}"+          payload: |+            {+              "text": ":loudspeaker: *${{ github.actor }}* is deploying *${{ env.SYSTEM_NAME }}* to *${{ env.DESTINATION }}* from *${{ steps.extract_branch.outputs.branch }}*",+              "attachments": [+                {+                  "fallback": "Deployment summary",+                  "color": "ffd966",+                  "fields": [+                    {+                      "title": "What is being deployed",+                      "value": ${{ toJSON(env.COMMIT_CHANGES) }},+                      "short": false+                    },+                    {+                      "title": "Status",+                      "short": true,+                      "value": "In Progress"+                    }+                  ]+                }+              ]+            }+      - if: "github.ref == 'refs/heads/master'"+        name: Push content to staging+        run: |+          set -e+          # XXX: Gitapult cannot distinguish datasets, so we push all data to+          # both endpoints and allow the server to decide what to do.+          # Kafka Webhooks accepts all datasets at once+          ./gitapult run \+            --git_root "$GITHUB_WORKSPACE" \+            --authorization_token "$KW_STAGING_API_KEY" \+            --changeset_url "$KW_STAGING_BASE_URL/gitapult/configuration/records" \+            --revision_url "$KW_STAGING_BASE_URL/gitapult/configuration/revision" \+            --api_key \+            --compress \+            --max-payload-bytes 3000000++          # XXX: V3 doesn't care about rendering data, but it is too large to send.+          # As at 2024-02-14 gitapult does not support selecting which data to send.+          git -C "$GIT_REPOSITORY_PATH" rm feeds/rendering_data/.gitapult.json+          ./gitapult run \+            --git_root "$GITHUB_WORKSPACE" \+            --authorization_token "$SF_STAGING_API_KEY" \+            --changeset_url "$SF_STAGING_BASE_URL/api/v1/configuration/shipping" \+            --revision_url "$SF_STAGING_BASE_URL/api/v1/configuration/shipping/revisions" && \+          git -C "$GIT_REPOSITORY_PATH" restore --staged feeds/rendering_data/.gitapult.json+          git -C "$GIT_REPOSITORY_PATH" restore feeds/rendering_data/.gitapult.json+      - if: "success() && github.ref == 'refs/heads/master'"+        name: Tag published staging content+        run: |+          git tag "${{ join(steps.current-time.outputs.*, '\n') }}_staging" && git push --tags+      - if: "github.ref == 'refs/heads/master'"+        name: Open PR to stable+        run: |+          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."}'+          PR_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)')+          echo "PR_URL=$PR_URL" >> $GITHUB_ENV+      - continue-on-error: true+        if: "github.ref == 'refs/heads/master'"+        name: Set assignee+        run: |+          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]')+          PR_NUM=$(echo "$PR_RESPONSE" | jq -r ".number")+          PR_ASSIGNEE=$(echo "$PR_RESPONSE" | jq -r ".assignee")+          COMMITS_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)+          FIRST_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')+          # special case for squashed PRs created by trike-deploy e.g. gha-workflows+          if [[ "$FIRST_COMMITTER" == "trike-deploy" ]]; then+              FIRST_COMMITTER=$(echo "$COMMITS_RESPONSE" | jq -r '. | sort_by(.commit.author.date) | .[0]' | sed -En "s/.*Co-authored-by: (.*) <.*@.*>\",/\1/p")+          fi+          if [[ "$PR_ASSIGNEE" == "null" ]]; then+              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\"]}"+          fi+      - if: "github.ref == 'refs/heads/stable'"+        name: Push content to production+        run: |+          set -e+          # XXX: Gitapult cannot distinguish datasets, so we push all data to+          # both endpoints and allow the server to decide what to do.+          # Kafka Webhooks accepts all datasets at once+          ./gitapult run \+            --git_root "$GITHUB_WORKSPACE" \+            --authorization_token "$KW_PRODUCTION_API_KEY" \+            --changeset_url "$KW_PRODUCTION_BASE_URL/gitapult/configuration/records" \+            --revision_url "$KW_PRODUCTION_BASE_URL/gitapult/configuration/revision" \+            --api_key \+            --compress \+            --max-payload-bytes 3000000++          # XXX: V3 doesn't care about rendering data, but it is too large to send.+          # As at 2024-02-14 gitapult does not support selecting which data to send.+          git -C "$GIT_REPOSITORY_PATH" rm feeds/rendering_data/.gitapult.json+          ./gitapult run \+            --git_root "$GITHUB_WORKSPACE" \+            --authorization_token "$SF_PRODUCTION_API_KEY" \+            --changeset_url "$SF_PRODUCTION_BASE_URL/api/v1/configuration/shipping" \+            --revision_url "$SF_PRODUCTION_BASE_URL/api/v1/configuration/shipping/revisions" && \+          git -C "$GIT_REPOSITORY_PATH" restore --staged feeds/rendering_data/.gitapult.json+          git -C "$GIT_REPOSITORY_PATH" restore feeds/rendering_data/.gitapult.json+      - if: "success() && github.ref == 'refs/heads/stable'"+        name: Tag published production content+        run: |+          git tag "${{ join(steps.current-time.outputs.*, '\n') }}_production" && git push --tags+      - if: "github.ref == 'refs/heads/master'"+        name: Set success message variable for staging+        run: |+          echo "SUCCESS_MESSAGE=:tada: \nA PR has been opened against the stable branch: ${{ env.PR_URL }}" >> $GITHUB_ENV+          echo "SUCCESS_MESSAGE_BODY=https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks\n\n*PR for Production:* ${{ env.PR_URL }}" >> $GITHUB_ENV+      - if: "github.ref == 'refs/heads/stable'"+        name: Set success message variable for production+        run: |+          echo "SUCCESS_MESSAGE=:tada:" >> $GITHUB_ENV+          echo "SUCCESS_MESSAGE_BODY=https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks" >> $GITHUB_ENV+      - env:+          SLACK_BOT_TOKEN: "${{ secrets.BELLROY_SLACK_TOKEN }}"+        id: slack-deploy-success+        if: "success() && (github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/master')"+        name: Post to Slack if published successfully+        uses: "slackapi/slack-github-action@70cd7be8e40a46e8b0eced40b0de447bdb42f68e"+        with:+          channel-id: "${{ env.SLACK_CHANNEL_ID }}"+          payload: |+            {+              "text": ":loudspeaker: *${{ github.actor }}* has deployed *${{ env.SYSTEM_NAME }}* to *${{ env.DESTINATION }}* from *${{ steps.extract_branch.outputs.branch }}* ${{ env.SUCCESS_MESSAGE }}",+              "attachments": [+                {+                  "fallback": "Deployment summary",+                  "color": "00ff00",+                  "fields": [+                    {+                      "title": "What was deployed",+                      "value": ${{ toJSON(env.COMMIT_CHANGES) }},+                      "short": false+                    },+                    {+                      "title": "Status",+                      "short": true,+                      "value": "Deployed"+                    },+                    {+                      "title": "Output",+                      "short": false,+                      "value": "${{ env.SUCCESS_MESSAGE_BODY }}"+                    }+                  ]+                }+              ]+            }+          update-ts: "${{ steps.slack-deploy.outputs.ts }}"+      - env:+          SLACK_BOT_TOKEN: "${{ secrets.BELLROY_SLACK_TOKEN }}"+        id: slack-deploy-failed+        if: "failure() && (github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/master')"+        name: Post to Slack if build/deploy fails+        uses: "slackapi/slack-github-action@70cd7be8e40a46e8b0eced40b0de447bdb42f68e"+        with:+          channel-id: "${{ env.SLACK_CHANNEL_ID }}"+          payload: |+            { "text": ":loudspeaker: *BUILD/DEPLOY FAILURE* of *${{ env.SYSTEM_NAME }}* to *${{ env.DESTINATION }}*",+              "attachments": [+                {+                  "fallback": "Deployment summary",+                  "color": "ff0000",+                  "fields": [+                    {+                      "title": "What was being deployed",+                      "value": ${{ toJSON(env.COMMIT_CHANGES) }},+                      "short": false+                    },+                    {+                      "title": "Status",+                      "short": true,+                      "value": "Failed"+                    },+                    {+                      "title": "Who broke it",+                      "value": "${{ github.actor }}"+                    },+                    {+                      "title": "Output",+                      "short": false,+                      "value": "https://github.com/${{ github.repository }}/commit/${{ github.sha }}/checks"+                    }+                  ]+                }+              ]+            }+          update-ts: "${{ steps.slack-deploy.outputs.ts }}"+      - if: "always() && env.PR_NUMBER != 0"+        name: Add comment to PR+        run: "gh pr comment ${{ env.PR_NUMBER }} --body-file /tmp/pr-comment.txt || true"+        working-directory: "${{ env.GITHUB_WORKSPACE }}"+  validateContent:+    runs-on: ubuntu-20.04+    steps:+      - id: checkout_branch+        name: checkout branch+        uses: "bellroy/gha-workflows/composite/checkout@master"+        with:+          fetch-depth: "1"+          path: "${{ env.GITHUB_WORKSPACE }}"+          token: "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}"+      - id: extract_branch+        name: Extract branch name+        run: |+          echo "branch=${GITHUB_REF#refs/heads/}" >>$GITHUB_OUTPUT+        shell: bash+      - name: Install and setup nix+        uses: "bellroy/gha-workflows/composite/install-nix@master"+        with:+          bellroy-nix-cache-access: none+      - id: fetch_gitapult+        name: Fetch gitapult+        uses: "bellroy/gha-workflows/composite/get-tool@master"+        with:+          token: "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}"+          tool-name: gitapult+          tool-version: latest+      - id: fetch_powerful-owl+        name: Fetch powerful-owl+        uses: "bellroy/gha-workflows/composite/get-tool@master"+        with:+          token: "${{ secrets.BELLROY_DEPLOY_USER_TOKEN }}"+          tool-name: powerful-owl+          tool-version: latest+      - name: Get Pull Request number+        run: |+          echo "PR_NUMBER=$(gh pr view --json number -q .number)" >> $GITHUB_ENV+        working-directory: "${{ env.GITHUB_WORKSPACE }}"+      - name: Ensure tmp directory exists+        run: mkdir -p tmp+        working-directory: "${{ env.GITHUB_WORKSPACE }}"+      - name: Validate with gitapult+        run: |+          ./gitapult validate --git_root "$GITHUB_WORKSPACE"+      - name: Validate content with powerful-owl+        run: |+          for CONTENT_DIR in feeds products+          do+            if [[ "$PR_NUMBER" ]]+            then+              ./powerful-owl validate --markdown --content_path "$GITHUB_WORKSPACE/$CONTENT_DIR" &>>/tmp/pr-comment.txt+            else+              ./powerful-owl validate --content_path "$GITHUB_WORKSPACE/$CONTENT_DIR"+            fi+          done+      - if: "always() && env.PR_NUMBER != 0"+        name: Add comment to PR+        run: "gh pr comment ${{ env.PR_NUMBER }} --body-file /tmp/pr-comment.txt || true"+        working-directory: "${{ env.GITHUB_WORKSPACE }}"+      - if: "failure() && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/stable')"+        name: Post to slack if build fails+        uses: "bellroy/gha-workflows/composite/slack-update@master"+        with:+          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 }}"+    timeout-minutes: 30+name: Publish+on:+  push:+    branches-ignore:+      - "refs/tags/*_staging"+      - "refs/tags/*_production"