packages feed

github-actions-0.1.0.0: src/Language/Github/Actions/Job.hs

{-# 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
    }