Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
9e77ea7
feat: add opslevel_campaign resource with full CRUD
jamescarr Apr 9, 2026
1917bd1
feat: register opslevel_campaign in provider and enable local SDK
jamescarr Apr 9, 2026
ef568bb
docs: add campaign resource documentation and examples
jamescarr Apr 9, 2026
4323673
feat: add opslevel_campaign and opslevel_campaigns data sources
jamescarr Apr 9, 2026
65586b6
feat: add start_date/target_date pair validation
jamescarr Apr 9, 2026
8361937
test: add mock terraform tests for campaign resource
jamescarr Apr 9, 2026
b5470d9
test: add remote integration test for campaign resource
jamescarr Apr 9, 2026
f77a198
chore: add changie changelog entry for campaign resource
jamescarr Apr 9, 2026
73cdff9
chore: update opslevel-go submodule with campaign CRUD methods
jamescarr Apr 13, 2026
9e17693
fix: pass dates directly in campaign create/update
jamescarr Apr 13, 2026
8133a0f
fix: create then schedule campaign in two API calls
jamescarr Apr 13, 2026
060a8ab
feat: add check_ids field to campaign resource
jamescarr Apr 13, 2026
0043e0f
fix: set CheckIds type in model to prevent DynamicPseudoType panic
jamescarr Apr 13, 2026
04865f7
feat: use checksCopyToCampaign for associating checks with campaigns
jamescarr Apr 13, 2026
395e4ac
fix: update SDK submodule with deletedId fix for campaign delete
jamescarr Apr 13, 2026
0f57158
fix: make check_ids create-only (OpsLevel API has no check removal)
jamescarr Apr 13, 2026
35b50c5
feat: reconcile campaign check_ids on update (add + remove)
jamescarr Apr 13, 2026
928fac4
fix: read campaign checks from API for proper state tracking
jamescarr Apr 13, 2026
ebbbbb1
extract DiffCheckIds, add unit tests, bump opslevel-go
jamescarr Apr 13, 2026
dabc6ee
code review fixes: conventions, docs, tests, and safety
jamescarr Apr 13, 2026
b2fd449
fix: use nullableID for filter_id to support unsetting
jamescarr Apr 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .changes/unreleased/added-campaign-resource.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
kind: Added
body: Added `opslevel_campaign` resource for managing OpsLevel campaigns as code, including scheduling support. Also added `opslevel_campaign` and `opslevel_campaigns` data sources.
41 changes: 41 additions & 0 deletions docs/data-sources/campaign.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
page_title: "opslevel_campaign Data Source - terraform-provider-opslevel"
subcategory: ""
description: |-
Campaign data source
---

# opslevel_campaign (Data Source)

Campaign data source

## Example Usage

```terraform
data "opslevel_campaign" "example" {
identifier = "Z2lkOi8vb3BzbGV2ZWwvQ2FtcGFpZ24vMTIz"
}

output "campaign_name" {
value = data.opslevel_campaign.example.name
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `identifier` (String) The id of the campaign to find.

### Read-Only

- `filter_id` (String) The ID of the filter applied to this campaign.
- `html_url` (String) The URL to the campaign in the OpsLevel UI.
- `id` (String) The ID of the campaign.
- `name` (String) The name of the campaign.
- `owner_id` (String) The ID of the team that owns this campaign.
- `project_brief` (String) The raw project brief of the campaign (Markdown).
- `start_date` (String) The start date of the campaign.
- `status` (String) The current status of the campaign.
- `target_date` (String) The target end date of the campaign.
48 changes: 48 additions & 0 deletions docs/data-sources/campaigns.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
page_title: "opslevel_campaigns Data Source - terraform-provider-opslevel"
subcategory: ""
description: |-
Campaign data sources
---

# opslevel_campaigns (Data Source)

Campaign data sources — lists all campaigns, optionally filtered by status.

## Example Usage

```terraform
data "opslevel_campaigns" "active" {
status = "in_progress"
}

