From a35da93709382622c6aa4753d5a7cbc07a10c1c3 Mon Sep 17 00:00:00 2001 From: Bobby Ho Date: Mon, 5 Oct 2026 10:49:33 -0700 Subject: [PATCH 1/4] feat: add coder_script_order data source Templates have no way to declare script ordering because the provider does not define the data source Coder already reads. This adds it. - New data source with one or more rule blocks. Each rule has run and after selector lists, an optional requires (default success) and an optional phase (start or stop, no default). - Validation covers structure and enum values only: at least one rule, at least one item in each list, no empty strings, requires in {success, completion}, phase in {start, stop}. Selector strings are passed through unchanged for Coder to resolve after plan. - phase has no default on purpose. Coder infers the phase when it is unset and warns when inference filtered a module selector. A default would hide both. - Tests: schema test over four rules, eleven rejection cases, and a test that pins the attribute names Coder decodes from the plan JSON. - Example and generated docs. The description notes that Terraform does not check selectors, that values must be known at plan time, and that older Coder versions ignore the data source. - coder_script description now says scripts run in parallel unless a coder_script_order orders them. Replaces draft #536, which predates the phase attribute. Refs https://linear.app/codercom/issue/PLAT-542 --- docs/data-sources/script_order.md | 68 +++++ docs/resources/script.md | 4 +- .../coder_script_order/data-source.tf | 22 ++ provider/examples_test.go | 1 + provider/provider.go | 1 + provider/script.go | 2 +- provider/script_order.go | 100 +++++++ provider/script_order_test.go | 248 ++++++++++++++++++ 8 files changed, 443 insertions(+), 3 deletions(-) create mode 100644 docs/data-sources/script_order.md create mode 100644 examples/data-sources/coder_script_order/data-source.tf create mode 100644 provider/script_order.go create mode 100644 provider/script_order_test.go diff --git a/docs/data-sources/script_order.md b/docs/data-sources/script_order.md new file mode 100644 index 00000000..8571ea8c --- /dev/null +++ b/docs/data-sources/script_order.md @@ -0,0 +1,68 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "coder_script_order Data Source - terraform-provider-coder" +subcategory: "" +description: |- + Use this data source to declare ordering constraints between coder_script resources on the same agent. It does not run scripts. Every script selected by run waits for every script selected by after. + Selectors are Terraform address strings (coder_script.name, coder_script.name[0], coder_script.name["key"], module.name), resolved by Coder relative to the module that declares the data source. Terraform and terraform validate do not check them; Coder reports unknown or unsupported selectors when the template is imported. All values must be known at plan time, and the data source needs no depends_on. + Ordering is enforced only by Coder versions that support it. Older versions ignore the data source and run scripts concurrently. +--- + +# coder_script_order (Data Source) + +Use this data source to declare ordering constraints between `coder_script` resources on the same agent. It does not run scripts. Every script selected by `run` waits for every script selected by `after`. + +Selectors are Terraform address strings (`coder_script.name`, `coder_script.name[0]`, `coder_script.name["key"]`, `module.name`), resolved by Coder relative to the module that declares the data source. Terraform and `terraform validate` do not check them; Coder reports unknown or unsupported selectors when the template is imported. All values must be known at plan time, and the data source needs no `depends_on`. + +Ordering is enforced only by Coder versions that support it. Older versions ignore the data source and run scripts concurrently. + +## Example Usage + +```terraform +provider "coder" {} + +data "coder_script_order" "startup_dependencies" { + rule { + run = ["coder_script.install_tools"] + after = ["coder_script.clone_repo"] + } + + rule { + run = ["coder_script.configure_workspace"] + after = ["module.bootstrap"] + requires = "completion" + } + + # module.cleanup has both start and stop scripts. phase keeps only the + # stop scripts and suppresses the inferred-phase warning. + rule { + run = ["coder_script.archive_logs"] + after = ["module.cleanup"] + phase = "stop" + } +} +``` + + +## Schema + +### Required + +- `rule` (Block List, Min: 1) One or more ordering rules. Rules from every `coder_script_order` in the template are merged into one graph per agent. (see [below for nested schema](#nestedblock--rule)) + +### Read-Only + +- `id` (String) The ID of this resource. + + +### Nested Schema for `rule` + +Required: + +- `after` (List of String) Selectors for the scripts they wait for. +- `run` (List of String) Selectors for the scripts that wait. + +Optional: + +- `phase` (String) Which lifecycle the rule applies to: `start` or `stop`. Omit it to let Coder infer the phase from the selected scripts. Set it when a `module` selector contains both start and stop scripts, so Coder filters the module to this phase without warning. +- `requires` (String) The outcome required from every `after` script before a `run` script starts. `success` (the default) skips the `run` scripts if any `after` script fails, times out or is skipped. `completion` runs them once every `after` script has finished, whatever the outcome. diff --git a/docs/resources/script.md b/docs/resources/script.md index 67a594ab..134b2e27 100644 --- a/docs/resources/script.md +++ b/docs/resources/script.md @@ -3,12 +3,12 @@ page_title: "coder_script Resource - terraform-provider-coder" subcategory: "" description: |- - Use this resource to run a script from an agent. When multiple scripts are assigned to the same agent, they are executed in parallel. + Use this resource to run a script on a workspace agent (coder_agent). When multiple scripts are assigned to the same workspace agent, they are executed in parallel unless a coder_script_order data source orders them. --- # coder_script (Resource) -Use this resource to run a script from an agent. When multiple scripts are assigned to the same agent, they are executed in parallel. +Use this resource to run a script on a workspace agent (`coder_agent`). When multiple scripts are assigned to the same workspace agent, they are executed in parallel unless a `coder_script_order` data source orders them. ## Example Usage diff --git a/examples/data-sources/coder_script_order/data-source.tf b/examples/data-sources/coder_script_order/data-source.tf new file mode 100644 index 00000000..deb3300d --- /dev/null +++ b/examples/data-sources/coder_script_order/data-source.tf @@ -0,0 +1,22 @@ +provider "coder" {} + +data "coder_script_order" "startup_dependencies" { + rule { + run = ["coder_script.install_tools"] + after = ["coder_script.clone_repo"] + } + + rule { + run = ["coder_script.configure_workspace"] + after = ["module.bootstrap"] + requires = "completion" + } + + # module.cleanup has both start and stop scripts. phase keeps only the + # stop scripts and suppresses the inferred-phase warning. + rule { + run = ["coder_script.archive_logs"] + after = ["module.cleanup"] + phase = "stop" + } +} diff --git a/provider/examples_test.go b/provider/examples_test.go index 1d17b1ba..49b19498 100644 --- a/provider/examples_test.go +++ b/provider/examples_test.go @@ -14,6 +14,7 @@ func TestExamples(t *testing.T) { for _, testDir := range []string{ "coder_parameter", + "coder_script_order", "coder_workspace_tags", "coder_resources_monitoring", } { diff --git a/provider/provider.go b/provider/provider.go index 7e4451b8..0f245158 100644 --- a/provider/provider.go +++ b/provider/provider.go @@ -65,6 +65,7 @@ func New() *schema.Provider { "coder_workspace_owner": workspaceOwnerDataSource(), "coder_workspace_preset": workspacePresetDataSource(), "coder_task": taskDatasource(), + "coder_script_order": scriptOrderDataSource(), }, ResourcesMap: map[string]*schema.Resource{ "coder_agent": agentResource(), diff --git a/provider/script.go b/provider/script.go index b6feddde..d8511146 100644 --- a/provider/script.go +++ b/provider/script.go @@ -44,7 +44,7 @@ func scriptResource() *schema.Resource { return &schema.Resource{ SchemaVersion: 1, - Description: "Use this resource to run a script from an agent. When multiple scripts are assigned to the same agent, they are executed in parallel.", + Description: "Use this resource to run a script on a workspace agent (`coder_agent`). When multiple scripts are assigned to the same workspace agent, they are executed in parallel unless a `coder_script_order` data source orders them.", CreateContext: func(_ context.Context, rd *schema.ResourceData, _ interface{}) diag.Diagnostics { rd.SetId(uuid.NewString()) runOnStart, _ := rd.Get("run_on_start").(bool) diff --git a/provider/script_order.go b/provider/script_order.go new file mode 100644 index 00000000..601e72fa --- /dev/null +++ b/provider/script_order.go @@ -0,0 +1,100 @@ +package provider + +import ( + "context" + + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" +) + +// Coder's provisioner accepts the same values. The provider and Coder +// share no Go types, so the plan JSON is the contract between them. +const ( + scriptOrderRequiresSuccess = "success" + scriptOrderRequiresCompletion = "completion" + + scriptOrderPhaseStart = "start" + scriptOrderPhaseStop = "stop" +) + +func scriptOrderDataSource() *schema.Resource { + // Selector syntax is not checked here. Coder resolves selectors + // against the whole plan and reports unknown or unsupported ones at + // template import. A stricter check here could reject selectors that + // Coder accepts. + selectorSchema := func(description string) *schema.Schema { + return &schema.Schema{ + Type: schema.TypeList, + Description: description, + Required: true, + MinItems: 1, + Elem: &schema.Schema{ + Type: schema.TypeString, + ValidateFunc: validation.StringIsNotEmpty, + }, + } + } + + return &schema.Resource{ + SchemaVersion: 0, + + Description: "Use this data source to declare ordering constraints between `coder_script` " + + "resources on the same agent. It does not run scripts. Every script selected by " + + "`run` waits for every script selected by `after`.\n\n" + + "Selectors are Terraform address strings (`coder_script.name`, " + + "`coder_script.name[0]`, `coder_script.name[\"key\"]`, `module.name`), resolved " + + "by Coder relative to the module that declares the data source. Terraform and " + + "`terraform validate` do not check them; Coder reports unknown or unsupported " + + "selectors when the template is imported. All values must be known at plan time, " + + "and the data source needs no `depends_on`.\n\n" + + "Ordering is enforced only by Coder versions that support it. Older versions " + + "ignore the data source and run scripts concurrently.", + ReadContext: func(_ context.Context, rd *schema.ResourceData, _ any) diag.Diagnostics { + rd.SetId(uuid.NewString()) + return nil + }, + Schema: map[string]*schema.Schema{ + "rule": { + Type: schema.TypeList, + Description: "One or more ordering rules. Rules from every `coder_script_order` in the template are merged into one graph per agent.", + Required: true, + MinItems: 1, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "run": selectorSchema("Selectors for the scripts that wait."), + "after": selectorSchema("Selectors for the scripts they wait for."), + "requires": { + Type: schema.TypeString, + Description: "The outcome required from every `after` script before a `run` script starts. " + + "`success` (the default) skips the `run` scripts if any `after` script fails, times out or is skipped. " + + "`completion` runs them once every `after` script has finished, whatever the outcome.", + Optional: true, + Default: scriptOrderRequiresSuccess, + ValidateFunc: validation.StringInSlice([]string{ + scriptOrderRequiresSuccess, + scriptOrderRequiresCompletion, + }, false), + }, + "phase": { + Type: schema.TypeString, + Description: "Which lifecycle the rule applies to: `start` or `stop`. " + + "Omit it to let Coder infer the phase from the selected scripts. " + + "Set it when a `module` selector contains both start and stop scripts, " + + "so Coder filters the module to this phase without warning.", + Optional: true, + // No default on purpose. Coder infers the phase when + // this is unset and warns when inference filtered a + // module selector. A default would hide both. + ValidateFunc: validation.StringInSlice([]string{ + scriptOrderPhaseStart, + scriptOrderPhaseStop, + }, false), + }, + }, + }, + }, + }, + } +} diff --git a/provider/script_order_test.go b/provider/script_order_test.go new file mode 100644 index 00000000..5b504138 --- /dev/null +++ b/provider/script_order_test.go @@ -0,0 +1,248 @@ +package provider_test + +import ( + "maps" + "regexp" + "slices" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + "github.com/stretchr/testify/require" + + "github.com/coder/terraform-provider-coder/v2/provider" +) + +func TestScriptOrder(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" {} + data "coder_script_order" "startup" { + rule { + run = [ + "coder_script.install_tools", + "coder_script.configure_shell", + ] + after = [ + "coder_script.clone_repo", + "coder_script.authenticate", + ] + } + rule { + run = ["coder_script.setup[\"api\"]"] + after = ["module.bootstrap"] + requires = "completion" + } + rule { + run = ["module.application"] + after = ["module.checkout"] + phase = "stop" + } + rule { + run = ["not valid selector syntax"] + after = ["also not valid"] + } + } + `, + Check: func(state *terraform.State) error { + require.Len(t, state.Modules, 1) + order := state.Modules[0].Resources["data.coder_script_order.startup"] + require.NotNil(t, order) + require.NotEmpty(t, order.Primary.ID) + + attributes := order.Primary.Attributes + for key, expected := range map[string]string{ + "rule.#": "4", + "rule.0.run.#": "2", + "rule.0.run.0": "coder_script.install_tools", + "rule.0.run.1": "coder_script.configure_shell", + "rule.0.after.#": "2", + "rule.0.after.0": "coder_script.clone_repo", + "rule.0.after.1": "coder_script.authenticate", + "rule.0.requires": "success", + "rule.0.phase": "", + "rule.1.run.0": `coder_script.setup["api"]`, + "rule.1.after.0": "module.bootstrap", + "rule.1.requires": "completion", + "rule.1.phase": "", + "rule.2.run.0": "module.application", + "rule.2.after.0": "module.checkout", + "rule.2.requires": "success", + "rule.2.phase": "stop", + "rule.3.run.0": "not valid selector syntax", + "rule.3.after.0": "also not valid", + "rule.3.requires": "success", + } { + require.Equal(t, expected, attributes[key], key) + } + return nil + }, + }}, + }) +} + +func TestScriptOrderValidation(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + body string + expectError string + }{ + { + name: "NoRules", + body: "", + expectError: `Insufficient rule blocks`, + }, + { + name: "MissingRun", + body: ` + rule { + after = ["coder_script.clone_repo"] + } + `, + expectError: `The argument "run" is required, but no definition was found.`, + }, + { + name: "MissingAfter", + body: ` + rule { + run = ["coder_script.install_tools"] + } + `, + expectError: `The argument "after" is required, but no definition was found.`, + }, + { + name: "EmptyRun", + body: ` + rule { + run = [] + after = ["coder_script.clone_repo"] + } + `, + expectError: `Attribute rule.0.run requires 1 item minimum`, + }, + { + name: "EmptyAfter", + body: ` + rule { + run = ["coder_script.install_tools"] + after = [] + } + `, + expectError: `Attribute rule.0.after requires 1 item minimum`, + }, + { + name: "EmptyRunSelector", + body: ` + rule { + run = [""] + after = ["coder_script.clone_repo"] + } + `, + expectError: `expected "rule.0.run.0" to not be an empty string`, + }, + { + name: "EmptyAfterSelector", + body: ` + rule { + run = ["coder_script.install_tools"] + after = [""] + } + `, + expectError: `expected "rule.0.after.0" to not be an empty string`, + }, + { + name: "InvalidRequires", + body: ` + rule { + run = ["coder_script.install_tools"] + after = ["coder_script.clone_repo"] + requires = "starts" + } + `, + expectError: `expected rule.0.requires to be one of \["success" "completion"\], got starts`, + }, + { + // Pins what the SDK does with an explicit empty string. Coder + // treats "" as "success", so either outcome is safe; the test + // exists so an SDK upgrade cannot change it unnoticed. + name: "EmptyRequires", + body: ` + rule { + run = ["coder_script.install_tools"] + after = ["coder_script.clone_repo"] + requires = "" + } + `, + expectError: `expected rule.0.requires to be one of \["success" "completion"\], got `, + }, + { + name: "InvalidPhase", + body: ` + rule { + run = ["coder_script.install_tools"] + after = ["coder_script.clone_repo"] + phase = "both" + } + `, + expectError: `expected rule.0.phase to be one of \["start" "stop"\], got both`, + }, + { + name: "InvalidPhaseCase", + body: ` + rule { + run = ["coder_script.install_tools"] + after = ["coder_script.clone_repo"] + phase = "Start" + } + `, + expectError: `expected rule.0.phase to be one of \["start" "stop"\], got Start`, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + resource.Test(t, resource.TestCase{ + ProviderFactories: coderFactory(), + IsUnitTest: true, + Steps: []resource.TestStep{{ + Config: ` + provider "coder" {} + data "coder_script_order" "startup" { + ` + test.body + ` + } + `, + ExpectError: regexp.MustCompile(test.expectError), + }}, + }) + }) + } +} + +// Coder's provisioner decodes these names from the plan JSON +// (coder/coder provisioner/terraform/scriptorder.go). Renaming one here +// breaks ordering there without a compile error on either side. +func TestScriptOrderContract(t *testing.T) { + t.Parallel() + + ds := provider.New().DataSourcesMap["coder_script_order"] + require.NotNil(t, ds) + + rule, ok := ds.Schema["rule"].Elem.(*schema.Resource) + require.True(t, ok) + require.ElementsMatch(t, + []string{"run", "after", "requires", "phase"}, + slices.Collect(maps.Keys(rule.Schema)), + ) + require.Equal(t, "success", rule.Schema["requires"].Default) + require.Nil(t, rule.Schema["phase"].Default) +} From fe15cf0bea0e8065feed3ba18ce7b52ebbc38042 Mon Sep 17 00:00:00 2001 From: Bobby Ho Date: Tue, 6 Oct 2026 09:36:00 -0700 Subject: [PATCH 2/4] docs: name the Coder version that enforces coder_script_order Both descriptions now use the provider's standard callout form and name Coder v2.39 as the first version that orders scripts. Older versions ignore the data source. --- docs/data-sources/script_order.md | 4 ++-- docs/resources/script.md | 3 +++ provider/script.go | 2 +- provider/script_order.go | 4 ++-- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/data-sources/script_order.md b/docs/data-sources/script_order.md index 8571ea8c..d802f8dd 100644 --- a/docs/data-sources/script_order.md +++ b/docs/data-sources/script_order.md @@ -5,7 +5,7 @@ subcategory: "" description: |- Use this data source to declare ordering constraints between coder_script resources on the same agent. It does not run scripts. Every script selected by run waits for every script selected by after. Selectors are Terraform address strings (coder_script.name, coder_script.name[0], coder_script.name["key"], module.name), resolved by Coder relative to the module that declares the data source. Terraform and terraform validate do not check them; Coder reports unknown or unsupported selectors when the template is imported. All values must be known at plan time, and the data source needs no depends_on. - Ordering is enforced only by Coder versions that support it. Older versions ignore the data source and run scripts concurrently. + -> This data source is only available in Coder v2.39 and later. Older versions ignore it and run scripts concurrently. --- # coder_script_order (Data Source) @@ -14,7 +14,7 @@ Use this data source to declare ordering constraints between `coder_script` reso Selectors are Terraform address strings (`coder_script.name`, `coder_script.name[0]`, `coder_script.name["key"]`, `module.name`), resolved by Coder relative to the module that declares the data source. Terraform and `terraform validate` do not check them; Coder reports unknown or unsupported selectors when the template is imported. All values must be known at plan time, and the data source needs no `depends_on`. -Ordering is enforced only by Coder versions that support it. Older versions ignore the data source and run scripts concurrently. +-> This data source is only available in Coder v2.39 and later. Older versions ignore it and run scripts concurrently. ## Example Usage diff --git a/docs/resources/script.md b/docs/resources/script.md index 134b2e27..93118a0b 100644 --- a/docs/resources/script.md +++ b/docs/resources/script.md @@ -4,12 +4,15 @@ page_title: "coder_script Resource - terraform-provider-coder" subcategory: "" description: |- Use this resource to run a script on a workspace agent (coder_agent). When multiple scripts are assigned to the same workspace agent, they are executed in parallel unless a coder_script_order data source orders them. + -> Ordering with coder_script_order is only available in Coder v2.39 and later. --- # coder_script (Resource) Use this resource to run a script on a workspace agent (`coder_agent`). When multiple scripts are assigned to the same workspace agent, they are executed in parallel unless a `coder_script_order` data source orders them. +-> Ordering with `coder_script_order` is only available in Coder v2.39 and later. + ## Example Usage ```terraform diff --git a/provider/script.go b/provider/script.go index d8511146..e74e6265 100644 --- a/provider/script.go +++ b/provider/script.go @@ -44,7 +44,7 @@ func scriptResource() *schema.Resource { return &schema.Resource{ SchemaVersion: 1, - Description: "Use this resource to run a script on a workspace agent (`coder_agent`). When multiple scripts are assigned to the same workspace agent, they are executed in parallel unless a `coder_script_order` data source orders them.", + Description: "Use this resource to run a script on a workspace agent (`coder_agent`). When multiple scripts are assigned to the same workspace agent, they are executed in parallel unless a `coder_script_order` data source orders them.\n\n-> Ordering with `coder_script_order` is only available in Coder v2.39 and later.", CreateContext: func(_ context.Context, rd *schema.ResourceData, _ interface{}) diag.Diagnostics { rd.SetId(uuid.NewString()) runOnStart, _ := rd.Get("run_on_start").(bool) diff --git a/provider/script_order.go b/provider/script_order.go index 601e72fa..38023b0f 100644 --- a/provider/script_order.go +++ b/provider/script_order.go @@ -49,8 +49,8 @@ func scriptOrderDataSource() *schema.Resource { "`terraform validate` do not check them; Coder reports unknown or unsupported " + "selectors when the template is imported. All values must be known at plan time, " + "and the data source needs no `depends_on`.\n\n" + - "Ordering is enforced only by Coder versions that support it. Older versions " + - "ignore the data source and run scripts concurrently.", + "-> This data source is only available in Coder v2.39 and later. Older versions " + + "ignore it and run scripts concurrently.", ReadContext: func(_ context.Context, rd *schema.ResourceData, _ any) diag.Diagnostics { rd.SetId(uuid.NewString()) return nil From 392916d74e11b14afd8fb9f5f62205b1b7224f71 Mon Sep 17 00:00:00 2001 From: Bobby Ho Date: Tue, 6 Oct 2026 09:48:57 -0700 Subject: [PATCH 3/4] docs: clarify coder_script_order selector direction and phase inference run and after each name the other attribute so the generated page reads correctly in alphabetical order. The phase description now says Coder rejects a module-only rule that spans both phases instead of implying omission always works. --- docs/data-sources/script_order.md | 6 +++--- provider/script_order.go | 18 +++++++++++------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/docs/data-sources/script_order.md b/docs/data-sources/script_order.md index d802f8dd..cfe0d07b 100644 --- a/docs/data-sources/script_order.md +++ b/docs/data-sources/script_order.md @@ -59,10 +59,10 @@ data "coder_script_order" "startup_dependencies" { Required: -- `after` (List of String) Selectors for the scripts they wait for. -- `run` (List of String) Selectors for the scripts that wait. +- `after` (List of String) Selectors for the scripts that must finish before any `run` script starts. +- `run` (List of String) Selectors for the scripts that wait for every `after` script. Optional: -- `phase` (String) Which lifecycle the rule applies to: `start` or `stop`. Omit it to let Coder infer the phase from the selected scripts. Set it when a `module` selector contains both start and stop scripts, so Coder filters the module to this phase without warning. +- `phase` (String) Which lifecycle the rule applies to: `start` or `stop`. When omitted, Coder infers the phase from any `coder_script` selector in the rule and filters `module` selectors to that phase with a warning. If the rule has only `module` selectors and they expand to both start and stop scripts, Coder cannot infer the phase and rejects the rule at template import, so set `phase` explicitly in that case. - `requires` (String) The outcome required from every `after` script before a `run` script starts. `success` (the default) skips the `run` scripts if any `after` script fails, times out or is skipped. `completion` runs them once every `after` script has finished, whatever the outcome. diff --git a/provider/script_order.go b/provider/script_order.go index 38023b0f..3b620686 100644 --- a/provider/script_order.go +++ b/provider/script_order.go @@ -63,8 +63,8 @@ func scriptOrderDataSource() *schema.Resource { MinItems: 1, Elem: &schema.Resource{ Schema: map[string]*schema.Schema{ - "run": selectorSchema("Selectors for the scripts that wait."), - "after": selectorSchema("Selectors for the scripts they wait for."), + "run": selectorSchema("Selectors for the scripts that wait for every `after` script."), + "after": selectorSchema("Selectors for the scripts that must finish before any `run` script starts."), "requires": { Type: schema.TypeString, Description: "The outcome required from every `after` script before a `run` script starts. " + @@ -80,13 +80,17 @@ func scriptOrderDataSource() *schema.Resource { "phase": { Type: schema.TypeString, Description: "Which lifecycle the rule applies to: `start` or `stop`. " + - "Omit it to let Coder infer the phase from the selected scripts. " + - "Set it when a `module` selector contains both start and stop scripts, " + - "so Coder filters the module to this phase without warning.", + "When omitted, Coder infers the phase from any `coder_script` selector in the rule " + + "and filters `module` selectors to that phase with a warning. " + + "If the rule has only `module` selectors and they expand to both start and stop " + + "scripts, Coder cannot infer the phase and rejects the rule at template import, " + + "so set `phase` explicitly in that case.", Optional: true, // No default on purpose. Coder infers the phase when - // this is unset and warns when inference filtered a - // module selector. A default would hide both. + // this is unset, warns when inference filtered a + // module selector, and rejects a module-only rule + // that spans both phases. A default would hide all + // three. ValidateFunc: validation.StringInSlice([]string{ scriptOrderPhaseStart, scriptOrderPhaseStop, From 61f33d9aeba7f5c9e746b983d4acaaf08dc0384d Mon Sep 17 00:00:00 2001 From: Bobby Ho Date: Wed, 7 Oct 2026 21:13:19 +0000 Subject: [PATCH 4/4] docs: note coder_script_order selected scripts need one lifecycle flag --- docs/data-sources/script_order.md | 4 ++-- provider/script_order.go | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/data-sources/script_order.md b/docs/data-sources/script_order.md index cfe0d07b..d0035488 100644 --- a/docs/data-sources/script_order.md +++ b/docs/data-sources/script_order.md @@ -4,7 +4,7 @@ page_title: "coder_script_order Data Source - terraform-provider-coder" subcategory: "" description: |- Use this data source to declare ordering constraints between coder_script resources on the same agent. It does not run scripts. Every script selected by run waits for every script selected by after. - Selectors are Terraform address strings (coder_script.name, coder_script.name[0], coder_script.name["key"], module.name), resolved by Coder relative to the module that declares the data source. Terraform and terraform validate do not check them; Coder reports unknown or unsupported selectors when the template is imported. All values must be known at plan time, and the data source needs no depends_on. + Selectors are Terraform address strings (coder_script.name, coder_script.name[0], coder_script.name["key"], module.name), resolved by Coder relative to the module that declares the data source. Terraform and terraform validate do not check them; Coder reports unknown or unsupported selectors when the template is imported. Every script a selector resolves to, including each script in a selected module, must set exactly one of run_on_start or run_on_stop. Coder rejects cron-only scripts and scripts that set both. All values must be known at plan time, and the data source needs no depends_on. -> This data source is only available in Coder v2.39 and later. Older versions ignore it and run scripts concurrently. --- @@ -12,7 +12,7 @@ description: |- Use this data source to declare ordering constraints between `coder_script` resources on the same agent. It does not run scripts. Every script selected by `run` waits for every script selected by `after`. -Selectors are Terraform address strings (`coder_script.name`, `coder_script.name[0]`, `coder_script.name["key"]`, `module.name`), resolved by Coder relative to the module that declares the data source. Terraform and `terraform validate` do not check them; Coder reports unknown or unsupported selectors when the template is imported. All values must be known at plan time, and the data source needs no `depends_on`. +Selectors are Terraform address strings (`coder_script.name`, `coder_script.name[0]`, `coder_script.name["key"]`, `module.name`), resolved by Coder relative to the module that declares the data source. Terraform and `terraform validate` do not check them; Coder reports unknown or unsupported selectors when the template is imported. Every script a selector resolves to, including each script in a selected module, must set exactly one of `run_on_start` or `run_on_stop`. Coder rejects cron-only scripts and scripts that set both. All values must be known at plan time, and the data source needs no `depends_on`. -> This data source is only available in Coder v2.39 and later. Older versions ignore it and run scripts concurrently. diff --git a/provider/script_order.go b/provider/script_order.go index 3b620686..a5bf91de 100644 --- a/provider/script_order.go +++ b/provider/script_order.go @@ -47,7 +47,10 @@ func scriptOrderDataSource() *schema.Resource { "`coder_script.name[0]`, `coder_script.name[\"key\"]`, `module.name`), resolved " + "by Coder relative to the module that declares the data source. Terraform and " + "`terraform validate` do not check them; Coder reports unknown or unsupported " + - "selectors when the template is imported. All values must be known at plan time, " + + "selectors when the template is imported. Every script a selector resolves to, " + + "including each script in a selected module, must set exactly one of " + + "`run_on_start` or `run_on_stop`. Coder rejects cron-only scripts and scripts " + + "that set both. All values must be known at plan time, " + "and the data source needs no `depends_on`.\n\n" + "-> This data source is only available in Coder v2.39 and later. Older versions " + "ignore it and run scripts concurrently.",