Skip to content

Add Windows environment variable resource - #1675

Open
Steve Lee (SteveL-MSFT) wants to merge 4 commits into
mainfrom
stevel-msft-add-windows-environment-resource
Open

Add Windows environment variable resource#1675
Steve Lee (SteveL-MSFT) wants to merge 4 commits into
mainfrom
stevel-msft-add-windows-environment-resource

Conversation

@SteveL-MSFT

Copy link
Copy Markdown
Member

Windows DSC needs a native resource for managing persistent user and machine environment variables without requiring callers to manipulate registry paths and value formatting directly.

This adds Microsoft.Windows/EnvironmentVariableList, a Windows-only Rust resource with an environmentVariables list schema. It supports scalar values, path arrays with prepend/append/clobber behavior, case-insensitive path deduplication, scope and existence defaults, and deletion through _exist: false. Values are stored in the appropriate CurrentUser or AllUsers registry location, preserving expandable-string behavior when needed.

AllUsers reads remain available without elevation, while AllUsers writes and removals fail before any mutation with a dedicated actionable elevation error. CurrentUser operations therefore do not unnecessarily require an elevated security context.

Pester coverage exercises get and set behavior, list ordering, defaults, all path actions, deduplication, deletion, invalid input, and the non-elevated AllUsers error path.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings August 13, 2026 20:52

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Windows-only DSC Rust resource (Microsoft.Windows/EnvironmentVariableList) to manage persistent user and machine environment variables via the registry, with support for scalar values and PATH-style list semantics.

Changes:

  • Introduces the new resources/environment_variable Rust resource with input validation, registry read/write/remove behavior, and localized user-facing messages.
  • Adds the resource manifest + embedded schema and wires the new crate into the workspace build.
  • Adds Pester coverage for get/set behaviors including path merge/dedup, defaults, deletion, and the non-elevated AllUsers error path.

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
resources/environment_variable/src/main.rs Resource entrypoint, CLI arg parsing, exit codes, JSON output/error formatting, and input validation plumbing.
resources/environment_variable/src/types.rs Input/output types and validation rules (scope, value vs pathValue, duplicate identities, etc.).
resources/environment_variable/src/environment.rs Windows registry implementation for get/set/remove and PATH merge/dedup behavior; elevation gating for AllUsers writes.
resources/environment_variable/locales/en-us.toml i18n strings for validation and runtime errors.
resources/environment_variable/environment_variable.dsc.resource.json Resource manifest and embedded JSON schema for environmentVariables list.
resources/environment_variable/Cargo.toml New resource crate definition and Windows-only dependencies.
resources/environment_variable/.project.data.json Build/packaging metadata for the resource binary and manifest.
resources/environment_variable/tests/environment_variable_get.tests.ps1 Pester tests for get behavior (default scope, pathValue projection, missing vars, ordering).
resources/environment_variable/tests/environment_variable_set.tests.ps1 Pester tests for set behavior (scalar, path actions, dedup, deletion, multi-set, invalid input, AllUsers elevation error).
Cargo.toml Adds the new resource crate to workspace members/default-members/Windows set.
Cargo.lock Locks the new environment_variable package into the workspace dependency graph.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Steve Lee (POWERSHELL HE/HIM) (from Dev Box) and others added 2 commits August 13, 2026 14:05
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@github-actions

github-actions Bot commented Aug 13, 2026

Copy link
Copy Markdown

😢 Code Coverage Report

Changed Code Coverage

36% (less than 70% coverage)

Metric Value
Changed lines analyzed 340
Lines covered by tests 123
Coverage percentage 36%

🔵 Full Codebase Coverage

82% (good)

Metric Value
Total executable lines 18820
Lines covered by tests 15500
Coverage percentage 82%