output "active_campaign_names" {
value = [for c in data.opslevel_campaigns.active.campaigns : c.name]
}
```

<!-- schema generated by tfplugindocs -->
## Schema

### Optional

- `status` (String) Filter campaigns by status (draft, scheduled, in_progress, delayed, ended). Defaults to in_progress.

### Read-Only

- `campaigns` (List of Object) List of Campaign data sources (see [below for nested schema](#nestedatt--campaigns))

<a id="nestedatt--campaigns"></a>
### Nested Schema for `campaigns`

Read-Only:

- `filter_id` (String) The ID of the filter applied to this campaign.
- `html_url` (String) The URL to the campaign in the OpsLevel UI.
- `id` (String) The ID of the campaign.
- `name` (String) The name of the campaign.
- `owner_id` (String) The ID of the team that owns this campaign.
- `project_brief` (String) The raw project brief of the campaign (Markdown).
- `start_date` (String) The start date of the campaign.
- `status` (String) The current status of the campaign.
- `target_date` (String) The target end date of the campaign.
125 changes: 125 additions & 0 deletions docs/resources/campaign.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
---
page_title: "opslevel_campaign Resource - terraform-provider-opslevel"
subcategory: ""
description: |-
Campaign Resource
---

# opslevel_campaign (Resource)

Campaign Resource

Manages an OpsLevel campaign. Campaigns allow you to roll out changes across your engineering organization that require orchestrated effort across multiple teams.

## Example Usage

### Draft campaign (no schedule)

```terraform
data "opslevel_team" "platform" {
alias = "platform"
}

data "opslevel_filter" "tier_1" {
filter {
field = "name"
value = "Tier 1 Services"
}
}

