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 +29/−0
- README.md +97/−0
- Setup.hs +3/−0
- github-actions.cabal +93/−0
- src/Language/Github/Actions/Concurrency.hs +81/−0
- src/Language/Github/Actions/Defaults.hs +97/−0
- src/Language/Github/Actions/Internal.hs +16/−0
- src/Language/Github/Actions/Job.hs +235/−0
- src/Language/Github/Actions/Job/Container.hs +125/−0
- src/Language/Github/Actions/Job/Environment.hs +97/−0
- src/Language/Github/Actions/Job/Id.hs +73/−0
- src/Language/Github/Actions/Job/Strategy.hs +158/−0
- src/Language/Github/Actions/Permissions.hs +202/−0
- src/Language/Github/Actions/Service.hs +147/−0
- src/Language/Github/Actions/Service/Id.hs +72/−0
- src/Language/Github/Actions/Shell.hs +158/−0
- src/Language/Github/Actions/Step.hs +186/−0
- src/Language/Github/Actions/Step/Id.hs +73/−0
- src/Language/Github/Actions/Step/With.hs +134/−0
- src/Language/Github/Actions/Workflow.hs +204/−0
- src/Language/Github/Actions/Workflow/Trigger.hs +1459/−0
- test/Language/Github/Actions/WorkflowTest.hs +68/−0
- test/Main.hs +1/−0
- test/golden/configuration-main.golden.yml +550/−0
- test/golden/configuration-main.hs.txt +1030/−0
- test/golden/configuration-main.yml +549/−0
+ 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++[](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"