Repository: jenkins-x/lighthouse
Pull request #974 (2020-08-25 13:49:47)

refactor: rework lighthouse config
This PR moves all lighthouse config to its own package.

Related issue #965


jenkins-x-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by:
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **macox**
You can assign the PR to them by writing `/assign @macox` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=jenkins-x%2Flighthouse).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/jenkins-x/lighthouse/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["macox"]} -->
abayer: /lgtm
jenkins-x-bot: [APPROVALNOTIFIER] This PR is **APPROVED**

This pull-request has been approved by: *[abayer](https://github.com/jenkins-x/lighthouse/pull/974#issuecomment-680077471 "LGTM")*

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=jenkins-x%2Flighthouse).

The pull request process is described [here](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process)

<details >
Needs approval from an approver in each of these files:

- ~~[OWNERS](https://github.com#issuecomment-680077471/jenkins-x/lighthouse/blob/master/OWNERS)~~ [abayer]

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":[]} -->
eddycharly: This was a big one !

Patch:
diff --git a/Makefile b/Makefile
index 5ee06a3f..10416f3a 100644
--- a/Makefile
+++ b/Makefile
@@ -184,11 +184,8 @@ crds-docs:
 .PHONY: config-docs
 config-docs:
 	cd hack && go run struct-docs.go \
-		--input-file ../pkg/config/branch_protection.go \
 		--input-file ../pkg/config/config.go \
-		--input-file ../pkg/config/constants.go \
 		--input-file ../pkg/config/githuboauth.go \
-		--input-file ../pkg/config/keeper.go \
 		--title "Lighthouse config" \
 		--output-path ../docs/config
 
diff --git a/docs/config/Lighthouse config.md b/docs/config/Lighthouse config.md
index cdb32b8c..2ad3131c 100644
--- a/docs/config/Lighthouse config.md
+++ b/docs/config/Lighthouse config.md
@@ -1,59 +1,12 @@
 # Lighthouse config
 
-- [Branch](#Branch)
-- [BranchProtection](#BranchProtection)
 - [Config](#Config)
-- [ContextPolicy](#ContextPolicy)
-- [Controller](#Controller)
 - [Cookie](#Cookie)
-- [GitHubOptions](#GitHubOptions)
 - [GithubOAuthConfig](#GithubOAuthConfig)
-- [JenkinsOperator](#JenkinsOperator)
 - [JobConfig](#JobConfig)
-- [Keeper](#Keeper)
-- [KeeperContextPolicy](#KeeperContextPolicy)
-- [KeeperContextPolicyOptions](#KeeperContextPolicyOptions)
-- [KeeperMergeCommitTemplate](#KeeperMergeCommitTemplate)
-- [KeeperOrgContextPolicy](#KeeperOrgContextPolicy)
-- [KeeperQueries](#KeeperQueries)
-- [KeeperQuery](#KeeperQuery)
-- [KeeperRepoContextPolicy](#KeeperRepoContextPolicy)
-- [Org](#Org)
-- [OwnersDirExcludes](#OwnersDirExcludes)
-- [PipelineKind](#PipelineKind)
-- [Plank](#Plank)
-- [Policy](#Policy)
-- [ProviderConfig](#ProviderConfig)
 - [ProwConfig](#ProwConfig)
-- [PubsubSubscriptions](#PubsubSubscriptions)
-- [PullRequestMergeType](#PullRequestMergeType)
-- [PushGateway](#PushGateway)
-- [QueryMap](#QueryMap)
-- [Repo](#Repo)
-- [Restrictions](#Restrictions)
-- [ReviewPolicy](#ReviewPolicy)
 
 
-## Branch
-
-Branch holds protection policy overrides for a particular branch.
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-|  |  | [Policy](#Policy) | Yes |  |
-
-## BranchProtection
-
-BranchProtection specifies the global branch protection policy
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-|  |  | [Policy](#Policy) | Yes |  |
-| ProtectTested | `protect-tested-repos` | bool | No | ProtectTested determines if branch protection rules are set for all repos<br />that Prow has registered jobs for, regardless of if those repos are in the<br />branch protection config. |
-| Orgs | `orgs` | map[string][Org](#Org) | No | Orgs holds branch protection options for orgs by name |
-| AllowDisabledPolicies | `allow_disabled_policies` | bool | No | AllowDisabledPolicies allows a child to disable all protection even if the<br />branch has inherited protection options from a parent. |
-| AllowDisabledJobPolicies | `allow_disabled_job_policies` | bool | No | AllowDisabledJobPolicies allows a branch to choose to opt out of branch protection<br />even if Prow has registered required jobs for that branch. |
-
 ## Config
 
 Config is a read-only snapshot of the config.
@@ -63,29 +16,6 @@ Config is a read-only snapshot of the config.
 |  |  | [JobConfig](#JobConfig) | Yes |  |
 |  |  | [ProwConfig](#ProwConfig) | Yes |  |
 
-## ContextPolicy
-
-ContextPolicy configures required github contexts.<br />When merging policies, contexts are appended to context list from parent.<br />Strict determines whether merging to the branch invalidates existing contexts.
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-| Contexts | `contexts` | []string | No | Contexts appends required contexts that must be green to merge |
-| Strict | `strict` | *bool | No | Strict overrides whether new commits in the base branch require updating the PR if set |
-
-## Controller
-
-Controller holds configuration applicable to all agent-specific<br />prow controllers.
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-| JobURLTemplateString | `job_url_template` | string | No | JobURLTemplateString compiles into JobURLTemplate at load time. |
-| JobURLTemplate | `-` | *template.Template | No | JobURLTemplate is compiled at load time from JobURLTemplateString. It<br />will be passed a builder.PipelineOptions and is used to set the URL for the<br />"Details" link on GitHub as well as the link from deck. |
-| ReportTemplateString | `report_template` | string | No | ReportTemplateString compiles into ReportTemplate at load time. |
-| ReportTemplate | `-` | *template.Template | No | ReportTemplate is compiled at load time from ReportTemplateString. It<br />will be passed a builder.PipelineOptions and can provide an optional blurb below<br />the test failures comment. |
-| MaxConcurrency | `max_concurrency` | int | No | MaxConcurrency is the maximum number of tests running concurrently that<br />will be allowed by the controller. 0 implies no limit. |
-| MaxGoroutines | `max_goroutines` | int | No | MaxGoroutines is the maximum number of goroutines spawned inside the<br />controller to handle tests. Defaults to 20. Needs to be a positive<br />number. |
-| AllowCancellations | `allow_cancellations` | bool | No | AllowCancellations enables aborting presubmit jobs for commits that<br />have been superseded by newer commits in Github pull requests. |
-
 ## Cookie
 
 Cookie holds the secret returned from github that authenticates the user who authorized this app.
@@ -94,15 +24,6 @@ Cookie holds the secret returned from github that authenticates the user who aut
 |---|---|---|---|---|
 | Secret | `secret` | string | No |  |
 
-## GitHubOptions
-
-GitHubOptions allows users to control how prow applications display GitHub website links.
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-| LinkURLFromConfig | `link_url` | string | No | LinkURLFromConfig is the string representation of the link_url config parameter.<br />This config parameter allows users to override the default GitHub link url for all plugins.<br />If this option is not set, we assume "https://github.com". |
-| LinkURL |  | *url.URL | No | LinkURL is the url representation of LinkURLFromConfig. This variable should be used<br />in all places internally. |
-
 ## GithubOAuthConfig
 
 GithubOAuthConfig is a config for requesting users access tokens from Github API. It also has<br />a Cookie Store that retains user credentials deriving from Github API.
@@ -116,254 +37,16 @@ GithubOAuthConfig is a config for requesting users access tokens from Github API
 | FinalRedirectURL | `final_redirect_url` | string | Yes |  |
 | CookieStore | `-` | *sessions.CookieStore | No |  |
 
-## JenkinsOperator
-
-JenkinsOperator is config for the jenkins-operator controller.
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-|  |  | [Controller](#Controller) | Yes |  |
-| LabelSelectorString | `label_selector` | string | No | LabelSelectorString compiles into LabelSelector at load time.<br />If set, this option needs to match --label-selector used by<br />the desired jenkins-operator. This option is considered<br />invalid when provided with a single jenkins-operator config.<br /><br />For label selector syntax, see below:<br />https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors |
-| LabelSelector | `-` | labels.Selector | Yes | LabelSelector is used so different jenkins-operator replicas<br />can use their own configuration. |
-
 ## JobConfig
 
 JobConfig is a type alias for job.Config
 
 
 
-## Keeper
-
-Keeper is config for the keeper pool.
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-| SyncPeriodString | `sync_period` | string | No | SyncPeriodString compiles into SyncPeriod at load time. |
-| SyncPeriod | `-` | time.Duration | Yes | SyncPeriod specifies how often Keeper will sync jobs with Github. Defaults to 1m. |
-| StatusUpdatePeriodString | `status_update_period` | string | No | StatusUpdatePeriodString compiles into StatusUpdatePeriod at load time. |
-| StatusUpdatePeriod | `-` | time.Duration | Yes | StatusUpdatePeriod specifies how often Keeper will update Github status contexts.<br />Defaults to the value of SyncPeriod. |
-| Queries | `queries` | [KeeperQueries](#KeeperQueries) | No | Queries represents a list of GitHub search queries that collectively<br />specify the set of PRs that meet merge requirements. |
-| MergeType | `merge_method` | map[string][PullRequestMergeType](#PullRequestMergeType) | No | A key/value pair of an org/repo as the key and merge method to override<br />the default method of merge. Valid options are squash, rebase, and merge. |
-| MergeTemplate | `merge_commit_template` | map[string][KeeperMergeCommitTemplate](#KeeperMergeCommitTemplate) | No | A key/value pair of an org/repo as the key and Go template to override<br />the default merge commit title and/or message. Template is passed the<br />PullRequest struct (prow/github/types.go#PullRequest) |
-| TargetURL | `target_url` | string | No | URL for keeper status contexts.<br />We can consider allowing this to be set separately for separate repos, or<br />allowing it to be a template. |
-| PRStatusBaseURL | `pr_status_base_url` | string | No | PRStatusBaseURL is the base URL for the PR status page.<br />This is used to link to a merge requirements overview<br />in the keeper status context. |
-| BlockerLabel | `blocker_label` | string | No | BlockerLabel is an optional label that is used to identify merge blocking<br />Github issues.<br />Leave this blank to disable this feature and save 1 API token per sync loop. |
-| SquashLabel | `squash_label` | string | No | SquashLabel is an optional label that is used to identify PRs that should<br />always be squash merged.<br />Leave this blank to disable this feature. |
-| RebaseLabel | `rebase_label` | string | No | RebaseLabel is an optional label that is used to identify PRs that should<br />always be rebased and merged.<br />Leave this blank to disable this feature. |
-| MergeLabel | `merge_label` | string | No | MergeLabel is an optional label that is used to identify PRs that should<br />always be merged with all individual commits from the PR.<br />Leave this blank to disable this feature. |
-| MaxGoroutines | `max_goroutines` | int | No | MaxGoroutines is the maximum number of goroutines spawned inside the<br />controller to handle org/repo:branch pools. Defaults to 20. Needs to be a<br />positive number. |
-| ContextOptions | `context_options` | [KeeperContextPolicyOptions](#KeeperContextPolicyOptions) | No | KeeperContextPolicyOptions defines merge options for context. If not set it will infer<br />the required and optional contexts from the prow jobs configured and use the github<br />combined status; otherwise it may apply the branch protection setting or let user<br />define their own options in case branch protection is not used. |
-| BatchSizeLimitMap | `batch_size_limit` | map[string]int | No | BatchSizeLimitMap is a key/value pair of an org or org/repo as the key and<br />integer batch size limit as the value. The empty string key can be used as<br />a global default.<br />Special values:<br /> 0 => unlimited batch size<br />-1 => batch merging disabled :( |
-
-## KeeperContextPolicy
-
-KeeperContextPolicy configures options about how to handle various contexts.
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-| SkipUnknownContexts | `skip-unknown-contexts` | *bool | No | whether to consider unknown contexts optional (skip) or required. |
-| RequiredContexts | `required-contexts` | []string | No |  |
-| RequiredIfPresentContexts | `required-if-present-contexts` | []string | No |  |
-| OptionalContexts | `optional-contexts` | []string | No |  |
-| FromBranchProtection | `from-branch-protection` | *bool | No | Infer required and optional jobs from Branch Protection configuration |
-
-## KeeperContextPolicyOptions
-
-KeeperContextPolicyOptions holds the default policy, and any org overrides.
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-|  |  | [KeeperContextPolicy](#KeeperContextPolicy) | Yes |  |
-| Orgs | `orgs` | map[string][KeeperOrgContextPolicy](#KeeperOrgContextPolicy) | No | Github Orgs |
-
-## KeeperMergeCommitTemplate
-
-KeeperMergeCommitTemplate holds templates to use for merge commits.
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-| TitleTemplate | `title` | string | No |  |
-| BodyTemplate | `body` | string | No |  |
-| Title | `-` | *template.Template | No |  |
-| Body | `-` | *template.Template | No |  |
-
-## KeeperOrgContextPolicy
-
-KeeperOrgContextPolicy overrides the policy for an org, and any repo overrides.
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-|  |  | [KeeperContextPolicy](#KeeperContextPolicy) | Yes |  |
-| Repos | `repos` | map[string][KeeperRepoContextPolicy](#KeeperRepoContextPolicy) | No |  |
-
-## KeeperQueries
-
-KeeperQueries is a KeeperQuery slice.
-
-
-
-## KeeperQuery
-
-KeeperQuery is turned into a GitHub search query. See the docs for details:<br />https://help.github.com/articles/searching-issues-and-pull-requests/
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-| Orgs | `orgs` | []string | No |  |
-| Repos | `repos` | []string | No |  |
-| ExcludedRepos | `excludedRepos` | []string | No |  |
-| ExcludedBranches | `excludedBranches` | []string | No |  |
-| IncludedBranches | `includedBranches` | []string | No |  |
-| Labels | `labels` | []string | No |  |
-| MissingLabels | `missingLabels` | []string | No |  |
-| Milestone | `milestone` | string | No |  |
-| ReviewApprovedRequired | `reviewApprovedRequired` | bool | No |  |
-
-## KeeperRepoContextPolicy
-
-KeeperRepoContextPolicy overrides the policy for repo, and any branch overrides.
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-|  |  | [KeeperContextPolicy](#KeeperContextPolicy) | Yes |  |
-| Branches | `branches` | map[string][KeeperContextPolicy](#KeeperContextPolicy) | No |  |
-
-## Org
-
-Org holds the default protection policy for an entire org, as well as any repo overrides.
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-|  |  | [Policy](#Policy) | Yes |  |
-| Repos | `repos` | map[string][Repo](#Repo) | No |  |
-
-## OwnersDirExcludes
-
-OwnersDirExcludes is used to configure which directories to ignore when<br />searching for OWNERS{,_ALIAS} files in a repo.
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-| Repos | `repos` | map[string][]string | No | Repos configures a directory blacklist per repo (or org) |
-| Default | `default` | []string | No | Default configures a default blacklist for repos (or orgs) not<br />specifically configured |
-
-## PipelineKind
-
-PipelineKind specifies how the job is triggered.
-
-
-
-## Plank
-
-Plank is config for the plank controller.
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-| ReportTemplateString | `report_template` | string | No | ReportTemplateString compiles into ReportTemplate at load time. |
-| ReportTemplate | `-` | *template.Template | No | ReportTemplate is compiled at load time from ReportTemplateString. It<br />will be passed a builder.PipelineOptions and can provide an optional blurb below<br />the test failures comment. |
-
-## Policy
-
-Policy for the config/org/repo/branch.<br />When merging policies, a nil value results in inheriting the parent policy.
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-| Protect | `protect` | *bool | No | Protect overrides whether branch protection is enabled if set. |
-| RequiredStatusChecks | `required_status_checks` | *[ContextPolicy](#ContextPolicy) | No | RequiredStatusChecks configures github contexts |
-| Admins | `enforce_admins` | *bool | No | Admins overrides whether protections apply to admins if set. |
-| Restrictions | `restrictions` | *[Restrictions](#Restrictions) | No | Restrictions limits who can merge |
-| RequiredPullRequestReviews | `required_pull_request_reviews` | *[ReviewPolicy](#ReviewPolicy) | No | RequiredPullRequestReviews specifies github approval/review criteria. |
-| Exclude | `exclude` | []string | No | Exclude specifies a set of regular expressions which identify branches<br />that should be excluded from the protection policy |
-
-## ProviderConfig
-
-ProviderConfig is optionally used to configure information about the SCM provider being used. These values will be<br />used as fallbacks if environment variables aren't set.
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-| Kind | `kind` | string | No | Kind is the go-scm driver name |
-| Server | `server` | string | No | Server is the base URL for the provider, like https://github.com |
-| BotUser | `botUser` | string | No | BotUser is the username on the provider the bot will use |
-
 ## ProwConfig
 
-ProwConfig is config for all prow controllers
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-| Keeper | `tide` | [Keeper](#Keeper) | No |  |
-| Plank | `plank` | [Plank](#Plank) | No |  |
-| BranchProtection | `branch-protection` | [BranchProtection](#BranchProtection) | No |  |
-| Orgs | `orgs` | map[string]org.Config | No |  |
-| JenkinsOperators | `jenkins_operators` | [][JenkinsOperator](#JenkinsOperator) | No | TODO: Move this out of the main config. |
-| LighthouseJobNamespace | `prowjob_namespace` | string | No | LighthouseJobNamespace is the namespace in the cluster that prow<br />components will use for looking up LighthouseJobs. The namespace<br />needs to exist and will not be created by prow.<br />Defaults to "default". |
-| PodNamespace | `pod_namespace` | string | No | PodNamespace is the namespace in the cluster that prow<br />components will use for looking up Pods owned by LighthouseJobs.<br />The namespace needs to exist and will not be created by prow.<br />Defaults to "default". |
-| LogLevel | `log_level` | string | No | LogLevel enables dynamically updating the log level of the<br />standard logger that is used by all prow components.<br /><br />Valid values:<br /><br />"debug", "info", "warn", "warning", "error", "fatal", "panic"<br /><br />Defaults to "info". |
-| PushGateway | `push_gateway` | [PushGateway](#PushGateway) | No | PushGateway is a prometheus push gateway. |
-| OwnersDirExcludes | `owners_dir_excludes` | *[OwnersDirExcludes](#OwnersDirExcludes) | No | OwnersDirExcludes is used to configure which directories to ignore when<br />searching for OWNERS{,_ALIAS} files in a repo. |
-| OwnersDirBlacklist | `owners_dir_blacklist` | *[OwnersDirExcludes](#OwnersDirExcludes) | No | OwnersDirExcludes is DEPRECATED in favor of OwnersDirExcludes |
-| PubSubSubscriptions | `pubsub_subscriptions` | [PubsubSubscriptions](#PubsubSubscriptions) | No | Pub/Sub Subscriptions that we want to listen to |
-| GitHubOptions | `github` | [GitHubOptions](#GitHubOptions) | No | GitHubOptions allows users to control how prow applications display GitHub website links. |
-| ProviderConfig | `providerConfig` | *[ProviderConfig](#ProviderConfig) | No | ProviderConfig contains optional SCM provider information |
-
-## PubsubSubscriptions
-
-PubsubSubscriptions maps GCP projects to a list of Topics.
-
-
-
-## PullRequestMergeType
-
-PullRequestMergeType inidicates the type of the pull request
-
+ProwConfig is a type alias for lighthouse.Config
 
 
-## PushGateway
-
-PushGateway is a prometheus push gateway.
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-| Endpoint | `endpoint` | string | No | Endpoint is the location of the prometheus pushgateway<br />where prow will push metrics to. |
-| IntervalString | `interval` | string | No | IntervalString compiles into Interval at load time. |
-| Interval | `-` | time.Duration | Yes | Interval specifies how often prow will push metrics<br />to the pushgateway. Defaults to 1m. |
-| ServeMetrics | `serve_metrics` | bool | Yes | ServeMetrics tells if or not the components serve metrics |
-
-## QueryMap
-
-QueryMap is a struct mapping from "org/repo" -> KeeperQueries that<br />apply to that org or repo. It is lazily populated, but threadsafe.
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-| queries |  | [KeeperQueries](#KeeperQueries) | Yes |  |
-| cache |  | map[string][KeeperQueries](#KeeperQueries) | No |  |
-|  |  | sync.Mutex | Yes |  |
-
-## Repo
-
-Repo holds protection policy overrides for all branches in a repo, as well as specific branch overrides.
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-|  |  | [Policy](#Policy) | Yes |  |
-| Branches | `branches` | map[string][Branch](#Branch) | No |  |
-
-## Restrictions
-
-Restrictions limits who can merge<br />Users and Teams items are appended to parent lists.
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-| Users | `users` | []string | No |  |
-| Teams | `teams` | []string | No |  |
-
-## ReviewPolicy
-
-ReviewPolicy specifies github approval/review criteria.<br />Any nil values inherit the policy from the parent, otherwise bool/ints are overridden.<br />Non-empty lists are appended to parent lists.
-
-| Variable Name | Stanza | Type | Required | Description |
-|---|---|---|---|---|
-| DismissalRestrictions | `dismissal_restrictions` | *[Restrictions](#Restrictions) | No | Restrictions appends users/teams that are allowed to merge |
-| DismissStale | `dismiss_stale_reviews` | *bool | No | DismissStale overrides whether new commits automatically dismiss old reviews if set |
-| RequireOwners | `require_code_owner_reviews` | *bool | No | RequireOwners overrides whether CODEOWNERS must approve PRs if set |
-| Approvals | `required_approving_review_count` | *int | No | Approvals overrides the number of approvals required if set (set to 0 to disable) |
 
 
diff --git a/docs/crds/Lighthouse (v1alpha1).md b/docs/crds/Lighthouse (v1alpha1).md
index 214738a0..b24465b0 100644
--- a/docs/crds/Lighthouse (v1alpha1).md
+++ b/docs/crds/Lighthouse (v1alpha1).md
@@ -106,7 +106,7 @@ LighthouseJobSpec the spec of a pipeline request
 
 | Variable Name | Stanza | Type | Required | Description |
 |---|---|---|---|---|
-| Type | `type` | config.PipelineKind | No | Type is the type of job and informs how<br />the jobs is triggered |
+| Type | `type` | job.PipelineKind | No | Type is the type of job and informs how<br />the jobs is triggered |
 | Agent | `agent` | string | No | Agent is what should run this job, if anything. |
 | Namespace | `namespace` | string | No | Namespace defines where to create pods/resources. |
 | Job | `job` | string | No | Job is the name of the job |
diff --git a/pkg/apis/lighthouse/v1alpha1/types.go b/pkg/apis/lighthouse/v1alpha1/types.go
index b1be16e3..9130d400 100644
--- a/pkg/apis/lighthouse/v1alpha1/types.go
+++ b/pkg/apis/lighthouse/v1alpha1/types.go
@@ -7,7 +7,6 @@ import (
 	"strings"
 	"time"
 
-	"github.com/jenkins-x/lighthouse/pkg/config"
 	"github.com/jenkins-x/lighthouse/pkg/config/job"
 	tektonv1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
 	corev1 "k8s.io/api/core/v1"
@@ -120,7 +119,7 @@ type LighthouseJobList struct {
 type LighthouseJobSpec struct {
 	// Type is the type of job and informs how
 	// the jobs is triggered
-	Type config.PipelineKind `json:"type,omitempty"`
+	Type job.PipelineKind `json:"type,omitempty"`
 	// Agent is what should run this job, if anything.
 	Agent string `json:"agent,omitempty"`
 	// Namespace defines where to create pods/resources.
@@ -154,10 +153,10 @@ type LighthouseJobSpec struct {
 // GetBranch returns the branch name corresponding to the refs on this spec.
 func (s *LighthouseJobSpec) GetBranch() string {
 	branch := s.Refs.BaseRef
-	if s.Type == config.PostsubmitJob {
+	if s.Type == job.PostsubmitJob {
 		return branch
 	}
-	if s.Type == config.BatchJob {
+	if s.Type == job.BatchJob {
 		return "batch"
 	}
 	if len(s.Refs.Pulls) > 0 {
@@ -175,7 +174,7 @@ func (s *LighthouseJobSpec) GetEnvVars() map[string]string {
 
 	env[JobSpecEnv] = fmt.Sprintf("type:%s", s.Type)
 
-	if s.Type == config.PeriodicJob {
+	if s.Type == job.PeriodicJob {
 		return env
 	}
 
@@ -185,7 +184,7 @@ func (s *LighthouseJobSpec) GetEnvVars() map[string]string {
 	env[PullBaseShaEnv] = s.Refs.BaseSHA
 	env[PullRefsEnv] = s.Refs.String()
 
-	if s.Type == config.PostsubmitJob || s.Type == config.BatchJob {
+	if s.Type == job.PostsubmitJob || s.Type == job.BatchJob {
 		return env
 	}
 
diff --git a/pkg/config/branch_protection.go b/pkg/config/branch_protection.go
deleted file mode 100644
index d4bc1193..00000000
--- a/pkg/config/branch_protection.go
+++ /dev/null
@@ -1,328 +0,0 @@
-/*
-Copyright 2018 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package config
-
-import (
-	"errors"
-	"fmt"
-
-	"github.com/jenkins-x/lighthouse/pkg/config/job"
-	"github.com/sirupsen/logrus"
-	"k8s.io/apimachinery/pkg/util/sets"
-)
-
-// Policy for the config/org/repo/branch.
-// When merging policies, a nil value results in inheriting the parent policy.
-type Policy struct {
-	// Protect overrides whether branch protection is enabled if set.
-	Protect *bool `json:"protect,omitempty"`
-	// RequiredStatusChecks configures github contexts
-	RequiredStatusChecks *ContextPolicy `json:"required_status_checks,omitempty"`
-	// Admins overrides whether protections apply to admins if set.
-	Admins *bool `json:"enforce_admins,omitempty"`
-	// Restrictions limits who can merge
-	Restrictions *Restrictions `json:"restrictions,omitempty"`
-	// RequiredPullRequestReviews specifies github approval/review criteria.
-	RequiredPullRequestReviews *ReviewPolicy `json:"required_pull_request_reviews,omitempty"`
-	// Exclude specifies a set of regular expressions which identify branches
-	// that should be excluded from the protection policy
-	Exclude []string `json:"exclude,omitempty"`
-}
-
-func (p Policy) defined() bool {
-	return p.Protect != nil || p.RequiredStatusChecks != nil || p.Admins != nil || p.Restrictions != nil || p.RequiredPullRequestReviews != nil
-}
-
-// ContextPolicy configures required github contexts.
-// When merging policies, contexts are appended to context list from parent.
-// Strict determines whether merging to the branch invalidates existing contexts.
-type ContextPolicy struct {
-	// Contexts appends required contexts that must be green to merge
-	Contexts []string `json:"contexts,omitempty"`
-	// Strict overrides whether new commits in the base branch require updating the PR if set
-	Strict *bool `json:"strict,omitempty"`
-}
-
-// ReviewPolicy specifies github approval/review criteria.
-// Any nil values inherit the policy from the parent, otherwise bool/ints are overridden.
-// Non-empty lists are appended to parent lists.
-type ReviewPolicy struct {
-	// Restrictions appends users/teams that are allowed to merge
-	DismissalRestrictions *Restrictions `json:"dismissal_restrictions,omitempty"`
-	// DismissStale overrides whether new commits automatically dismiss old reviews if set
-	DismissStale *bool `json:"dismiss_stale_reviews,omitempty"`
-	// RequireOwners overrides whether CODEOWNERS must approve PRs if set
-	RequireOwners *bool `json:"require_code_owner_reviews,omitempty"`
-	// Approvals overrides the number of approvals required if set (set to 0 to disable)
-	Approvals *int `json:"required_approving_review_count,omitempty"`
-}
-
-// Restrictions limits who can merge
-// Users and Teams items are appended to parent lists.
-type Restrictions struct {
-	Users []string `json:"users"`
-	Teams []string `json:"teams"`
-}
-
-// selectInt returns the child if set, else parent
-func selectInt(parent, child *int) *int {
-	if child != nil {
-		return child
-	}
-	return parent
-}
-
-// selectBool returns the child argument if set, otherwise the parent
-func selectBool(parent, child *bool) *bool {
-	if child != nil {
-		return child
-	}
-	return parent
-}
-
-// unionStrings merges the parent and child items together
-func unionStrings(parent, child []string) []string {
-	if child == nil {
-		return parent
-	}
-	if parent == nil {
-		return child
-	}
-	s := sets.NewString(parent...)
-	s.Insert(child...)
-	return s.List()
-}
-
-func mergeContextPolicy(parent, child *ContextPolicy) *ContextPolicy {
-	if child == nil {
-		return parent
-	}
-	if parent == nil {
-		return child
-	}
-	return &ContextPolicy{
-		Contexts: unionStrings(parent.Contexts, child.Contexts),
-		Strict:   selectBool(parent.Strict, child.Strict),
-	}
-}
-
-func mergeReviewPolicy(parent, child *ReviewPolicy) *ReviewPolicy {
-	if child == nil {
-		return parent
-	}
-	if parent == nil {
-		return child
-	}
-	return &ReviewPolicy{
-		DismissalRestrictions: mergeRestrictions(parent.DismissalRestrictions, child.DismissalRestrictions),
-		DismissStale:          selectBool(parent.DismissStale, child.DismissStale),
-		RequireOwners:         selectBool(parent.RequireOwners, child.RequireOwners),
-		Approvals:             selectInt(parent.Approvals, child.Approvals),
-	}
-}
-
-func mergeRestrictions(parent, child *Restrictions) *Restrictions {
-	if child == nil {
-		return parent
-	}
-	if parent == nil {
-		return child
-	}
-	return &Restrictions{
-		Users: unionStrings(parent.Users, child.Users),
-		Teams: unionStrings(parent.Teams, child.Teams),
-	}
-}
-
-// Apply returns a policy that merges the child into the parent
-func (p Policy) Apply(child Policy) Policy {
-	return Policy{
-		Protect:                    selectBool(p.Protect, child.Protect),
-		RequiredStatusChecks:       mergeContextPolicy(p.RequiredStatusChecks, child.RequiredStatusChecks),
-		Admins:                     selectBool(p.Admins, child.Admins),
-		Restrictions:               mergeRestrictions(p.Restrictions, child.Restrictions),
-		RequiredPullRequestReviews: mergeReviewPolicy(p.RequiredPullRequestReviews, child.RequiredPullRequestReviews),
-		Exclude:                    unionStrings(p.Exclude, child.Exclude),
-	}
-}
-
-// BranchProtection specifies the global branch protection policy
-type BranchProtection struct {
-	Policy
-	// ProtectTested determines if branch protection rules are set for all repos
-	// that Prow has registered jobs for, regardless of if those repos are in the
-	// branch protection config.
-	ProtectTested bool `json:"protect-tested-repos,omitempty"`
-	// Orgs holds branch protection options for orgs by name
-	Orgs map[string]Org `json:"orgs,omitempty"`
-	// AllowDisabledPolicies allows a child to disable all protection even if the
-	// branch has inherited protection options from a parent.
-	AllowDisabledPolicies bool `json:"allow_disabled_policies,omitempty"`
-	// AllowDisabledJobPolicies allows a branch to choose to opt out of branch protection
-	// even if Prow has registered required jobs for that branch.
-	AllowDisabledJobPolicies bool `json:"allow_disabled_job_policies,omitempty"`
-}
-
-// GetOrg returns the org config after merging in any global policies.
-func (bp BranchProtection) GetOrg(name string) *Org {
-	o, ok := bp.Orgs[name]
-	if ok {
-		o.Policy = bp.Apply(o.Policy)
-	} else {
-		o.Policy = bp.Policy
-	}
-	return &o
-}
-
-// Org holds the default protection policy for an entire org, as well as any repo overrides.
-type Org struct {
-	Policy
-	Repos map[string]Repo `json:"repos,omitempty"`
-}
-
-// GetRepo returns the repo config after merging in any org policies.
-func (o Org) GetRepo(name string) *Repo {
-	r, ok := o.Repos[name]
-	if ok {
-		r.Policy = o.Apply(r.Policy)
-	} else {
-		r.Policy = o.Policy
-	}
-	return &r
-}
-
-// Repo holds protection policy overrides for all branches in a repo, as well as specific branch overrides.
-type Repo struct {
-	Policy
-	Branches map[string]Branch `json:"branches,omitempty"`
-}
-
-// GetBranch returns the branch config after merging in any repo policies.
-func (r Repo) GetBranch(name string) (*Branch, error) {
-	b, ok := r.Branches[name]
-	if ok {
-		b.Policy = r.Apply(b.Policy)
-		if b.Protect == nil {
-			return nil, errors.New("defined branch policies must set protect")
-		}
-	} else {
-		b.Policy = r.Policy
-	}
-	return &b, nil
-}
-
-// Branch holds protection policy overrides for a particular branch.
-type Branch struct {
-	Policy
-}
-
-// GetBranchProtection returns the policy for a given branch.
-//
-// Handles merging any policies defined at repo/org/global levels into the branch policy.
-func (c *Config) GetBranchProtection(org, repo, branch string) (*Policy, error) {
-	if _, present := c.BranchProtection.Orgs[org]; !present {
-		return nil, nil // only consider branches in configured orgs
-	}
-	b, err := c.BranchProtection.GetOrg(org).GetRepo(repo).GetBranch(branch)
-	if err != nil {
-		return nil, err
-	}
-
-	return c.GetPolicy(org, repo, branch, *b)
-}
-
-// GetPolicy returns the protection policy for the branch, after merging in presubmits.
-func (c *Config) GetPolicy(org, repo, branch string, b Branch) (*Policy, error) {
-	policy := b.Policy
-
-	// Automatically require contexts from prow which must always be present
-	if prowContexts, _, _ := BranchRequirements(org, repo, branch, c.Presubmits); len(prowContexts) > 0 {
-		// Error if protection is disabled
-		if policy.Protect != nil && !*policy.Protect {
-			if c.BranchProtection.AllowDisabledJobPolicies {
-				logrus.Warnf("%s/%s=%s has required jobs but has protect: false", org, repo, branch)
-				return nil, nil
-			}
-			return nil, fmt.Errorf("required prow jobs require branch protection")
-
-		}
-		ps := Policy{
-			RequiredStatusChecks: &ContextPolicy{
-				Contexts: prowContexts,
-			},
-		}
-		// Require protection by default if ProtectTested is true
-		if c.BranchProtection.ProtectTested {
-			yes := true
-			ps.Protect = &yes
-		}
-		policy = policy.Apply(ps)
-	}
-
-	if policy.Protect != nil && !*policy.Protect {
-		// Ensure that protection is false => no protection settings
-		var old *bool
-		old, policy.Protect = policy.Protect, old
-		switch {
-		case policy.defined() && c.BranchProtection.AllowDisabledPolicies:
-			logrus.Warnf("%s/%s=%s defines a policy but has protect: false", org, repo, branch)
-			policy = Policy{
-				Protect: policy.Protect,
-			}
-		case policy.defined():
-			return nil, fmt.Errorf("%s/%s=%s defines a policy, which requires protect: true", org, repo, branch)
-		}
-		policy.Protect = old
-	}
-
-	if !policy.defined() {
-		return nil, nil
-	}
-	return &policy, nil
-}
-
-// BranchRequirements partitions status contexts for a given org, repo branch into three buckets:
-//  - contexts that are always required to be present
-//  - contexts that are required, _if_ present
-//  - contexts that are always optional
-func BranchRequirements(org, repo, branch string, presubmits map[string][]job.Presubmit) ([]string, []string, []string) {
-	jobs, ok := presubmits[org+"/"+repo]
-	if !ok {
-		return nil, nil, nil
-	}
-	var required, requiredIfPresent, optional []string
-	for _, j := range jobs {
-		if !j.CouldRun(branch) {
-			continue
-		}
-
-		if j.ContextRequired() {
-			if j.TriggersConditionally() {
-				// jobs that trigger conditionally cannot be
-				// required as their status may not exist on PRs
-				requiredIfPresent = append(requiredIfPresent, j.Context)
-			} else {
-				// jobs that produce required contexts and will
-				// always run should be required at all times
-				required = append(required, j.Context)
-			}
-		} else {
-			optional = append(optional, j.Context)
-		}
-	}
-	return required, requiredIfPresent, optional
-}
diff --git a/pkg/config/branchprotection/branch.go b/pkg/config/branchprotection/branch.go
new file mode 100644
index 00000000..33431758
--- /dev/null
+++ b/pkg/config/branchprotection/branch.go
@@ -0,0 +1,22 @@
+/*
+Copyright 2018 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package branchprotection
+
+// Branch holds protection policy overrides for a particular branch.
+type Branch struct {
+	Policy
+}
diff --git a/pkg/config/branchprotection/config.go b/pkg/config/branchprotection/config.go
new file mode 100644
index 00000000..6788e33e
--- /dev/null
+++ b/pkg/config/branchprotection/config.go
@@ -0,0 +1,45 @@
+/*
+Copyright 2018 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package branchprotection
+
+// Config specifies the global branch protection policy
+type Config struct {
+	Policy
+	// ProtectTested determines if branch protection rules are set for all repos
+	// that Prow has registered jobs for, regardless of if those repos are in the
+	// branch protection config.
+	ProtectTested bool `json:"protect-tested-repos,omitempty"`
+	// Orgs holds branch protection options for orgs by name
+	Orgs map[string]Org `json:"orgs,omitempty"`
+	// AllowDisabledPolicies allows a child to disable all protection even if the
+	// branch has inherited protection options from a parent.
+	AllowDisabledPolicies bool `json:"allow_disabled_policies,omitempty"`
+	// AllowDisabledJobPolicies allows a branch to choose to opt out of branch protection
+	// even if Prow has registered required jobs for that branch.
+	AllowDisabledJobPolicies bool `json:"allow_disabled_job_policies,omitempty"`
+}
+
+// GetOrg returns the org config after merging in any global policies.
+func (bp Config) GetOrg(name string) *Org {
+	o, ok := bp.Orgs[name]
+	if ok {
+		o.Policy = bp.Apply(o.Policy)
+	} else {
+		o.Policy = bp.Policy
+	}
+	return &o
+}
diff --git a/pkg/config/branchprotection/contextpolicy.go b/pkg/config/branchprotection/contextpolicy.go
new file mode 100644
index 00000000..2ea6879b
--- /dev/null
+++ b/pkg/config/branchprotection/contextpolicy.go
@@ -0,0 +1,40 @@
+/*
+Copyright 2018 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package branchprotection
+
+// ContextPolicy configures required github contexts.
+// When merging policies, contexts are appended to context list from parent.
+// Strict determines whether merging to the branch invalidates existing contexts.
+type ContextPolicy struct {
+	// Contexts appends required contexts that must be green to merge
+	Contexts []string `json:"contexts,omitempty"`
+	// Strict overrides whether new commits in the base branch require updating the PR if set
+	Strict *bool `json:"strict,omitempty"`
+}
+
+func mergeContextPolicy(parent, child *ContextPolicy) *ContextPolicy {
+	if child == nil {
+		return parent
+	}
+	if parent == nil {
+		return child
+	}
+	return &ContextPolicy{
+		Contexts: unionStrings(parent.Contexts, child.Contexts),
+		Strict:   selectBool(parent.Strict, child.Strict),
+	}
+}
diff --git a/pkg/config/branchprotection/org.go b/pkg/config/branchprotection/org.go
new file mode 100644
index 00000000..0b795462
--- /dev/null
+++ b/pkg/config/branchprotection/org.go
@@ -0,0 +1,34 @@
+/*
+Copyright 2018 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package branchprotection
+
+// Org holds the default protection policy for an entire org, as well as any repo overrides.
+type Org struct {
+	Policy
+	Repos map[string]Repo `json:"repos,omitempty"`
+}
+
+// GetRepo returns the repo config after merging in any org policies.
+func (o Org) GetRepo(name string) *Repo {
+	r, ok := o.Repos[name]
+	if ok {
+		r.Policy = o.Apply(r.Policy)
+	} else {
+		r.Policy = o.Policy
+	}
+	return &r
+}
diff --git a/pkg/config/branchprotection/policy.go b/pkg/config/branchprotection/policy.go
new file mode 100644
index 00000000..c2217529
--- /dev/null
+++ b/pkg/config/branchprotection/policy.go
@@ -0,0 +1,52 @@
+/*
+Copyright 2018 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package branchprotection
+
+// Policy for the config/org/repo/branch.
+// When merging policies, a nil value results in inheriting the parent policy.
+type Policy struct {
+	// Protect overrides whether branch protection is enabled if set.
+	Protect *bool `json:"protect,omitempty"`
+	// RequiredStatusChecks configures github contexts
+	RequiredStatusChecks *ContextPolicy `json:"required_status_checks,omitempty"`
+	// Admins overrides whether protections apply to admins if set.
+	Admins *bool `json:"enforce_admins,omitempty"`
+	// Restrictions limits who can merge
+	Restrictions *Restrictions `json:"restrictions,omitempty"`
+	// RequiredPullRequestReviews specifies github approval/review criteria.
+	RequiredPullRequestReviews *ReviewPolicy `json:"required_pull_request_reviews,omitempty"`
+	// Exclude specifies a set of regular expressions which identify branches
+	// that should be excluded from the protection policy
+	Exclude []string `json:"exclude,omitempty"`
+}
+
+// IsDefined returns true if at least one of its fields is defined (not nil)
+func (p Policy) IsDefined() bool {
+	return p.Protect != nil || p.RequiredStatusChecks != nil || p.Admins != nil || p.Restrictions != nil || p.RequiredPullRequestReviews != nil
+}
+
+// Apply returns a policy that merges the child into the parent
+func (p Policy) Apply(child Policy) Policy {
+	return Policy{
+		Protect:                    selectBool(p.Protect, child.Protect),
+		RequiredStatusChecks:       mergeContextPolicy(p.RequiredStatusChecks, child.RequiredStatusChecks),
+		Admins:                     selectBool(p.Admins, child.Admins),
+		Restrictions:               mergeRestrictions(p.Restrictions, child.Restrictions),
+		RequiredPullRequestReviews: mergeReviewPolicy(p.RequiredPullRequestReviews, child.RequiredPullRequestReviews),
+		Exclude:                    unionStrings(p.Exclude, child.Exclude),
+	}
+}
diff --git a/pkg/config/branchprotection/repo.go b/pkg/config/branchprotection/repo.go
new file mode 100644
index 00000000..3d84a27f
--- /dev/null
+++ b/pkg/config/branchprotection/repo.go
@@ -0,0 +1,41 @@
+/*
+Copyright 2018 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package branchprotection
+
+import (
+	"errors"
+)
+
+// Repo holds protection policy overrides for all branches in a repo, as well as specific branch overrides.
+type Repo struct {
+	Policy
+	Branches map[string]Branch `json:"branches,omitempty"`
+}
+
+// GetBranch returns the branch config after merging in any repo policies.
+func (r Repo) GetBranch(name string) (*Branch, error) {
+	b, ok := r.Branches[name]
+	if ok {
+		b.Policy = r.Apply(b.Policy)
+		if b.Protect == nil {
+			return nil, errors.New("defined branch policies must set protect")
+		}
+	} else {
+		b.Policy = r.Policy
+	}
+	return &b, nil
+}
diff --git a/pkg/config/branchprotection/restrictions.go b/pkg/config/branchprotection/restrictions.go
new file mode 100644
index 00000000..6e4ef42d
--- /dev/null
+++ b/pkg/config/branchprotection/restrictions.go
@@ -0,0 +1,37 @@
+/*
+Copyright 2018 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package branchprotection
+
+// Restrictions limits who can merge
+// Users and Teams items are appended to parent lists.
+type Restrictions struct {
+	Users []string `json:"users"`
+	Teams []string `json:"teams"`
+}
+
+func mergeRestrictions(parent, child *Restrictions) *Restrictions {
+	if child == nil {
+		return parent
+	}
+	if parent == nil {
+		return child
+	}
+	return &Restrictions{
+		Users: unionStrings(parent.Users, child.Users),
+		Teams: unionStrings(parent.Teams, child.Teams),
+	}
+}
diff --git a/pkg/config/branchprotection/reviewpolicy.go b/pkg/config/branchprotection/reviewpolicy.go
new file mode 100644
index 00000000..0fe62eaf
--- /dev/null
+++ b/pkg/config/branchprotection/reviewpolicy.go
@@ -0,0 +1,46 @@
+/*
+Copyright 2018 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package branchprotection
+
+// ReviewPolicy specifies github approval/review criteria.
+// Any nil values inherit the policy from the parent, otherwise bool/ints are overridden.
+// Non-empty lists are appended to parent lists.
+type ReviewPolicy struct {
+	// Restrictions appends users/teams that are allowed to merge
+	DismissalRestrictions *Restrictions `json:"dismissal_restrictions,omitempty"`
+	// DismissStale overrides whether new commits automatically dismiss old reviews if set
+	DismissStale *bool `json:"dismiss_stale_reviews,omitempty"`
+	// RequireOwners overrides whether CODEOWNERS must approve PRs if set
+	RequireOwners *bool `json:"require_code_owner_reviews,omitempty"`
+	// Approvals overrides the number of approvals required if set (set to 0 to disable)
+	Approvals *int `json:"required_approving_review_count,omitempty"`
+}
+
+func mergeReviewPolicy(parent, child *ReviewPolicy) *ReviewPolicy {
+	if child == nil {
+		return parent
+	}
+	if parent == nil {
+		return child
+	}
+	return &ReviewPolicy{
+		DismissalRestrictions: mergeRestrictions(parent.DismissalRestrictions, child.DismissalRestrictions),
+		DismissStale:          selectBool(parent.DismissStale, child.DismissStale),
+		RequireOwners:         selectBool(parent.RequireOwners, child.RequireOwners),
+		Approvals:             selectInt(parent.Approvals, child.Approvals),
+	}
+}
diff --git a/pkg/config/branchprotection/util.go b/pkg/config/branchprotection/util.go
new file mode 100644
index 00000000..ae7627b7
--- /dev/null
+++ b/pkg/config/branchprotection/util.go
@@ -0,0 +1,50 @@
+/*
+Copyright 2018 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package branchprotection
+
+import (
+	"k8s.io/apimachinery/pkg/util/sets"
+)
+
+// selectInt returns the child if set, else parent
+func selectInt(parent, child *int) *int {
+	if child != nil {
+		return child
+	}
+	return parent
+}
+
+// selectBool returns the child argument if set, otherwise the parent
+func selectBool(parent, child *bool) *bool {
+	if child != nil {
+		return child
+	}
+	return parent
+}
+
+// unionStrings merges the parent and child items together
+func unionStrings(parent, child []string) []string {
+	if child == nil {
+		return parent
+	}
+	if parent == nil {
+		return child
+	}
+	s := sets.NewString(parent...)
+	s.Insert(child...)
+	return s.List()
+}
diff --git a/pkg/config/config.go b/pkg/config/config.go
index d89f68aa..5a29ad35 100644
--- a/pkg/config/config.go
+++ b/pkg/config/config.go
@@ -18,27 +18,20 @@ limitations under the License.
 package config
 
 import (
-	"errors"
 	"fmt"
 	"io/ioutil"
-	"net/url"
 	"os"
 	"path/filepath"
-	"regexp"
 	"strings"
-	"text/template"
-	"time"
 
 	"github.com/jenkins-x/go-scm/scm"
+	"github.com/jenkins-x/lighthouse/pkg/config/branchprotection"
 	"github.com/jenkins-x/lighthouse/pkg/config/job"
-	"github.com/jenkins-x/lighthouse/pkg/config/org"
+	"github.com/jenkins-x/lighthouse/pkg/config/keeper"
+	"github.com/jenkins-x/lighthouse/pkg/config/lighthouse"
 	"github.com/jenkins-x/lighthouse/pkg/config/util"
 	"github.com/sirupsen/logrus"
-	"gopkg.in/robfig/cron.v2"
-	v1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/util/sets"
-	"k8s.io/apimachinery/pkg/util/validation"
 	"sigs.k8s.io/yaml"
 )
 
@@ -53,176 +46,18 @@ const (
 	gcsCredentialsMountPath = "/secrets/gcs"
 )
 
-var jobNameRegex = regexp.MustCompile(`^[A-Za-z0-9-._]+$`)
-
 // JobConfig is a type alias for job.Config
 type JobConfig = job.Config
 
+// ProwConfig is a type alias for lighthouse.Config
+type ProwConfig = lighthouse.Config
+
 // Config is a read-only snapshot of the config.
 type Config struct {
 	JobConfig
 	ProwConfig
 }
 
-// ProwConfig is config for all prow controllers
-type ProwConfig struct {
-	Keeper           Keeper                `json:"tide,omitempty"`
-	Plank            Plank                 `json:"plank,omitempty"`
-	BranchProtection BranchProtection      `json:"branch-protection,omitempty"`
-	Orgs             map[string]org.Config `json:"orgs,omitempty"`
-
-	// TODO: Move this out of the main config.
-	JenkinsOperators []JenkinsOperator `json:"jenkins_operators,omitempty"`
-
-	// LighthouseJobNamespace is the namespace in the cluster that prow
-	// components will use for looking up LighthouseJobs. The namespace
-	// needs to exist and will not be created by prow.
-	// Defaults to "default".
-	LighthouseJobNamespace string `json:"prowjob_namespace,omitempty"`
-	// PodNamespace is the namespace in the cluster that prow
-	// components will use for looking up Pods owned by LighthouseJobs.
-	// The namespace needs to exist and will not be created by prow.
-	// Defaults to "default".
-	PodNamespace string `json:"pod_namespace,omitempty"`
-
-	// LogLevel enables dynamically updating the log level of the
-	// standard logger that is used by all prow components.
-	//
-	// Valid values:
-	//
-	// "debug", "info", "warn", "warning", "error", "fatal", "panic"
-	//
-	// Defaults to "info".
-	LogLevel string `json:"log_level,omitempty"`
-
-	// PushGateway is a prometheus push gateway.
-	PushGateway PushGateway `json:"push_gateway,omitempty"`
-
-	// OwnersDirExcludes is used to configure which directories to ignore when
-	// searching for OWNERS{,_ALIAS} files in a repo.
-	OwnersDirExcludes *OwnersDirExcludes `json:"owners_dir_excludes,omitempty"`
-
-	// OwnersDirExcludes is DEPRECATED in favor of OwnersDirExcludes
-	OwnersDirBlacklist *OwnersDirExcludes `json:"owners_dir_blacklist,omitempty"`
-
-	// Pub/Sub Subscriptions that we want to listen to
-	PubSubSubscriptions PubsubSubscriptions `json:"pubsub_subscriptions,omitempty"`
-
-	// GitHubOptions allows users to control how prow applications display GitHub website links.
-	GitHubOptions GitHubOptions `json:"github,omitempty"`
-
-	// ProviderConfig contains optional SCM provider information
-	ProviderConfig *ProviderConfig `json:"providerConfig,omitempty"`
-}
-
-// ProviderConfig is optionally used to configure information about the SCM provider being used. These values will be
-// used as fallbacks if environment variables aren't set.
-type ProviderConfig struct {
-	// Kind is the go-scm driver name
-	Kind string `json:"kind,omitempty"`
-	// Server is the base URL for the provider, like https://github.com
-	Server string `json:"server,omitempty"`
-	// BotUser is the username on the provider the bot will use
-	BotUser string `json:"botUser,omitempty"`
-}
-
-// OwnersDirExcludes is used to configure which directories to ignore when
-// searching for OWNERS{,_ALIAS} files in a repo.
-type OwnersDirExcludes struct {
-	// Repos configures a directory blacklist per repo (or org)
-	Repos map[string][]string `json:"repos"`
-	// Default configures a default blacklist for repos (or orgs) not
-	// specifically configured
-	Default []string `json:"default"`
-}
-
-// PushGateway is a prometheus push gateway.
-type PushGateway struct {
-	// Endpoint is the location of the prometheus pushgateway
-	// where prow will push metrics to.
-	Endpoint string `json:"endpoint,omitempty"`
-	// IntervalString compiles into Interval at load time.
-	IntervalString string `json:"interval,omitempty"`
-	// Interval specifies how often prow will push metrics
-	// to the pushgateway. Defaults to 1m.
-	Interval time.Duration `json:"-"`
-	// ServeMetrics tells if or not the components serve metrics
-	ServeMetrics bool `json:"serve_metrics"`
-}
-
-// Controller holds configuration applicable to all agent-specific
-// prow controllers.
-type Controller struct {
-	// JobURLTemplateString compiles into JobURLTemplate at load time.
-	JobURLTemplateString string `json:"job_url_template,omitempty"`
-	// JobURLTemplate is compiled at load time from JobURLTemplateString. It
-	// will be passed a builder.PipelineOptions and is used to set the URL for the
-	// "Details" link on GitHub as well as the link from deck.
-	JobURLTemplate *template.Template `json:"-"`
-
-	// ReportTemplateString compiles into ReportTemplate at load time.
-	ReportTemplateString string `json:"report_template,omitempty"`
-	// ReportTemplate is compiled at load time from ReportTemplateString. It
-	// will be passed a builder.PipelineOptions and can provide an optional blurb below
-	// the test failures comment.
-	ReportTemplate *template.Template `json:"-"`
-
-	// MaxConcurrency is the maximum number of tests running concurrently that
-	// will be allowed by the controller. 0 implies no limit.
-	MaxConcurrency int `json:"max_concurrency,omitempty"`
-
-	// MaxGoroutines is the maximum number of goroutines spawned inside the
-	// controller to handle tests. Defaults to 20. Needs to be a positive
-	// number.
-	MaxGoroutines int `json:"max_goroutines,omitempty"`
-
-	// AllowCancellations enables aborting presubmit jobs for commits that
-	// have been superseded by newer commits in Github pull requests.
-	AllowCancellations bool `json:"allow_cancellations,omitempty"`
-}
-
-// Plank is config for the plank controller.
-type Plank struct {
-
-	// ReportTemplateString compiles into ReportTemplate at load time.
-	ReportTemplateString string `json:"report_template,omitempty"`
-	// ReportTemplate is compiled at load time from ReportTemplateString. It
-	// will be passed a builder.PipelineOptions and can provide an optional blurb below
-	// the test failures comment.
-	ReportTemplate *template.Template `json:"-"`
-}
-
-// JenkinsOperator is config for the jenkins-operator controller.
-type JenkinsOperator struct {
-	Controller `json:",inline"`
-	// LabelSelectorString compiles into LabelSelector at load time.
-	// If set, this option needs to match --label-selector used by
-	// the desired jenkins-operator. This option is considered
-	// invalid when provided with a single jenkins-operator config.
-	//
-	// For label selector syntax, see below:
-	// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors
-	LabelSelectorString string `json:"label_selector,omitempty"`
-	// LabelSelector is used so different jenkins-operator replicas
-	// can use their own configuration.
-	LabelSelector labels.Selector `json:"-"`
-}
-
-// PubsubSubscriptions maps GCP projects to a list of Topics.
-type PubsubSubscriptions map[string][]string
-
-// GitHubOptions allows users to control how prow applications display GitHub website links.
-type GitHubOptions struct {
-	// LinkURLFromConfig is the string representation of the link_url config parameter.
-	// This config parameter allows users to override the default GitHub link url for all plugins.
-	// If this option is not set, we assume "https://github.com".
-	LinkURLFromConfig string `json:"link_url,omitempty"`
-
-	// LinkURL is the url representation of LinkURLFromConfig. This variable should be used
-	// in all places internally.
-	LinkURL *url.URL
-}
-
 // Load loads and parses the config at path.
 func Load(prowConfig, jobConfig string) (c *Config, err error) {
 	// we never want config loading to take down the prow components
@@ -235,20 +70,6 @@ func Load(prowConfig, jobConfig string) (c *Config, err error) {
 	if err != nil {
 		return nil, err
 	}
-
-	return c.finalizeAndValidate()
-}
-
-// LoadYAMLConfig loads the configuration from the given data
-func LoadYAMLConfig(data []byte) (*Config, error) {
-	c := &Config{}
-	if err := yaml.Unmarshal(data, c); err != nil {
-		return c, err
-	}
-	if err := parseProwConfig(c); err != nil {
-		return c, err
-	}
-
 	return c.finalizeAndValidate()
 }
 
@@ -288,7 +109,7 @@ func loadConfigFromFiles(prowConfig, jobConfig string) (*Config, error) {
 		if err := yamlToConfig(jobConfig, &jc); err != nil {
 			return nil, err
 		}
-		if err := nc.mergeJobConfig(jc); err != nil {
+		if err := nc.Merge(jc); err != nil {
 			return nil, err
 		}
 		return &nc, nil
@@ -332,7 +153,7 @@ func loadConfigFromFiles(prowConfig, jobConfig string) (*Config, error) {
 		if err := yamlToConfig(path, &subConfig); err != nil {
 			return err
 		}
-		return nc.mergeJobConfig(subConfig)
+		return nc.Merge(subConfig)
 	})
 
 	if err != nil {
@@ -384,273 +205,43 @@ func yamlToConfig(path string, nc interface{}) error {
 	return nil
 }
 
-// mergeConfig merges two job.Config together
-// It will try to merge:
-//	- Presubmits
-//	- Postsubmits
-// 	- Periodics
-//	- PodPresets
-func (c *Config) mergeJobConfig(jc job.Config) error {
-	// Merge everything
-	// *** Presets ***
-	c.Presets = append(c.Presets, jc.Presets...)
-
-	// validate no duplicated preset key-value pairs
-	validLabels := map[string]bool{}
-	for _, preset := range c.Presets {
-		for label, val := range preset.Labels {
-			pair := label + ":" + val
-			if _, ok := validLabels[pair]; ok {
-				return fmt.Errorf("duplicated preset 'label:value' pair : %s", pair)
-			}
-			validLabels[pair] = true
-		}
-	}
-
-	// *** Periodics ***
-	c.Periodics = append(c.Periodics, jc.Periodics...)
-
-	// *** Presubmits ***
-	if c.Presubmits == nil {
-		c.Presubmits = make(map[string][]job.Presubmit)
+// LoadYAMLConfig loads the configuration from the given data
+func LoadYAMLConfig(data []byte) (*Config, error) {
+	c := &Config{}
+	if err := yaml.Unmarshal(data, c); err != nil {
+		return c, err
 	}
-	for repo, jobs := range jc.Presubmits {
-		c.Presubmits[repo] = append(c.Presubmits[repo], jobs...)
+	if err := parseProwConfig(c); err != nil {
+		return c, err
 	}
 
-	// *** Postsubmits ***
-	if c.Postsubmits == nil {
-		c.Postsubmits = make(map[string][]job.Postsubmit)
+	return c.finalizeAndValidate()
+}
+
+func parseProwConfig(c *Config) error {
+	if err := c.ProwConfig.Parse(); err != nil {
+		return err
 	}
-	for repo, jobs := range jc.Postsubmits {
-		c.Postsubmits[repo] = append(c.Postsubmits[repo], jobs...)
+	lvl, err := logrus.ParseLevel(c.LogLevel)
+	if err != nil {
+		return err
 	}
-
+	logrus.WithField("level", lvl.String()).Infof("setting the log level")
+	logrus.SetLevel(lvl)
 	return nil
 }
 
-func setPresubmitDecorationDefaults(c *Config, ps *job.Presubmit) {
-	/*	if ps.Decorate {
-			ps.DecorationConfig = ps.DecorationConfig.ApplyDefault(c.Plank.DefaultDecorationConfig)
-		}
-	*/
-}
-
-func setPostsubmitDecorationDefaults(c *Config, ps *job.Postsubmit) {
-	/*
-		if ps.Decorate {
-			ps.DecorationConfig = ps.DecorationConfig.ApplyDefault(c.Plank.DefaultDecorationConfig)
-		}
-
-	*/
-}
-
-func setPeriodicDecorationDefaults(c *Config, ps *job.Periodic) {
-	/*
-		if ps.Decorate {
-			ps.DecorationConfig = ps.DecorationConfig.ApplyDefault(c.Plank.DefaultDecorationConfig)
-		}
-	*/
-}
-
 // finalizeAndValidate sets default configurations, validates the configuration, etc
 func (c *Config) finalizeAndValidate() (*Config, error) {
-	if err := c.finalizeJobConfig(); err != nil {
+	if err := c.JobConfig.Init(c.ProwConfig); err != nil {
 		return nil, err
 	}
-	if err := c.validateJobConfig(); err != nil {
+	if err := c.JobConfig.Validate(c.ProwConfig); err != nil {
 		return nil, err
 	}
 	return c, nil
 }
 
-// finalizeJobConfig mutates and fixes entries for jobspecs
-func (c *Config) finalizeJobConfig() error {
-	if c.DecorationRequested() {
-		/*		if c.Plank.DefaultDecorationConfig == nil {
-					return errors.New("no default decoration config provided for plank")
-				}
-				if c.Plank.DefaultDecorationConfig.UtilityImages == nil {
-					return errors.New("no default decoration image pull specs provided for plank")
-				}
-				if c.Plank.DefaultDecorationConfig.GCSConfiguration == nil {
-					return errors.New("no default GCS decoration config provided for plank")
-				}
-				if c.Plank.DefaultDecorationConfig.GCSCredentialsSecret == "" {
-					return errors.New("no default GCS credentials secret provided for plank")
-				}
-		*/
-		for _, vs := range c.Presubmits {
-			for i := range vs {
-				setPresubmitDecorationDefaults(c, &vs[i])
-			}
-		}
-
-		for _, js := range c.Postsubmits {
-			for i := range js {
-				setPostsubmitDecorationDefaults(c, &js[i])
-			}
-		}
-
-		for i := range c.Periodics {
-			setPeriodicDecorationDefaults(c, &c.Periodics[i])
-		}
-	}
-
-	// Ensure that regexes are valid and set defaults.
-	for _, ps := range c.Presubmits {
-		for i := range ps {
-			ps[i].SetDefaults(c.PodNamespace)
-			if err := ps[i].SetRegexes(); err != nil {
-				return fmt.Errorf("could not set regex: %v", err)
-			}
-			if err := resolvePresets(ps[i].Name, ps[i].Labels, ps[i].Spec, c.Presets); err != nil {
-				return err
-			}
-		}
-	}
-
-	for _, ps := range c.Postsubmits {
-		for i := range ps {
-			ps[i].SetDefaults(c.PodNamespace)
-			if err := ps[i].SetRegexes(); err != nil {
-				return fmt.Errorf("could not set regex: %v", err)
-			}
-			if err := resolvePresets(ps[i].Name, ps[i].Labels, ps[i].Spec, c.Presets); err != nil {
-				return err
-			}
-		}
-	}
-
-	for i := range c.Periodics {
-		c.Periodics[i].SetDefaults(c.PodNamespace)
-		if err := resolvePresets(c.Periodics[i].Name, c.Periodics[i].Labels, c.Periodics[i].Spec, c.Presets); err != nil {
-			return err
-		}
-	}
-
-	if c.OwnersDirExcludes == nil {
-		c.OwnersDirExcludes = c.OwnersDirBlacklist
-	}
-
-	return nil
-}
-
-func validateJobBase(v job.Base, jobType PipelineKind, podNamespace string) error {
-	if !jobNameRegex.MatchString(v.Name) {
-		return fmt.Errorf("name: must match regex %q", jobNameRegex.String())
-	}
-	// Ensure max_concurrency is non-negative.
-	if v.MaxConcurrency < 0 {
-		return fmt.Errorf("max_concurrency: %d must be a non-negative number", v.MaxConcurrency)
-	}
-	if err := validateAgent(v, podNamespace); err != nil {
-		return err
-	}
-	if err := validatePodSpec(jobType, v.Spec); err != nil {
-		return err
-	}
-	if err := validateLabels(v.Labels); err != nil {
-		return err
-	}
-	if v.Spec == nil || len(v.Spec.Containers) == 0 {
-		return nil // knative-build and jenkins jobs have no spec
-	}
-	return nil
-}
-
-// validateJobConfig validates if all the jobspecs/presets are valid
-// if you are mutating the jobs, please add it to finalizeJobConfig above
-func (c *Config) validateJobConfig() error {
-	type orgRepoJobName struct {
-		orgRepo, jobName string
-	}
-
-	// Validate presubmits.
-	// Checking that no duplicate job in prow config exists on the same org / repo / branch.
-	validPresubmits := map[orgRepoJobName][]job.Presubmit{}
-	for repo, jobs := range c.Presubmits {
-		for _, job := range jobs {
-			repoJobName := orgRepoJobName{repo, job.Name}
-			for _, existingJob := range validPresubmits[repoJobName] {
-				if existingJob.Brancher.Intersects(job.Brancher) {
-					return fmt.Errorf("duplicated presubmit job: %s", job.Name)
-				}
-			}
-			validPresubmits[repoJobName] = append(validPresubmits[repoJobName], job)
-		}
-	}
-
-	for _, ps := range c.Presubmits {
-		for _, j := range ps {
-			if err := validateJobBase(j.Base, PresubmitJob, c.PodNamespace); err != nil {
-				return fmt.Errorf("invalid presubmit job %s: %v", j.Name, err)
-			}
-			if err := validateTriggering(j); err != nil {
-				return err
-			}
-		}
-	}
-
-	// Validate postsubmits.
-	// Checking that no duplicate job in prow config exists on the same org / repo / branch.
-	validPostsubmits := map[orgRepoJobName][]job.Postsubmit{}
-	for repo, jobs := range c.Postsubmits {
-		for _, job := range jobs {
-			repoJobName := orgRepoJobName{repo, job.Name}
-			for _, existingJob := range validPostsubmits[repoJobName] {
-				if existingJob.Brancher.Intersects(job.Brancher) {
-					return fmt.Errorf("duplicated postsubmit job: %s", job.Name)
-				}
-			}
-			validPostsubmits[repoJobName] = append(validPostsubmits[repoJobName], job)
-		}
-	}
-
-	for _, ps := range c.Postsubmits {
-		for _, j := range ps {
-			if err := validateJobBase(j.Base, PostsubmitJob, c.PodNamespace); err != nil {
-				return fmt.Errorf("invalid postsubmit job %s: %v", j.Name, err)
-			}
-		}
-	}
-
-	// validate no duplicated periodics
-	validPeriodics := sets.NewString()
-	// Ensure that the periodic durations are valid and specs exist.
-	for _, p := range c.Periodics {
-		if validPeriodics.Has(p.Name) {
-			return fmt.Errorf("duplicated periodic job : %s", p.Name)
-		}
-		validPeriodics.Insert(p.Name)
-		if err := validateJobBase(p.Base, PeriodicJob, c.PodNamespace); err != nil {
-			return fmt.Errorf("invalid periodic job %s: %v", p.Name, err)
-		}
-	}
-
-	// Set the interval on the periodic jobs. It doesn't make sense to do this
-	// for child jobs.
-	for j, p := range c.Periodics {
-		if p.Cron != "" && p.Interval != "" {
-			return fmt.Errorf("cron and interval cannot be both set in periodic %s", p.Name)
-		} else if p.Cron == "" && p.Interval == "" {
-			return fmt.Errorf("cron and interval cannot be both empty in periodic %s", p.Name)
-		} else if p.Cron != "" {
-			if _, err := cron.Parse(p.Cron); err != nil {
-				return fmt.Errorf("invalid cron string %s in periodic %s: %v", p.Cron, p.Name, err)
-			}
-		} else {
-			d, err := time.ParseDuration(c.Periodics[j].Interval)
-			if err != nil {
-				return fmt.Errorf("cannot parse duration for %s: %v", c.Periodics[j].Name, err)
-			}
-			c.Periodics[j].SetInterval(d)
-		}
-	}
-
-	return nil
-}
-
 // GetPostsubmits lets return all the post submits
 func (c *Config) GetPostsubmits(repository scm.Repository) []job.Postsubmit {
 	fullNames := util.FullNames(repository)
@@ -671,256 +262,143 @@ func (c *Config) GetPresubmits(repository scm.Repository) []job.Presubmit {
 	return answer
 }
 
-func parseProwConfig(c *Config) error {
-	reportTmpl, err := template.New("Report").Parse(c.Plank.ReportTemplateString)
-	if err != nil {
-		return fmt.Errorf("parsing template: %v", err)
-	}
-	c.Plank.ReportTemplate = reportTmpl
-
-	for i := range c.JenkinsOperators {
-		if err := ValidateController(&c.JenkinsOperators[i].Controller); err != nil {
-			return fmt.Errorf("validating jenkins_operators config: %v", err)
-		}
-		sel, err := labels.Parse(c.JenkinsOperators[i].LabelSelectorString)
-		if err != nil {
-			return fmt.Errorf("invalid jenkins_operators.label_selector option: %v", err)
-		}
-		c.JenkinsOperators[i].LabelSelector = sel
-		// TODO: Invalidate overlapping selectors more
-		if len(c.JenkinsOperators) > 1 && c.JenkinsOperators[i].LabelSelectorString == "" {
-			return errors.New("selector overlap: cannot use an empty label_selector with multiple selectors")
-		}
-		if len(c.JenkinsOperators) == 1 && c.JenkinsOperators[0].LabelSelectorString != "" {
-			return errors.New("label_selector is invalid when used for a single jenkins-operator")
-		}
-	}
-
-	if c.PushGateway.IntervalString == "" {
-		c.PushGateway.Interval = time.Minute
-	} else {
-		interval, err := time.ParseDuration(c.PushGateway.IntervalString)
-		if err != nil {
-			return fmt.Errorf("cannot parse duration for push_gateway.interval: %v", err)
-		}
-		c.PushGateway.Interval = interval
-	}
-
-	if c.Keeper.SyncPeriodString == "" {
-		c.Keeper.SyncPeriod = time.Minute
-	} else {
-		period, err := time.ParseDuration(c.Keeper.SyncPeriodString)
-		if err != nil {
-			return fmt.Errorf("cannot parse duration for tide.sync_period: %v", err)
-		}
-		c.Keeper.SyncPeriod = period
-	}
-	if c.Keeper.StatusUpdatePeriodString == "" {
-		c.Keeper.StatusUpdatePeriod = c.Keeper.SyncPeriod
-	} else {
-		period, err := time.ParseDuration(c.Keeper.StatusUpdatePeriodString)
-		if err != nil {
-			return fmt.Errorf("cannot parse duration for tide.status_update_period: %v", err)
-		}
-		c.Keeper.StatusUpdatePeriod = period
-	}
-
-	if c.Keeper.MaxGoroutines == 0 {
-		c.Keeper.MaxGoroutines = 20
-	}
-	if c.Keeper.MaxGoroutines <= 0 {
-		return fmt.Errorf("keeper has invalid max_goroutines (%d), it needs to be a positive number", c.Keeper.MaxGoroutines)
-	}
-
-	for name, method := range c.Keeper.MergeType {
-		if method != MergeMerge &&
-			method != MergeRebase &&
-			method != MergeSquash {
-			return fmt.Errorf("merge type %q for %s is not a valid type", method, name)
-		}
-	}
-
-	for i, tq := range c.Keeper.Queries {
-		if err := tq.Validate(); err != nil {
-			return fmt.Errorf("keeper query (index %d) is invalid: %v", i, err)
-		}
-	}
-
-	if c.LighthouseJobNamespace == "" {
-		c.LighthouseJobNamespace = "default"
-	}
-	if c.PodNamespace == "" {
-		c.PodNamespace = "default"
-	}
-
-	if c.GitHubOptions.LinkURLFromConfig == "" {
-		c.GitHubOptions.LinkURLFromConfig = "https://github.com"
-	}
-	linkURL, err := url.Parse(c.GitHubOptions.LinkURLFromConfig)
-	if err != nil {
-		return fmt.Errorf("unable to parse github.link_url, might not be a valid url: %v", err)
-	}
-	c.GitHubOptions.LinkURL = linkURL
-
-	if c.LogLevel == "" {
-		c.LogLevel = os.Getenv("LOG_LEVEL")
-		if c.LogLevel == "" {
-			c.LogLevel = "info"
-		}
-	}
-	lvl, err := logrus.ParseLevel(c.LogLevel)
-	if err != nil {
-		return err
-	}
-	logrus.WithField("level", lvl.String()).Infof("setting the log level")
-	logrus.SetLevel(lvl)
-
-	return nil
-}
-
-func validateLabels(labels map[string]string) error {
-	for label, value := range labels {
-		for _, prowLabel := range Labels() {
-			if label == prowLabel {
-				return fmt.Errorf("label %s is reserved for decoration", label)
+// BranchRequirements partitions status contexts for a given org, repo branch into three buckets:
+//  - contexts that are always required to be present
+//  - contexts that are required, _if_ present
+//  - contexts that are always optional
+func BranchRequirements(org, repo, branch string, presubmits map[string][]job.Presubmit) ([]string, []string, []string) {
+	jobs, ok := presubmits[org+"/"+repo]
+	if !ok {
+		return nil, nil, nil
+	}
+	var required, requiredIfPresent, optional []string
+	for _, j := range jobs {
+		if !j.CouldRun(branch) {
+			continue
+		}
+
+		if j.ContextRequired() {
+			if j.TriggersConditionally() {
+				// jobs that trigger conditionally cannot be
+				// required as their status may not exist on PRs
+				requiredIfPresent = append(requiredIfPresent, j.Context)
+			} else {
+				// jobs that produce required contexts and will
+				// always run should be required at all times
+				required = append(required, j.Context)
 			}
-		}
-		if errs := validation.IsQualifiedName(label); len(errs) != 0 {
-			return fmt.Errorf("invalid label %s: %v", label, errs)
-		}
-		if errs := validation.IsValidLabelValue(labels[label]); len(errs) != 0 {
-			return fmt.Errorf("label %s has invalid value %s: %v", label, value, errs)
+		} else {
+			optional = append(optional, j.Context)
 		}
 	}
-	return nil
+	return required, requiredIfPresent, optional
 }
 
-func validateAgent(v job.Base, podNamespace string) error {
-	agents := sets.NewString(job.AvailablePipelineAgentTypes()...)
-	agent := v.Agent
-	switch {
-	case !agents.Has(agent):
-		return fmt.Errorf("agent must be one of %s (found %q)", strings.Join(agents.List(), ", "), agent)
-		/*	case v.Spec != nil && agent != k:
-				return fmt.Errorf("job specs require agent: %s (found %q)", k, agent)
-			case agent == k && v.Spec == nil:
-				return errors.New("kubernetes jobs require a spec")
-			case v.BuildSpec != nil && agent != b:
-				return fmt.Errorf("job build_specs require agent: %s (found %q)", b, agent)
-			case agent == b && v.BuildSpec == nil:
-				return errors.New("knative-build jobs require a build_spec")
-			case v.DecorationConfig != nil && agent != k && agent != b:
-				// TODO(fejta): only source decoration supported...
-				return fmt.Errorf("decoration requires agent: %s or %s (found %q)", k, b, agent)
-			case v.ErrorOnEviction && agent != k:
-				return fmt.Errorf("error_on_eviction only applies to agent: %s (found %q)", k, agent)
-			case v.Namespace == nil || *v.Namespace == "":
-				return fmt.Errorf("failed to default namespace")
-			case *v.Namespace != podNamespace && agent != b:
-				// TODO(fejta): update plank to allow this (depends on client change)
-				return fmt.Errorf("namespace customization requires agent: %s (found %q)", b, agent)
-		*/
+// GetBranchProtection returns the policy for a given branch.
+//
+// Handles merging any policies defined at repo/org/global levels into the branch policy.
+func (c *Config) GetBranchProtection(org, repo, branch string) (*branchprotection.Policy, error) {
+	if _, present := c.BranchProtection.Orgs[org]; !present {
+		return nil, nil // only consider branches in configured orgs
 	}
-	return nil
-}
-
-func resolvePresets(name string, labels map[string]string, spec *v1.PodSpec, presets []job.Preset) error {
-	for _, preset := range presets {
-		if err := job.MergePreset(preset, labels, spec); err != nil {
-			return fmt.Errorf("job %s failed to merge presets: %v", name, err)
-		}
+	b, err := c.BranchProtection.GetOrg(org).GetRepo(repo).GetBranch(branch)
+	if err != nil {
+		return nil, err
 	}
 
-	return nil
+	return c.GetPolicy(org, repo, branch, *b)
 }
 
-func validatePodSpec(jobType PipelineKind, spec *v1.PodSpec) error {
-	if spec == nil {
-		return nil
-	}
-
-	if len(spec.InitContainers) != 0 {
-		return errors.New("pod spec may not use init containers")
-	}
-
-	if n := len(spec.Containers); n != 1 {
-		return fmt.Errorf("pod spec must specify exactly 1 container, found: %d", n)
-	}
+// GetPolicy returns the protection policy for the branch, after merging in presubmits.
+func (c *Config) GetPolicy(org, repo, branch string, b branchprotection.Branch) (*branchprotection.Policy, error) {
+	policy := b.Policy
 
-	/*	for _, env := range spec.Containers[0].Env {
-			for _, prowEnv := range downwardapi.EnvForType(jobType) {
-				if env.Name == prowEnv {
-					// TODO(fejta): consider allowing this
-					return fmt.Errorf("env %s is reserved", env.Name)
-				}
+	// Automatically require contexts from prow which must always be present
+	if prowContexts, _, _ := BranchRequirements(org, repo, branch, c.Presubmits); len(prowContexts) > 0 {
+		// Error if protection is disabled
+		if policy.Protect != nil && !*policy.Protect {
+			if c.BranchProtection.AllowDisabledJobPolicies {
+				logrus.Warnf("%s/%s=%s has required jobs but has protect: false", org, repo, branch)
+				return nil, nil
 			}
-		}
-	*/
+			return nil, fmt.Errorf("required prow jobs require branch protection")
 
-	for _, mount := range spec.Containers[0].VolumeMounts {
-		for _, prowMount := range VolumeMounts() {
-			if mount.Name == prowMount {
-				return fmt.Errorf("volumeMount name %s is reserved for decoration", prowMount)
-			}
 		}
-		for _, prowMountPath := range VolumeMountPaths() {
-			if strings.HasPrefix(mount.MountPath, prowMountPath) || strings.HasPrefix(prowMountPath, mount.MountPath) {
-				return fmt.Errorf("mount %s at %s conflicts with decoration mount at %s", mount.Name, mount.MountPath, prowMountPath)
-			}
+		ps := branchprotection.Policy{
+			RequiredStatusChecks: &branchprotection.ContextPolicy{
+				Contexts: prowContexts,
+			},
+		}
+		// Require protection by default if ProtectTested is true
+		if c.BranchProtection.ProtectTested {
+			yes := true
+			ps.Protect = &yes
 		}
+		policy = policy.Apply(ps)
 	}
 
-	for _, volume := range spec.Volumes {
-		for _, prowVolume := range VolumeMounts() {
-			if volume.Name == prowVolume {
-				return fmt.Errorf("volume %s is a reserved for decoration", volume.Name)
+	if policy.Protect != nil && !*policy.Protect {
+		// Ensure that protection is false => no protection settings
+		var old *bool
+		old, policy.Protect = policy.Protect, old
+		switch {
+		case policy.IsDefined() && c.BranchProtection.AllowDisabledPolicies:
+			logrus.Warnf("%s/%s=%s defines a policy but has protect: false", org, repo, branch)
+			policy = branchprotection.Policy{
+				Protect: policy.Protect,
 			}
+		case policy.IsDefined():
+			return nil, fmt.Errorf("%s/%s=%s defines a policy, which requires protect: true", org, repo, branch)
 		}
+		policy.Protect = old
 	}
 
-	return nil
-}
-
-func validateTriggering(job job.Presubmit) error {
-	if job.AlwaysRun && job.RunIfChanged != "" {
-		return fmt.Errorf("job %s is set to always run but also declares run_if_changed targets, which are mutually exclusive", job.Name)
+	if !policy.IsDefined() {
+		return nil, nil
 	}
+	return &policy, nil
+}
 
-	if !job.SkipReport && job.Context == "" {
-		return fmt.Errorf("job %s is set to report but has no context configured", job.Name)
-	}
+// GetKeeperContextPolicy parses the prow config to find context merge options.
+// If none are set, it will use the prow jobs configured and use the default github combined status.
+// Otherwise if set it will use the branch protection setting, or the listed jobs.
+func (c Config) GetKeeperContextPolicy(org, repo, branch string) (*keeper.ContextPolicy, error) {
+	options := c.Keeper.ContextOptions.Parse(org, repo, branch)
+	// Adding required and optional contexts from options
+	required := sets.NewString(options.RequiredContexts...)
+	requiredIfPresent := sets.NewString(options.RequiredIfPresentContexts...)
+	optional := sets.NewString(options.OptionalContexts...)
 
-	return nil
-}
+	// automatically generate required and optional entries for Prow Pipelines
+	prowRequired, prowRequiredIfPresent, prowOptional := BranchRequirements(org, repo, branch, c.Presubmits)
+	required.Insert(prowRequired...)
+	requiredIfPresent.Insert(prowRequiredIfPresent...)
+	optional.Insert(prowOptional...)
 
-// ValidateController validates the provided controller config.
-func ValidateController(c *Controller) error {
-	urlTmpl, err := template.New("JobURL").Parse(c.JobURLTemplateString)
-	if err != nil {
-		return fmt.Errorf("parsing template: %v", err)
+	// Using Branch protection configuration
+	if options.FromBranchProtection != nil && *options.FromBranchProtection {
+		bp, err := c.GetBranchProtection(org, repo, branch)
+		if err != nil {
+			logrus.WithError(err).Warningf("Error getting branch protection for %s/%s+%s", org, repo, branch)
+		} else if bp != nil && bp.Protect != nil && *bp.Protect && bp.RequiredStatusChecks != nil {
+			required.Insert(bp.RequiredStatusChecks.Contexts...)
+		}
 	}
-	c.JobURLTemplate = urlTmpl
 
-	reportTmpl, err := template.New("Report").Parse(c.ReportTemplateString)
-	if err != nil {
-		return fmt.Errorf("parsing template: %v", err)
-	}
-	c.ReportTemplate = reportTmpl
-	if c.MaxConcurrency < 0 {
-		return fmt.Errorf("controller has invalid max_concurrency (%d), it needs to be a non-negative number", c.MaxConcurrency)
-	}
-	if c.MaxGoroutines == 0 {
-		c.MaxGoroutines = 20
+	// Remove anything from the required list that's also in the required if present list, since that may have been
+	// duplicated by branch protection.
+	required.Delete(requiredIfPresent.List()...)
+
+	t := &keeper.ContextPolicy{
+		RequiredContexts:          required.List(),
+		RequiredIfPresentContexts: requiredIfPresent.List(),
+		OptionalContexts:          optional.List(),
+		SkipUnknownContexts:       options.SkipUnknownContexts,
 	}
-	if c.MaxGoroutines <= 0 {
-		return fmt.Errorf("controller has invalid max_goroutines (%d), it needs to be a positive number", c.MaxGoroutines)
+	if err := t.Validate(); err != nil {
+		return t, err
 	}
-	return nil
-}
-
-// Labels returns a string slice with label consts from kube.
-func Labels() []string {
-	return []string{LighthouseJobTypeLabel, CreatedByLighthouse, LighthouseJobIDLabel}
+	return t, nil
 }
 
 // VolumeMounts returns a string slice with *MountName consts in it.
diff --git a/pkg/config/job/constants.go b/pkg/config/job/agent.go
similarity index 98%
rename from pkg/config/job/constants.go
rename to pkg/config/job/agent.go
index fcb1015a..3cc2abf3 100644
--- a/pkg/config/job/constants.go
+++ b/pkg/config/job/agent.go
@@ -24,6 +24,7 @@
 
 package job
 
+// Various agents.
 const (
 	// JenkinsXAgent is the agent type for running Jenkins X pipelines
 	JenkinsXAgent = "jenkins-x"
diff --git a/pkg/config/job/base.go b/pkg/config/job/base.go
index f1d95f85..57a23088 100644
--- a/pkg/config/job/base.go
+++ b/pkg/config/job/base.go
@@ -17,12 +17,26 @@ limitations under the License.
 package job
 
 import (
+	"errors"
+	"fmt"
+	"regexp"
+	"strings"
+
 	tektonv1beta1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
 	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/util/sets"
+)
+
+const (
+	// DefaultClusterAlias specifies the default context for resources owned by jobs (pods/builds).
+	DefaultClusterAlias = "default"
 )
 
+var jobNameRegex = regexp.MustCompile(`^[A-Za-z0-9-._]+$`)
+
 // Base contains attributes common to all job types
 type Base struct {
+	UtilityConfig
 	// The name of the job. Must match regex [A-Za-z0-9-._]+
 	// e.g. pull-test-infra-bazel-build
 	Name string `json:"name"`
@@ -54,8 +68,6 @@ type Base struct {
 	PipelineRunSpec *tektonv1beta1.PipelineRunSpec `json:"pipeline_run_spec,omitempty"`
 	// PipelineRunParams are the params used by the pipeline run
 	PipelineRunParams []PipelineRunParam `json:"pipeline_run_params,omitempty"`
-
-	UtilityConfig
 }
 
 // SetDefaults initializes default values
@@ -69,6 +81,102 @@ func (b *Base) SetDefaults(namespace string) {
 		b.Namespace = &s
 	}
 	if b.Cluster == "" {
-		b.Cluster = "default"
+		b.Cluster = DefaultClusterAlias
+	}
+}
+
+// Validate validates job base
+func (b *Base) Validate(jobType PipelineKind, podNamespace string) error {
+	if !jobNameRegex.MatchString(b.Name) {
+		return fmt.Errorf("name: must match regex %q", jobNameRegex.String())
+	}
+	// Ensure max_concurrency is non-negative.
+	if b.MaxConcurrency < 0 {
+		return fmt.Errorf("max_concurrency: %d must be a non-negative number", b.MaxConcurrency)
+	}
+	if err := b.ValidateAgent(podNamespace); err != nil {
+		return err
+	}
+	if err := b.ValidatePodSpec(jobType); err != nil {
+		return err
+	}
+	if err := ValidateLabels(b.Labels); err != nil {
+		return err
+	}
+	if b.Spec == nil || len(b.Spec.Containers) == 0 {
+		return nil // knative-build and jenkins jobs have no spec
+	}
+	return nil
+}
+
+// ValidateAgent validates job agent
+func (b *Base) ValidateAgent(podNamespace string) error {
+	agents := sets.NewString(AvailablePipelineAgentTypes()...)
+	agent := b.Agent
+	switch {
+	case !agents.Has(agent):
+		return fmt.Errorf("agent must be one of %s (found %q)", strings.Join(agents.List(), ", "), agent)
+		/*	case b.Spec != nil && agent != k:
+				return fmt.Errorf("job specs require agent: %s (found %q)", k, agent)
+			case agent == k && b.Spec == nil:
+				return errors.New("kubernetes jobs require a spec")
+			case b.BuildSpec != nil && agent != b:
+				return fmt.Errorf("job build_specs require agent: %s (found %q)", b, agent)
+			case agent == b && b.BuildSpec == nil:
+				return errors.New("knative-build jobs require a build_spec")
+			case b.DecorationConfig != nil && agent != k && agent != b:
+				// TODO(fejta): only source decoration supported...
+				return fmt.Errorf("decoration requires agent: %s or %s (found %q)", k, b, agent)
+			case b.ErrorOnEviction && agent != k:
+				return fmt.Errorf("error_on_eviction only applies to agent: %s (found %q)", k, agent)
+			case b.Namespace == nil || *b.Namespace == "":
+				return fmt.Errorf("failed to default namespace")
+			case *b.Namespace != podNamespace && agent != b:
+				// TODO(fejta): update plank to allow this (depends on client change)
+				return fmt.Errorf("namespace customization requires agent: %s (found %q)", b, agent)
+		*/
+	}
+	return nil
+}
+
+// ValidatePodSpec validates job pod spec
+func (b *Base) ValidatePodSpec(jobType PipelineKind) error {
+	if b.Spec == nil {
+		return nil
+	}
+	if len(b.Spec.InitContainers) != 0 {
+		return errors.New("pod spec may not use init containers")
+	}
+	if n := len(b.Spec.Containers); n != 1 {
+		return fmt.Errorf("pod spec must specify exactly 1 container, found: %d", n)
 	}
+	/*	for _, env := range spec.Containers[0].Env {
+			for _, prowEnv := range downwardapi.EnvForType(jobType) {
+				if env.Name == prowEnv {
+					// TODO(fejta): consider allowing this
+					return fmt.Errorf("env %s is reserved", env.Name)
+				}
+			}
+		}
+	*/
+	// for _, mount := range b.Spec.Containers[0].VolumeMounts {
+	// 	for _, prowMount := range VolumeMounts() {
+	// 		if mount.Name == prowMount {
+	// 			return fmt.Errorf("volumeMount name %s is reserved for decoration", prowMount)
+	// 		}
+	// 	}
+	// 	for _, prowMountPath := range VolumeMountPaths() {
+	// 		if strings.HasPrefix(mount.MountPath, prowMountPath) || strings.HasPrefix(prowMountPath, mount.MountPath) {
+	// 			return fmt.Errorf("mount %s at %s conflicts with decoration mount at %s", mount.Name, mount.MountPath, prowMountPath)
+	// 		}
+	// 	}
+	// }
+	// for _, volume := range spec.Volumes {
+	// 	for _, prowVolume := range VolumeMounts() {
+	// 		if volume.Name == prowVolume {
+	// 			return fmt.Errorf("volume %s is a reserved for decoration", volume.Name)
+	// 		}
+	// 	}
+	// }
+	return nil
 }
diff --git a/pkg/config/job/config.go b/pkg/config/job/config.go
index 6afc3d4d..9c4bf776 100644
--- a/pkg/config/job/config.go
+++ b/pkg/config/job/config.go
@@ -16,6 +16,16 @@ limitations under the License.
 
 package job
 
+import (
+	"fmt"
+	"time"
+
+	"github.com/jenkins-x/lighthouse/pkg/config/lighthouse"
+	"gopkg.in/robfig/cron.v2"
+	v1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/util/sets"
+)
+
 // Config is config for all prow jobs
 type Config struct {
 	// Presets apply to all job types.
@@ -23,11 +33,198 @@ type Config struct {
 	// Full repo name (such as "kubernetes/kubernetes") -> list of jobs.
 	Presubmits  map[string][]Presubmit  `json:"presubmits,omitempty"`
 	Postsubmits map[string][]Postsubmit `json:"postsubmits,omitempty"`
-
 	// Periodics are not associated with any repo.
 	Periodics []Periodic `json:"periodics,omitempty"`
 }
 
+func resolvePresets(name string, labels map[string]string, spec *v1.PodSpec, presets []Preset) error {
+	for _, preset := range presets {
+		if err := MergePreset(preset, labels, spec); err != nil {
+			return fmt.Errorf("job %s failed to merge presets: %v", name, err)
+		}
+	}
+
+	return nil
+}
+
+// Merge merges one Config with another one
+func (c *Config) Merge(other Config) error {
+	c.Presets = append(c.Presets, other.Presets...)
+	// validate no duplicated preset key-value pairs
+	validLabels := map[string]bool{}
+	for _, preset := range c.Presets {
+		for label, val := range preset.Labels {
+			pair := label + ":" + val
+			if _, ok := validLabels[pair]; ok {
+				return fmt.Errorf("duplicated preset 'label:value' pair : %s", pair)
+			}
+			validLabels[pair] = true
+		}
+	}
+	c.Periodics = append(c.Periodics, other.Periodics...)
+	if c.Presubmits == nil {
+		c.Presubmits = make(map[string][]Presubmit)
+	}
+	for repo, jobs := range other.Presubmits {
+		c.Presubmits[repo] = append(c.Presubmits[repo], jobs...)
+	}
+	if c.Postsubmits == nil {
+		c.Postsubmits = make(map[string][]Postsubmit)
+	}
+	for repo, jobs := range other.Postsubmits {
+		c.Postsubmits[repo] = append(c.Postsubmits[repo], jobs...)
+	}
+	return nil
+}
+
+// Init sets defaults and initializes Config
+func (c *Config) Init(lh lighthouse.Config) error {
+	decoration := c.DecorationRequested()
+	if decoration {
+		// if c.Plank.DefaultDecorationConfig == nil {
+		// 	return errors.New("no default decoration config provided for plank")
+		// }
+		// if c.Plank.DefaultDecorationConfig.UtilityImages == nil {
+		// 	return errors.New("no default decoration image pull specs provided for plank")
+		// }
+		// if c.Plank.DefaultDecorationConfig.GCSConfiguration == nil {
+		// 	return errors.New("no default GCS decoration config provided for plank")
+		// }
+		// if c.Plank.DefaultDecorationConfig.GCSCredentialsSecret == "" {
+		// 	return errors.New("no default GCS credentials secret provided for plank")
+		// }
+		// for _, vs := range c.Presubmits {
+		// 	for i := range vs {
+		// 		// if ps.Decorate {
+		// 		// 	ps.DecorationConfig = ps.DecorationConfig.ApplyDefault(c.Plank.DefaultDecorationConfig)
+		// 		// }
+		// 	}
+		// }
+		// for _, js := range c.Postsubmits {
+		// 	for i := range js {
+		// 		// if ps.Decorate {
+		// 		// 	ps.DecorationConfig = ps.DecorationConfig.ApplyDefault(c.Plank.DefaultDecorationConfig)
+		// 		// }
+		// 	}
+		// }
+		// for i := range c.Periodics {
+		// 	// if ps.Decorate {
+		// 	// 	ps.DecorationConfig = ps.DecorationConfig.ApplyDefault(c.Plank.DefaultDecorationConfig)
+		// 	// }
+		// }
+	}
+	for _, ps := range c.Presubmits {
+		for i := range ps {
+			ps[i].SetDefaults(lh.PodNamespace)
+			if err := ps[i].SetRegexes(); err != nil {
+				return fmt.Errorf("could not set regex: %v", err)
+			}
+			if err := resolvePresets(ps[i].Name, ps[i].Labels, ps[i].Spec, c.Presets); err != nil {
+				return err
+			}
+		}
+	}
+	for _, ps := range c.Postsubmits {
+		for i := range ps {
+			ps[i].SetDefaults(lh.PodNamespace)
+			if err := ps[i].SetRegexes(); err != nil {
+				return fmt.Errorf("could not set regex: %v", err)
+			}
+			if err := resolvePresets(ps[i].Name, ps[i].Labels, ps[i].Spec, c.Presets); err != nil {
+				return err
+			}
+		}
+	}
+	for i := range c.Periodics {
+		c.Periodics[i].SetDefaults(lh.PodNamespace)
+		if err := resolvePresets(c.Periodics[i].Name, c.Periodics[i].Labels, c.Periodics[i].Spec, c.Presets); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// Validate validates Config
+func (c *Config) Validate(lh lighthouse.Config) error {
+	type orgRepoJobName struct {
+		orgRepo, jobName string
+	}
+	// Validate presubmits.
+	// Checking that no duplicate job in prow config exists on the same org / repo / branch.
+	validPresubmits := map[orgRepoJobName][]Presubmit{}
+	for repo, jobs := range c.Presubmits {
+		for _, job := range jobs {
+			repoJobName := orgRepoJobName{repo, job.Name}
+			for _, existingJob := range validPresubmits[repoJobName] {
+				if existingJob.Brancher.Intersects(job.Brancher) {
+					return fmt.Errorf("duplicated presubmit job: %s", job.Name)
+				}
+			}
+			validPresubmits[repoJobName] = append(validPresubmits[repoJobName], job)
+		}
+	}
+	for _, ps := range c.Presubmits {
+		for _, j := range ps {
+			if err := j.Validate(lh.PodNamespace); err != nil {
+				return err
+			}
+		}
+	}
+	// Validate postsubmits.
+	// Checking that no duplicate job in prow config exists on the same org / repo / branch.
+	validPostsubmits := map[orgRepoJobName][]Postsubmit{}
+	for repo, jobs := range c.Postsubmits {
+		for _, job := range jobs {
+			repoJobName := orgRepoJobName{repo, job.Name}
+			for _, existingJob := range validPostsubmits[repoJobName] {
+				if existingJob.Brancher.Intersects(job.Brancher) {
+					return fmt.Errorf("duplicated postsubmit job: %s", job.Name)
+				}
+			}
+			validPostsubmits[repoJobName] = append(validPostsubmits[repoJobName], job)
+		}
+	}
+	for _, ps := range c.Postsubmits {
+		for _, j := range ps {
+			if err := j.Base.Validate(PostsubmitJob, lh.PodNamespace); err != nil {
+				return fmt.Errorf("invalid postsubmit job %s: %v", j.Name, err)
+			}
+		}
+	}
+	// validate no duplicated periodics
+	validPeriodics := sets.NewString()
+	// Ensure that the periodic durations are valid and specs exist.
+	for _, p := range c.Periodics {
+		if validPeriodics.Has(p.Name) {
+			return fmt.Errorf("duplicated periodic job : %s", p.Name)
+		}
+		validPeriodics.Insert(p.Name)
+		if err := p.Base.Validate(PeriodicJob, lh.PodNamespace); err != nil {
+			return fmt.Errorf("invalid periodic job %s: %v", p.Name, err)
+		}
+	}
+	// Set the interval on the periodic jobs. It doesn't make sense to do this
+	// for child jobs.
+	for j, p := range c.Periodics {
+		if p.Cron != "" && p.Interval != "" {
+			return fmt.Errorf("cron and interval cannot be both set in periodic %s", p.Name)
+		} else if p.Cron == "" && p.Interval == "" {
+			return fmt.Errorf("cron and interval cannot be both empty in periodic %s", p.Name)
+		} else if p.Cron != "" {
+			if _, err := cron.Parse(p.Cron); err != nil {
+				return fmt.Errorf("invalid cron string %s in periodic %s: %v", p.Cron, p.Name, err)
+			}
+		} else {
+			d, err := time.ParseDuration(c.Periodics[j].Interval)
+			if err != nil {
+				return fmt.Errorf("cannot parse duration for %s: %v", c.Periodics[j].Name, err)
+			}
+			c.Periodics[j].SetInterval(d)
+		}
+	}
+	return nil
+}
+
 // DecorationRequested checks if decoration was requested
 func (c *Config) DecorationRequested() bool {
 	for _, vs := range c.Presubmits {
@@ -37,7 +234,6 @@ func (c *Config) DecorationRequested() bool {
 			}
 		}
 	}
-
 	for _, js := range c.Postsubmits {
 		for i := range js {
 			if js[i].Decorate {
@@ -45,13 +241,11 @@ func (c *Config) DecorationRequested() bool {
 			}
 		}
 	}
-
 	for i := range c.Periodics {
 		if c.Periodics[i].Decorate {
 			return true
 		}
 	}
-
 	return false
 }
 
@@ -99,24 +293,20 @@ func (c *Config) AllPostsubmits(repos []string) []Postsubmit {
 
 // AllPeriodics returns all prow periodic jobs.
 func (c *Config) AllPeriodics() []Periodic {
-	listPeriodic := func(ps []Periodic) []Periodic {
-		var res []Periodic
-		res = append(res, ps...)
-		return res
-	}
-
-	return listPeriodic(c.Periodics)
+	return c.Periodics
 }
 
 // SetPresubmits updates c.Presubmits to jobs, after compiling and validating their regexes.
 func (c *Config) SetPresubmits(jobs map[string][]Presubmit) error {
 	nj := map[string][]Presubmit{}
 	for k, v := range jobs {
+		for i := range v {
+			if err := v[i].SetRegexes(); err != nil {
+				return err
+			}
+		}
 		nj[k] = make([]Presubmit, len(v))
 		copy(nj[k], v)
-		if err := SetPresubmitRegexes(nj[k]); err != nil {
-			return err
-		}
 	}
 	c.Presubmits = nj
 	return nil
@@ -126,45 +316,14 @@ func (c *Config) SetPresubmits(jobs map[string][]Presubmit) error {
 func (c *Config) SetPostsubmits(jobs map[string][]Postsubmit) error {
 	nj := map[string][]Postsubmit{}
 	for k, v := range jobs {
+		for i := range v {
+			if err := v[i].SetRegexes(); err != nil {
+				return err
+			}
+		}
 		nj[k] = make([]Postsubmit, len(v))
 		copy(nj[k], v)
-		if err := SetPostsubmitRegexes(nj[k]); err != nil {
-			return err
-		}
 	}
 	c.Postsubmits = nj
 	return nil
 }
-
-// SetPresubmitRegexes compiles and validates all the regular expressions for
-// the provided presubmits.
-func SetPresubmitRegexes(ps []Presubmit) error {
-	for i := range ps {
-		if err := ps[i].SetRegexes(); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-// SetPostsubmitRegexes compiles and validates all the regular expressions for
-// the provided postsubmits.
-func SetPostsubmitRegexes(ps []Postsubmit) error {
-	for i := range ps {
-		if err := ps[i].SetRegexes(); err != nil {
-			return err
-		}
-	}
-	return nil
-}
-
-// ClearCompiledRegexes removes compiled regexes from the presubmits,
-// useful for testing when deep equality is needed between presubmits
-func ClearCompiledRegexes(presubmits []Presubmit) {
-	for i := range presubmits {
-		presubmits[i].re = nil
-		presubmits[i].Brancher.re = nil
-		presubmits[i].Brancher.reSkip = nil
-		presubmits[i].RegexpChangeMatcher.reChanges = nil
-	}
-}
diff --git a/pkg/config/job/jobs.go b/pkg/config/job/jobs.go
deleted file mode 100644
index 26c1dc5a..00000000
--- a/pkg/config/job/jobs.go
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
-Copyright 2017 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package job
-
-// // RetestPresubmits returns all presubmits that should be run given a /retest command.
-// // This is the set of all presubmits intersected with ((alwaysRun + runContexts) - skipContexts)
-// func (c *JobConfig) RetestPresubmits(fullRepoName string, skipContexts, runContexts sets.String) []Presubmit {
-// 	var result []Presubmit
-// 	if jobs, ok := c.Presubmits[fullRepoName]; ok {
-// 		for _, job := range jobs {
-// 			if skipContexts.Has(job.Context) {
-// 				continue
-// 			}
-// 			if job.AlwaysRun || job.RunIfChanged != "" || runContexts.Has(job.Context) {
-// 				result = append(result, job)
-// 			}
-// 		}
-// 	}
-// 	return result
-// }
-
-// // GetPresubmit returns the presubmit job for the provided repo and job name.
-// func (c *JobConfig) GetPresubmit(repo, jobName string) *Presubmit {
-// 	presubmits := c.AllPresubmits([]string{repo})
-// 	for i := range presubmits {
-// 		ps := presubmits[i]
-// 		if ps.Name == jobName {
-// 			return &ps
-// 		}
-// 	}
-// 	return nil
-// }
diff --git a/pkg/config/constants.go b/pkg/config/job/label.go
similarity index 63%
rename from pkg/config/constants.go
rename to pkg/config/job/label.go
index 7a86308a..7c8b514c 100644
--- a/pkg/config/constants.go
+++ b/pkg/config/job/label.go
@@ -22,31 +22,12 @@
  * THE SOFTWARE.
  */
 
-package config
+package job
 
-// PullRequestMergeType inidicates the type of the pull request
-type PullRequestMergeType string
+import (
+	"fmt"
 
-// Possible types of merges for the GitHub merge API
-const (
-	MergeMerge  PullRequestMergeType = "merge"
-	MergeRebase PullRequestMergeType = "rebase"
-	MergeSquash PullRequestMergeType = "squash"
-)
-
-// PipelineKind specifies how the job is triggered.
-type PipelineKind string
-
-// Various job types.
-const (
-	// PresubmitJob means it runs on unmerged PRs.
-	PresubmitJob PipelineKind = "presubmit"
-	// PostsubmitJob means it runs on each new commit.
-	PostsubmitJob PipelineKind = "postsubmit"
-	// Periodic job means it runs on a time-basis, unrelated to git changes.
-	PeriodicJob PipelineKind = "periodic"
-	// BatchJob tests multiple unmerged PRs at the same time.
-	BatchJob PipelineKind = "batch"
+	"k8s.io/apimachinery/pkg/util/validation"
 )
 
 const (
@@ -54,20 +35,38 @@ const (
 	// carries the job type (presubmit, postsubmit, periodic, batch)
 	// that the pod is running.
 	LighthouseJobTypeLabel = "lighthouse.jenkins-x.io/type"
-
 	// LighthouseJobIDLabel is added in resources created by lighthouse and
 	// carries the ID of the LighthouseJob that the pod is fulfilling.
 	// We also name resources after the LighthouseJob that spawned them but
 	// this allows for multiple resources to be linked to one
 	// LighthouseJob.
 	LighthouseJobIDLabel = "lighthouse.jenkins-x.io/id"
-
-	// CreatedByLighthouse is added on resources created by Lighthosue.
+	// CreatedByLighthouseLabel is added on resources created by Lighthosue.
 	// Since resources often live in another cluster/namespace,
 	// the k8s garbage collector would immediately delete these
 	// resources
-	CreatedByLighthouse = "created-by-lighthouse"
-
-	// DefaultClusterAlias specifies the default context for resources owned by jobs (pods/builds).
-	DefaultClusterAlias = "default"
+	CreatedByLighthouseLabel = "created-by-lighthouse"
 )
+
+// Labels returns a string slice with label consts from kube.
+func Labels() []string {
+	return []string{LighthouseJobTypeLabel, CreatedByLighthouseLabel, LighthouseJobIDLabel}
+}
+
+// ValidateLabels validates labels (not using reserved labels, valid names and valid values)
+func ValidateLabels(labels map[string]string) error {
+	for label, value := range labels {
+		for _, prowLabel := range Labels() {
+			if label == prowLabel {
+				return fmt.Errorf("label %s is reserved for decoration", label)
+			}
+		}
+		if errs := validation.IsQualifiedName(label); len(errs) != 0 {
+			return fmt.Errorf("invalid label %s: %v", label, errs)
+		}
+		if errs := validation.IsValidLabelValue(labels[label]); len(errs) != 0 {
+			return fmt.Errorf("label %s has invalid value %s: %v", label, value, errs)
+		}
+	}
+	return nil
+}
diff --git a/pkg/config/job/pipelinekind.go b/pkg/config/job/pipelinekind.go
new file mode 100644
index 00000000..306dddb5
--- /dev/null
+++ b/pkg/config/job/pipelinekind.go
@@ -0,0 +1,40 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2020, CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package job
+
+// PipelineKind specifies how the job is triggered.
+type PipelineKind string
+
+// Various job types.
+const (
+	// PresubmitJob means it runs on unmerged PRs.
+	PresubmitJob PipelineKind = "presubmit"
+	// PostsubmitJob means it runs on each new commit.
+	PostsubmitJob PipelineKind = "postsubmit"
+	// Periodic job means it runs on a time-basis, unrelated to git changes.
+	PeriodicJob PipelineKind = "periodic"
+	// BatchJob tests multiple unmerged PRs at the same time.
+	BatchJob PipelineKind = "batch"
+)
diff --git a/pkg/config/job/presubmit.go b/pkg/config/job/presubmit.go
index 082227a9..629ab4da 100644
--- a/pkg/config/job/presubmit.go
+++ b/pkg/config/job/presubmit.go
@@ -26,30 +26,23 @@ import (
 // Presubmit runs on PRs.
 type Presubmit struct {
 	Base
-
+	Brancher
+	RegexpChangeMatcher
+	Reporter
 	// AlwaysRun automatically for every PR, or only when a comment triggers it.
 	AlwaysRun bool `json:"always_run"`
-
 	// Optional indicates that the job's status context should not be required for merge.
 	Optional bool `json:"optional,omitempty"`
-
 	// Trigger is the regular expression to trigger the job.
 	// e.g. `@k8s-bot e2e test this`
 	// RerunCommand must also be specified if this field is specified.
 	// (Default: `(?m)^/test (?:.*? )?<job name>(?: .*?)?$`)
 	Trigger string `json:"trigger,omitempty"`
-
 	// The RerunCommand to give users. Must match Trigger.
 	// Trigger must also be specified if this field is specified.
 	// (Default: `/test <job name>`)
 	RerunCommand string `json:"rerun_command,omitempty"`
 
-	Brancher
-
-	RegexpChangeMatcher
-
-	Reporter
-
 	// We'll set these when we load it.
 	//re *regexp.Regexp // from Trigger.
 }
@@ -92,6 +85,13 @@ func (p *Presubmit) SetRegexes() error {
 	return nil
 }
 
+// ClearCompiledRegexes compiles and validates all the regular expressions
+func (p *Presubmit) ClearCompiledRegexes() {
+	p.Brancher.re = nil
+	p.Brancher.reSkip = nil
+	p.RegexpChangeMatcher.reChanges = nil
+}
+
 // CouldRun determines if the presubmit could run against a specific
 // base ref
 func (p Presubmit) CouldRun(baseRef string) bool {
@@ -151,3 +151,17 @@ func (p Presubmit) TriggerMatches(body string) bool {
 func (p Presubmit) ContextRequired() bool {
 	return !p.Optional && !p.SkipReport
 }
+
+// Validate validates job base
+func (p *Presubmit) Validate(podNamespace string) error {
+	if err := p.Base.Validate(PresubmitJob, podNamespace); err != nil {
+		return fmt.Errorf("invalid presubmit job %s: %v", p.Name, err)
+	}
+	if p.AlwaysRun && p.RunIfChanged != "" {
+		return fmt.Errorf("job %s is set to always run but also declares run_if_changed targets, which are mutually exclusive", p.Name)
+	}
+	if !p.SkipReport && p.Context == "" {
+		return fmt.Errorf("job %s is set to report but has no context configured", p.Name)
+	}
+	return nil
+}
diff --git a/pkg/config/job/utilityconfig.go b/pkg/config/job/utilityconfig.go
index f079c696..cf65c019 100644
--- a/pkg/config/job/utilityconfig.go
+++ b/pkg/config/job/utilityconfig.go
@@ -20,7 +20,6 @@ package job
 type UtilityConfig struct {
 	// Decorate determines if we decorate the PodSpec or not
 	Decorate bool `json:"decorate,omitempty"`
-
 	// PathAlias is the location under <root-dir>/src
 	// where the repository under test is cloned. If this
 	// is not set, <root-dir>/src/github.com/org/repo will
diff --git a/pkg/config/keeper.go b/pkg/config/keeper.go
deleted file mode 100644
index 82ea52f8..00000000
--- a/pkg/config/keeper.go
+++ /dev/null
@@ -1,572 +0,0 @@
-/*
-Copyright 2017 The Kubernetes Authors.
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-    http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
-*/
-
-package config
-
-import (
-	"errors"
-	"fmt"
-	"strings"
-	"sync"
-	"text/template"
-	"time"
-
-	"github.com/sirupsen/logrus"
-
-	"k8s.io/apimachinery/pkg/util/sets"
-)
-
-// KeeperQueries is a KeeperQuery slice.
-type KeeperQueries []KeeperQuery
-
-// KeeperContextPolicy configures options about how to handle various contexts.
-type KeeperContextPolicy struct {
-	// whether to consider unknown contexts optional (skip) or required.
-	SkipUnknownContexts       *bool    `json:"skip-unknown-contexts,omitempty"`
-	RequiredContexts          []string `json:"required-contexts,omitempty"`
-	RequiredIfPresentContexts []string `json:"required-if-present-contexts"`
-	OptionalContexts          []string `json:"optional-contexts,omitempty"`
-	// Infer required and optional jobs from Branch Protection configuration
-	FromBranchProtection *bool `json:"from-branch-protection,omitempty"`
-}
-
-// KeeperOrgContextPolicy overrides the policy for an org, and any repo overrides.
-type KeeperOrgContextPolicy struct {
-	KeeperContextPolicy
-	Repos map[string]KeeperRepoContextPolicy `json:"repos,omitempty"`
-}
-
-// KeeperRepoContextPolicy overrides the policy for repo, and any branch overrides.
-type KeeperRepoContextPolicy struct {
-	KeeperContextPolicy
-	Branches map[string]KeeperContextPolicy `json:"branches,omitempty"`
-}
-
-// KeeperContextPolicyOptions holds the default policy, and any org overrides.
-type KeeperContextPolicyOptions struct {
-	KeeperContextPolicy
-	// Github Orgs
-	Orgs map[string]KeeperOrgContextPolicy `json:"orgs,omitempty"`
-}
-
-// KeeperMergeCommitTemplate holds templates to use for merge commits.
-type KeeperMergeCommitTemplate struct {
-	TitleTemplate string `json:"title,omitempty"`
-	BodyTemplate  string `json:"body,omitempty"`
-
-	Title *template.Template `json:"-"`
-	Body  *template.Template `json:"-"`
-}
-
-// Keeper is config for the keeper pool.
-type Keeper struct {
-	// SyncPeriodString compiles into SyncPeriod at load time.
-	SyncPeriodString string `json:"sync_period,omitempty"`
-	// SyncPeriod specifies how often Keeper will sync jobs with Github. Defaults to 1m.
-	SyncPeriod time.Duration `json:"-"`
-	// StatusUpdatePeriodString compiles into StatusUpdatePeriod at load time.
-	StatusUpdatePeriodString string `json:"status_update_period,omitempty"`
-	// StatusUpdatePeriod specifies how often Keeper will update Github status contexts.
-	// Defaults to the value of SyncPeriod.
-	StatusUpdatePeriod time.Duration `json:"-"`
-	// Queries represents a list of GitHub search queries that collectively
-	// specify the set of PRs that meet merge requirements.
-	Queries KeeperQueries `json:"queries,omitempty"`
-
-	// A key/value pair of an org/repo as the key and merge method to override
-	// the default method of merge. Valid options are squash, rebase, and merge.
-	MergeType map[string]PullRequestMergeType `json:"merge_method,omitempty"`
-
-	// A key/value pair of an org/repo as the key and Go template to override
-	// the default merge commit title and/or message. Template is passed the
-	// PullRequest struct (prow/github/types.go#PullRequest)
-	MergeTemplate map[string]KeeperMergeCommitTemplate `json:"merge_commit_template,omitempty"`
-
-	// URL for keeper status contexts.
-	// We can consider allowing this to be set separately for separate repos, or
-	// allowing it to be a template.
-	TargetURL string `json:"target_url,omitempty"`
-
-	// PRStatusBaseURL is the base URL for the PR status page.
-	// This is used to link to a merge requirements overview
-	// in the keeper status context.
-	PRStatusBaseURL string `json:"pr_status_base_url,omitempty"`
-
-	// BlockerLabel is an optional label that is used to identify merge blocking
-	// Github issues.
-	// Leave this blank to disable this feature and save 1 API token per sync loop.
-	BlockerLabel string `json:"blocker_label,omitempty"`
-
-	// SquashLabel is an optional label that is used to identify PRs that should
-	// always be squash merged.
-	// Leave this blank to disable this feature.
-	SquashLabel string `json:"squash_label,omitempty"`
-
-	// RebaseLabel is an optional label that is used to identify PRs that should
-	// always be rebased and merged.
-	// Leave this blank to disable this feature.
-	RebaseLabel string `json:"rebase_label,omitempty"`
-
-	// MergeLabel is an optional label that is used to identify PRs that should
-	// always be merged with all individual commits from the PR.
-	// Leave this blank to disable this feature.
-	MergeLabel string `json:"merge_label,omitempty"`
-
-	// MaxGoroutines is the maximum number of goroutines spawned inside the
-	// controller to handle org/repo:branch pools. Defaults to 20. Needs to be a
-	// positive number.
-	MaxGoroutines int `json:"max_goroutines,omitempty"`
-
-	// KeeperContextPolicyOptions defines merge options for context. If not set it will infer
-	// the required and optional contexts from the prow jobs configured and use the github
-	// combined status; otherwise it may apply the branch protection setting or let user
-	// define their own options in case branch protection is not used.
-	ContextOptions KeeperContextPolicyOptions `json:"context_options,omitempty"`
-
-	// BatchSizeLimitMap is a key/value pair of an org or org/repo as the key and
-	// integer batch size limit as the value. The empty string key can be used as
-	// a global default.
-	// Special values:
-	//  0 => unlimited batch size
-	// -1 => batch merging disabled :(
-	BatchSizeLimitMap map[string]int `json:"batch_size_limit,omitempty"`
-}
-
-// MergeMethod returns the merge method to use for a repo. The default of merge is
-// returned when not overridden.
-func (t *Keeper) MergeMethod(org, repo string) PullRequestMergeType {
-	name := org + "/" + repo
-
-	v, ok := t.MergeType[name]
-	if !ok {
-		if ov, found := t.MergeType[org]; found {
-			return ov
-		}
-
-		return MergeMerge
-	}
-
-	return v
-}
-
-// BatchSizeLimit return the batch size limit for the given repo
-func (t *Keeper) BatchSizeLimit(org, repo string) int {
-	// TODO: Remove once #564 is fixed and batch builds can work again. (APB)
-	return -1
-	//if limit, ok := t.BatchSizeLimitMap[fmt.Sprintf("%s/%s", org, repo)]; ok {
-	//	return limit
-	//}
-	//if limit, ok := t.BatchSizeLimitMap[org]; ok {
-	//	return limit
-	//}
-	//return t.BatchSizeLimitMap["*"]
-}
-
-// MergeCommitTemplate returns a struct with Go template string(s) or nil
-func (t *Keeper) MergeCommitTemplate(org, repo string) KeeperMergeCommitTemplate {
-	name := org + "/" + repo
-
-	v, ok := t.MergeTemplate[name]
-	if !ok {
-		return t.MergeTemplate[org]
-	}
-
-	return v
-}
-
-// KeeperQuery is turned into a GitHub search query. See the docs for details:
-// https://help.github.com/articles/searching-issues-and-pull-requests/
-type KeeperQuery struct {
-	Orgs          []string `json:"orgs,omitempty"`
-	Repos         []string `json:"repos,omitempty"`
-	ExcludedRepos []string `json:"excludedRepos,omitempty"`
-
-	ExcludedBranches []string `json:"excludedBranches,omitempty"`
-	IncludedBranches []string `json:"includedBranches,omitempty"`
-
-	Labels        []string `json:"labels,omitempty"`
-	MissingLabels []string `json:"missingLabels,omitempty"`
-
-	Milestone string `json:"milestone,omitempty"`
-
-	ReviewApprovedRequired bool `json:"reviewApprovedRequired,omitempty"`
-}
-
-// Query returns the corresponding github search string for the keeper query.
-func (tq *KeeperQuery) Query() string {
-	toks := []string{"is:pr", "state:open"}
-	for _, o := range tq.Orgs {
-		toks = append(toks, fmt.Sprintf("org:\"%s\"", o))
-	}
-	for _, r := range tq.Repos {
-		toks = append(toks, fmt.Sprintf("repo:\"%s\"", r))
-	}
-	for _, r := range tq.ExcludedRepos {
-		toks = append(toks, fmt.Sprintf("-repo:\"%s\"", r))
-	}
-	for _, b := range tq.ExcludedBranches {
-		toks = append(toks, fmt.Sprintf("-base:\"%s\"", b))
-	}
-	for _, b := range tq.IncludedBranches {
-		toks = append(toks, fmt.Sprintf("base:\"%s\"", b))
-	}
-	for _, l := range tq.Labels {
-		toks = append(toks, fmt.Sprintf("label:\"%s\"", l))
-	}
-	for _, l := range tq.MissingLabels {
-		toks = append(toks, fmt.Sprintf("-label:\"%s\"", l))
-	}
-	if tq.Milestone != "" {
-		toks = append(toks, fmt.Sprintf("milestone:\"%s\"", tq.Milestone))
-	}
-	if tq.ReviewApprovedRequired {
-		toks = append(toks, "review:approved")
-	}
-	return strings.Join(toks, " ")
-}
-
-// ForRepo indicates if the keeper query applies to the specified repo.
-func (tq KeeperQuery) ForRepo(org, repo string) bool {
-	fullName := fmt.Sprintf("%s/%s", org, repo)
-	for _, queryOrg := range tq.Orgs {
-		if queryOrg != org {
-			continue
-		}
-		// Check for repos excluded from the org.
-		for _, excludedRepo := range tq.ExcludedRepos {
-			if excludedRepo == fullName {
-				return false
-			}
-		}
-		return true
-	}
-	for _, queryRepo := range tq.Repos {
-		if queryRepo == fullName {
-			return true
-		}
-	}
-	return false
-}
-
-func reposInOrg(org string, repos []string) []string {
-	prefix := org + "/"
-	var res []string
-	for _, repo := range repos {
-		if strings.HasPrefix(repo, prefix) {
-			res = append(res, repo)
-		}
-	}
-	return res
-}
-
-// OrgExceptionsAndRepos determines which orgs and repos a set of queries cover.
-// Output is returned as a mapping from 'included org'->'repos excluded in the org'
-// and a set of included repos.
-func (tqs KeeperQueries) OrgExceptionsAndRepos() (map[string]sets.String, sets.String) {
-	orgs := make(map[string]sets.String)
-	for i := range tqs {
-		for _, org := range tqs[i].Orgs {
-			applicableRepos := sets.NewString(reposInOrg(org, tqs[i].ExcludedRepos)...)
-			if excepts, ok := orgs[org]; !ok {
-				// We have not seen this org so the exceptions are just applicable
-				// members of 'excludedRepos'.
-				orgs[org] = applicableRepos
-			} else {
-				// We have seen this org so the exceptions are the applicable
-				// members of 'excludedRepos' intersected with existing exceptions.
-				orgs[org] = excepts.Intersection(applicableRepos)
-			}
-		}
-	}
-	repos := sets.NewString()
-	for i := range tqs {
-		repos.Insert(tqs[i].Repos...)
-	}
-	// Remove any org exceptions that are explicitly included in a different query.
-	reposList := repos.UnsortedList()
-	for _, excepts := range orgs {
-		excepts.Delete(reposList...)
-	}
-	return orgs, repos
-}
-
-// QueryMap is a struct mapping from "org/repo" -> KeeperQueries that
-// apply to that org or repo. It is lazily populated, but threadsafe.
-type QueryMap struct {
-	queries KeeperQueries
-
-	cache map[string]KeeperQueries
-	sync.Mutex
-}
-
-// QueryMap creates a QueryMap from KeeperQueries
-func (tqs KeeperQueries) QueryMap() *QueryMap {
-	return &QueryMap{
-		queries: tqs,
-		cache:   make(map[string]KeeperQueries),
-	}
-}
-
-// ForRepo returns the keeper queries that apply to a repo.
-func (qm *QueryMap) ForRepo(org, repo string) KeeperQueries {
-	res := KeeperQueries(nil)
-	fullName := fmt.Sprintf("%s/%s", org, repo)
-
-	qm.Lock()
-	defer qm.Unlock()
-
-	if qs, ok := qm.cache[fullName]; ok {
-		return append(res, qs...) // Return a copy.
-	}
-	// Cache miss. Need to determine relevant queries.
-
-	for _, query := range qm.queries {
-		if query.ForRepo(org, repo) {
-			res = append(res, query)
-		}
-	}
-	qm.cache[fullName] = res
-	return res
-}
-
-// Validate returns an error if the query has any errors.
-//
-// Examples include:
-// * an org name that is empty or includes a /
-// * repos that are not org/repo
-// * a label that is in both the labels and missing_labels section
-// * a branch that is in both included and excluded branch set.
-func (tq *KeeperQuery) Validate() error {
-	duplicates := func(field string, list []string) error {
-		dups := sets.NewString()
-		seen := sets.NewString()
-		for _, elem := range list {
-			if seen.Has(elem) {
-				dups.Insert(elem)
-			} else {
-				seen.Insert(elem)
-			}
-		}
-		dupCount := len(list) - seen.Len()
-		if dupCount == 0 {
-			return nil
-		}
-		return fmt.Errorf("%q contains %d duplicate entries: %s", field, dupCount, strings.Join(dups.List(), ", "))
-	}
-
-	orgs := sets.NewString()
-	for o := range tq.Orgs {
-		if strings.Contains(tq.Orgs[o], "/") {
-			return fmt.Errorf("orgs[%d]: %q contains a '/' which is not valid", o, tq.Orgs[o])
-		}
-		if len(tq.Orgs[o]) == 0 {
-			return fmt.Errorf("orgs[%d]: is an empty string", o)
-		}
-		orgs.Insert(tq.Orgs[o])
-	}
-	if err := duplicates("orgs", tq.Orgs); err != nil {
-		return err
-	}
-
-	for r := range tq.Repos {
-		parts := strings.Split(tq.Repos[r], "/")
-		if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 {
-			return fmt.Errorf("repos[%d]: %q is not of the form \"org/repo\"", r, tq.Repos[r])
-		}
-		if orgs.Has(parts[0]) {
-			return fmt.Errorf("repos[%d]: %q is already included via org: %q", r, tq.Repos[r], parts[0])
-		}
-	}
-	if err := duplicates("repos", tq.Repos); err != nil {
-		return err
-	}
-
-	if len(tq.Orgs) == 0 && len(tq.Repos) == 0 {
-		return errors.New("'orgs' and 'repos' cannot both be empty")
-	}
-
-	for er := range tq.ExcludedRepos {
-		parts := strings.Split(tq.ExcludedRepos[er], "/")
-		if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 {
-			return fmt.Errorf("excludedRepos[%d]: %q is not of the form \"org/repo\"", er, tq.ExcludedRepos[er])
-		}
-		if !orgs.Has(parts[0]) {
-			return fmt.Errorf("excludedRepos[%d]: %q has no effect because org %q is not included", er, tq.ExcludedRepos[er], parts[0])
-		}
-		// Note: At this point we also know that this excludedRepo is not found in 'repos'.
-	}
-	if err := duplicates("excludedRepos", tq.ExcludedRepos); err != nil {
-		return err
-	}
-
-	if invalids := sets.NewString(tq.Labels...).Intersection(sets.NewString(tq.MissingLabels...)); len(invalids) > 0 {
-		return fmt.Errorf("the labels: %q are both required and forbidden", invalids.List())
-	}
-	if err := duplicates("labels", tq.Labels); err != nil {
-		return err
-	}
-	if err := duplicates("missingLabels", tq.MissingLabels); err != nil {
-		return err
-	}
-
-	if len(tq.ExcludedBranches) > 0 && len(tq.IncludedBranches) > 0 {
-		return errors.New("both 'includedBranches' and 'excludedBranches' are specified ('excludedBranches' have no effect)")
-	}
-	if err := duplicates("includedBranches", tq.IncludedBranches); err != nil {
-		return err
-	}
-	if err := duplicates("excludedBranches", tq.ExcludedBranches); err != nil {
-		return err
-	}
-
-	return nil
-}
-
-// Validate returns an error if any contexts are listed more than once in the config.
-func (cp *KeeperContextPolicy) Validate() error {
-	if inter := sets.NewString(cp.RequiredContexts...).Intersection(sets.NewString(cp.OptionalContexts...)); inter.Len() > 0 {
-		return fmt.Errorf("contexts %s are defined as required and optional", strings.Join(inter.List(), ", "))
-	}
-	if inter := sets.NewString(cp.RequiredContexts...).Intersection(sets.NewString(cp.RequiredIfPresentContexts...)); inter.Len() > 0 {
-		return fmt.Errorf("contexts %s are defined as required and required if present", strings.Join(inter.List(), ", "))
-	}
-	if inter := sets.NewString(cp.OptionalContexts...).Intersection(sets.NewString(cp.RequiredIfPresentContexts...)); inter.Len() > 0 {
-		return fmt.Errorf("contexts %s are defined as optional and required if present", strings.Join(inter.List(), ", "))
-	}
-	return nil
-}
-
-func mergeKeeperContextPolicy(a, b KeeperContextPolicy) KeeperContextPolicy {
-	mergeBool := func(a, b *bool) *bool {
-		if b == nil {
-			return a
-		}
-		return b
-	}
-	c := KeeperContextPolicy{}
-	c.FromBranchProtection = mergeBool(a.FromBranchProtection, b.FromBranchProtection)
-	c.SkipUnknownContexts = mergeBool(a.SkipUnknownContexts, b.SkipUnknownContexts)
-	required := sets.NewString(a.RequiredContexts...)
-	requiredIfPresent := sets.NewString(a.RequiredIfPresentContexts...)
-	optional := sets.NewString(a.OptionalContexts...)
-	required.Insert(b.RequiredContexts...)
-	requiredIfPresent.Insert(b.RequiredIfPresentContexts...)
-	optional.Insert(b.OptionalContexts...)
-	if required.Len() > 0 {
-		c.RequiredContexts = required.List()
-	}
-	if requiredIfPresent.Len() > 0 {
-		c.RequiredIfPresentContexts = requiredIfPresent.List()
-	}
-	if optional.Len() > 0 {
-		c.OptionalContexts = optional.List()
-	}
-	return c
-}
-
-func parseKeeperContextPolicyOptions(org, repo, branch string, options KeeperContextPolicyOptions) KeeperContextPolicy {
-	option := options.KeeperContextPolicy
-	if o, ok := options.Orgs[org]; ok {
-		option = mergeKeeperContextPolicy(option, o.KeeperContextPolicy)
-		if r, ok := o.Repos[repo]; ok {
-			option = mergeKeeperContextPolicy(option, r.KeeperContextPolicy)
-			if b, ok := r.Branches[branch]; ok {
-				option = mergeKeeperContextPolicy(option, b)
-			}
-		}
-	}
-	return option
-}
-
-// GetKeeperContextPolicy parses the prow config to find context merge options.
-// If none are set, it will use the prow jobs configured and use the default github combined status.
-// Otherwise if set it will use the branch protection setting, or the listed jobs.
-func (c Config) GetKeeperContextPolicy(org, repo, branch string) (*KeeperContextPolicy, error) {
-	options := parseKeeperContextPolicyOptions(org, repo, branch, c.Keeper.ContextOptions)
-	// Adding required and optional contexts from options
-	required := sets.NewString(options.RequiredContexts...)
-	requiredIfPresent := sets.NewString(options.RequiredIfPresentContexts...)
-	optional := sets.NewString(options.OptionalContexts...)
-
-	// automatically generate required and optional entries for Prow Pipelines
-	prowRequired, prowRequiredIfPresent, prowOptional := BranchRequirements(org, repo, branch, c.Presubmits)
-	required.Insert(prowRequired...)
-	requiredIfPresent.Insert(prowRequiredIfPresent...)
-	optional.Insert(prowOptional...)
-
-	// Using Branch protection configuration
-	if options.FromBranchProtection != nil && *options.FromBranchProtection {
-		bp, err := c.GetBranchProtection(org, repo, branch)
-		if err != nil {
-			logrus.WithError(err).Warningf("Error getting branch protection for %s/%s+%s", org, repo, branch)
-		} else if bp != nil && bp.Protect != nil && *bp.Protect && bp.RequiredStatusChecks != nil {
-			required.Insert(bp.RequiredStatusChecks.Contexts...)
-		}
-	}
-
-	// Remove anything from the required list that's also in the required if present list, since that may have been
-	// duplicated by branch protection.
-	required.Delete(requiredIfPresent.List()...)
-
-	t := &KeeperContextPolicy{
-		RequiredContexts:          required.List(),
-		RequiredIfPresentContexts: requiredIfPresent.List(),
-		OptionalContexts:          optional.List(),
-		SkipUnknownContexts:       options.SkipUnknownContexts,
-	}
-	if err := t.Validate(); err != nil {
-		return t, err
-	}
-	return t, nil
-}
-
-// IsOptional checks whether a context can be ignored.
-// Will return true if
-// - context is registered as optional
-// - required contexts are registered and the context provided is not required
-// Will return false otherwise. Every context is required.
-func (cp *KeeperContextPolicy) IsOptional(c string) bool {
-	if sets.NewString(cp.OptionalContexts...).Has(c) {
-		return true
-	}
-	if sets.NewString(cp.RequiredContexts...).Has(c) {
-		return false
-	}
-	// assume if we're asking that the context is present on the PR
-	if sets.NewString(cp.RequiredIfPresentContexts...).Has(c) {
-		return false
-	}
-	if cp.SkipUnknownContexts != nil && *cp.SkipUnknownContexts {
-		return true
-	}
-	return false
-}
-
-// MissingRequiredContexts discard the optional contexts and only look of extra required contexts that are not provided.
-func (cp *KeeperContextPolicy) MissingRequiredContexts(contexts []string) []string {
-	if len(cp.RequiredContexts) == 0 {
-		return nil
-	}
-	existingContexts := sets.NewString()
-	for _, c := range contexts {
-		existingContexts.Insert(c)
-	}
-	var missingContexts []string
-	for c := range sets.NewString(cp.RequiredContexts...).Difference(existingContexts) {
-		missingContexts = append(missingContexts, c)
-	}
-	return missingContexts
-}
diff --git a/pkg/config/keeper/config.go b/pkg/config/keeper/config.go
new file mode 100644
index 00000000..29449a8e
--- /dev/null
+++ b/pkg/config/keeper/config.go
@@ -0,0 +1,166 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package keeper
+
+import (
+	"fmt"
+	"time"
+)
+
+// Config is the config for the keeper pool.
+type Config struct {
+	// SyncPeriodString compiles into SyncPeriod at load time.
+	SyncPeriodString string `json:"sync_period,omitempty"`
+	// SyncPeriod specifies how often Keeper will sync jobs with Github. Defaults to 1m.
+	SyncPeriod time.Duration `json:"-"`
+	// StatusUpdatePeriodString compiles into StatusUpdatePeriod at load time.
+	StatusUpdatePeriodString string `json:"status_update_period,omitempty"`
+	// StatusUpdatePeriod specifies how often Keeper will update Github status contexts.
+	// Defaults to the value of SyncPeriod.
+	StatusUpdatePeriod time.Duration `json:"-"`
+	// Queries represents a list of GitHub search queries that collectively
+	// specify the set of PRs that meet merge requirements.
+	Queries Queries `json:"queries,omitempty"`
+	// A key/value pair of an org/repo as the key and merge method to override
+	// the default method of merge. Valid options are squash, rebase, and merge.
+	MergeType map[string]PullRequestMergeType `json:"merge_method,omitempty"`
+	// A key/value pair of an org/repo as the key and Go template to override
+	// the default merge commit title and/or message. Template is passed the
+	// PullRequest struct (prow/github/types.go#PullRequest)
+	MergeTemplate map[string]MergeCommitTemplate `json:"merge_commit_template,omitempty"`
+	// URL for keeper status contexts.
+	// We can consider allowing this to be set separately for separate repos, or
+	// allowing it to be a template.
+	TargetURL string `json:"target_url,omitempty"`
+	// PRStatusBaseURL is the base URL for the PR status page.
+	// This is used to link to a merge requirements overview
+	// in the keeper status context.
+	PRStatusBaseURL string `json:"pr_status_base_url,omitempty"`
+	// BlockerLabel is an optional label that is used to identify merge blocking
+	// Github issues.
+	// Leave this blank to disable this feature and save 1 API token per sync loop.
+	BlockerLabel string `json:"blocker_label,omitempty"`
+	// SquashLabel is an optional label that is used to identify PRs that should
+	// always be squash merged.
+	// Leave this blank to disable this feature.
+	SquashLabel string `json:"squash_label,omitempty"`
+	// RebaseLabel is an optional label that is used to identify PRs that should
+	// always be rebased and merged.
+	// Leave this blank to disable this feature.
+	RebaseLabel string `json:"rebase_label,omitempty"`
+	// MergeLabel is an optional label that is used to identify PRs that should
+	// always be merged with all individual commits from the PR.
+	// Leave this blank to disable this feature.
+	MergeLabel string `json:"merge_label,omitempty"`
+	// MaxGoroutines is the maximum number of goroutines spawned inside the
+	// controller to handle org/repo:branch pools. Defaults to 20. Needs to be a
+	// positive number.
+	MaxGoroutines int `json:"max_goroutines,omitempty"`
+	// KeeperContextPolicyOptions defines merge options for context. If not set it will infer
+	// the required and optional contexts from the prow jobs configured and use the github
+	// combined status; otherwise it may apply the branch protection setting or let user
+	// define their own options in case branch protection is not used.
+	ContextOptions ContextPolicyOptions `json:"context_options,omitempty"`
+	// BatchSizeLimitMap is a key/value pair of an org or org/repo as the key and
+	// integer batch size limit as the value. The empty string key can be used as
+	// a global default.
+	// Special values:
+	//  0 => unlimited batch size
+	// -1 => batch merging disabled :(
+	BatchSizeLimitMap map[string]int `json:"batch_size_limit,omitempty"`
+}
+
+// MergeMethod returns the merge method to use for a repo. The default of merge is
+// returned when not overridden.
+func (c *Config) MergeMethod(org, repo string) PullRequestMergeType {
+	name := org + "/" + repo
+
+	v, ok := c.MergeType[name]
+	if !ok {
+		if ov, found := c.MergeType[org]; found {
+			return ov
+		}
+
+		return MergeMerge
+	}
+
+	return v
+}
+
+// BatchSizeLimit return the batch size limit for the given repo
+func (c *Config) BatchSizeLimit(org, repo string) int {
+	// TODO: Remove once #564 is fixed and batch builds can work again. (APB)
+	return -1
+	//if limit, ok := t.BatchSizeLimitMap[fmt.Sprintf("%s/%s", org, repo)]; ok {
+	//	return limit
+	//}
+	//if limit, ok := t.BatchSizeLimitMap[org]; ok {
+	//	return limit
+	//}
+	//return t.BatchSizeLimitMap["*"]
+}
+
+// MergeCommitTemplate returns a struct with Go template string(s) or nil
+func (c *Config) MergeCommitTemplate(org, repo string) MergeCommitTemplate {
+	name := org + "/" + repo
+
+	v, ok := c.MergeTemplate[name]
+	if !ok {
+		return c.MergeTemplate[org]
+	}
+
+	return v
+}
+
+// Parse initializes and validates the Config
+func (c *Config) Parse() error {
+	if c.SyncPeriodString == "" {
+		c.SyncPeriod = time.Minute
+	} else {
+		period, err := time.ParseDuration(c.SyncPeriodString)
+		if err != nil {
+			return fmt.Errorf("cannot parse duration for tide.sync_period: %v", err)
+		}
+		c.SyncPeriod = period
+	}
+	if c.StatusUpdatePeriodString == "" {
+		c.StatusUpdatePeriod = c.SyncPeriod
+	} else {
+		period, err := time.ParseDuration(c.StatusUpdatePeriodString)
+		if err != nil {
+			return fmt.Errorf("cannot parse duration for tide.status_update_period: %v", err)
+		}
+		c.StatusUpdatePeriod = period
+	}
+	if c.MaxGoroutines == 0 {
+		c.MaxGoroutines = 20
+	}
+	if c.MaxGoroutines <= 0 {
+		return fmt.Errorf("keeper has invalid max_goroutines (%d), it needs to be a positive number", c.MaxGoroutines)
+	}
+	for name, method := range c.MergeType {
+		if !method.IsValid() {
+			return fmt.Errorf("merge type %q for %s is not a valid type", method, name)
+		}
+	}
+	for i, tq := range c.Queries {
+		if err := tq.Validate(); err != nil {
+			return fmt.Errorf("keeper query (index %d) is invalid: %v", i, err)
+		}
+	}
+	return nil
+}
diff --git a/pkg/config/keeper/contextpolicy.go b/pkg/config/keeper/contextpolicy.go
new file mode 100644
index 00000000..d8340acb
--- /dev/null
+++ b/pkg/config/keeper/contextpolicy.go
@@ -0,0 +1,116 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package keeper
+
+import (
+	"fmt"
+	"strings"
+
+	"k8s.io/apimachinery/pkg/util/sets"
+)
+
+// ContextPolicy configures options about how to handle various contexts.
+type ContextPolicy struct {
+	// whether to consider unknown contexts optional (skip) or required.
+	SkipUnknownContexts       *bool    `json:"skip-unknown-contexts,omitempty"`
+	RequiredContexts          []string `json:"required-contexts,omitempty"`
+	RequiredIfPresentContexts []string `json:"required-if-present-contexts"`
+	OptionalContexts          []string `json:"optional-contexts,omitempty"`
+	// Infer required and optional jobs from Branch Protection configuration
+	FromBranchProtection *bool `json:"from-branch-protection,omitempty"`
+}
+
+// Validate returns an error if any contexts are listed more than once in the config.
+func (cp *ContextPolicy) Validate() error {
+	if inter := sets.NewString(cp.RequiredContexts...).Intersection(sets.NewString(cp.OptionalContexts...)); inter.Len() > 0 {
+		return fmt.Errorf("contexts %s are defined as required and optional", strings.Join(inter.List(), ", "))
+	}
+	if inter := sets.NewString(cp.RequiredContexts...).Intersection(sets.NewString(cp.RequiredIfPresentContexts...)); inter.Len() > 0 {
+		return fmt.Errorf("contexts %s are defined as required and required if present", strings.Join(inter.List(), ", "))
+	}
+	if inter := sets.NewString(cp.OptionalContexts...).Intersection(sets.NewString(cp.RequiredIfPresentContexts...)); inter.Len() > 0 {
+		return fmt.Errorf("contexts %s are defined as optional and required if present", strings.Join(inter.List(), ", "))
+	}
+	return nil
+}
+
+// Merge merges one ContextPolicy with another one
+func (cp ContextPolicy) Merge(other ContextPolicy) ContextPolicy {
+	mergeBool := func(a, b *bool) *bool {
+		if b == nil {
+			return a
+		}
+		return b
+	}
+	c := ContextPolicy{}
+	c.FromBranchProtection = mergeBool(cp.FromBranchProtection, other.FromBranchProtection)
+	c.SkipUnknownContexts = mergeBool(cp.SkipUnknownContexts, other.SkipUnknownContexts)
+	required := sets.NewString(cp.RequiredContexts...)
+	requiredIfPresent := sets.NewString(cp.RequiredIfPresentContexts...)
+	optional := sets.NewString(cp.OptionalContexts...)
+	required.Insert(other.RequiredContexts...)
+	requiredIfPresent.Insert(other.RequiredIfPresentContexts...)
+	optional.Insert(other.OptionalContexts...)
+	if required.Len() > 0 {
+		c.RequiredContexts = required.List()
+	}
+	if requiredIfPresent.Len() > 0 {
+		c.RequiredIfPresentContexts = requiredIfPresent.List()
+	}
+	if optional.Len() > 0 {
+		c.OptionalContexts = optional.List()
+	}
+	return c
+}
+
+// IsOptional checks whether a context can be ignored.
+// Will return true if
+// - context is registered as optional
+// - required contexts are registered and the context provided is not required
+// Will return false otherwise. Every context is required.
+func (cp *ContextPolicy) IsOptional(c string) bool {
+	if sets.NewString(cp.OptionalContexts...).Has(c) {
+		return true
+	}
+	if sets.NewString(cp.RequiredContexts...).Has(c) {
+		return false
+	}
+	// assume if we're asking that the context is present on the PR
+	if sets.NewString(cp.RequiredIfPresentContexts...).Has(c) {
+		return false
+	}
+	if cp.SkipUnknownContexts != nil && *cp.SkipUnknownContexts {
+		return true
+	}
+	return false
+}
+
+// MissingRequiredContexts discard the optional contexts and only look of extra required contexts that are not provided.
+func (cp *ContextPolicy) MissingRequiredContexts(contexts []string) []string {
+	if len(cp.RequiredContexts) == 0 {
+		return nil
+	}
+	existingContexts := sets.NewString()
+	for _, c := range contexts {
+		existingContexts.Insert(c)
+	}
+	var missingContexts []string
+	for c := range sets.NewString(cp.RequiredContexts...).Difference(existingContexts) {
+		missingContexts = append(missingContexts, c)
+	}
+	return missingContexts
+}
diff --git a/pkg/config/keeper/contextpolicyoptions.go b/pkg/config/keeper/contextpolicyoptions.go
new file mode 100644
index 00000000..863cfc8c
--- /dev/null
+++ b/pkg/config/keeper/contextpolicyoptions.go
@@ -0,0 +1,39 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package keeper
+
+// ContextPolicyOptions holds the default policy, and any org overrides.
+type ContextPolicyOptions struct {
+	ContextPolicy
+	// Github Orgs
+	Orgs map[string]OrgContextPolicy `json:"orgs,omitempty"`
+}
+
+// Parse returns the context policy for an org/repo/branch
+func (options ContextPolicyOptions) Parse(org, repo, branch string) ContextPolicy {
+	option := options.ContextPolicy
+	if o, ok := options.Orgs[org]; ok {
+		option = option.Merge(o.ContextPolicy)
+		if r, ok := o.Repos[repo]; ok {
+			option = option.Merge(r.ContextPolicy)
+			if b, ok := r.Branches[branch]; ok {
+				option = option.Merge(b)
+			}
+		}
+	}
+	return option
+}
diff --git a/pkg/config/keeper/mergecommittemplate.go b/pkg/config/keeper/mergecommittemplate.go
new file mode 100644
index 00000000..21346229
--- /dev/null
+++ b/pkg/config/keeper/mergecommittemplate.go
@@ -0,0 +1,30 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package keeper
+
+import (
+	"text/template"
+)
+
+// MergeCommitTemplate holds templates to use for merge commits.
+type MergeCommitTemplate struct {
+	TitleTemplate string `json:"title,omitempty"`
+	BodyTemplate  string `json:"body,omitempty"`
+
+	Title *template.Template `json:"-"`
+	Body  *template.Template `json:"-"`
+}
diff --git a/pkg/config/keeper/orgcontextpolicy.go b/pkg/config/keeper/orgcontextpolicy.go
new file mode 100644
index 00000000..022bb2f8
--- /dev/null
+++ b/pkg/config/keeper/orgcontextpolicy.go
@@ -0,0 +1,23 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package keeper
+
+// OrgContextPolicy overrides the policy for an org, and any repo overrides.
+type OrgContextPolicy struct {
+	ContextPolicy
+	Repos map[string]RepoContextPolicy `json:"repos,omitempty"`
+}
diff --git a/pkg/config/keeper/pullrequestmergetype.go b/pkg/config/keeper/pullrequestmergetype.go
new file mode 100644
index 00000000..10a2ef49
--- /dev/null
+++ b/pkg/config/keeper/pullrequestmergetype.go
@@ -0,0 +1,40 @@
+/*
+ * The MIT License
+ *
+ * Copyright (c) 2020, CloudBees, Inc.
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+
+package keeper
+
+// PullRequestMergeType inidicates the type of the pull request
+type PullRequestMergeType string
+
+// Possible types of merges for the GitHub merge API
+const (
+	MergeMerge  PullRequestMergeType = "merge"
+	MergeRebase PullRequestMergeType = "rebase"
+	MergeSquash PullRequestMergeType = "squash"
+)
+
+// IsValid checks that the merge type is valid
+func (c PullRequestMergeType) IsValid() bool {
+	return c == MergeMerge || c == MergeRebase || c == MergeSquash
+}
diff --git a/pkg/config/keeper/queries.go b/pkg/config/keeper/queries.go
new file mode 100644
index 00000000..474a1e42
--- /dev/null
+++ b/pkg/config/keeper/queries.go
@@ -0,0 +1,76 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package keeper
+
+import (
+	"strings"
+
+	"k8s.io/apimachinery/pkg/util/sets"
+)
+
+// Queries is a Query slice.
+type Queries []Query
+
+func reposInOrg(org string, repos []string) []string {
+	prefix := org + "/"
+	var res []string
+	for _, repo := range repos {
+		if strings.HasPrefix(repo, prefix) {
+			res = append(res, repo)
+		}
+	}
+	return res
+}
+
+// OrgExceptionsAndRepos determines which orgs and repos a set of queries cover.
+// Output is returned as a mapping from 'included org'->'repos excluded in the org'
+// and a set of included repos.
+func (tqs Queries) OrgExceptionsAndRepos() (map[string]sets.String, sets.String) {
+	orgs := make(map[string]sets.String)
+	for i := range tqs {
+		for _, org := range tqs[i].Orgs {
+			applicableRepos := sets.NewString(reposInOrg(org, tqs[i].ExcludedRepos)...)
+			if excepts, ok := orgs[org]; !ok {
+				// We have not seen this org so the exceptions are just applicable
+				// members of 'excludedRepos'.
+				orgs[org] = applicableRepos
+			} else {
+				// We have seen this org so the exceptions are the applicable
+				// members of 'excludedRepos' intersected with existing exceptions.
+				orgs[org] = excepts.Intersection(applicableRepos)
+			}
+		}
+	}
+	repos := sets.NewString()
+	for i := range tqs {
+		repos.Insert(tqs[i].Repos...)
+	}
+	// Remove any org exceptions that are explicitly included in a different query.
+	reposList := repos.UnsortedList()
+	for _, excepts := range orgs {
+		excepts.Delete(reposList...)
+	}
+	return orgs, repos
+}
+
+// QueryMap creates a QueryMap from KeeperQueries
+func (tqs Queries) QueryMap() *QueryMap {
+	return &QueryMap{
+		queries: tqs,
+		cache:   make(map[string]Queries),
+	}
+}
diff --git a/pkg/config/keeper/query.go b/pkg/config/keeper/query.go
new file mode 100644
index 00000000..104f8eb5
--- /dev/null
+++ b/pkg/config/keeper/query.go
@@ -0,0 +1,188 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package keeper
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	"k8s.io/apimachinery/pkg/util/sets"
+)
+
+// Query is turned into a GitHub search query. See the docs for details:
+// https://help.github.com/articles/searching-issues-and-pull-requests/
+type Query struct {
+	Orgs                   []string `json:"orgs,omitempty"`
+	Repos                  []string `json:"repos,omitempty"`
+	ExcludedRepos          []string `json:"excludedRepos,omitempty"`
+	ExcludedBranches       []string `json:"excludedBranches,omitempty"`
+	IncludedBranches       []string `json:"includedBranches,omitempty"`
+	Labels                 []string `json:"labels,omitempty"`
+	MissingLabels          []string `json:"missingLabels,omitempty"`
+	Milestone              string   `json:"milestone,omitempty"`
+	ReviewApprovedRequired bool     `json:"reviewApprovedRequired,omitempty"`
+}
+
+// Query returns the corresponding github search string for the keeper query.
+func (tq *Query) Query() string {
+	toks := []string{"is:pr", "state:open"}
+	for _, o := range tq.Orgs {
+		toks = append(toks, fmt.Sprintf("org:\"%s\"", o))
+	}
+	for _, r := range tq.Repos {
+		toks = append(toks, fmt.Sprintf("repo:\"%s\"", r))
+	}
+	for _, r := range tq.ExcludedRepos {
+		toks = append(toks, fmt.Sprintf("-repo:\"%s\"", r))
+	}
+	for _, b := range tq.ExcludedBranches {
+		toks = append(toks, fmt.Sprintf("-base:\"%s\"", b))
+	}
+	for _, b := range tq.IncludedBranches {
+		toks = append(toks, fmt.Sprintf("base:\"%s\"", b))
+	}
+	for _, l := range tq.Labels {
+		toks = append(toks, fmt.Sprintf("label:\"%s\"", l))
+	}
+	for _, l := range tq.MissingLabels {
+		toks = append(toks, fmt.Sprintf("-label:\"%s\"", l))
+	}
+	if tq.Milestone != "" {
+		toks = append(toks, fmt.Sprintf("milestone:\"%s\"", tq.Milestone))
+	}
+	if tq.ReviewApprovedRequired {
+		toks = append(toks, "review:approved")
+	}
+	return strings.Join(toks, " ")
+}
+
+// ForRepo indicates if the keeper query applies to the specified repo.
+func (tq Query) ForRepo(org, repo string) bool {
+	fullName := fmt.Sprintf("%s/%s", org, repo)
+	for _, queryOrg := range tq.Orgs {
+		if queryOrg != org {
+			continue
+		}
+		// Check for repos excluded from the org.
+		for _, excludedRepo := range tq.ExcludedRepos {
+			if excludedRepo == fullName {
+				return false
+			}
+		}
+		return true
+	}
+	for _, queryRepo := range tq.Repos {
+		if queryRepo == fullName {
+			return true
+		}
+	}
+	return false
+}
+
+// Validate returns an error if the query has any errors.
+//
+// Examples include:
+// * an org name that is empty or includes a /
+// * repos that are not org/repo
+// * a label that is in both the labels and missing_labels section
+// * a branch that is in both included and excluded branch set.
+func (tq *Query) Validate() error {
+	duplicates := func(field string, list []string) error {
+		dups := sets.NewString()
+		seen := sets.NewString()
+		for _, elem := range list {
+			if seen.Has(elem) {
+				dups.Insert(elem)
+			} else {
+				seen.Insert(elem)
+			}
+		}
+		dupCount := len(list) - seen.Len()
+		if dupCount == 0 {
+			return nil
+		}
+		return fmt.Errorf("%q contains %d duplicate entries: %s", field, dupCount, strings.Join(dups.List(), ", "))
+	}
+
+	orgs := sets.NewString()
+	for o := range tq.Orgs {
+		if strings.Contains(tq.Orgs[o], "/") {
+			return fmt.Errorf("orgs[%d]: %q contains a '/' which is not valid", o, tq.Orgs[o])
+		}
+		if len(tq.Orgs[o]) == 0 {
+			return fmt.Errorf("orgs[%d]: is an empty string", o)
+		}
+		orgs.Insert(tq.Orgs[o])
+	}
+	if err := duplicates("orgs", tq.Orgs); err != nil {
+		return err
+	}
+
+	for r := range tq.Repos {
+		parts := strings.Split(tq.Repos[r], "/")
+		if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 {
+			return fmt.Errorf("repos[%d]: %q is not of the form \"org/repo\"", r, tq.Repos[r])
+		}
+		if orgs.Has(parts[0]) {
+			return fmt.Errorf("repos[%d]: %q is already included via org: %q", r, tq.Repos[r], parts[0])
+		}
+	}
+	if err := duplicates("repos", tq.Repos); err != nil {
+		return err
+	}
+
+	if len(tq.Orgs) == 0 && len(tq.Repos) == 0 {
+		return errors.New("'orgs' and 'repos' cannot both be empty")
+	}
+
+	for er := range tq.ExcludedRepos {
+		parts := strings.Split(tq.ExcludedRepos[er], "/")
+		if len(parts) != 2 || len(parts[0]) == 0 || len(parts[1]) == 0 {
+			return fmt.Errorf("excludedRepos[%d]: %q is not of the form \"org/repo\"", er, tq.ExcludedRepos[er])
+		}
+		if !orgs.Has(parts[0]) {
+			return fmt.Errorf("excludedRepos[%d]: %q has no effect because org %q is not included", er, tq.ExcludedRepos[er], parts[0])
+		}
+		// Note: At this point we also know that this excludedRepo is not found in 'repos'.
+	}
+	if err := duplicates("excludedRepos", tq.ExcludedRepos); err != nil {
+		return err
+	}
+
+	if invalids := sets.NewString(tq.Labels...).Intersection(sets.NewString(tq.MissingLabels...)); len(invalids) > 0 {
+		return fmt.Errorf("the labels: %q are both required and forbidden", invalids.List())
+	}
+	if err := duplicates("labels", tq.Labels); err != nil {
+		return err
+	}
+	if err := duplicates("missingLabels", tq.MissingLabels); err != nil {
+		return err
+	}
+
+	if len(tq.ExcludedBranches) > 0 && len(tq.IncludedBranches) > 0 {
+		return errors.New("both 'includedBranches' and 'excludedBranches' are specified ('excludedBranches' have no effect)")
+	}
+	if err := duplicates("includedBranches", tq.IncludedBranches); err != nil {
+		return err
+	}
+	if err := duplicates("excludedBranches", tq.ExcludedBranches); err != nil {
+		return err
+	}
+
+	return nil
+}
diff --git a/pkg/config/keeper/querymap.go b/pkg/config/keeper/querymap.go
new file mode 100644
index 00000000..a634abd8
--- /dev/null
+++ b/pkg/config/keeper/querymap.go
@@ -0,0 +1,53 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package keeper
+
+import (
+	"fmt"
+	"sync"
+)
+
+// QueryMap is a struct mapping from "org/repo" -> KeeperQueries that
+// apply to that org or repo. It is lazily populated, but threadsafe.
+type QueryMap struct {
+	queries Queries
+
+	cache map[string]Queries
+	sync.Mutex
+}
+
+// ForRepo returns the keeper queries that apply to a repo.
+func (qm *QueryMap) ForRepo(org, repo string) Queries {
+	res := Queries(nil)
+	fullName := fmt.Sprintf("%s/%s", org, repo)
+
+	qm.Lock()
+	defer qm.Unlock()
+
+	if qs, ok := qm.cache[fullName]; ok {
+		return append(res, qs...) // Return a copy.
+	}
+	// Cache miss. Need to determine relevant queries.
+
+	for _, query := range qm.queries {
+		if query.ForRepo(org, repo) {
+			res = append(res, query)
+		}
+	}
+	qm.cache[fullName] = res
+	return res
+}
diff --git a/pkg/config/keeper/repocontextpolicy.go b/pkg/config/keeper/repocontextpolicy.go
new file mode 100644
index 00000000..ee62f7ee
--- /dev/null
+++ b/pkg/config/keeper/repocontextpolicy.go
@@ -0,0 +1,23 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package keeper
+
+// RepoContextPolicy overrides the policy for repo, and any branch overrides.
+type RepoContextPolicy struct {
+	ContextPolicy
+	Branches map[string]ContextPolicy `json:"branches,omitempty"`
+}
diff --git a/pkg/config/lighthouse/config.go b/pkg/config/lighthouse/config.go
new file mode 100644
index 00000000..e3dc131e
--- /dev/null
+++ b/pkg/config/lighthouse/config.go
@@ -0,0 +1,112 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package lighthouse
+
+import (
+	"errors"
+	"os"
+
+	"github.com/jenkins-x/lighthouse/pkg/config/branchprotection"
+	"github.com/jenkins-x/lighthouse/pkg/config/keeper"
+	"github.com/jenkins-x/lighthouse/pkg/config/org"
+)
+
+// Config is config for all lighthouse controllers
+type Config struct {
+	Keeper           keeper.Config           `json:"tide,omitempty"`
+	Plank            Plank                   `json:"plank,omitempty"`
+	BranchProtection branchprotection.Config `json:"branch-protection,omitempty"`
+	Orgs             map[string]org.Config   `json:"orgs,omitempty"`
+	// TODO: Move this out of the main config.
+	JenkinsOperators []JenkinsOperator `json:"jenkins_operators,omitempty"`
+	// LighthouseJobNamespace is the namespace in the cluster that prow
+	// components will use for looking up LighthouseJobs. The namespace
+	// needs to exist and will not be created by prow.
+	// Defaults to "default".
+	LighthouseJobNamespace string `json:"prowjob_namespace,omitempty"`
+	// PodNamespace is the namespace in the cluster that prow
+	// components will use for looking up Pods owned by LighthouseJobs.
+	// The namespace needs to exist and will not be created by prow.
+	// Defaults to "default".
+	PodNamespace string `json:"pod_namespace,omitempty"`
+	// LogLevel enables dynamically updating the log level of the
+	// standard logger that is used by all prow components.
+	//
+	// Valid values:
+	//
+	// "debug", "info", "warn", "warning", "error", "fatal", "panic"
+	//
+	// Defaults to "info".
+	LogLevel string `json:"log_level,omitempty"`
+	// PushGateway is a prometheus push gateway.
+	PushGateway PushGateway `json:"push_gateway,omitempty"`
+	// OwnersDirExcludes is used to configure which directories to ignore when
+	// searching for OWNERS{,_ALIAS} files in a repo.
+	OwnersDirExcludes *OwnersDirExcludes `json:"owners_dir_excludes,omitempty"`
+	// OwnersDirBlacklist is DEPRECATED in favor of OwnersDirExcludes
+	OwnersDirBlacklist *OwnersDirExcludes `json:"owners_dir_blacklist,omitempty"`
+	// Pub/Sub Subscriptions that we want to listen to
+	PubSubSubscriptions PubsubSubscriptions `json:"pubsub_subscriptions,omitempty"`
+	// GitHubOptions allows users to control how prow applications display GitHub website links.
+	GitHubOptions GitHubOptions `json:"github,omitempty"`
+	// ProviderConfig contains optional SCM provider information
+	ProviderConfig *ProviderConfig `json:"providerConfig,omitempty"`
+}
+
+// Parse initializes and validates the Config
+func (c *Config) Parse() error {
+	if err := c.Plank.Parse(); err != nil {
+		return err
+	}
+	for i := range c.JenkinsOperators {
+		if err := c.JenkinsOperators[i].Parse(); err != nil {
+			return err
+		}
+		// TODO: Invalidate overlapping selectors more
+		if len(c.JenkinsOperators) > 1 && c.JenkinsOperators[i].LabelSelectorString == "" {
+			return errors.New("selector overlap: cannot use an empty label_selector with multiple selectors")
+		}
+		if len(c.JenkinsOperators) == 1 && c.JenkinsOperators[0].LabelSelectorString != "" {
+			return errors.New("label_selector is invalid when used for a single jenkins-operator")
+		}
+	}
+	if err := c.PushGateway.Parse(); err != nil {
+		return err
+	}
+	if err := c.Keeper.Parse(); err != nil {
+		return err
+	}
+	if c.LighthouseJobNamespace == "" {
+		c.LighthouseJobNamespace = "default"
+	}
+	if c.PodNamespace == "" {
+		c.PodNamespace = "default"
+	}
+	if err := c.GitHubOptions.Parse(); err != nil {
+		return err
+	}
+	if c.LogLevel == "" {
+		c.LogLevel = os.Getenv("LOG_LEVEL")
+		if c.LogLevel == "" {
+			c.LogLevel = "info"
+		}
+	}
+	if c.OwnersDirExcludes == nil {
+		c.OwnersDirExcludes = c.OwnersDirBlacklist
+	}
+	return nil
+}
diff --git a/pkg/config/lighthouse/controller.go b/pkg/config/lighthouse/controller.go
new file mode 100644
index 00000000..1f4a596f
--- /dev/null
+++ b/pkg/config/lighthouse/controller.go
@@ -0,0 +1,77 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package lighthouse
+
+import (
+	"fmt"
+	"text/template"
+)
+
+// Controller holds configuration applicable to all agent-specific
+// prow controllers.
+type Controller struct {
+	// JobURLTemplateString compiles into JobURLTemplate at load time.
+	JobURLTemplateString string `json:"job_url_template,omitempty"`
+	// JobURLTemplate is compiled at load time from JobURLTemplateString. It
+	// will be passed a builder.PipelineOptions and is used to set the URL for the
+	// "Details" link on GitHub as well as the link from deck.
+	JobURLTemplate *template.Template `json:"-"`
+
+	// ReportTemplateString compiles into ReportTemplate at load time.
+	ReportTemplateString string `json:"report_template,omitempty"`
+	// ReportTemplate is compiled at load time from ReportTemplateString. It
+	// will be passed a builder.PipelineOptions and can provide an optional blurb below
+	// the test failures comment.
+	ReportTemplate *template.Template `json:"-"`
+
+	// MaxConcurrency is the maximum number of tests running concurrently that
+	// will be allowed by the controller. 0 implies no limit.
+	MaxConcurrency int `json:"max_concurrency,omitempty"`
+
+	// MaxGoroutines is the maximum number of goroutines spawned inside the
+	// controller to handle tests. Defaults to 20. Needs to be a positive
+	// number.
+	MaxGoroutines int `json:"max_goroutines,omitempty"`
+
+	// AllowCancellations enables aborting presubmit jobs for commits that
+	// have been superseded by newer commits in Github pull requests.
+	AllowCancellations bool `json:"allow_cancellations,omitempty"`
+}
+
+// Parse initializes and validates the Config
+func (c *Controller) Parse() error {
+	urlTmpl, err := template.New("JobURL").Parse(c.JobURLTemplateString)
+	if err != nil {
+		return fmt.Errorf("parsing template: %v", err)
+	}
+	c.JobURLTemplate = urlTmpl
+	reportTmpl, err := template.New("Report").Parse(c.ReportTemplateString)
+	if err != nil {
+		return fmt.Errorf("parsing template: %v", err)
+	}
+	c.ReportTemplate = reportTmpl
+	if c.MaxConcurrency < 0 {
+		return fmt.Errorf("controller has invalid max_concurrency (%d), it needs to be a non-negative number", c.MaxConcurrency)
+	}
+	if c.MaxGoroutines == 0 {
+		c.MaxGoroutines = 20
+	}
+	if c.MaxGoroutines <= 0 {
+		return fmt.Errorf("controller has invalid max_goroutines (%d), it needs to be a positive number", c.MaxGoroutines)
+	}
+	return nil
+}
diff --git a/pkg/config/lighthouse/githuboptions.go b/pkg/config/lighthouse/githuboptions.go
new file mode 100644
index 00000000..41e301e5
--- /dev/null
+++ b/pkg/config/lighthouse/githuboptions.go
@@ -0,0 +1,47 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package lighthouse
+
+import (
+	"fmt"
+	"net/url"
+)
+
+// GitHubOptions allows users to control how prow applications display GitHub website links.
+type GitHubOptions struct {
+	// LinkURLFromConfig is the string representation of the link_url config parameter.
+	// This config parameter allows users to override the default GitHub link url for all plugins.
+	// If this option is not set, we assume "https://github.com".
+	LinkURLFromConfig string `json:"link_url,omitempty"`
+
+	// LinkURL is the url representation of LinkURLFromConfig. This variable should be used
+	// in all places internally.
+	LinkURL *url.URL
+}
+
+// Parse initializes and validates the Config
+func (c *GitHubOptions) Parse() error {
+	if c.LinkURLFromConfig == "" {
+		c.LinkURLFromConfig = "https://github.com"
+	}
+	linkURL, err := url.Parse(c.LinkURLFromConfig)
+	if err != nil {
+		return fmt.Errorf("unable to parse github.link_url, might not be a valid url: %v", err)
+	}
+	c.LinkURL = linkURL
+	return nil
+}
diff --git a/pkg/config/lighthouse/jenkinsoperator.go b/pkg/config/lighthouse/jenkinsoperator.go
new file mode 100644
index 00000000..5223a5e4
--- /dev/null
+++ b/pkg/config/lighthouse/jenkinsoperator.go
@@ -0,0 +1,52 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package lighthouse
+
+import (
+	"fmt"
+
+	"k8s.io/apimachinery/pkg/labels"
+)
+
+// JenkinsOperator is config for the jenkins-operator controller.
+type JenkinsOperator struct {
+	Controller `json:",inline"`
+	// LabelSelectorString compiles into LabelSelector at load time.
+	// If set, this option needs to match --label-selector used by
+	// the desired jenkins-operator. This option is considered
+	// invalid when provided with a single jenkins-operator config.
+	//
+	// For label selector syntax, see below:
+	// https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/#label-selectors
+	LabelSelectorString string `json:"label_selector,omitempty"`
+	// LabelSelector is used so different jenkins-operator replicas
+	// can use their own configuration.
+	LabelSelector labels.Selector `json:"-"`
+}
+
+// Parse initializes and validates the Config
+func (c *JenkinsOperator) Parse() error {
+	if err := c.Controller.Parse(); err != nil {
+		return fmt.Errorf("validating jenkins_operators config: %v", err)
+	}
+	sel, err := labels.Parse(c.LabelSelectorString)
+	if err != nil {
+		return fmt.Errorf("invalid jenkins_operators.label_selector option: %v", err)
+	}
+	c.LabelSelector = sel
+	return nil
+}
diff --git a/pkg/config/lighthouse/ownersdirexclude.go b/pkg/config/lighthouse/ownersdirexclude.go
new file mode 100644
index 00000000..46a76f50
--- /dev/null
+++ b/pkg/config/lighthouse/ownersdirexclude.go
@@ -0,0 +1,27 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package lighthouse
+
+// OwnersDirExcludes is used to configure which directories to ignore when
+// searching for OWNERS{,_ALIAS} files in a repo.
+type OwnersDirExcludes struct {
+	// Repos configures a directory blacklist per repo (or org)
+	Repos map[string][]string `json:"repos"`
+	// Default configures a default blacklist for repos (or orgs) not
+	// specifically configured
+	Default []string `json:"default"`
+}
diff --git a/pkg/config/lighthouse/plank.go b/pkg/config/lighthouse/plank.go
new file mode 100644
index 00000000..3f9d5fb5
--- /dev/null
+++ b/pkg/config/lighthouse/plank.go
@@ -0,0 +1,42 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package lighthouse
+
+import (
+	"fmt"
+	"text/template"
+)
+
+// Plank is config for the plank controller.
+type Plank struct {
+	// ReportTemplateString compiles into ReportTemplate at load time.
+	ReportTemplateString string `json:"report_template,omitempty"`
+	// ReportTemplate is compiled at load time from ReportTemplateString. It
+	// will be passed a builder.PipelineOptions and can provide an optional blurb below
+	// the test failures comment.
+	ReportTemplate *template.Template `json:"-"`
+}
+
+// Parse initializes and validates the Config
+func (c *Plank) Parse() error {
+	reportTmpl, err := template.New("Report").Parse(c.ReportTemplateString)
+	if err != nil {
+		return fmt.Errorf("parsing template: %v", err)
+	}
+	c.ReportTemplate = reportTmpl
+	return nil
+}
diff --git a/pkg/config/lighthouse/providerconfig.go b/pkg/config/lighthouse/providerconfig.go
new file mode 100644
index 00000000..841811dd
--- /dev/null
+++ b/pkg/config/lighthouse/providerconfig.go
@@ -0,0 +1,28 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package lighthouse
+
+// ProviderConfig is optionally used to configure information about the SCM provider being used. These values will be
+// used as fallbacks if environment variables aren't set.
+type ProviderConfig struct {
+	// Kind is the go-scm driver name
+	Kind string `json:"kind,omitempty"`
+	// Server is the base URL for the provider, like https://github.com
+	Server string `json:"server,omitempty"`
+	// BotUser is the username on the provider the bot will use
+	BotUser string `json:"botUser,omitempty"`
+}
diff --git a/pkg/config/lighthouse/pubsubsubscriptions.go b/pkg/config/lighthouse/pubsubsubscriptions.go
new file mode 100644
index 00000000..50a92c2c
--- /dev/null
+++ b/pkg/config/lighthouse/pubsubsubscriptions.go
@@ -0,0 +1,20 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package lighthouse
+
+// PubsubSubscriptions maps GCP projects to a list of Topics.
+type PubsubSubscriptions map[string][]string
diff --git a/pkg/config/lighthouse/pushgateway.go b/pkg/config/lighthouse/pushgateway.go
new file mode 100644
index 00000000..f037c496
--- /dev/null
+++ b/pkg/config/lighthouse/pushgateway.go
@@ -0,0 +1,50 @@
+/*
+Copyright 2017 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package lighthouse
+
+import (
+	"fmt"
+	"time"
+)
+
+// PushGateway is a prometheus push gateway.
+type PushGateway struct {
+	// Endpoint is the location of the prometheus pushgateway
+	// where prow will push metrics to.
+	Endpoint string `json:"endpoint,omitempty"`
+	// IntervalString compiles into Interval at load time.
+	IntervalString string `json:"interval,omitempty"`
+	// Interval specifies how often prow will push metrics
+	// to the pushgateway. Defaults to 1m.
+	Interval time.Duration `json:"-"`
+	// ServeMetrics tells if or not the components serve metrics
+	ServeMetrics bool `json:"serve_metrics"`
+}
+
+// Parse initializes and validates the Config
+func (c *PushGateway) Parse() error {
+	if c.IntervalString == "" {
+		c.Interval = time.Minute
+	} else {
+		interval, err := time.ParseDuration(c.IntervalString)
+		if err != nil {
+			return fmt.Errorf("cannot parse duration for push_gateway.interval: %v", err)
+		}
+		c.Interval = interval
+	}
+	return nil
+}
diff --git a/pkg/engines/tekton/activity.go b/pkg/engines/tekton/activity.go
index 8aa78f91..c236a2f4 100644
--- a/pkg/engines/tekton/activity.go
+++ b/pkg/engines/tekton/activity.go
@@ -4,7 +4,7 @@ import (
 	"strings"
 
 	"github.com/jenkins-x/lighthouse/pkg/apis/lighthouse/v1alpha1"
-	"github.com/jenkins-x/lighthouse/pkg/config"
+	"github.com/jenkins-x/lighthouse/pkg/config/job"
 	"github.com/jenkins-x/lighthouse/pkg/util"
 	"github.com/tektoncd/pipeline/pkg/apis/pipeline/v1beta1"
 	corev1 "k8s.io/api/core/v1"
@@ -23,7 +23,7 @@ func ConvertPipelineRun(pr *v1beta1.PipelineRun) *v1alpha1.ActivityRecord {
 
 	record.Name = pr.Name
 
-	record.JobID = pr.Labels[config.LighthouseJobIDLabel]
+	record.JobID = pr.Labels[job.LighthouseJobIDLabel]
 	record.BaseSHA = pr.Labels[util.BaseSHALabel]
 	record.Repo = pr.Labels[util.RepoLabel]
 	record.Context = pr.Labels[util.ContextLabel]
diff --git a/pkg/foghorn/controller.go b/pkg/foghorn/controller.go
index 1f31050d..9947caea 100644
--- a/pkg/foghorn/controller.go
+++ b/pkg/foghorn/controller.go
@@ -11,6 +11,7 @@ import (
 	"github.com/jenkins-x/go-scm/scm"
 	lighthousev1alpha1 "github.com/jenkins-x/lighthouse/pkg/apis/lighthouse/v1alpha1"
 	"github.com/jenkins-x/lighthouse/pkg/config"
+	"github.com/jenkins-x/lighthouse/pkg/config/job"
 	"github.com/jenkins-x/lighthouse/pkg/plugins"
 	"github.com/jenkins-x/lighthouse/pkg/scmprovider/reporter"
 	"github.com/jenkins-x/lighthouse/pkg/util"
@@ -138,7 +139,7 @@ func (r *LighthouseJobReconciler) updateJobStatusForActivity(activity *lighthous
 	}
 }
 
-func (r *LighthouseJobReconciler) reportStatus(activity *lighthousev1alpha1.ActivityRecord, job *lighthousev1alpha1.LighthouseJob) {
+func (r *LighthouseJobReconciler) reportStatus(activity *lighthousev1alpha1.ActivityRecord, j *lighthousev1alpha1.LighthouseJob) {
 	sha := activity.LastCommitSHA
 
 	owner := activity.Owner
@@ -181,18 +182,18 @@ func (r *LighthouseJobReconciler) reportStatus(activity *lighthousev1alpha1.Acti
 		return
 	}
 
-	switch scm.ToState(job.Status.LastReportState) {
+	switch scm.ToState(j.Status.LastReportState) {
 	// already completed - avoid reporting again if a promotion happens after a PR has merged and the pipeline updates status
 	case scm.StateFailure, scm.StateError, scm.StateSuccess, scm.StateCanceled:
 		return
 	}
 
-	r.logger.WithFields(fields).Warnf("last report: %s, current: %s, last desc: %s, current: %s", job.Status.LastReportState, statusInfo.scmStatus.String(),
-		job.Status.Description, statusInfo.description)
+	r.logger.WithFields(fields).Warnf("last report: %s, current: %s, last desc: %s, current: %s", j.Status.LastReportState, statusInfo.scmStatus.String(),
+		j.Status.Description, statusInfo.description)
 
 	// Check if state and running stages haven't changed and return if they haven't
-	if scm.ToState(job.Status.LastReportState) == statusInfo.scmStatus &&
-		job.Status.Description == statusInfo.description {
+	if scm.ToState(j.Status.LastReportState) == statusInfo.scmStatus &&
+		j.Status.Description == statusInfo.description {
 		return
 	}
 
@@ -210,7 +211,7 @@ func (r *LighthouseJobReconciler) reportStatus(activity *lighthousev1alpha1.Acti
 		State:  statusInfo.scmStatus,
 		Label:  pipelineContext,
 		Desc:   statusInfo.description,
-		Target: job.Status.ReportURL,
+		Target: j.Status.ReportURL,
 	}
 	scmClient, _, _, _, err := util.GetSCMClient(owner, r.jobConfig.Config)
 	if err != nil {
@@ -225,14 +226,14 @@ func (r *LighthouseJobReconciler) reportStatus(activity *lighthousev1alpha1.Acti
 		return
 	}
 
-	err = reporter.Report(scmClient, r.jobConfig.Config().Plank.ReportTemplate, job, []config.PipelineKind{config.PresubmitJob})
+	err = reporter.Report(scmClient, r.jobConfig.Config().Plank.ReportTemplate, j, []job.PipelineKind{job.PresubmitJob})
 	if err != nil {
 		// For now, we're just going to ignore failures here.
 		r.logger.WithFields(fields).WithError(err).Warnf("failed to update comments on the PR")
 	}
 	r.logger.WithFields(fields).Info("reported git status")
-	job.Status.Description = statusInfo.description
-	job.Status.LastReportState = statusInfo.scmStatus.String()
+	j.Status.Description = statusInfo.description
+	j.Status.LastReportState = statusInfo.scmStatus.String()
 }
 
 type reportStatusInfo struct {
diff --git a/pkg/jobutil/jobutil.go b/pkg/jobutil/jobutil.go
index 0be551fd..d6b70c48 100644
--- a/pkg/jobutil/jobutil.go
+++ b/pkg/jobutil/jobutil.go
@@ -25,7 +25,6 @@ import (
 
 	"github.com/jenkins-x/go-scm/scm"
 	"github.com/jenkins-x/lighthouse/pkg/apis/lighthouse/v1alpha1"
-	"github.com/jenkins-x/lighthouse/pkg/config"
 	"github.com/jenkins-x/lighthouse/pkg/config/job"
 	"github.com/jenkins-x/lighthouse/pkg/scmprovider"
 	"github.com/jenkins-x/lighthouse/pkg/util"
@@ -103,7 +102,7 @@ func NewPresubmit(pr *scm.PullRequest, baseSHA string, job job.Presubmit, eventG
 // PresubmitSpec initializes a PipelineOptionsSpec for a given presubmit job.
 func PresubmitSpec(p job.Presubmit, refs v1alpha1.Refs) v1alpha1.LighthouseJobSpec {
 	pjs := specFromJobBase(p.Base)
-	pjs.Type = config.PresubmitJob
+	pjs.Type = job.PresubmitJob
 	pjs.Context = p.Context
 	pjs.RerunCommand = p.RerunCommand
 	pjs.Refs = completePrimaryRefs(refs, p.Base)
@@ -114,7 +113,7 @@ func PresubmitSpec(p job.Presubmit, refs v1alpha1.Refs) v1alpha1.LighthouseJobSp
 // PostsubmitSpec initializes a PipelineOptionsSpec for a given postsubmit job.
 func PostsubmitSpec(p job.Postsubmit, refs v1alpha1.Refs) v1alpha1.LighthouseJobSpec {
 	pjs := specFromJobBase(p.Base)
-	pjs.Type = config.PostsubmitJob
+	pjs.Type = job.PostsubmitJob
 	pjs.Context = p.Context
 	pjs.Refs = completePrimaryRefs(refs, p.Base)
 
@@ -124,7 +123,7 @@ func PostsubmitSpec(p job.Postsubmit, refs v1alpha1.Refs) v1alpha1.LighthouseJob
 // PeriodicSpec initializes a PipelineOptionsSpec for a given periodic job.
 func PeriodicSpec(p job.Periodic) v1alpha1.LighthouseJobSpec {
 	pjs := specFromJobBase(p.Base)
-	pjs.Type = config.PeriodicJob
+	pjs.Type = job.PeriodicJob
 
 	return pjs
 }
@@ -132,7 +131,7 @@ func PeriodicSpec(p job.Periodic) v1alpha1.LighthouseJobSpec {
 // BatchSpec initializes a PipelineOptionsSpec for a given batch job and ref spec.
 func BatchSpec(p job.Presubmit, refs v1alpha1.Refs) v1alpha1.LighthouseJobSpec {
 	pjs := specFromJobBase(p.Base)
-	pjs.Type = config.BatchJob
+	pjs.Type = job.BatchJob
 	pjs.Context = p.Context
 	pjs.Refs = completePrimaryRefs(refs, p.Base)
 
@@ -211,14 +210,14 @@ func LabelsAndAnnotationsForSpec(spec v1alpha1.LighthouseJobSpec, extraLabels, e
 		}).Info("Cannot use full context name, will truncate.")
 	}
 	labels := map[string]string{
-		config.CreatedByLighthouse:    "true",
-		config.LighthouseJobTypeLabel: string(spec.Type),
-		util.LighthouseJobAnnotation:  jobNameForLabel,
+		job.CreatedByLighthouseLabel: "true",
+		job.LighthouseJobTypeLabel:   string(spec.Type),
+		util.LighthouseJobAnnotation: jobNameForLabel,
 	}
 	if contextNameForLabel != "" {
 		labels[util.ContextLabel] = contextNameForLabel
 	}
-	if spec.Type != config.PeriodicJob && spec.Refs != nil {
+	if spec.Type != job.PeriodicJob && spec.Refs != nil {
 		labels[util.OrgLabel] = strings.ToLower(spec.Refs.Org)
 		labels[util.RepoLabel] = spec.Refs.Repo
 		labels[util.BranchLabel] = spec.GetBranch()
@@ -272,7 +271,7 @@ func LabelsAndAnnotationsForJob(lj v1alpha1.LighthouseJob, buildID string) (map[
 	if extraLabels = lj.ObjectMeta.Labels; extraLabels == nil {
 		extraLabels = map[string]string{}
 	}
-	extraLabels[config.LighthouseJobIDLabel] = lj.ObjectMeta.Name
+	extraLabels[job.LighthouseJobIDLabel] = lj.ObjectMeta.Name
 	if buildID != "" {
 		extraLabels[util.BuildNumLabel] = buildID
 	}
diff --git a/pkg/keeper/githubapp/owner_query.go b/pkg/keeper/githubapp/owner_query.go
index c685bdfb..fea0d513 100644
--- a/pkg/keeper/githubapp/owner_query.go
+++ b/pkg/keeper/githubapp/owner_query.go
@@ -3,19 +3,19 @@ package githubapp
 import (
 	"strings"
 
-	"github.com/jenkins-x/lighthouse/pkg/config"
+	"github.com/jenkins-x/lighthouse/pkg/config/keeper"
 	"github.com/sirupsen/logrus"
 )
 
 // OwnerQueries separates keeper queries by the owner
 type OwnerQueries struct {
 	Owner   string
-	Queries config.KeeperQueries
+	Queries keeper.Queries
 }
 
 // SplitKeeperQueries splits the keeper queries into a sequence of owner queries
-func SplitKeeperQueries(queries config.KeeperQueries) map[string]config.KeeperQueries {
-	answer := map[string]config.KeeperQueries{}
+func SplitKeeperQueries(queries keeper.Queries) map[string]keeper.Queries {
+	answer := map[string]keeper.Queries{}
 	for _, q1 := range queries {
 		for org, repos := range SplitRepositories(q1.Repos) {
 			q := q1
diff --git a/pkg/keeper/keeper.go b/pkg/keeper/keeper.go
index f79b5e1a..2520c7a5 100644
--- a/pkg/keeper/keeper.go
+++ b/pkg/keeper/keeper.go
@@ -35,6 +35,7 @@ import (
 	clientset "github.com/jenkins-x/lighthouse/pkg/client/clientset/versioned"
 	"github.com/jenkins-x/lighthouse/pkg/config"
 	"github.com/jenkins-x/lighthouse/pkg/config/job"
+	"github.com/jenkins-x/lighthouse/pkg/config/keeper"
 	"github.com/jenkins-x/lighthouse/pkg/errorutil"
 	"github.com/jenkins-x/lighthouse/pkg/git"
 	"github.com/jenkins-x/lighthouse/pkg/jobutil"
@@ -691,7 +692,7 @@ func accumulateBatch(presubmits map[int][]job.Presubmit, prs []PullRequest, pjs
 	}
 	states := make(map[string]*accState)
 	for _, pj := range pjs {
-		if pj.Spec.Type != config.BatchJob {
+		if pj.Spec.Type != job.BatchJob {
 			continue
 		}
 		// First validate the batch job's refs.
@@ -773,7 +774,7 @@ func accumulate(presubmits map[int][]job.Presubmit, prs []PullRequest, pjs []v1a
 		// We can ignore the baseSHA here because the subPool only contains PipelineActivitys with the correct baseSHA
 		psStates := make(map[string]simpleState)
 		for _, pj := range pjs {
-			if pj.Spec.Type != config.PresubmitJob {
+			if pj.Spec.Type != job.PresubmitJob {
 				continue
 			}
 			if len(pj.Spec.Refs.Pulls) == 0 || pj.Spec.Refs.Pulls[0].Number != int(pr.Number) {
@@ -889,18 +890,18 @@ func (c *DefaultController) pickBatch(sp subpool, cc contextChecker) ([]PullRequ
 	return res, nil
 }
 
-func checkMergeLabels(pr PullRequest, squash, rebase, merge string, method config.PullRequestMergeType) (config.PullRequestMergeType, error) {
+func checkMergeLabels(pr PullRequest, squash, rebase, merge string, method keeper.PullRequestMergeType) (keeper.PullRequestMergeType, error) {
 	labelCount := 0
 	for _, prlabel := range pr.Labels.Nodes {
 		switch string(prlabel.Name) {
 		case squash:
-			method = config.MergeSquash
+			method = keeper.MergeSquash
 			labelCount++
 		case rebase:
-			method = config.MergeRebase
+			method = keeper.MergeRebase
 			labelCount++
 		case merge:
-			method = config.MergeMerge
+			method = keeper.MergeMerge
 			labelCount++
 		}
 		if labelCount > 1 {
@@ -910,7 +911,7 @@ func checkMergeLabels(pr PullRequest, squash, rebase, merge string, method confi
 	return method, nil
 }
 
-func (c *DefaultController) prepareMergeDetails(commitTemplates config.KeeperMergeCommitTemplate, pr PullRequest, mergeMethod config.PullRequestMergeType) scmprovider.MergeDetails {
+func (c *DefaultController) prepareMergeDetails(commitTemplates keeper.MergeCommitTemplate, pr PullRequest, mergeMethod keeper.PullRequestMergeType) scmprovider.MergeDetails {
 	ghMergeDetails := scmprovider.MergeDetails{
 		SHA:         string(pr.HeadRefOID),
 		MergeMethod: string(mergeMethod),
@@ -1472,7 +1473,7 @@ func (c *DefaultController) dividePool(pool map[string]PullRequest, pjs []v1alph
 		sps[fn].prs = append(sps[fn].prs, pr)
 	}
 	for _, pj := range pjs {
-		if pj.Spec.Type != config.PresubmitJob && pj.Spec.Type != config.BatchJob {
+		if pj.Spec.Type != job.PresubmitJob && pj.Spec.Type != job.BatchJob {
 			continue
 		}
 		fn := poolKey(pj.Spec.Refs.Org, pj.Spec.Refs.Repo, pj.Spec.Refs.BaseRef)
@@ -1641,8 +1642,8 @@ func orgRepoQueryString(orgs, repos []string, orgExceptions map[string]sets.Stri
 	return strings.Join(toks, " ")
 }
 
-func reposToQueries(queries config.KeeperQueries) map[string][]config.KeeperQuery {
-	queryMap := make(map[string][]config.KeeperQuery)
+func reposToQueries(queries keeper.Queries) map[string][]keeper.Query {
+	queryMap := make(map[string][]keeper.Query)
 	// Create a map of each repo to the relevant queries
 	for _, q := range queries {
 		for _, repo := range q.Repos {
@@ -1652,7 +1653,7 @@ func reposToQueries(queries config.KeeperQueries) map[string][]config.KeeperQuer
 	return queryMap
 }
 
-func restAPISearch(spc scmProviderClient, log *logrus.Entry, queries config.KeeperQueries, start, end time.Time) ([]PullRequest, error) {
+func restAPISearch(spc scmProviderClient, log *logrus.Entry, queries keeper.Queries, start, end time.Time) ([]PullRequest, error) {
 	var relevantPRs []PullRequest
 
 	queryMap := reposToQueries(queries)
diff --git a/pkg/keeper/status.go b/pkg/keeper/status.go
index 5c499f23..ad6afc7e 100644
--- a/pkg/keeper/status.go
+++ b/pkg/keeper/status.go
@@ -27,6 +27,7 @@ import (
 	"time"
 
 	"github.com/jenkins-x/lighthouse/pkg/config"
+	"github.com/jenkins-x/lighthouse/pkg/config/keeper"
 	"github.com/jenkins-x/lighthouse/pkg/keeper/blockers"
 	"github.com/jenkins-x/lighthouse/pkg/scmprovider"
 	"github.com/pkg/errors"
@@ -107,7 +108,7 @@ func (sc *statusController) shutdown() {
 // Note: an empty diff can be returned if the reason that the PR does not match
 // the KeeperQuery is unknown. This can happen if this function's logic
 // does not match GitHub's and does not indicate that the PR matches the query.
-func requirementDiff(pr *PullRequest, q *config.KeeperQuery, cc contextChecker) (string, int) {
+func requirementDiff(pr *PullRequest, q *keeper.Query, cc contextChecker) (string, int) {
 	const maxLabelChars = 50
 	var desc string
 	var diff int
@@ -233,7 +234,7 @@ func requirementDiff(pr *PullRequest, q *config.KeeperQuery, cc contextChecker)
 // in order to generate a diff for the status description. We choose the query
 // for the repo that the PR is closest to meeting (as determined by the number
 // of unmet/violated requirements).
-func expectedStatus(queryMap *config.QueryMap, pr *PullRequest, pool map[string]PullRequest, cc contextChecker, blocks blockers.Blockers, providerType string) (string, string) {
+func expectedStatus(queryMap *keeper.QueryMap, pr *PullRequest, pool map[string]PullRequest, cc contextChecker, blocks blockers.Blockers, providerType string) (string, string) {
 	if _, ok := pool[prKey(pr)]; !ok {
 		// if the branch is blocked forget checking for a diff
 		blockingIssues := blocks.GetApplicable(string(pr.Repository.Owner.Login), string(pr.Repository.Name), string(pr.BaseRef.Name))
@@ -455,12 +456,12 @@ func (sc *statusController) search() []PullRequest {
 	if sc.spc.SupportsGraphQL() {
 		prs, err = graphQLSearch(sc.spc.Query, sc.logger, query, sc.LatestPR.Time, now)
 	} else {
-		kq := config.KeeperQuery{}
+		kq := keeper.Query{}
 		for _, r := range repos.List() {
 			kq.Repos = append(kq.Repos, r)
 		}
 
-		prs, err = restAPISearch(sc.spc, sc.logger, config.KeeperQueries{kq}, sc.LatestPR.Time, now)
+		prs, err = restAPISearch(sc.spc, sc.logger, keeper.Queries{kq}, sc.LatestPR.Time, now)
 	}
 	log.WithField("duration", time.Since(now).String()).Debugf("Found %d open PRs.", len(prs))
 	if err != nil {
diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go
index 0084ca29..97f775f5 100644
--- a/pkg/metrics/metrics.go
+++ b/pkg/metrics/metrics.go
@@ -28,14 +28,14 @@ import (
 	"github.com/prometheus/client_golang/prometheus/push"
 	"github.com/sirupsen/logrus"
 
-	"github.com/jenkins-x/lighthouse/pkg/config"
+	"github.com/jenkins-x/lighthouse/pkg/config/lighthouse"
 	"github.com/jenkins-x/lighthouse/pkg/interrupts"
 )
 
 const metricsPort = 9090
 
 // ExposeMetrics chooses whether to serve or push metrics for the service
-func ExposeMetrics(component string, pushGateway config.PushGateway) {
+func ExposeMetrics(component string, pushGateway lighthouse.PushGateway) {
 	if pushGateway.Endpoint != "" {
 		pushMetrics(component, pushGateway.Endpoint, pushGateway.Interval)
 		if pushGateway.ServeMetrics {
diff --git a/pkg/repoconfig/merge/combine.go b/pkg/repoconfig/merge/combine.go
index 3cb55403..c1ef0317 100644
--- a/pkg/repoconfig/merge/combine.go
+++ b/pkg/repoconfig/merge/combine.go
@@ -1,7 +1,7 @@
 package merge
 
 import (
-	"github.com/jenkins-x/lighthouse/pkg/config"
+	"github.com/jenkins-x/lighthouse/pkg/config/branchprotection"
 	"github.com/jenkins-x/lighthouse/pkg/repoconfig"
 )
 
@@ -21,7 +21,7 @@ func CombineConfigs(a, b *repoconfig.RepositoryConfig) *repoconfig.RepositoryCon
 	}
 	if b.Spec.BranchProtection != nil {
 		if a.Spec.BranchProtection == nil {
-			a.Spec.BranchProtection = &config.ContextPolicy{}
+			a.Spec.BranchProtection = &branchprotection.ContextPolicy{}
 		}
 		for _, s := range b.Spec.BranchProtection.Contexts {
 			a.Spec.BranchProtection.Contexts = append(a.Spec.BranchProtection.Contexts, s)
diff --git a/pkg/repoconfig/merge/merge.go b/pkg/repoconfig/merge/merge.go
index 8def7d8f..21dd9d00 100644
--- a/pkg/repoconfig/merge/merge.go
+++ b/pkg/repoconfig/merge/merge.go
@@ -2,6 +2,7 @@ package merge
 
 import (
 	"github.com/jenkins-x/lighthouse/pkg/config"
+	"github.com/jenkins-x/lighthouse/pkg/config/branchprotection"
 	"github.com/jenkins-x/lighthouse/pkg/config/job"
 	"github.com/jenkins-x/lighthouse/pkg/plugins"
 	"github.com/jenkins-x/lighthouse/pkg/repoconfig"
@@ -35,11 +36,11 @@ func ConfigMerge(cfg *config.Config, pluginsCfg *plugins.Configuration, repoConf
 
 	if repoConfig.Spec.BranchProtection != nil {
 		if cfg.BranchProtection.Orgs == nil {
-			cfg.BranchProtection.Orgs = map[string]config.Org{}
+			cfg.BranchProtection.Orgs = map[string]branchprotection.Org{}
 		}
 		org := cfg.BranchProtection.Orgs[repoOwner]
 		if org.Repos == nil {
-			org.Repos = map[string]config.Repo{}
+			org.Repos = map[string]branchprotection.Repo{}
 		}
 		repo := org.Repos[repoName]
 		repo.RequiredStatusChecks = repoConfig.Spec.BranchProtection
diff --git a/pkg/repoconfig/repoconfig_types.go b/pkg/repoconfig/repoconfig_types.go
index fc767a11..1b764dd8 100644
--- a/pkg/repoconfig/repoconfig_types.go
+++ b/pkg/repoconfig/repoconfig_types.go
@@ -1,7 +1,8 @@
 package repoconfig
 
 import (
-	"github.com/jenkins-x/lighthouse/pkg/config"
+	"github.com/jenkins-x/lighthouse/pkg/config/branchprotection"
+	"github.com/jenkins-x/lighthouse/pkg/config/keeper"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
@@ -28,7 +29,7 @@ type RepositoryConfigSpec struct {
 	Plugins []string `json:"plugins,omitempty"`
 
 	// BranchProtections to configure branch protection
-	BranchProtection *config.ContextPolicy `json:"branchProtection,omitempty"`
+	BranchProtection *branchprotection.ContextPolicy `json:"branchProtection,omitempty"`
 
 	// PluginConfig the configuration for the plugins
 	PluginConfig *PluginConfig `json:"pluginConfig,omitempty"`
@@ -48,7 +49,7 @@ type RepositoryConfigList struct {
 // Keeper for keeper specific queries
 type Keeper struct {
 	// Query the query to add for the repository
-	Query *config.KeeperQuery `json:"query,omitempty"`
+	Query *keeper.Query `json:"query,omitempty"`
 }
 
 // PluginConfig configuration for plugins
diff --git a/pkg/scmprovider/reporter/reporter.go b/pkg/scmprovider/reporter/reporter.go
index a0b5f0fc..40a7cbb0 100644
--- a/pkg/scmprovider/reporter/reporter.go
+++ b/pkg/scmprovider/reporter/reporter.go
@@ -25,7 +25,7 @@ import (
 
 	"github.com/jenkins-x/go-scm/scm"
 	"github.com/jenkins-x/lighthouse/pkg/apis/lighthouse/v1alpha1"
-	"github.com/jenkins-x/lighthouse/pkg/config"
+	"github.com/jenkins-x/lighthouse/pkg/config/job"
 	"github.com/jenkins-x/lighthouse/pkg/plugins"
 )
 
@@ -45,7 +45,7 @@ type SCMProviderClient interface {
 }
 
 // ShouldReport determines whether this LighthouseJob is of a type to be reporting back.
-func ShouldReport(lhj *v1alpha1.LighthouseJob, validTypes []config.PipelineKind) bool {
+func ShouldReport(lhj *v1alpha1.LighthouseJob, validTypes []job.PipelineKind) bool {
 	valid := false
 	for _, t := range validTypes {
 		if lhj.Spec.Type == t {
@@ -62,7 +62,7 @@ func ShouldReport(lhj *v1alpha1.LighthouseJob, validTypes []config.PipelineKind)
 
 // Report is creating/updating/removing report comments in the SCM provider based on the state of
 // the provided LighthouseJob.
-func Report(spc SCMProviderClient, reportTemplate *template.Template, lhj *v1alpha1.LighthouseJob, validTypes []config.PipelineKind) error {
+func Report(spc SCMProviderClient, reportTemplate *template.Template, lhj *v1alpha1.LighthouseJob, validTypes []job.PipelineKind) error {
 	if spc == nil {
 		return fmt.Errorf("trying to report lhj %s, but found empty SCM provider client", lhj.ObjectMeta.Name)
 	}


Test patch:
diff --git a/pkg/apis/lighthouse/v1alpha1/types_test.go b/pkg/apis/lighthouse/v1alpha1/types_test.go
index a99caefb..9ae40100 100644
--- a/pkg/apis/lighthouse/v1alpha1/types_test.go
+++ b/pkg/apis/lighthouse/v1alpha1/types_test.go
@@ -6,7 +6,7 @@ import (
 
 	"github.com/google/go-cmp/cmp"
 	"github.com/jenkins-x/lighthouse/pkg/apis/lighthouse/v1alpha1"
-	"github.com/jenkins-x/lighthouse/pkg/config"
+	"github.com/jenkins-x/lighthouse/pkg/config/job"
 )
 
 func TestPipelineOptionsSpec_GetEnvVars(t *testing.T) {
@@ -18,20 +18,20 @@ func TestPipelineOptionsSpec_GetEnvVars(t *testing.T) {
 		{
 			name: "periodic",
 			spec: &v1alpha1.LighthouseJobSpec{
-				Type:      config.PeriodicJob,
+				Type:      job.PeriodicJob,
 				Namespace: "jx",
 				Job:       "some-job",
 			},
 			env: map[string]string{
 				v1alpha1.JobNameEnv: "some-job",
-				v1alpha1.JobTypeEnv: string(config.PeriodicJob),
-				v1alpha1.JobSpecEnv: fmt.Sprintf("type:%s", config.PeriodicJob),
+				v1alpha1.JobTypeEnv: string(job.PeriodicJob),
+				v1alpha1.JobSpecEnv: fmt.Sprintf("type:%s", job.PeriodicJob),
 			},
 		},
 		{
 			name: "postsubmit",
 			spec: &v1alpha1.LighthouseJobSpec{
-				Type:      config.PostsubmitJob,
+				Type:      job.PostsubmitJob,
 				Namespace: "jx",
 				Job:       "some-release-job",
 				Refs: &v1alpha1.Refs{
@@ -44,8 +44,8 @@ func TestPipelineOptionsSpec_GetEnvVars(t *testing.T) {
 			},
 			env: map[string]string{
 				v1alpha1.JobNameEnv:     "some-release-job",
-				v1alpha1.JobTypeEnv:     string(config.PostsubmitJob),
-				v1alpha1.JobSpecEnv:     fmt.Sprintf("type:%s", config.PostsubmitJob),
+				v1alpha1.JobTypeEnv:     string(job.PostsubmitJob),
+				v1alpha1.JobSpecEnv:     fmt.Sprintf("type:%s", job.PostsubmitJob),
 				v1alpha1.RepoNameEnv:    "some-repo",
 				v1alpha1.RepoOwnerEnv:   "some-org",
 				v1alpha1.PullBaseRefEnv: "master",
@@ -56,7 +56,7 @@ func TestPipelineOptionsSpec_GetEnvVars(t *testing.T) {
 		{
 			name: "presubmit",
 			spec: &v1alpha1.LighthouseJobSpec{
-				Type:      config.PresubmitJob,
+				Type:      job.PresubmitJob,
 				Namespace: "jx",
 				Job:       "some-pr-job",
 				Refs: &v1alpha1.Refs{
@@ -75,8 +75,8 @@ func TestPipelineOptionsSpec_GetEnvVars(t *testing.T) {
 			},
 			env: map[string]string{
 				v1alpha1.JobNameEnv:     "some-pr-job",
-				v1alpha1.JobTypeEnv:     string(config.PresubmitJob),
-				v1alpha1.JobSpecEnv:     fmt.Sprintf("type:%s", config.PresubmitJob),
+				v1alpha1.JobTypeEnv:     string(job.PresubmitJob),
+				v1alpha1.JobSpecEnv:     fmt.Sprintf("type:%s", job.PresubmitJob),
 				v1alpha1.RepoNameEnv:    "some-repo",
 				v1alpha1.RepoOwnerEnv:   "some-org",
 				v1alpha1.PullBaseRefEnv: "master",
@@ -89,7 +89,7 @@ func TestPipelineOptionsSpec_GetEnvVars(t *testing.T) {
 		{
 			name: "batch",
 			spec: &v1alpha1.LighthouseJobSpec{
-				Type:      config.BatchJob,
+				Type:      job.BatchJob,
 				Namespace: "jx",
 				Job:       "some-pr-job",
 				Refs: &v1alpha1.Refs{
@@ -112,8 +112,8 @@ func TestPipelineOptionsSpec_GetEnvVars(t *testing.T) {
 			},
 			env: map[string]string{
 				v1alpha1.JobNameEnv:     "some-pr-job",
-				v1alpha1.JobTypeEnv:     string(config.BatchJob),
-				v1alpha1.JobSpecEnv:     fmt.Sprintf("type:%s", config.BatchJob),
+				v1alpha1.JobTypeEnv:     string(job.BatchJob),
+				v1alpha1.JobSpecEnv:     fmt.Sprintf("type:%s", job.BatchJob),
 				v1alpha1.RepoNameEnv:    "some-repo",
 				v1alpha1.RepoOwnerEnv:   "some-org",
 				v1alpha1.PullBaseRefEnv: "master",
diff --git a/pkg/config/branch_protection_test.go b/pkg/config/branch_protection_test.go
index 5da51bb3..3b9bcb53 100644
--- a/pkg/config/branch_protection_test.go
+++ b/pkg/config/branch_protection_test.go
@@ -21,6 +21,7 @@ import (
 	"sort"
 	"testing"
 
+	"github.com/jenkins-x/lighthouse/pkg/config/branchprotection"
 	"github.com/jenkins-x/lighthouse/pkg/config/job"
 	"k8s.io/apimachinery/pkg/util/diff"
 )
@@ -32,7 +33,7 @@ var (
 	no  = &n
 )
 
-func normalize(policy *Policy) {
+func normalize(policy *branchprotection.Policy) {
 	if policy == nil || policy.RequiredStatusChecks == nil {
 		return
 	}
@@ -40,268 +41,6 @@ func normalize(policy *Policy) {
 	sort.Strings(policy.Exclude)
 }
 
-func TestSelectBool(t *testing.T) {
-	cases := []struct {
-		name     string
-		parent   *bool
-		child    *bool
-		expected *bool
-	}{
-		{
-			name: "default is nil",
-		},
-		{
-			name:     "use child if set",
-			child:    yes,
-			expected: yes,
-		},
-		{
-			name:     "child overrides parent",
-			child:    yes,
-			parent:   no,
-			expected: yes,
-		},
-		{
-			name:     "use parent if child unset",
-			parent:   no,
-			expected: no,
-		},
-	}
-	for _, tc := range cases {
-		t.Run(tc.name, func(t *testing.T) {
-			actual := selectBool(tc.parent, tc.child)
-			if !reflect.DeepEqual(actual, tc.expected) {
-				t.Errorf("actual %v != expected %v", actual, tc.expected)
-			}
-		})
-	}
-}
-
-func TestSelectInt(t *testing.T) {
-	one := 1
-	two := 2
-	cases := []struct {
-		name     string
-		parent   *int
-		child    *int
-		expected *int
-	}{
-		{
-			name: "default is nil",
-		},
-		{
-			name:     "use child if set",
-			child:    &one,
-			expected: &one,
-		},
-		{
-			name:     "child overrides parent",
-			child:    &one,
-			parent:   &two,
-			expected: &one,
-		},
-		{
-			name:     "use parent if child unset",
-			parent:   &two,
-			expected: &two,
-		},
-	}
-	for _, tc := range cases {
-		t.Run(tc.name, func(t *testing.T) {
-			actual := selectInt(tc.parent, tc.child)
-			if !reflect.DeepEqual(actual, tc.expected) {
-				t.Errorf("actual %v != expected %v", actual, tc.expected)
-			}
-		})
-	}
-}
-
-func TestUnionStrings(t *testing.T) {
-	cases := []struct {
-		name     string
-		parent   []string
-		child    []string
-		expected []string
-	}{
-		{
-			name: "empty list",
-		},
-		{
-			name:     "all parent items",
-			parent:   []string{"hi", "there"},
-			expected: []string{"hi", "there"},
-		},
-		{
-			name:     "all child items",
-			child:    []string{"hi", "there"},
-			expected: []string{"hi", "there"},
-		},
-		{
-			name:     "both child and parent items, no duplicates",
-			child:    []string{"hi", "world"},
-			parent:   []string{"hi", "there"},
-			expected: []string{"hi", "there", "world"},
-		},
-	}
-	for _, tc := range cases {
-		t.Run(tc.name, func(t *testing.T) {
-			actual := unionStrings(tc.parent, tc.child)
-			sort.Strings(actual)
-			sort.Strings(tc.expected)
-			if !reflect.DeepEqual(actual, tc.expected) {
-				t.Errorf("actual %v != expected %v", actual, tc.expected)
-			}
-		})
-	}
-}
-
-func TestApply(test *testing.T) {
-	t := true
-	f := false
-	basic := Policy{
-		Protect: &t,
-	}
-	ebasic := Policy{
-		Protect: &t,
-	}
-	cases := []struct {
-		name     string
-		parent   Policy
-		child    Policy
-		expected Policy
-	}{
-		{
-			name:     "nil child",
-			parent:   basic,
-			expected: ebasic,
-		},
-		{
-			name: "merge parent and child",
-			parent: Policy{
-				Protect: &t,
-			},
-			child: Policy{
-				Admins: &f,
-			},
-			expected: Policy{
-				Protect: &t,
-				Admins:  &f,
-			},
-		},
-		{
-			name: "child overrides parent",
-			parent: Policy{
-				Protect: &t,
-			},
-			child: Policy{
-				Protect: &f,
-			},
-			expected: Policy{
-				Protect: &f,
-			},
-		},
-		{
-			name: "append strings",
-			parent: Policy{
-				RequiredStatusChecks: &ContextPolicy{
-					Contexts: []string{"hello", "world"},
-				},
-			},
-			child: Policy{
-				RequiredStatusChecks: &ContextPolicy{
-					Contexts: []string{"world", "of", "thrones"},
-				},
-			},
-			expected: Policy{
-				RequiredStatusChecks: &ContextPolicy{
-					Contexts: []string{"hello", "of", "thrones", "world"},
-				},
-			},
-		},
-		{
-			name: "merge struct",
-			parent: Policy{
-				RequiredStatusChecks: &ContextPolicy{
-					Contexts: []string{"hi"},
-				},
-			},
-			child: Policy{
-				RequiredStatusChecks: &ContextPolicy{
-					Strict: &t,
-				},
-			},
-			expected: Policy{
-				RequiredStatusChecks: &ContextPolicy{
-					Contexts: []string{"hi"},
-					Strict:   &t,
-				},
-			},
-		},
-		{
-			name: "nil child struct",
-			parent: Policy{
-				RequiredStatusChecks: &ContextPolicy{
-					Strict: &f,
-				},
-			},
-			child: Policy{
-				Protect: &t,
-			},
-			expected: Policy{
-				RequiredStatusChecks: &ContextPolicy{
-					Strict: &f,
-				},
-				Protect: &t,
-			},
-		},
-		{
-			name: "nil parent struct",
-			child: Policy{
-				RequiredStatusChecks: &ContextPolicy{
-					Strict: &f,
-				},
-			},
-			parent: Policy{
-				Protect: &t,
-			},
-			expected: Policy{
-				RequiredStatusChecks: &ContextPolicy{
-					Strict: &f,
-				},
-				Protect: &t,
-			},
-		},
-		{
-			name: "merge exclusion strings",
-			child: Policy{
-				Exclude: []string{"foo*"},
-			},
-			parent: Policy{
-				Exclude: []string{"bar*"},
-			},
-			expected: Policy{
-				Exclude: []string{"bar*", "foo*"},
-			},
-		},
-	}
-
-	for _, tc := range cases {
-		test.Run(tc.name, func(test *testing.T) {
-			defer func() {
-				if r := recover(); r != nil {
-					test.Errorf("unexpected panic: %s", r)
-				}
-			}()
-			actual := tc.parent.Apply(tc.child)
-			normalize(&actual)
-			normalize(&tc.expected)
-			if !reflect.DeepEqual(actual, tc.expected) {
-				test.Errorf("bad merged policy:\n%s", diff.ObjectReflectDiff(tc.expected, actual))
-			}
-		})
-	}
-}
-
 func TestBranchRequirements(t *testing.T) {
 	cases := []struct {
 		name                            string
@@ -366,8 +105,10 @@ func TestBranchRequirements(t *testing.T) {
 	}
 
 	for _, tc := range cases {
-		if err := job.SetPresubmitRegexes(tc.config); err != nil {
-			t.Fatalf("could not set regexes: %v", err)
+		for i := range tc.config {
+			if err := tc.config[i].SetRegexes(); err != nil {
+				t.Fatalf("could not set regexes: %v", err)
+			}
 		}
 		presubmits := map[string][]job.Presubmit{
 			"o/r": tc.config,
@@ -400,7 +141,7 @@ func TestConfig_GetBranchProtection(t *testing.T) {
 		name     string
 		config   Config
 		err      bool
-		expected *Policy
+		expected *branchprotection.Policy
 	}{
 		{
 			name: "unprotected by default",
@@ -409,11 +150,11 @@ func TestConfig_GetBranchProtection(t *testing.T) {
 			name: "undefined org not protected",
 			config: Config{
 				ProwConfig: ProwConfig{
-					BranchProtection: BranchProtection{
-						Policy: Policy{
+					BranchProtection: branchprotection.Config{
+						Policy: branchprotection.Policy{
 							Protect: yes,
 						},
-						Orgs: map[string]Org{
+						Orgs: map[string]branchprotection.Org{
 							"unknown": {},
 						},
 					},
@@ -424,26 +165,26 @@ func TestConfig_GetBranchProtection(t *testing.T) {
 			name: "protect via config default",
 			config: Config{
 				ProwConfig: ProwConfig{
-					BranchProtection: BranchProtection{
-						Policy: Policy{
+					BranchProtection: branchprotection.Config{
+						Policy: branchprotection.Policy{
 							Protect: yes,
 						},
-						Orgs: map[string]Org{
+						Orgs: map[string]branchprotection.Org{
 							"org": {},
 						},
 					},
 				},
 			},
-			expected: &Policy{Protect: yes},
+			expected: &branchprotection.Policy{Protect: yes},
 		},
 		{
 			name: "protect via org default",
 			config: Config{
 				ProwConfig: ProwConfig{
-					BranchProtection: BranchProtection{
-						Orgs: map[string]Org{
+					BranchProtection: branchprotection.Config{
+						Orgs: map[string]branchprotection.Org{
 							"org": {
-								Policy: Policy{
+								Policy: branchprotection.Policy{
 									Protect: yes,
 								},
 							},
@@ -451,18 +192,18 @@ func TestConfig_GetBranchProtection(t *testing.T) {
 					},
 				},
 			},
-			expected: &Policy{Protect: yes},
+			expected: &branchprotection.Policy{Protect: yes},
 		},
 		{
 			name: "protect via repo default",
 			config: Config{
 				ProwConfig: ProwConfig{
-					BranchProtection: BranchProtection{
-						Orgs: map[string]Org{
+					BranchProtection: branchprotection.Config{
+						Orgs: map[string]branchprotection.Org{
 							"org": {
-								Repos: map[string]Repo{
+								Repos: map[string]branchprotection.Repo{
 									"repo": {
-										Policy: Policy{
+										Policy: branchprotection.Policy{
 											Protect: yes,
 										},
 									},
@@ -472,20 +213,20 @@ func TestConfig_GetBranchProtection(t *testing.T) {
 					},
 				},
 			},
-			expected: &Policy{Protect: yes},
+			expected: &branchprotection.Policy{Protect: yes},
 		},
 		{
 			name: "protect specific branch",
 			config: Config{
 				ProwConfig: ProwConfig{
-					BranchProtection: BranchProtection{
-						Orgs: map[string]Org{
+					BranchProtection: branchprotection.Config{
+						Orgs: map[string]branchprotection.Org{
 							"org": {
-								Repos: map[string]Repo{
+								Repos: map[string]branchprotection.Repo{
 									"repo": {
-										Branches: map[string]Branch{
+										Branches: map[string]branchprotection.Branch{
 											"branch": {
-												Policy: Policy{
+												Policy: branchprotection.Policy{
 													Protect: yes,
 												},
 											},
@@ -497,37 +238,37 @@ func TestConfig_GetBranchProtection(t *testing.T) {
 					},
 				},
 			},
-			expected: &Policy{Protect: yes},
+			expected: &branchprotection.Policy{Protect: yes},
 		},
 		{
 			name: "ignore other org settings",
 			config: Config{
 				ProwConfig: ProwConfig{
-					BranchProtection: BranchProtection{
-						Policy: Policy{
+					BranchProtection: branchprotection.Config{
+						Policy: branchprotection.Policy{
 							Protect: no,
 						},
-						Orgs: map[string]Org{
+						Orgs: map[string]branchprotection.Org{
 							"other": {
-								Policy: Policy{Protect: yes},
+								Policy: branchprotection.Policy{Protect: yes},
 							},
 							"org": {},
 						},
 					},
 				},
 			},
-			expected: &Policy{Protect: no},
+			expected: &branchprotection.Policy{Protect: no},
 		},
 		{
 			name: "defined branches must make a protection decision",
 			config: Config{
 				ProwConfig: ProwConfig{
-					BranchProtection: BranchProtection{
-						Orgs: map[string]Org{
+					BranchProtection: branchprotection.Config{
+						Orgs: map[string]branchprotection.Org{
 							"org": {
-								Repos: map[string]Repo{
+								Repos: map[string]branchprotection.Repo{
 									"repo": {
-										Branches: map[string]Branch{
+										Branches: map[string]branchprotection.Branch{
 											"branch": {},
 										},
 									},
@@ -543,14 +284,14 @@ func TestConfig_GetBranchProtection(t *testing.T) {
 			name: "pushers require protection",
 			config: Config{
 				ProwConfig: ProwConfig{
-					BranchProtection: BranchProtection{
-						Policy: Policy{
+					BranchProtection: branchprotection.Config{
+						Policy: branchprotection.Policy{
 							Protect: no,
-							Restrictions: &Restrictions{
+							Restrictions: &branchprotection.Restrictions{
 								Teams: []string{"oncall"},
 							},
 						},
-						Orgs: map[string]Org{
+						Orgs: map[string]branchprotection.Org{
 							"org": {},
 						},
 					},
@@ -562,14 +303,14 @@ func TestConfig_GetBranchProtection(t *testing.T) {
 			name: "required contexts require protection",
 			config: Config{
 				ProwConfig: ProwConfig{
-					BranchProtection: BranchProtection{
-						Policy: Policy{
+					BranchProtection: branchprotection.Config{
+						Policy: branchprotection.Policy{
 							Protect: no,
-							RequiredStatusChecks: &ContextPolicy{
+							RequiredStatusChecks: &branchprotection.ContextPolicy{
 								Contexts: []string{"test-foo"},
 							},
 						},
-						Orgs: map[string]Org{
+						Orgs: map[string]branchprotection.Org{
 							"org": {},
 						},
 					},
@@ -581,17 +322,17 @@ func TestConfig_GetBranchProtection(t *testing.T) {
 			name: "child policy with defined parent can disable protection",
 			config: Config{
 				ProwConfig: ProwConfig{
-					BranchProtection: BranchProtection{
+					BranchProtection: branchprotection.Config{
 						AllowDisabledPolicies: true,
-						Policy: Policy{
+						Policy: branchprotection.Policy{
 							Protect: yes,
-							Restrictions: &Restrictions{
+							Restrictions: &branchprotection.Restrictions{
 								Teams: []string{"oncall"},
 							},
 						},
-						Orgs: map[string]Org{
+						Orgs: map[string]branchprotection.Org{
 							"org": {
-								Policy: Policy{
+								Policy: branchprotection.Policy{
 									Protect: no,
 								},
 							},
@@ -599,7 +340,7 @@ func TestConfig_GetBranchProtection(t *testing.T) {
 					},
 				},
 			},
-			expected: &Policy{
+			expected: &branchprotection.Policy{
 				Protect: no,
 			},
 		},
@@ -607,14 +348,14 @@ func TestConfig_GetBranchProtection(t *testing.T) {
 			name: "Make required presubmits required",
 			config: Config{
 				ProwConfig: ProwConfig{
-					BranchProtection: BranchProtection{
-						Policy: Policy{
+					BranchProtection: branchprotection.Config{
+						Policy: branchprotection.Policy{
 							Protect: yes,
-							RequiredStatusChecks: &ContextPolicy{
+							RequiredStatusChecks: &branchprotection.ContextPolicy{
 								Contexts: []string{"cla"},
 							},
 						},
-						Orgs: map[string]Org{
+						Orgs: map[string]branchprotection.Org{
 							"org": {},
 						},
 					},
@@ -635,9 +376,9 @@ func TestConfig_GetBranchProtection(t *testing.T) {
 					},
 				},
 			},
-			expected: &Policy{
+			expected: &branchprotection.Policy{
 				Protect: yes,
-				RequiredStatusChecks: &ContextPolicy{
+				RequiredStatusChecks: &branchprotection.ContextPolicy{
 					Contexts: []string{"required presubmit", "cla"},
 				},
 			},
@@ -646,9 +387,9 @@ func TestConfig_GetBranchProtection(t *testing.T) {
 			name: "ProtectTested opts into protection",
 			config: Config{
 				ProwConfig: ProwConfig{
-					BranchProtection: BranchProtection{
+					BranchProtection: branchprotection.Config{
 						ProtectTested: true,
-						Orgs: map[string]Org{
+						Orgs: map[string]branchprotection.Org{
 							"org": {},
 						},
 					},
@@ -669,9 +410,9 @@ func TestConfig_GetBranchProtection(t *testing.T) {
 					},
 				},
 			},
-			expected: &Policy{
+			expected: &branchprotection.Policy{
 				Protect: yes,
-				RequiredStatusChecks: &ContextPolicy{
+				RequiredStatusChecks: &branchprotection.ContextPolicy{
 					Contexts: []string{"required presubmit"},
 				},
 			},
@@ -680,11 +421,11 @@ func TestConfig_GetBranchProtection(t *testing.T) {
 			name: "required presubmits require protection",
 			config: Config{
 				ProwConfig: ProwConfig{
-					BranchProtection: BranchProtection{
-						Policy: Policy{
+					BranchProtection: branchprotection.Config{
+						Policy: branchprotection.Policy{
 							Protect: no,
 						},
-						Orgs: map[string]Org{
+						Orgs: map[string]branchprotection.Org{
 							"org": {},
 						},
 					},
@@ -711,9 +452,9 @@ func TestConfig_GetBranchProtection(t *testing.T) {
 			name: "Optional presubmits do not force protection",
 			config: Config{
 				ProwConfig: ProwConfig{
-					BranchProtection: BranchProtection{
+					BranchProtection: branchprotection.Config{
 						ProtectTested: true,
-						Orgs: map[string]Org{
+						Orgs: map[string]branchprotection.Org{
 							"org": {},
 						},
 					},
@@ -740,11 +481,11 @@ func TestConfig_GetBranchProtection(t *testing.T) {
 			name: "Explicit configuration takes precedence over ProtectTested",
 			config: Config{
 				ProwConfig: ProwConfig{
-					BranchProtection: BranchProtection{
+					BranchProtection: branchprotection.Config{
 						ProtectTested: true,
-						Orgs: map[string]Org{
+						Orgs: map[string]branchprotection.Org{
 							"org": {
-								Policy: Policy{
+								Policy: branchprotection.Policy{
 									Protect: yes,
 								},
 							},
@@ -768,20 +509,20 @@ func TestConfig_GetBranchProtection(t *testing.T) {
 					},
 				},
 			},
-			expected: &Policy{Protect: yes},
+			expected: &branchprotection.Policy{Protect: yes},
 		},
 		{
 			name: "Explicit non-configuration takes precedence over ProtectTested",
 			config: Config{
 				ProwConfig: ProwConfig{
-					BranchProtection: BranchProtection{
+					BranchProtection: branchprotection.Config{
 						AllowDisabledJobPolicies: true,
 						ProtectTested:            true,
-						Orgs: map[string]Org{
+						Orgs: map[string]branchprotection.Org{
 							"org": {
-								Repos: map[string]Repo{
+								Repos: map[string]branchprotection.Repo{
 									"repo": {
-										Policy: Policy{
+										Policy: branchprotection.Policy{
 											Protect: no,
 										},
 									},
diff --git a/pkg/config/branchprotection/policy_test.go b/pkg/config/branchprotection/policy_test.go
new file mode 100644
index 00000000..56d83524
--- /dev/null
+++ b/pkg/config/branchprotection/policy_test.go
@@ -0,0 +1,180 @@
+/*
+Copyright 2018 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package branchprotection
+
+import (
+	"reflect"
+	"sort"
+	"testing"
+
+	"k8s.io/apimachinery/pkg/util/diff"
+)
+
+func normalize(policy *Policy) {
+	if policy == nil || policy.RequiredStatusChecks == nil {
+		return
+	}
+	sort.Strings(policy.RequiredStatusChecks.Contexts)
+	sort.Strings(policy.Exclude)
+}
+
+func TestApply(test *testing.T) {
+	t := true
+	f := false
+	basic := Policy{
+		Protect: &t,
+	}
+	ebasic := Policy{
+		Protect: &t,
+	}
+	cases := []struct {
+		name     string
+		parent   Policy
+		child    Policy
+		expected Policy
+	}{
+		{
+			name:     "nil child",
+			parent:   basic,
+			expected: ebasic,
+		},
+		{
+			name: "merge parent and child",
+			parent: Policy{
+				Protect: &t,
+			},
+			child: Policy{
+				Admins: &f,
+			},
+			expected: Policy{
+				Protect: &t,
+				Admins:  &f,
+			},
+		},
+		{
+			name: "child overrides parent",
+			parent: Policy{
+				Protect: &t,
+			},
+			child: Policy{
+				Protect: &f,
+			},
+			expected: Policy{
+				Protect: &f,
+			},
+		},
+		{
+			name: "append strings",
+			parent: Policy{
+				RequiredStatusChecks: &ContextPolicy{
+					Contexts: []string{"hello", "world"},
+				},
+			},
+			child: Policy{
+				RequiredStatusChecks: &ContextPolicy{
+					Contexts: []string{"world", "of", "thrones"},
+				},
+			},
+			expected: Policy{
+				RequiredStatusChecks: &ContextPolicy{
+					Contexts: []string{"hello", "of", "thrones", "world"},
+				},
+			},
+		},
+		{
+			name: "merge struct",
+			parent: Policy{
+				RequiredStatusChecks: &ContextPolicy{
+					Contexts: []string{"hi"},
+				},
+			},
+			child: Policy{
+				RequiredStatusChecks: &ContextPolicy{
+					Strict: &t,
+				},
+			},
+			expected: Policy{
+				RequiredStatusChecks: &ContextPolicy{
+					Contexts: []string{"hi"},
+					Strict:   &t,
+				},
+			},
+		},
+		{
+			name: "nil child struct",
+			parent: Policy{
+				RequiredStatusChecks: &ContextPolicy{
+					Strict: &f,
+				},
+			},
+			child: Policy{
+				Protect: &t,
+			},
+			expected: Policy{
+				RequiredStatusChecks: &ContextPolicy{
+					Strict: &f,
+				},
+				Protect: &t,
+			},
+		},
+		{
+			name: "nil parent struct",
+			child: Policy{
+				RequiredStatusChecks: &ContextPolicy{
+					Strict: &f,
+				},
+			},
+			parent: Policy{
+				Protect: &t,
+			},
+			expected: Policy{
+				RequiredStatusChecks: &ContextPolicy{
+					Strict: &f,
+				},
+				Protect: &t,
+			},
+		},
+		{
+			name: "merge exclusion strings",
+			child: Policy{
+				Exclude: []string{"foo*"},
+			},
+			parent: Policy{
+				Exclude: []string{"bar*"},
+			},
+			expected: Policy{
+				Exclude: []string{"bar*", "foo*"},
+			},
+		},
+	}
+
+	for _, tc := range cases {
+		test.Run(tc.name, func(test *testing.T) {
+			defer func() {
+				if r := recover(); r != nil {
+					test.Errorf("unexpected panic: %s", r)
+				}
+			}()
+			actual := tc.parent.Apply(tc.child)
+			normalize(&actual)
+			normalize(&tc.expected)
+			if !reflect.DeepEqual(actual, tc.expected) {
+				test.Errorf("bad merged policy:\n%s", diff.ObjectReflectDiff(tc.expected, actual))
+			}
+		})
+	}
+}
diff --git a/pkg/config/branchprotection/util_test.go b/pkg/config/branchprotection/util_test.go
new file mode 100644
index 00000000..d6c0df73
--- /dev/null
+++ b/pkg/config/branchprotection/util_test.go
@@ -0,0 +1,145 @@
+/*
+Copyright 2018 The Kubernetes Authors.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package branchprotection
+
+import (
+	"reflect"
+	"sort"
+	"testing"
+)
+
+var (
+	y   = true
+	n   = false
+	yes = &y
+	no  = &n
+)
+
+func TestSelectBool(t *testing.T) {
+	cases := []struct {
+		name     string
+		parent   *bool
+		child    *bool
+		expected *bool
+	}{
+		{
+			name: "default is nil",
+		},
+		{
+			name:     "use child if set",
+			child:    yes,
+			expected: yes,
+		},
+		{
+			name:     "child overrides parent",
+			child:    yes,
+			parent:   no,
+			expected: yes,
+		},
+		{
+			name:     "use parent if child unset",
+			parent:   no,
+			expected: no,
+		},
+	}
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			actual := selectBool(tc.parent, tc.child)
+			if !reflect.DeepEqual(actual, tc.expected) {
+				t.Errorf("actual %v != expected %v", actual, tc.expected)
+			}
+		})
+	}
+}
+
+func TestSelectInt(t *testing.T) {
+	one := 1
+	two := 2
+	cases := []struct {
+		name     string
+		parent   *int
+		child    *int
+		expected *int
+	}{
+		{
+			name: "default is nil",
+		},
+		{
+			name:     "use child if set",
+			child:    &one,
+			expected: &one,
+		},
+		{
+			name:     "child overrides parent",
+			child:    &one,
+			parent:   &two,
+			expected: &one,
+		},
+		{
+			name:     "use parent if child unset",
+			parent:   &two,
+			expected: &two,
+		},
+	}
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			actual := selectInt(tc.parent, tc.child)
+			if !reflect.DeepEqual(actual, tc.expected) {
+				t.Errorf("actual %v != expected %v", actual, tc.expected)
+			}
+		})
+	}
+}
+
+func TestUnionStrings(t *testing.T) {
+	cases := []struct {
+		name     string
+		parent   []string
+		child    []string
+		expected []string
+	}{
+		{
+			name: "empty list",
+		},
+		{
+			name:     "all parent items",
+			parent:   []string{"hi", "there"},
+			expected: []string{"hi", "there"},
+		},
+		{
+			name:     "all child items",
+			child:    []string{"hi", "there"},
+			expected: []string{"hi", "there"},
+		},
+		{
+			name:     "both child and parent items, no duplicates",
+			child:    []string{"hi", "world"},
+			parent:   []string{"hi", "there"},
+			expected: []string{"hi", "there", "world"},
+		},
+	}
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			actual := unionStrings(tc.parent, tc.child)
+			sort.Strings(actual)
+			sort.Strings(tc.expected)
+			if !reflect.DeepEqual(actual, tc.expected) {
+				t.Errorf("actual %v != expected %v", actual, tc.expected)
+			}
+		})
+	}
+}
diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go
index d29eb4f4..fb5aaa9e 100644
--- a/pkg/config/config_test.go
+++ b/pkg/config/config_test.go
@@ -91,7 +91,7 @@ func TestDefaultJobBase(t *testing.T) {
 				j.Cluster = ""
 			},
 			expected: func(j *job.Base) {
-				j.Cluster = DefaultClusterAlias
+				j.Cluster = job.DefaultClusterAlias
 			},
 		},
 	}
@@ -142,7 +142,7 @@ func TestValidateAgent(t *testing.T) {
 			if tc.base != nil {
 				tc.base(&jb)
 			}
-			switch err := validateAgent(jb, ns); {
+			switch err := jb.ValidateAgent(ns); {
 			case err == nil && !tc.pass:
 				t.Error("validation failed to raise an error")
 			case err != nil && tc.pass:
@@ -165,7 +165,7 @@ func TestValidateLabels(t *testing.T) {
 		{
 			name: "reject reserved label",
 			labels: map[string]string{
-				Labels()[0]: "anything",
+				job.Labels()[0]: "anything",
 			},
 		},
 		{
@@ -184,7 +184,7 @@ func TestValidateLabels(t *testing.T) {
 
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
-			switch err := validateLabels(tc.labels); {
+			switch err := job.ValidateLabels(tc.labels); {
 			case err == nil && !tc.pass:
 				t.Error("validation failed to raise an error")
 			case err != nil && tc.pass:
@@ -231,7 +231,7 @@ func TestValidateJobBase(t *testing.T) {
 
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
-			switch err := validateJobBase(tc.base, PresubmitJob, ns); {
+			switch err := tc.base.Validate(job.PresubmitJob, ns); {
 			case err == nil && !tc.pass:
 				t.Error("validation failed to raise an error")
 			case err != nil && tc.pass:
diff --git a/pkg/config/keeper_test.go b/pkg/config/keeper_test.go
index 255b4e0f..cacb1d2c 100644
--- a/pkg/config/keeper_test.go
+++ b/pkg/config/keeper_test.go
@@ -3,14 +3,16 @@ package config
 import (
 	"testing"
 
+	"github.com/jenkins-x/lighthouse/pkg/config/branchprotection"
 	"github.com/jenkins-x/lighthouse/pkg/config/job"
+	"github.com/jenkins-x/lighthouse/pkg/config/keeper"
 	"github.com/stretchr/testify/assert"
 )
 
 func TestConfigGetKeeperContextPolicy(t *testing.T) {
 	cases := []struct {
 		name                 string
-		bpOrgs               map[string]Org
+		bpOrgs               map[string]branchprotection.Org
 		presubmits           []job.Presubmit
 		skipUnknownContexts  bool
 		fromBranchProtection bool
@@ -116,13 +118,13 @@ func TestConfigGetKeeperContextPolicy(t *testing.T) {
 				},
 			},
 			fromBranchProtection: true,
-			bpOrgs: map[string]Org{
+			bpOrgs: map[string]branchprotection.Org{
 				"o": {
-					Policy: Policy{},
-					Repos: map[string]Repo{
+					Policy: branchprotection.Policy{},
+					Repos: map[string]branchprotection.Repo{
 						"r": {
-							Policy: Policy{
-								RequiredStatusChecks: &ContextPolicy{
+							Policy: branchprotection.Policy{
+								RequiredStatusChecks: &branchprotection.ContextPolicy{
 									Contexts: []string{
 										"always-run",
 										"run-if-changed",
@@ -184,13 +186,13 @@ func TestConfigGetKeeperContextPolicy(t *testing.T) {
 				},
 			},
 			fromBranchProtection: true,
-			bpOrgs: map[string]Org{
+			bpOrgs: map[string]branchprotection.Org{
 				"o": {
-					Policy: Policy{},
-					Repos: map[string]Repo{
+					Policy: branchprotection.Policy{},
+					Repos: map[string]branchprotection.Repo{
 						"r": {
-							Policy: Policy{
-								RequiredStatusChecks: &ContextPolicy{
+							Policy: branchprotection.Policy{
+								RequiredStatusChecks: &branchprotection.ContextPolicy{
 									Contexts: []string{
 										"always-run",
 										"run-if-changed",
@@ -253,13 +255,13 @@ func TestConfigGetKeeperContextPolicy(t *testing.T) {
 				},
 			},
 			fromBranchProtection: true,
-			bpOrgs: map[string]Org{
+			bpOrgs: map[string]branchprotection.Org{
 				"o": {
-					Policy: Policy{},
-					Repos: map[string]Repo{
+					Policy: branchprotection.Policy{},
+					Repos: map[string]branchprotection.Repo{
 						"r": {
-							Policy: Policy{
-								RequiredStatusChecks: &ContextPolicy{
+							Policy: branchprotection.Policy{
+								RequiredStatusChecks: &branchprotection.ContextPolicy{
 									Contexts: []string{
 										"always-run",
 										"run-if-changed",
@@ -280,9 +282,10 @@ func TestConfigGetKeeperContextPolicy(t *testing.T) {
 
 	for _, tc := range cases {
 		t.Run(tc.name, func(t *testing.T) {
-
-			if err := job.SetPresubmitRegexes(tc.presubmits); err != nil {
-				t.Fatalf("could not set regexes: %v", err)
+			for i := range tc.presubmits {
+				if err := tc.presubmits[i].SetRegexes(); err != nil {
+					t.Fatalf("could not set regexes: %v", err)
+				}
 			}
 			presubmits := map[string][]job.Presubmit{
 				"o/r": tc.presubmits,
@@ -292,9 +295,9 @@ func TestConfigGetKeeperContextPolicy(t *testing.T) {
 					Presubmits: presubmits,
 				},
 				ProwConfig: ProwConfig{
-					Keeper: Keeper{
-						ContextOptions: KeeperContextPolicyOptions{
-							KeeperContextPolicy: KeeperContextPolicy{
+					Keeper: keeper.Config{
+						ContextOptions: keeper.ContextPolicyOptions{
+							ContextPolicy: keeper.ContextPolicy{
 								SkipUnknownContexts:  &tc.skipUnknownContexts,
 								FromBranchProtection: &tc.fromBranchProtection,
 							},
@@ -303,7 +306,7 @@ func TestConfigGetKeeperContextPolicy(t *testing.T) {
 				},
 			}
 			if tc.bpOrgs != nil {
-				cfg.ProwConfig.BranchProtection = BranchProtection{
+				cfg.ProwConfig.BranchProtection = branchprotection.Config{
 					ProtectTested: true,
 					Orgs:          tc.bpOrgs,
 				}
diff --git a/pkg/foghorn/controller_test.go b/pkg/foghorn/controller_test.go
index 4ec895c8..58259191 100644
--- a/pkg/foghorn/controller_test.go
+++ b/pkg/foghorn/controller_test.go
@@ -10,7 +10,10 @@ import (
 	"github.com/google/go-cmp/cmp"
 	lighthousev1alpha1 "github.com/jenkins-x/lighthouse/pkg/apis/lighthouse/v1alpha1"
 	"github.com/jenkins-x/lighthouse/pkg/config"
+	"github.com/jenkins-x/lighthouse/pkg/config/branchprotection"
 	"github.com/jenkins-x/lighthouse/pkg/config/job"
+	"github.com/jenkins-x/lighthouse/pkg/config/keeper"
+	"github.com/jenkins-x/lighthouse/pkg/config/lighthouse"
 	"github.com/jenkins-x/lighthouse/pkg/plugins"
 	"github.com/jenkins-x/lighthouse/pkg/util"
 	"github.com/jenkins-x/lighthouse/pkg/watcher"
@@ -44,20 +47,20 @@ func TestReconcile(t *testing.T) {
 	configAgent.Set(&config.Config{
 		JobConfig: job.Config{},
 		ProwConfig: config.ProwConfig{
-			Keeper:                 config.Keeper{},
-			Plank:                  config.Plank{},
-			BranchProtection:       config.BranchProtection{},
+			Keeper:                 keeper.Config{},
+			Plank:                  lighthouse.Plank{},
+			BranchProtection:       branchprotection.Config{},
 			Orgs:                   nil,
 			JenkinsOperators:       nil,
 			LighthouseJobNamespace: "",
 			PodNamespace:           "",
 			LogLevel:               "",
-			PushGateway:            config.PushGateway{},
+			PushGateway:            lighthouse.PushGateway{},
 			OwnersDirExcludes:      nil,
 			OwnersDirBlacklist:     nil,
 			PubSubSubscriptions:    nil,
-			GitHubOptions:          config.GitHubOptions{},
-			ProviderConfig: &config.ProviderConfig{
+			GitHubOptions:          lighthouse.GitHubOptions{},
+			ProviderConfig: &lighthouse.ProviderConfig{
 				Kind:    "fake",
 				Server:  "https://github.com",
 				BotUser: "jenkins-x-bot",
diff --git a/pkg/jobutil/filter_test.go b/pkg/jobutil/filter_test.go
index 905c10fa..1279314e 100644
--- a/pkg/jobutil/filter_test.go
+++ b/pkg/jobutil/filter_test.go
@@ -85,8 +85,10 @@ func TestTestAllFilter(t *testing.T) {
 			if len(testCase.presubmits) != len(testCase.expected) {
 				t.Fatalf("%s: have %d presubmits but only %d expected filter outputs", testCase.name, len(testCase.presubmits), len(testCase.expected))
 			}
-			if err := job.SetPresubmitRegexes(testCase.presubmits); err != nil {
-				t.Fatalf("%s: could not set presubmit regexes: %v", testCase.name, err)
+			for i := range testCase.presubmits {
+				if err := testCase.presubmits[i].SetRegexes(); err != nil {
+					t.Fatalf("%s: could not set presubmit regexes: %v", testCase.name, err)
+				}
 			}
 			filter := TestAllFilter()
 			for i, presubmit := range testCase.presubmits {
@@ -141,8 +143,10 @@ func TestCommandFilter(t *testing.T) {
 			if len(testCase.presubmits) != len(testCase.expected) {
 				t.Fatalf("%s: have %d presubmits but only %d expected filter outputs", testCase.name, len(testCase.presubmits), len(testCase.expected))
 			}
-			if err := job.SetPresubmitRegexes(testCase.presubmits); err != nil {
-				t.Fatalf("%s: could not set presubmit regexes: %v", testCase.name, err)
+			for i := range testCase.presubmits {
+				if err := testCase.presubmits[i].SetRegexes(); err != nil {
+					t.Fatalf("%s: could not set presubmit regexes: %v", testCase.name, err)
+				}
 			}
 			filter := CommandFilter(testCase.body)
 			for i, presubmit := range testCase.presubmits {
@@ -816,8 +820,10 @@ func TestPresubmitFilter(t *testing.T) {
 			if len(testCase.presubmits) != len(testCase.expected) {
 				t.Fatalf("%s: have %d presubmits but only %d expected filter outputs", testCase.name, len(testCase.presubmits), len(testCase.expected))
 			}
-			if err := job.SetPresubmitRegexes(testCase.presubmits); err != nil {
-				t.Fatalf("%s: could not set presubmit regexes: %v", testCase.name, err)
+			for i := range testCase.presubmits {
+				if err := testCase.presubmits[i].SetRegexes(); err != nil {
+					t.Fatalf("%s: could not set presubmit regexes: %v", testCase.name, err)
+				}
 			}
 			fsg := &fakeContextGetter{
 				errors: map[orgRepoRef]error{},
diff --git a/pkg/jobutil/jobutil_test.go b/pkg/jobutil/jobutil_test.go
index 7bf40343..f3857393 100644
--- a/pkg/jobutil/jobutil_test.go
+++ b/pkg/jobutil/jobutil_test.go
@@ -26,8 +26,6 @@ import (
 	"github.com/jenkins-x/lighthouse/pkg/util"
 	"k8s.io/apimachinery/pkg/api/equality"
 	"k8s.io/apimachinery/pkg/util/diff"
-
-	"github.com/jenkins-x/lighthouse/pkg/config"
 )
 
 func TestPostsubmitSpec(t *testing.T) {
@@ -48,7 +46,7 @@ func TestPostsubmitSpec(t *testing.T) {
 				},
 			},
 			expected: v1alpha1.LighthouseJobSpec{
-				Type: config.PostsubmitJob,
+				Type: job.PostsubmitJob,
 				Refs: &v1alpha1.Refs{
 					PathAlias: "foo",
 					CloneURI:  "bar",
@@ -62,7 +60,7 @@ func TestPostsubmitSpec(t *testing.T) {
 				CloneURI:  "cats",
 			},
 			expected: v1alpha1.LighthouseJobSpec{
-				Type: config.PostsubmitJob,
+				Type: job.PostsubmitJob,
 				Refs: &v1alpha1.Refs{
 					PathAlias: "fancy",
 					CloneURI:  "cats",
@@ -84,7 +82,7 @@ func TestPostsubmitSpec(t *testing.T) {
 				CloneURI:  "cats",
 			},
 			expected: v1alpha1.LighthouseJobSpec{
-				Type: config.PostsubmitJob,
+				Type: job.PostsubmitJob,
 				Refs: &v1alpha1.Refs{
 					PathAlias: "foo",
 					CloneURI:  "bar",
@@ -119,7 +117,7 @@ func TestPresubmitSpec(t *testing.T) {
 				},
 			},
 			expected: v1alpha1.LighthouseJobSpec{
-				Type: config.PresubmitJob,
+				Type: job.PresubmitJob,
 				Refs: &v1alpha1.Refs{
 					PathAlias: "foo",
 					CloneURI:  "bar",
@@ -133,7 +131,7 @@ func TestPresubmitSpec(t *testing.T) {
 				CloneURI:  "cats",
 			},
 			expected: v1alpha1.LighthouseJobSpec{
-				Type: config.PresubmitJob,
+				Type: job.PresubmitJob,
 				Refs: &v1alpha1.Refs{
 					PathAlias: "fancy",
 					CloneURI:  "cats",
@@ -155,7 +153,7 @@ func TestPresubmitSpec(t *testing.T) {
 				CloneURI:  "cats",
 			},
 			expected: v1alpha1.LighthouseJobSpec{
-				Type: config.PresubmitJob,
+				Type: job.PresubmitJob,
 				Refs: &v1alpha1.Refs{
 					PathAlias: "foo",
 					CloneURI:  "bar",
@@ -190,7 +188,7 @@ func TestBatchSpec(t *testing.T) {
 				},
 			},
 			expected: v1alpha1.LighthouseJobSpec{
-				Type: config.BatchJob,
+				Type: job.BatchJob,
 				Refs: &v1alpha1.Refs{
 					PathAlias: "foo",
 					CloneURI:  "bar",
@@ -204,7 +202,7 @@ func TestBatchSpec(t *testing.T) {
 				CloneURI:  "cats",
 			},
 			expected: v1alpha1.LighthouseJobSpec{
-				Type: config.BatchJob,
+				Type: job.BatchJob,
 				Refs: &v1alpha1.Refs{
 					PathAlias: "fancy",
 					CloneURI:  "cats",
@@ -226,7 +224,7 @@ func TestBatchSpec(t *testing.T) {
 				CloneURI:  "cats",
 			},
 			expected: v1alpha1.LighthouseJobSpec{
-				Type: config.BatchJob,
+				Type: job.BatchJob,
 				Refs: &v1alpha1.Refs{
 					PathAlias: "foo",
 					CloneURI:  "bar",
@@ -256,13 +254,13 @@ func TestNewLighthouseJob(t *testing.T) {
 			name: "periodic job, no extra labels",
 			spec: v1alpha1.LighthouseJobSpec{
 				Job:  "job",
-				Type: config.PeriodicJob,
+				Type: job.PeriodicJob,
 			},
 			labels: map[string]string{},
 			expectedLabels: map[string]string{
-				config.CreatedByLighthouse:    "true",
-				util.LighthouseJobAnnotation:  "job",
-				config.LighthouseJobTypeLabel: "periodic",
+				job.CreatedByLighthouseLabel: "true",
+				util.LighthouseJobAnnotation: "job",
+				job.LighthouseJobTypeLabel:   "periodic",
 			},
 			expectedAnnotations: map[string]string{
 				util.LighthouseJobAnnotation: "job",
@@ -272,16 +270,16 @@ func TestNewLighthouseJob(t *testing.T) {
 			name: "periodic job, extra labels",
 			spec: v1alpha1.LighthouseJobSpec{
 				Job:  "job",
-				Type: config.PeriodicJob,
+				Type: job.PeriodicJob,
 			},
 			labels: map[string]string{
 				"extra": "stuff",
 			},
 			expectedLabels: map[string]string{
-				config.CreatedByLighthouse:    "true",
-				util.LighthouseJobAnnotation:  "job",
-				config.LighthouseJobTypeLabel: "periodic",
-				"extra":                       "stuff",
+				job.CreatedByLighthouseLabel: "true",
+				util.LighthouseJobAnnotation: "job",
+				job.LighthouseJobTypeLabel:   "periodic",
+				"extra":                      "stuff",
 			},
 			expectedAnnotations: map[string]string{
 				util.LighthouseJobAnnotation: "job",
@@ -291,7 +289,7 @@ func TestNewLighthouseJob(t *testing.T) {
 			name: "presubmit job",
 			spec: v1alpha1.LighthouseJobSpec{
 				Job:  "job",
-				Type: config.PresubmitJob,
+				Type: job.PresubmitJob,
 				Refs: &v1alpha1.Refs{
 					Org:     "org",
 					Repo:    "repo",
@@ -307,16 +305,16 @@ func TestNewLighthouseJob(t *testing.T) {
 			},
 			labels: map[string]string{},
 			expectedLabels: map[string]string{
-				config.CreatedByLighthouse:    "true",
-				util.LighthouseJobAnnotation:  "job",
-				config.LighthouseJobTypeLabel: "presubmit",
-				util.OrgLabel:                 "org",
-				util.RepoLabel:                "repo",
-				util.PullLabel:                "1",
-				util.BranchLabel:              "PR-1",
-				util.ContextLabel:             "pr-build",
-				util.BaseSHALabel:             "abcd1234",
-				util.LastCommitSHALabel:       "1234abcd",
+				job.CreatedByLighthouseLabel: "true",
+				util.LighthouseJobAnnotation: "job",
+				job.LighthouseJobTypeLabel:   "presubmit",
+				util.OrgLabel:                "org",
+				util.RepoLabel:               "repo",
+				util.PullLabel:               "1",
+				util.BranchLabel:             "PR-1",
+				util.ContextLabel:            "pr-build",
+				util.BaseSHALabel:            "abcd1234",
+				util.LastCommitSHALabel:      "1234abcd",
 			},
 			expectedAnnotations: map[string]string{
 				util.LighthouseJobAnnotation: "job",
@@ -326,7 +324,7 @@ func TestNewLighthouseJob(t *testing.T) {
 			name: "non-github presubmit job",
 			spec: v1alpha1.LighthouseJobSpec{
 				Job:  "job",
-				Type: config.PresubmitJob,
+				Type: job.PresubmitJob,
 				Refs: &v1alpha1.Refs{
 					Org:     "https://some-gerrit-instance.foo.com",
 					Repo:    "some/invalid/repo",
@@ -341,15 +339,15 @@ func TestNewLighthouseJob(t *testing.T) {
 			},
 			labels: map[string]string{},
 			expectedLabels: map[string]string{
-				config.CreatedByLighthouse:    "true",
-				util.LighthouseJobAnnotation:  "job",
-				config.LighthouseJobTypeLabel: "presubmit",
-				util.OrgLabel:                 "some-gerrit-instance.foo.com",
-				util.RepoLabel:                "repo",
-				util.PullLabel:                "1",
-				util.BranchLabel:              "PR-1",
-				util.BaseSHALabel:             "abcd1234",
-				util.LastCommitSHALabel:       "1234abcd",
+				job.CreatedByLighthouseLabel: "true",
+				util.LighthouseJobAnnotation: "job",
+				job.LighthouseJobTypeLabel:   "presubmit",
+				util.OrgLabel:                "some-gerrit-instance.foo.com",
+				util.RepoLabel:               "repo",
+				util.PullLabel:               "1",
+				util.BranchLabel:             "PR-1",
+				util.BaseSHALabel:            "abcd1234",
+				util.LastCommitSHALabel:      "1234abcd",
 			},
 			expectedAnnotations: map[string]string{
 				util.LighthouseJobAnnotation: "job",
@@ -358,7 +356,7 @@ func TestNewLighthouseJob(t *testing.T) {
 			name: "job with name too long to fit in a label",
 			spec: v1alpha1.LighthouseJobSpec{
 				Job:  "job-created-by-someone-who-loves-very-very-very-long-names-so-long-that-it-does-not-fit-into-the-Kubernetes-label-so-it-needs-to-be-truncated-to-63-characters",
-				Type: config.PresubmitJob,
+				Type: job.PresubmitJob,
 				Refs: &v1alpha1.Refs{
 					Org:      "org",
 					Repo:     "repo",
@@ -374,15 +372,15 @@ func TestNewLighthouseJob(t *testing.T) {
 			},
 			labels: map[string]string{},
 			expectedLabels: map[string]string{
-				config.CreatedByLighthouse:    "true",
-				util.LighthouseJobAnnotation:  "job-created-by-someone-who-loves-very-very-very-long-names-so-l",
-				config.LighthouseJobTypeLabel: "presubmit",
-				util.OrgLabel:                 "org",
-				util.RepoLabel:                "repo",
-				util.PullLabel:                "1",
-				util.BranchLabel:              "PR-1",
-				util.BaseSHALabel:             "abcd1234",
-				util.LastCommitSHALabel:       "1234abcd",
+				job.CreatedByLighthouseLabel: "true",
+				util.LighthouseJobAnnotation: "job-created-by-someone-who-loves-very-very-very-long-names-so-l",
+				job.LighthouseJobTypeLabel:   "presubmit",
+				util.OrgLabel:                "org",
+				util.RepoLabel:               "repo",
+				util.PullLabel:               "1",
+				util.BranchLabel:             "PR-1",
+				util.BaseSHALabel:            "abcd1234",
+				util.LastCommitSHALabel:      "1234abcd",
 			},
 			expectedAnnotations: map[string]string{
 				util.LighthouseJobAnnotation: "job-created-by-someone-who-loves-very-very-very-long-names-so-long-that-it-does-not-fit-into-the-Kubernetes-label-so-it-needs-to-be-truncated-to-63-characters",
@@ -393,7 +391,7 @@ func TestNewLighthouseJob(t *testing.T) {
 			name: "periodic job, extra labels, extra annotations",
 			spec: v1alpha1.LighthouseJobSpec{
 				Job:  "job",
-				Type: config.PeriodicJob,
+				Type: job.PeriodicJob,
 			},
 			labels: map[string]string{
 				"extra": "stuff",
@@ -402,10 +400,10 @@ func TestNewLighthouseJob(t *testing.T) {
 				"extraannotation": "foo",
 			},
 			expectedLabels: map[string]string{
-				config.CreatedByLighthouse:    "true",
-				util.LighthouseJobAnnotation:  "job",
-				config.LighthouseJobTypeLabel: "periodic",
-				"extra":                       "stuff",
+				job.CreatedByLighthouseLabel: "true",
+				util.LighthouseJobAnnotation: "job",
+				job.LighthouseJobTypeLabel:   "periodic",
+				"extra":                      "stuff",
 			},
 			expectedAnnotations: map[string]string{
 				util.LighthouseJobAnnotation: "job",
@@ -440,7 +438,7 @@ func TestNewLighthouseJobWithAnnotations(t *testing.T) {
 			name: "job without annotation",
 			spec: v1alpha1.LighthouseJobSpec{
 				Job:  "job",
-				Type: config.PeriodicJob,
+				Type: job.PeriodicJob,
 			},
 			annotations: nil,
 			expectedAnnotations: map[string]string{
@@ -451,7 +449,7 @@ func TestNewLighthouseJobWithAnnotations(t *testing.T) {
 			name: "job with annotation",
 			spec: v1alpha1.LighthouseJobSpec{
 				Job:  "job",
-				Type: config.PeriodicJob,
+				Type: job.PeriodicJob,
 			},
 			annotations: map[string]string{
 				"annotation": "foo",
diff --git a/pkg/keeper/githubapp/owner_query_test.go b/pkg/keeper/githubapp/owner_query_test.go
index 9120d1b4..eb0982b1 100644
--- a/pkg/keeper/githubapp/owner_query_test.go
+++ b/pkg/keeper/githubapp/owner_query_test.go
@@ -7,6 +7,7 @@ import (
 	"testing"
 
 	"github.com/jenkins-x/lighthouse/pkg/config"
+	"github.com/jenkins-x/lighthouse/pkg/config/keeper"
 	"github.com/jenkins-x/lighthouse/pkg/keeper/githubapp"
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
@@ -24,7 +25,7 @@ func TestSplitKeeperQueries(t *testing.T) {
 	assertOwnerQueries(t, results["rawlingsj"], "rawlingj", 2, 1, " for file %s", prowConfig)
 }
 
-func assertOwnerQueries(t *testing.T, ownerQueries config.KeeperQueries, owner string, expectedQueryCount int, expectedRepoCount int, format string, args ...interface{}) {
+func assertOwnerQueries(t *testing.T, ownerQueries keeper.Queries, owner string, expectedQueryCount int, expectedRepoCount int, format string, args ...interface{}) {
 	message := fmt.Sprintf(format, args...)
 	require.NotNil(t, ownerQueries, "ownerQueries should not be nil for owner %s %s", owner, message)
 	assert.Equal(t, len(ownerQueries), expectedQueryCount, "query count for owner %s %s", owner, message)
diff --git a/pkg/keeper/keeper_test.go b/pkg/keeper/keeper_test.go
index 489a1f08..772d08a1 100644
--- a/pkg/keeper/keeper_test.go
+++ b/pkg/keeper/keeper_test.go
@@ -33,6 +33,7 @@ import (
 	"github.com/jenkins-x/lighthouse/pkg/apis/lighthouse/v1alpha1"
 	"github.com/jenkins-x/lighthouse/pkg/client/clientset/versioned/fake"
 	"github.com/jenkins-x/lighthouse/pkg/config/job"
+	"github.com/jenkins-x/lighthouse/pkg/config/keeper"
 	"github.com/jenkins-x/lighthouse/pkg/scmprovider"
 	githubql "github.com/shurcooL/githubv4"
 	"github.com/sirupsen/logrus"
@@ -67,6 +68,12 @@ func testPullsMatchList(t *testing.T, test string, actual []PullRequest, expecte
 	}
 }
 
+func clearCompiledRegexes(presubmits []job.Presubmit) {
+	for i := range presubmits {
+		presubmits[i].ClearCompiledRegexes()
+	}
+}
+
 func TestAccumulateBatch(t *testing.T) {
 	jobSet := []job.Presubmit{
 		{
@@ -235,7 +242,7 @@ func TestAccumulateBatch(t *testing.T) {
 					Spec: v1alpha1.LighthouseJobSpec{
 						Job:     pj.job,
 						Context: pj.job,
-						Type:    config.BatchJob,
+						Type:    job.BatchJob,
 						Refs:    new(v1alpha1.Refs),
 					},
 					Status: v1alpha1.LighthouseJobStatus{State: pj.state},
@@ -474,7 +481,7 @@ func TestAccumulate(t *testing.T) {
 					Spec: v1alpha1.LighthouseJobSpec{
 						Job:     pj.job,
 						Context: pj.job,
-						Type:    config.PresubmitJob,
+						Type:    job.PresubmitJob,
 						Refs:    &v1alpha1.Refs{Pulls: []v1alpha1.Pull{{Number: pj.prNumber, SHA: pj.sha}}},
 					},
 					Status: v1alpha1.LighthouseJobStatus{State: pj.state},
@@ -660,52 +667,52 @@ func TestDividePool(t *testing.T) {
 		},
 	}
 	testPJs := []struct {
-		jobType config.PipelineKind
+		jobType job.PipelineKind
 		org     string
 		repo    string
 		baseRef string
 		baseSHA string
 	}{
 		{
-			jobType: config.PresubmitJob,
+			jobType: job.PresubmitJob,
 			org:     "k",
 			repo:    "t-i",
 			baseRef: "master",
 			baseSHA: "123",
 		},
 		{
-			jobType: config.BatchJob,
+			jobType: job.BatchJob,
 			org:     "k",
 			repo:    "t-i",
 			baseRef: "master",
 			baseSHA: "123",
 		},
 		{
-			jobType: config.PeriodicJob,
+			jobType: job.PeriodicJob,
 		},
 		{
-			jobType: config.PresubmitJob,
+			jobType: job.PresubmitJob,
 			org:     "k",
 			repo:    "t-i",
 			baseRef: "patch",
 			baseSHA: "123",
 		},
 		{
-			jobType: config.PresubmitJob,
+			jobType: job.PresubmitJob,
 			org:     "k",
 			repo:    "t-i",
 			baseRef: "master",
 			baseSHA: "abc",
 		},
 		{
-			jobType: config.PresubmitJob,
+			jobType: job.PresubmitJob,
 			org:     "o",
 			repo:    "t-i",
 			baseRef: "master",
 			baseSHA: "123",
 		},
 		{
-			jobType: config.PresubmitJob,
+			jobType: job.PresubmitJob,
 			org:     "k",
 			repo:    "other",
 			baseRef: "master",
@@ -765,7 +772,7 @@ func TestDividePool(t *testing.T) {
 			}
 		}
 		for _, pj := range sp.ljs {
-			if pj.Spec.Type != config.PresubmitJob && pj.Spec.Type != config.BatchJob {
+			if pj.Spec.Type != job.PresubmitJob && pj.Spec.Type != job.BatchJob {
 				t.Errorf("PJ with bad type in subpool %s: %+v", name, pj)
 			}
 			if pj.Spec.Refs.Org != sp.org || pj.Spec.Refs.Repo != sp.repo || pj.Spec.Refs.BaseRef != sp.branch || pj.Spec.Refs.BaseSHA != sp.sha {
@@ -879,7 +886,7 @@ func TestPickBatch(t *testing.T) {
 	ca := &config.Agent{}
 	ca.Set(&config.Config{
 		ProwConfig: config.ProwConfig{
-			Keeper: config.Keeper{
+			Keeper: keeper.Config{
 				BatchSizeLimitMap: map[string]int{"*": 5},
 			},
 		},
@@ -889,7 +896,7 @@ func TestPickBatch(t *testing.T) {
 		gc:     gc,
 		config: ca.Config,
 	}
-	prs, err := c.pickBatch(sp, &config.KeeperContextPolicy{})
+	prs, err := c.pickBatch(sp, &keeper.ContextPolicy{})
 	if err != nil {
 		t.Fatalf("Error from pickBatch: %v", err)
 	}
@@ -918,15 +925,15 @@ func TestCheckMergeLabels(t *testing.T) {
 		name string
 
 		pr        PullRequest
-		method    config.PullRequestMergeType
-		expected  config.PullRequestMergeType
+		method    keeper.PullRequestMergeType
+		expected  keeper.PullRequestMergeType
 		expectErr bool
 	}{
 		{
 			name:      "default method without PR label override",
 			pr:        PullRequest{},
-			method:    config.MergeMerge,
-			expected:  config.MergeMerge,
+			method:    keeper.MergeMerge,
+			expected:  keeper.MergeMerge,
 			expectErr: false,
 		},
 		{
@@ -936,8 +943,8 @@ func TestCheckMergeLabels(t *testing.T) {
 					Nodes []struct{ Name githubql.String }
 				}{Nodes: []struct{ Name githubql.String }{{Name: githubql.String("sig/testing")}}},
 			},
-			method:    config.MergeMerge,
-			expected:  config.MergeMerge,
+			method:    keeper.MergeMerge,
+			expected:  keeper.MergeMerge,
 			expectErr: false,
 		},
 		{
@@ -947,8 +954,8 @@ func TestCheckMergeLabels(t *testing.T) {
 					Nodes []struct{ Name githubql.String }
 				}{Nodes: []struct{ Name githubql.String }{{Name: githubql.String(squashLabel)}}},
 			},
-			method:    config.MergeMerge,
-			expected:  config.MergeSquash,
+			method:    keeper.MergeMerge,
+			expected:  keeper.MergeSquash,
 			expectErr: false,
 		},
 		{
@@ -961,8 +968,8 @@ func TestCheckMergeLabels(t *testing.T) {
 					{Name: githubql.String(rebaseLabel)}},
 				},
 			},
-			method:    config.MergeMerge,
-			expected:  config.MergeSquash,
+			method:    keeper.MergeMerge,
+			expected:  keeper.MergeSquash,
 			expectErr: true,
 		},
 	}
@@ -1333,7 +1340,7 @@ func TestTakeAction(t *testing.T) {
 			sp := subpool{
 				log:        logrus.WithField("component", "keeper"),
 				presubmits: tc.presubmits,
-				cc:         &config.KeeperContextPolicy{},
+				cc:         &keeper.ContextPolicy{},
 				org:        "o",
 				repo:       "r",
 				branch:     "master",
@@ -1432,7 +1439,7 @@ func TestTakeAction(t *testing.T) {
 						fgc.combinedStatus[pjSha][activity.Spec.Context].status)
 				}
 				numCreated++
-				if activity.Spec.Type == config.BatchJob {
+				if activity.Spec.Type == job.BatchJob {
 					batchJobs = append(batchJobs, activity)
 				}
 			}
@@ -1688,8 +1695,8 @@ func TestSync(t *testing.T) {
 			ca := &config.Agent{}
 			ca.Set(&config.Config{
 				ProwConfig: config.ProwConfig{
-					Keeper: config.Keeper{
-						Queries:            []config.KeeperQuery{{}},
+					Keeper: keeper.Config{
+						Queries:            []keeper.Query{{}},
 						MaxGoroutines:      4,
 						StatusUpdatePeriod: time.Second * 0,
 					},
@@ -1754,7 +1761,7 @@ func TestFilterSubpool(t *testing.T) {
 	}
 
 	trueVar := true
-	cc := &config.KeeperContextPolicy{
+	cc := &keeper.ContextPolicy{
 		RequiredContexts:    []string{"pj-a", "pj-b", "other-a"},
 		OptionalContexts:    []string{"keeper", "pj-c"},
 		SkipUnknownContexts: &trueVar,
@@ -2083,7 +2090,7 @@ func TestIsPassing(t *testing.T) {
 	testCases := []struct {
 		name             string
 		passing          bool
-		config           config.KeeperContextPolicy
+		config           keeper.ContextPolicy
 		combinedContexts map[string]commitStatus
 	}{
 		{
@@ -2099,7 +2106,7 @@ func TestIsPassing(t *testing.T) {
 		{
 			name:    "passing (trust combined status)",
 			passing: true,
-			config: config.KeeperContextPolicy{
+			config: keeper.ContextPolicy{
 				RequiredContexts:    []string{"c1", "c2", "c3"},
 				SkipUnknownContexts: &no,
 			},
@@ -2108,7 +2115,7 @@ func TestIsPassing(t *testing.T) {
 		{
 			name:    "failing because of missing required check c3",
 			passing: false,
-			config: config.KeeperContextPolicy{
+			config: keeper.ContextPolicy{
 				RequiredContexts: []string{"c1", "c2", "c3"},
 			},
 			combinedContexts: map[string]commitStatus{"c1": toCommitStatus(success, ""), "c2": toCommitStatus(success, ""), statusContext: toCommitStatus(failure, "")},
@@ -2117,7 +2124,7 @@ func TestIsPassing(t *testing.T) {
 			name:             "failing because of failed context c2",
 			passing:          false,
 			combinedContexts: map[string]commitStatus{"c1": toCommitStatus(success, ""), "c2": toCommitStatus(failure, "")},
-			config: config.KeeperContextPolicy{
+			config: keeper.ContextPolicy{
 				RequiredContexts: []string{"c1", "c2", "c3"},
 				OptionalContexts: []string{"c4"},
 			},
@@ -2127,7 +2134,7 @@ func TestIsPassing(t *testing.T) {
 			passing: true,
 
 			combinedContexts: map[string]commitStatus{"c1": toCommitStatus(success, ""), "c2": toCommitStatus(success, ""), "c3": toCommitStatus(success, ""), "c4": toCommitStatus(failure, "")},
-			config: config.KeeperContextPolicy{
+			config: keeper.ContextPolicy{
 				RequiredContexts: []string{"c1", "c2", "c3"},
 				OptionalContexts: []string{"c4"},
 			},
@@ -2135,7 +2142,7 @@ func TestIsPassing(t *testing.T) {
 		{
 			name:    "skipping unknown contexts - failing because of missing required context c3",
 			passing: false,
-			config: config.KeeperContextPolicy{
+			config: keeper.ContextPolicy{
 				RequiredContexts:    []string{"c1", "c2", "c3"},
 				SkipUnknownContexts: &yes,
 			},
@@ -2145,7 +2152,7 @@ func TestIsPassing(t *testing.T) {
 			name:             "skipping unknown contexts - failing because c2 is failing",
 			passing:          false,
 			combinedContexts: map[string]commitStatus{"c1": toCommitStatus(success, ""), "c2": toCommitStatus(failure, "")},
-			config: config.KeeperContextPolicy{
+			config: keeper.ContextPolicy{
 				RequiredContexts:    []string{"c1", "c2"},
 				OptionalContexts:    []string{"c4"},
 				SkipUnknownContexts: &yes,
@@ -2155,7 +2162,7 @@ func TestIsPassing(t *testing.T) {
 			name:             "skipping unknown contexts - passing because c4 is optional",
 			passing:          true,
 			combinedContexts: map[string]commitStatus{"c1": toCommitStatus(success, ""), "c2": toCommitStatus(success, ""), "c3": toCommitStatus(success, ""), "c4": toCommitStatus(failure, "")},
-			config: config.KeeperContextPolicy{
+			config: keeper.ContextPolicy{
 				RequiredContexts:    []string{"c1", "c3"},
 				OptionalContexts:    []string{"c4"},
 				SkipUnknownContexts: &yes,
@@ -2166,7 +2173,7 @@ func TestIsPassing(t *testing.T) {
 			passing: true,
 
 			combinedContexts: map[string]commitStatus{"c1": toCommitStatus(success, ""), "c2": toCommitStatus(success, ""), "c3": toCommitStatus(success, ""), "c4": toCommitStatus(failure, ""), "c5": toCommitStatus(failure, "")},
-			config: config.KeeperContextPolicy{
+			config: keeper.ContextPolicy{
 				RequiredContexts:    []string{"c1", "c3"},
 				OptionalContexts:    []string{"c4"},
 				SkipUnknownContexts: &yes,
@@ -2436,7 +2443,7 @@ func TestPresubmitsByPull(t *testing.T) {
 		c.changedFiles.prune()
 		// for equality we need to clear the compiled regexes
 		for _, jobs := range presubmits {
-			job.ClearCompiledRegexes(jobs)
+			clearCompiledRegexes(jobs)
 		}
 		if !equality.Semantic.DeepEqual(presubmits, tc.expectedPresubmits) {
 			t.Errorf("got incorrect presubmit mapping: %v\n", diff.ObjectReflectDiff(tc.expectedPresubmits, presubmits))
@@ -2463,13 +2470,13 @@ func TestPrepareMergeDetails(t *testing.T) {
 
 	testCases := []struct {
 		name        string
-		tpl         config.KeeperMergeCommitTemplate
+		tpl         keeper.MergeCommitTemplate
 		pr          PullRequest
-		mergeMethod config.PullRequestMergeType
+		mergeMethod keeper.PullRequestMergeType
 		expected    scmprovider.MergeDetails
 	}{{
 		name:        "No commit template",
-		tpl:         config.KeeperMergeCommitTemplate{},
+		tpl:         keeper.MergeCommitTemplate{},
 		pr:          pr,
 		mergeMethod: "merge",
 		expected: scmprovider.MergeDetails{
@@ -2478,7 +2485,7 @@ func TestPrepareMergeDetails(t *testing.T) {
 		},
 	}, {
 		name: "No commit template fields",
-		tpl: config.KeeperMergeCommitTemplate{
+		tpl: keeper.MergeCommitTemplate{
 			Title: nil,
 			Body:  nil,
 		},
@@ -2490,7 +2497,7 @@ func TestPrepareMergeDetails(t *testing.T) {
 		},
 	}, {
 		name: "Static commit template",
-		tpl: config.KeeperMergeCommitTemplate{
+		tpl: keeper.MergeCommitTemplate{
 			Title: getTemplate("CommitTitle", "static title"),
 			Body:  getTemplate("CommitBody", "static body"),
 		},
@@ -2504,7 +2511,7 @@ func TestPrepareMergeDetails(t *testing.T) {
 		},
 	}, {
 		name: "Commit template uses PullRequest fields",
-		tpl: config.KeeperMergeCommitTemplate{
+		tpl: keeper.MergeCommitTemplate{
 			Title: getTemplate("CommitTitle", "{{ .Number }}: {{ .Title }}"),
 			Body:  getTemplate("CommitBody", "{{ .HeadRefOID }} - {{ .Body }}"),
 		},
@@ -2518,7 +2525,7 @@ func TestPrepareMergeDetails(t *testing.T) {
 		},
 	}, {
 		name: "Commit template uses nonexistent fields",
-		tpl: config.KeeperMergeCommitTemplate{
+		tpl: keeper.MergeCommitTemplate{
 			Title: getTemplate("CommitTitle", "{{ .Hello }}"),
 			Body:  getTemplate("CommitBody", "{{ .World }}"),
 		},
@@ -2579,7 +2586,7 @@ func TestAccumulateReturnsCorrectMissingTests(t *testing.T) {
 			}},
 			pjs: []v1alpha1.LighthouseJob{{
 				Spec: v1alpha1.LighthouseJobSpec{
-					Type: config.PresubmitJob,
+					Type: job.PresubmitJob,
 					Refs: &v1alpha1.Refs{
 						Pulls: []v1alpha1.Pull{{
 							Number: 1,
@@ -2602,7 +2609,7 @@ func TestAccumulateReturnsCorrectMissingTests(t *testing.T) {
 			}},
 			pjs: []v1alpha1.LighthouseJob{{
 				Spec: v1alpha1.LighthouseJobSpec{
-					Type: config.PresubmitJob,
+					Type: job.PresubmitJob,
 					Refs: &v1alpha1.Refs{
 						Pulls: []v1alpha1.Pull{{
 							Number: 1,
@@ -2625,7 +2632,7 @@ func TestAccumulateReturnsCorrectMissingTests(t *testing.T) {
 			pjs: []v1alpha1.LighthouseJob{
 				{
 					Spec: v1alpha1.LighthouseJobSpec{
-						Type: config.PresubmitJob,
+						Type: job.PresubmitJob,
 						Refs: &v1alpha1.Refs{
 							Pulls: []v1alpha1.Pull{{
 								Number: 1,
@@ -2638,7 +2645,7 @@ func TestAccumulateReturnsCorrectMissingTests(t *testing.T) {
 				},
 				{
 					Spec: v1alpha1.LighthouseJobSpec{
-						Type: config.PresubmitJob,
+						Type: job.PresubmitJob,
 						Refs: &v1alpha1.Refs{
 							Pulls: []v1alpha1.Pull{{
 								Number: 1,
@@ -2651,7 +2658,7 @@ func TestAccumulateReturnsCorrectMissingTests(t *testing.T) {
 				},
 				{
 					Spec: v1alpha1.LighthouseJobSpec{
-						Type: config.PresubmitJob,
+						Type: job.PresubmitJob,
 						Refs: &v1alpha1.Refs{
 							Pulls: []v1alpha1.Pull{{
 								Number: 1,
@@ -2691,7 +2698,7 @@ func TestAccumulateReturnsCorrectMissingTests(t *testing.T) {
 			pjs: []v1alpha1.LighthouseJob{
 				{
 					Spec: v1alpha1.LighthouseJobSpec{
-						Type: config.PresubmitJob,
+						Type: job.PresubmitJob,
 						Refs: &v1alpha1.Refs{
 							Pulls: []v1alpha1.Pull{{
 								Number: 1,
@@ -2704,7 +2711,7 @@ func TestAccumulateReturnsCorrectMissingTests(t *testing.T) {
 				},
 				{
 					Spec: v1alpha1.LighthouseJobSpec{
-						Type: config.PresubmitJob,
+						Type: job.PresubmitJob,
 						Refs: &v1alpha1.Refs{
 							Pulls: []v1alpha1.Pull{{
 								Number: 1,
@@ -2717,7 +2724,7 @@ func TestAccumulateReturnsCorrectMissingTests(t *testing.T) {
 				},
 				{
 					Spec: v1alpha1.LighthouseJobSpec{
-						Type: config.PresubmitJob,
+						Type: job.PresubmitJob,
 						Refs: &v1alpha1.Refs{
 							Pulls: []v1alpha1.Pull{{
 								Number: 1,
@@ -2730,7 +2737,7 @@ func TestAccumulateReturnsCorrectMissingTests(t *testing.T) {
 				},
 				{
 					Spec: v1alpha1.LighthouseJobSpec{
-						Type: config.PresubmitJob,
+						Type: job.PresubmitJob,
 						Refs: &v1alpha1.Refs{
 							Pulls: []v1alpha1.Pull{{
 								Number: 2,
@@ -2743,7 +2750,7 @@ func TestAccumulateReturnsCorrectMissingTests(t *testing.T) {
 				},
 				{
 					Spec: v1alpha1.LighthouseJobSpec{
-						Type: config.PresubmitJob,
+						Type: job.PresubmitJob,
 						Refs: &v1alpha1.Refs{
 							Pulls: []v1alpha1.Pull{{
 								Number: 2,
@@ -2756,7 +2763,7 @@ func TestAccumulateReturnsCorrectMissingTests(t *testing.T) {
 				},
 				{
 					Spec: v1alpha1.LighthouseJobSpec{
-						Type: config.PresubmitJob,
+						Type: job.PresubmitJob,
 						Refs: &v1alpha1.Refs{
 							Pulls: []v1alpha1.Pull{{
 								Number: 2,
@@ -2809,16 +2816,16 @@ func TestAccumulateReturnsCorrectMissingTests(t *testing.T) {
 }
 
 func TestReposToQueries(t *testing.T) {
-	firstQuery := config.KeeperQuery{
+	firstQuery := keeper.Query{
 		Repos:  []string{"a", "b"},
 		Labels: []string{"first-label"},
 	}
-	secondQuery := config.KeeperQuery{
+	secondQuery := keeper.Query{
 		Repos:  []string{"b", "c"},
 		Labels: []string{"second-label"},
 	}
 
-	queries := config.KeeperQueries{
+	queries := keeper.Queries{
 		firstQuery,
 		secondQuery,
 	}
diff --git a/pkg/keeper/status_test.go b/pkg/keeper/status_test.go
index 11ca60ca..ae21b6e1 100644
--- a/pkg/keeper/status_test.go
+++ b/pkg/keeper/status_test.go
@@ -21,6 +21,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/jenkins-x/lighthouse/pkg/config/keeper"
 	"github.com/jenkins-x/lighthouse/pkg/keeper/blockers"
 	"github.com/jenkins-x/lighthouse/pkg/scmprovider"
 	githubql "github.com/shurcooL/githubv4"
@@ -209,7 +210,7 @@ func TestExpectedStatus(t *testing.T) {
 
 	for _, tc := range testcases {
 		t.Logf("Test Case: %q\n", tc.name)
-		secondQuery := config.KeeperQuery{
+		secondQuery := keeper.Query{
 			Orgs:      []string{""},
 			Labels:    []string{"1", "2", "3", "4", "5", "6", "7"}, // lots of requirements
 			Milestone: "v1.0",
@@ -218,8 +219,8 @@ func TestExpectedStatus(t *testing.T) {
 			secondQuery.ExcludedBranches = tc.branchExcludeList
 			secondQuery.IncludedBranches = tc.branchIncludeList
 		}
-		queriesByRepo := config.KeeperQueries{
-			config.KeeperQuery{
+		queriesByRepo := keeper.Queries{
+			keeper.Query{
 				Orgs:             []string{""},
 				ExcludedBranches: tc.branchExcludeList,
 				IncludedBranches: tc.branchIncludeList,
@@ -274,7 +275,7 @@ func TestExpectedStatus(t *testing.T) {
 		}
 		blocks.Repo[blockers.OrgRepo{Org: "", Repo: ""}] = items
 
-		state, desc := expectedStatus(queriesByRepo, &pr, pool, &config.KeeperContextPolicy{}, blocks, "fake")
+		state, desc := expectedStatus(queriesByRepo, &pr, pool, &keeper.ContextPolicy{}, blocks, "fake")
 		if state != tc.state {
 			t.Errorf("Expected status state %q, but got %q.", string(tc.state), string(state))
 		}
@@ -409,26 +410,26 @@ func TestTargetUrl(t *testing.T) {
 	testcases := []struct {
 		name   string
 		pr     *PullRequest
-		config config.Keeper
+		config keeper.Config
 
 		expectedURL string
 	}{
 		{
 			name:        "no config",
 			pr:          &PullRequest{},
-			config:      config.Keeper{},
+			config:      keeper.Config{},
 			expectedURL: "",
 		},
 		{
 			name:        "keeper overview config",
 			pr:          &PullRequest{},
-			config:      config.Keeper{TargetURL: "tide.com"},
+			config:      keeper.Config{TargetURL: "tide.com"},
 			expectedURL: "tide.com",
 		},
 		{
 			name:        "PR dashboard config and overview config",
 			pr:          &PullRequest{},
-			config:      config.Keeper{TargetURL: "tide.com", PRStatusBaseURL: "pr.status.com"},
+			config:      keeper.Config{TargetURL: "tide.com", PRStatusBaseURL: "pr.status.com"},
 			expectedURL: "tide.com",
 		},
 		{
@@ -442,7 +443,7 @@ func TestTargetUrl(t *testing.T) {
 				},
 				HeadRefName: "head",
 			},
-			config:      config.Keeper{PRStatusBaseURL: "pr.status.com"},
+			config:      keeper.Config{PRStatusBaseURL: "pr.status.com"},
 			expectedURL: "pr.status.com?query=is%3Apr+repo%3Aorg%2Frepo+author%3Aauthor+head%3Ahead",
 		},
 	}
diff --git a/pkg/plugins/skip/skip_test.go b/pkg/plugins/skip/skip_test.go
index ce1271e9..87223475 100644
--- a/pkg/plugins/skip/skip_test.go
+++ b/pkg/plugins/skip/skip_test.go
@@ -343,10 +343,11 @@ func TestSkipStatus(t *testing.T) {
 	}
 	for _, test := range tests {
 		t.Run(test.name, func(t *testing.T) {
-			if err := job.SetPresubmitRegexes(test.presubmits); err != nil {
-				t.Fatalf("%s: could not set presubmit regexes: %v", test.name, err)
+			for i := range test.presubmits {
+				if err := test.presubmits[i].SetRegexes(); err != nil {
+					t.Fatalf("%s: could not set presubmit regexes: %v", test.name, err)
+				}
 			}
-
 			fspc := &fake.SCMClient{
 				IssueComments: make(map[int][]*scm.Comment),
 				PullRequests: map[int]*scm.PullRequest{
diff --git a/pkg/plugins/trigger/generic-comment_test.go b/pkg/plugins/trigger/generic-comment_test.go
index 743a6066..b368e5d6 100644
--- a/pkg/plugins/trigger/generic-comment_test.go
+++ b/pkg/plugins/trigger/generic-comment_test.go
@@ -1048,8 +1048,10 @@ func TestRetestFilter(t *testing.T) {
 			if len(testCase.presubmits) != len(testCase.expected) {
 				t.Fatalf("%s: have %d presubmits but only %d expected filter outputs", testCase.name, len(testCase.presubmits), len(testCase.expected))
 			}
-			if err := job.SetPresubmitRegexes(testCase.presubmits); err != nil {
-				t.Fatalf("%s: could not set presubmit regexes: %v", testCase.name, err)
+			for i := range testCase.presubmits {
+				if err := testCase.presubmits[i].SetRegexes(); err != nil {
+					t.Fatalf("%s: could not set presubmit regexes: %v", testCase.name, err)
+				}
 			}
 			filter := jobutil.RetestFilter(testCase.failedContexts, testCase.allContexts)
 			for i, presubmit := range testCase.presubmits {
diff --git a/pkg/repoconfig/merge/merge_test.go b/pkg/repoconfig/merge/merge_test.go
index ab9666a4..e3837640 100644
--- a/pkg/repoconfig/merge/merge_test.go
+++ b/pkg/repoconfig/merge/merge_test.go
@@ -8,6 +8,7 @@ import (
 
 	"github.com/google/go-cmp/cmp"
 	"github.com/jenkins-x/lighthouse/pkg/config"
+	"github.com/jenkins-x/lighthouse/pkg/config/keeper"
 	"github.com/jenkins-x/lighthouse/pkg/plugins"
 	"github.com/jenkins-x/lighthouse/pkg/repoconfig"
 	"github.com/jenkins-x/lighthouse/pkg/repoconfig/merge"
@@ -52,7 +53,7 @@ func TestMergeRepositoryConfig(t *testing.T) {
 						},
 					},
 					Keeper: &repoconfig.Keeper{
-						Query: &config.KeeperQuery{
+						Query: &keeper.Query{
 							Labels:        []string{"mylabel"},
 							MissingLabels: []string{"foo", "bar"},
 						},
diff --git a/pkg/repoowners/repoowners_test.go b/pkg/repoowners/repoowners_test.go
index b7f940aa..16e3927b 100644
--- a/pkg/repoowners/repoowners_test.go
+++ b/pkg/repoowners/repoowners_test.go
@@ -24,6 +24,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/jenkins-x/lighthouse/pkg/config/lighthouse"
 	"github.com/jenkins-x/lighthouse/pkg/git/localgit"
 	"github.com/jenkins-x/lighthouse/pkg/scmprovider/fake"
 	"github.com/sirupsen/logrus"
@@ -158,7 +159,7 @@ func getTestClient(
 			},
 			config: &prowConf.Config{
 				ProwConfig: prowConf.ProwConfig{
-					OwnersDirExcludes: &prowConf.OwnersDirExcludes{
+					OwnersDirExcludes: &lighthouse.OwnersDirExcludes{
 						Repos:   ownersDirExcludesByRepo,
 						Default: ownersDirExcludesDefault,
 					},
diff --git a/pkg/scmprovider/reporter/reporter_test.go b/pkg/scmprovider/reporter/reporter_test.go
index 96d54d87..6627e098 100644
--- a/pkg/scmprovider/reporter/reporter_test.go
+++ b/pkg/scmprovider/reporter/reporter_test.go
@@ -22,7 +22,7 @@ import (
 
 	"github.com/jenkins-x/go-scm/scm"
 	"github.com/jenkins-x/lighthouse/pkg/apis/lighthouse/v1alpha1"
-	"github.com/jenkins-x/lighthouse/pkg/config"
+	"github.com/jenkins-x/lighthouse/pkg/config/job"
 )
 
 func TestParsePRComment(t *testing.T) {
@@ -227,36 +227,36 @@ func TestShouldReport(t *testing.T) {
 	var testcases = []struct {
 		name       string
 		lhj        *v1alpha1.LighthouseJob
-		validTypes []config.PipelineKind
+		validTypes []job.PipelineKind
 		report     bool
 	}{
 		{
 			name: "should report presubmit job",
 			lhj: &v1alpha1.LighthouseJob{
 				Spec: v1alpha1.LighthouseJobSpec{
-					Type: config.PresubmitJob,
+					Type: job.PresubmitJob,
 				},
 			},
-			validTypes: []config.PipelineKind{config.PresubmitJob},
+			validTypes: []job.PipelineKind{job.PresubmitJob},
 			report:     true,
 		},
 		{
 			name: "should not report postsubmit job",
 			lhj: &v1alpha1.LighthouseJob{
 				Spec: v1alpha1.LighthouseJobSpec{
-					Type: config.PostsubmitJob,
+					Type: job.PostsubmitJob,
 				},
 			},
-			validTypes: []config.PipelineKind{config.PresubmitJob},
+			validTypes: []job.PipelineKind{job.PresubmitJob},
 		},
 		{
 			name: "should report postsubmit job if told to",
 			lhj: &v1alpha1.LighthouseJob{
 				Spec: v1alpha1.LighthouseJobSpec{
-					Type: config.PostsubmitJob,
+					Type: job.PostsubmitJob,
 				},
 			},
-			validTypes: []config.PipelineKind{config.PresubmitJob, config.PostsubmitJob},
+			validTypes: []job.PipelineKind{job.PresubmitJob, job.PostsubmitJob},
 			report:     true,
 		},
 	}