resource "opslevel_campaign" "upgrade_rails" {
name = "Upgrade to Rails 7"
owner_id = data.opslevel_team.platform.id
filter_id = data.opslevel_filter.tier_1.id

project_brief = <<-EOT
## Overview
All Rails services must upgrade to Rails 7 by end of Q3.

## What you need to do
1. Update your Gemfile to target Rails 7
2. Run the Rails upgrade checklist
3. Verify all tests pass
EOT
}
```

### Campaign with checks

```terraform
resource "opslevel_campaign" "soc2_compliance" {
name = "SOC2 Compliance Rollout"
owner_id = data.opslevel_team.platform.id
filter_id = data.opslevel_filter.tier_1.id

check_ids = [
opslevel_check_custom_event.secret_rotation.id,
opslevel_check_custom_event.dependency_scanning.id,
]

project_brief = <<-EOT
All Tier 1 services must pass SOC2 checks by end of Q3.
EOT
}
```

### Scheduled campaign

```terraform
resource "opslevel_campaign" "python_upgrade" {
name = "Upgrade to Python 3.12"
owner_id = data.opslevel_team.platform.id
filter_id = data.opslevel_filter.tier_1.id

start_date = "2026-07-01"
target_date = "2026-09-30"

project_brief = <<-EOT
Upgrade all Python services to 3.12 for security and performance.
EOT
}
```

## Check Management

The `check_ids` attribute accepts a list of rubric check IDs. On create, these checks are copied into the campaign. On update, the provider reconciles the list — adding new checks and removing stale ones to match the desired configuration.

Terraform detects drift: if a check is removed from the campaign outside of Terraform (e.g. via the UI), the next `terraform plan` will show it as needing to be re-added.

~> **Note:** The OpsLevel API copies checks into campaigns as separate instances with different IDs but the same name. The provider matches rubric checks to campaign checks by name. If two rubric checks share the same name, the provider may not be able to distinguish them, which can lead to incorrect removal. Ensure rubric check names are unique when using `check_ids`.

## Schedule Management

Setting both `start_date` and `target_date` schedules the campaign. Removing both fields unschedules it back to draft status.

Both fields must be set together — setting only one will result in an error.

<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `name` (String) The name of the campaign.
- `owner_id` (String) The ID of the team that owns this campaign.

### Optional

- `check_ids` (List of String) List of rubric check IDs to associate with this campaign. On create, checks are copied into the campaign. On update, checks are added or removed to match the desired set.
- `filter_id` (String) The ID of the filter applied to this campaign.
- `project_brief` (String) The project brief of the campaign (Markdown).
- `start_date` (String) The start date of the campaign (YYYY-MM-DD). Setting both start_date and target_date schedules the campaign.
- `target_date` (String) The target end date of the campaign (YYYY-MM-DD). Setting both start_date and target_date schedules the campaign.

### Read-Only

- `html_url` (String) The URL to the campaign in the OpsLevel UI.
- `id` (String) The ID of the campaign.
- `status` (String) The current status of the campaign (draft, scheduled, in_progress, delayed, ended).

## Import

Import is supported using the following syntax:

```shell
terraform import opslevel_campaign.example Z2lkOi8vb3BzbGV2ZWwvQ2FtcGFpZ24vMTIz
```
1 change: 1 addition & 0 deletions examples/resources/opslevel_campaign/import.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
terraform import opslevel_campaign.example Z2lkOi8vb3BzbGV2ZWwvQ2FtcGFpZ24vMTIz
29 changes: 29 additions & 0 deletions examples/resources/opslevel_campaign/resource.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
data "opslevel_team" "platform" {
alias = "platform"
}

data "opslevel_filter" "tier_1" {
filter {
field = "name"
value = "Tier 1 Services"
}
}

resource "opslevel_campaign" "upgrade_rails" {
name = "Upgrade to Rails 7"
owner_id = data.opslevel_team.platform.id
filter_id = data.opslevel_filter.tier_1.id

start_date = "2026-07-01"
target_date = "2026-09-30"

project_brief = <<-EOT
## Overview
All Rails services must upgrade to Rails 7 by end of Q3.

## What you need to do
1. Update your Gemfile to target Rails 7
2. Run the Rails upgrade checklist
3. Verify all tests pass
EOT
}
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,8 @@ require (
gopkg.in/yaml.v3 v3.0.1 // indirect
)

// replace github.com/opslevel/opslevel-go/v2026 => ./submodules/opslevel-go
// TODO: Remove this replace directive before merging upstream.
// It points at the local submodule for fork development; once
// OpsLevel/opslevel-go merges the campaign CRUD PR (#611) and
// cuts a release, switch to the published version.
replace github.com/opslevel/opslevel-go/v2026 => ./submodules/opslevel-go
2 changes: 0 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -104,8 +104,6 @@ github.com/oklog/run v1.2.0 h1:O8x3yXwah4A73hJdlrwo/2X6J62gE5qTMusH0dvz60E=
github.com/oklog/run v1.2.0/go.mod h1:mgDbKRSwPhJfesJ4PntqFUbKQRZ50NgmZTSPlFA0YFk=
github.com/opslevel/moredefaults v0.0.0-20240529152742-17d1318a3c12 h1:OQZ3W8kbyCcdS8QUWFTnZd6xtdkfhdckc7Paro7nXio=
github.com/opslevel/moredefaults v0.0.0-20240529152742-17d1318a3c12/go.mod h1:g2GSXVP6LO+5+AIsnMRPN+BeV86OXuFRTX7HXCDtYeI=
github.com/opslevel/opslevel-go/v2026 v2026.3.6 h1:XdAmWIrzKYUTOHIHV42B5bVTBzU2j4OQzhDiaQ75m7I=
github.com/opslevel/opslevel-go/v2026 v2026.3.6/go.mod h1:FClwt6mxlVa2f4l+z/dUi5u8eYEiNJuSOWMhB6Y9JqI=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
Expand Down
Loading