Changed code coverage measures only Rust lines added/modified in this PR.
Full codebase coverage measures all instrumented Rust lines across the project.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A few thoughts on the design and implementation:

  1. I think we should probably add some tests that cover the test operation because it's not obvious to me how we report changes and out-of-state details for path values to the user. Consider the following scenarios:

    • The variable defines pathValue with two paths and pathAction as prepend. One of the values is already defined but the other would be inserted. The final value includes one or more segments not defined in the input.

      We don't seem to emit any messages indicating which path segments are missing or will be inserted. Comparing the actual or final path value to the input requires manually verifying each segment.

    • The variable defines pathValue with two paths and pathAction as clobber. Both values are already defined but the actual value includes an empty segment.

      Does the resource report that the instance is currently out-of-state since it will need to remove the duplicate when canonicalizing the value? When the user invokes the set operation, how does the user know that the empty segment was removed?

  2. Not required for this PR, but we may want to make the canonicalizing behavior more explicit and opt-out with a write-only property like canonicalize which defaults to true.

    This would make it easier to use the resource for setting up test environments, since specifying path values with duplicates and empty segments is technically valid though discouraged.

  3. While it doesn't have to be in this PR, I think we should always try to ensure that whenever we define a *List resource we also define a single-instance resource when it's coherent to do so.

    Needing to wrap a single-variable definition in an array and losing the reporting granularity/readability is frustrating as an end user.

    In the current engine, this resource can only ever indicate that the environmentVariables property is out-of-state. The user then needs to compare every entry in desired/actual to see which environment variable is actually in an invalid state and how it's invalid.

  4. Also in the current implementation, we use the items keyword to define each environment variable as an object in an array. There's no way to represent the constraint "every item in this array must be unique by the combination of name and scope property values" - though that is handled in the code in the validation step.

    We could address this by altering the design to using the propertyNames and unevaluatedProperties keywords:

    Alternate schema
    type: object
    propertyNames:
      description: >-
        Every property name must be a non-empty string, optionally prefixed
        with a scope (allUsers\ or currentUser\). When no prefix is provided,
        the property name is assumed to be scoped to the current user.
      pattern: '^(?:(?<scope>(allUsers|currentUser)\\))?\S+$'
    # Unevaluated properties (in this case, every property) uses the provided
    # schema to validate the property value. We could use additionalProperties, but
    # that precludes extending the schema.
    unevaluatedProperties:
      type: object
      properties:
        value:
          title: Value
          description: >-
            The value for the environment variable.
          type: string
        pathValue:
          title: Path Value
          description: >-
            The semicolon-delimited value for the environment variable
            represented as path entries.
          type: string
          minLength: 1
        _exist:
          $ref: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/exist.json
      # pathAction is only valid when pathValue is specified
      dependentSchemas:
        pathValue:
          properties:
            pathAction:
              title: Path Action
              description: >-
                How pathValue entries are combined with the current value of the
                environment variable. If not specified, the default is to clobber
                the current value with the new value.
              writeOnly: true
              default: clobber
              type: string
              enum:
                - prepend
                - append
                - clobber
      # Always nest constraints in an allOf to support adding new constraints
      # and giving readers a way to quickly review them.
      allOf:
        - not: { required: [value, pathValue] }
          description: >-
            Either value or pathValue must be specified, but not both.
    # Bundle the canonical property definition in $defs, don't redefine it inline.
    $defs:
      "https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/exist.json":
        $schema: https://json-schema.org/draft/2020-12/schema
        $id: https://raw.githubusercontent.com/PowerShell/DSC/main/schemas/v3/resource/properties/inDesiredState.json
        title: Instance is in the Desired State
        description: Indicates whether the instance is in the desired state. This property is only returned by the `test` method.
        type:
          - boolean
          - "null"
        readOnly: true

    Where the design would then be to parse each property name for the variable scope/name. This would be a significant design of the resource implementation but compare the effective definition and result data:

    Synthetic test result for current design
    desiredState:
      environmentVariables:
        - name: foo
          scope: AllUsers
          value: bar
        - name: MY_PATH
          scope: CurrentUser
          pathValue:
            - D:\infra\dsc\resources
            - D:\infra\dsc\extensions
          pathAction: prepend
    actualState:
      environmentVariables:
        - name: foo
          scope: AllUsers
          value: baz
        - name: MY_PATH
          scope: CurrentUser
          pathValue:
            - D:\infra\utils\bin
    inDesiredState: false
    differingProperties:
      - environmentVariables
    Synthetic test result for proposed design
    desiredState:
      AllUsers\foo:
        value: bar
      CurrentUser\MY_PATH:
        pathValue:
          - D:\infra\dsc\resources
          - D:\infra\dsc\extensions
    actualState:
      AllUsers\foo:
        value: baz
      CurrentUser\MY_PATH:
        pathValue:
          - D:\infra\utils\bin
    inDesiredState: false
    differingProperties:
      - AllUsers\foo
      - CurrentUser\MY_PATH

    I bring this up because the ergonomics (particularly for test/set) of list resources is currently very painful and worse the more entries a user defines for that resource.

    I think migrating towards a *Set pattern would make it much easier to reason about these convenience resources and review their results.

Comment on lines +100 to +111
"pathAction": {
"type": "string",
"title": "Path action",
"description": "How pathValue entries are combined with the current value.",
"writeOnly": true,
"default": "clobber",
"enum": [
"prepend",
"append",
"clobber"
]
},

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is more properly extracted into a dependentSchemas keyword since it's only valid when pathValue is defined and should never be specified with value.

Relatedly, we could use the dependentRequired keyword to mark an instance as invalid when it defines pathAction without pathValue.

Comment on lines +15 to +19
pub enum Scope {
AllUsers,
#[default]
CurrentUser,
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason not to camel case these enum values to match existing conventions for DSC and resources generally? I know that PascalCase is what the Windows API emits, but this does break from how we name properties and enum values in this and other resources.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be changed to camelCase

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants