Skip to content

Feature request: First-class array input for Feature options (array option type + multiple invocations) [blocked on spec#766] #1298

Description

Feature request: First-class array input for Feature options (array option type + multiple invocations)

Complex feature request — depends on the spec change in
devcontainers/spec#766 (RFC).

This issue tracks the reference CLI implementation work once the spec RFC is accepted.
Related long-standing issues: spec#57
(array option type, open since 2022) and spec#44
(install a feature more than once).


Problem

The dev container spec restricts Feature option values to boolean and string. There is no
array type
. As a result, every Feature that needs a list (packages, extensions, tools, versions)
is forced to accept a comma-separated string and split it inside install.sh.

This CLI encodes that limitation directly in its TypeScript types, which is the root cause that
blocks any progress:

// src/spec-configuration/containerFeaturesConfiguration.ts
export type FeatureOption = {
    type: 'boolean';
    default?: boolean;
    description?: string;
} | {
    type: 'string';
    enum?: string[];
    default?: string;
    description?: string;
} | {
    type: 'string';
    proposals?: string[];
    default?: string;
    description?: string;
};
// src/spec-configuration/configuration.ts
export interface DevContainerFeature {
    userFeatureId: string;
    options: boolean | string | Record<string, boolean | string | undefined>;
}
// ...
features?: Record<string, string | boolean | Record<string, string | boolean>>;

FeatureOption has no 'array' variant. DevContainerFeature.options and the features map
only permit boolean | string for option values — arrays are not representable, so they can
never reach install.sh no matter what a user writes in devcontainer.json.

Real-world impact (shipped Features, today)

Feature Option Today Symptom
ghcr.io/rocker-org/devcontainer-features/r-packages:1 packages "cli,rlang" Comma-joined; values containing commas are unrepresentable.
ghcr.io/devcontainers/features/github-cli extensions "github/gh-copilot" Comma-joined; extension refs/args with commas break.
mwmahlberg/devcontainer-features npm-packages packages "typescript,eslint" Accepts comma or whitespace or newline — three delimiters, because the spec gives no canonical list form.

The npm-packages case is the clearest failure signal: with no array type, every Feature author
invents a different delimiter
. Consumers cannot reason about a list option without reading each
Feature's install.sh.

This was always meant to be temporary. From spec#57,
maintainer Chuck Lantz (@Chuxel) (2022):

Right now things in devcontainers/features are using a comma separated string as a near term
workaround
. Converting this into an array is pretty easy…

That workaround has now been the de facto standard for 3+ years.


Requirements (hard — no alternatives)

This is a required capability, not a nice-to-have. Comma-separated strings are not an acceptable
long-term substitute (they are ambiguous, lossy, and force per-Feature delimiter conventions). The
CLI must implement:

  1. array option typedevcontainer-feature.json may declare "type": "array" for an
    option, with default as an array and proposals/enum constraining elements.
  2. Array option values in devcontainer.json"packages": ["curl","git","jq"] must be
    accepted, validated, and propagated.
  3. Multiple invocations via array of option objects — a Feature value may be an array of option
    objects ("dotnet": [{"version":"3.1"},{"version":"6.0"}]), invoking install.sh once per
    element in order (resolves spec#44).
  4. Option Resolution for arrays — array values are serialized to devcontainer-features.env as
    a JSON array string (PACKAGES='["curl","git","jq"]'), the only delimiter-free, unambiguous
    encoding.
  5. Backward-compatible string→array coercion — if a string is supplied for an array-typed
    option: parse as JSON if it looks like a JSON array, else split on commas. Existing
    comma-separated Features keep working when they migrate to type: "array".
  6. CLI flag support — accept JSON array values in --override-features, and support repeated
    flags (--feature-option <feature>.<option> <value>) that append to an array option.

Proposed implementation

1. Type changes

src/spec-configuration/containerFeaturesConfiguration.ts — extend FeatureOption:

export type FeatureOption = {
    type: 'boolean';
    default?: boolean;
    description?: string;
} | {
    type: 'string';
    enum?: string[];
    default?: string;
    description?: string;
} | {
    type: 'string';
    proposals?: string[];
    default?: string;
    description?: string;
} | {
    type: 'array';                          // NEW
    enum?: string[];                        // constrains elements
    proposals?: string[];                   // suggests elements
    default?: (string | boolean | number)[];// array default
    description?: string;
};

src/spec-configuration/configuration.ts — widen option value and Feature value types:

export interface DevContainerFeature {
    userFeatureId: string;
    // allow arrays of primitives as option values, and array-of-option-objects as the Feature value
    options: boolean | string | (string | boolean | number)[] | Record<string, boolean | string | (string | boolean | number)[] | undefined> | Record<string, boolean | string | (string | boolean | number)[]>[];
}
// ...
features?: Record<string, string | boolean | (string | boolean | number)[] | Record<string, boolean | string | (string | boolean | number)[]> | Record<string, boolean | string | (string | boolean | number)[]>[]>;

2. Option parsing & normalization

Where feature option values are read and normalized (the getFeatureValueDefaults /
option-resolution path in containerFeaturesConfiguration.ts):

  • When option.type === 'array':
    • Accept a JSON array value as-is.
    • If the supplied value is a string, attempt JSON.parse; if it yields an array, use it;
      otherwise split on , and trim each element. Emit a warning recommending the array form.
    • Validate each element against enum/proposals when present.
    • Default to [] when omitted and no default.

3. Option Resolution (env serialization)

In the code that writes devcontainer-features.env (<OPTION_NAME>=<value>):

  • For an array option, serialize the value with JSON.stringify(value) so the env var holds the
    canonical JSON array string. Example output:
    PACKAGES='["curl","git","jq"]'
  • This keeps a single, unambiguous source of truth. install.sh parses with jq (already
    ubiquitous in dev container images):
    for pkg in $(printf '%s' "$PACKAGES" | jq -r '.[]'); do apt-get install -y "$pkg"; done

4. Multiple invocations (array of option objects)

In the feature install layer (getFeatureLayers / getFeatureInstallWrapperScript and the
surrounding orchestration):

  • When a Feature's value is an array of option objects, emit one install layer/wrapper per element,
    in array order, each with its own devcontainer-features.env. All invocations of a given Feature
    run consecutively at that Feature's position in the install order (do not interleave with other
    Features).

5. CLI flags

  • --override-features: already accepts a JSON blob; ensure array option values and array-of-objects
    Feature values parse and flow through the new types.
  • Add/confirm a repeated-flag form: --feature-option <feature>.<option> <value> appends to an
    array-typed option (and sets/replaces for scalar options).

6. Validation & errors

  • Reject non-array values for array-typed options (after string coercion) with a clear error
    naming the Feature and option.
  • Validate enum elements; report the offending element.

Test plan

  • devcontainer-feature.json with type: "array" parses and surfaces default/enum/proposals.
  • devcontainer.json with "packages": ["curl","git"] reaches install.sh as
    PACKAGES='["curl","git"]'.
  • String "curl,git" for an array option coerces to ["curl","git"] with a warning.
  • String '["curl","git"]' (JSON) coerces to the array without warning.
  • enum-constrained array option rejects an out-of-set element.
  • Array-of-option-objects Feature value invokes install.sh twice with distinct env vars, in
    order.
  • Repeated --feature-option flags append to an array option.
  • Existing comma-separated Features (e.g. r-packages) continue to work unchanged after
    migrating their option to type: "array".
  • JSON Schema (devContainerFeature.schema.json) validates the new shapes.

Use cases

  1. Package listsr-packages, npm-packages, github-cli extensions, Homebrew formulae:
    explicit arrays, values may contain commas.
  2. Multiple runtime versions — install .NET 3.1 and 6.0 (or Node 18 + 20) in one image
    via array-of-option-objects, without bespoke per-Feature "multi-version" options.
  3. Multi-select tool bundles — a Feature offering a curated subset of tools; with enum
    elements the UX can render a multi-select picker.
  4. Tool-readable config — schema validation, IntelliSense, and diffs work natively on JSON
    arrays; comma-strings are opaque to every tool except the splitting install.sh.

Prior art (alternative stacks)

The dev container spec is the only major dev-environment format lacking a native list type for
user-supplied option values:

Stack List input Notes
Coder coder_parameter type = "list(string)"; UI multi-select/tag-select; defaults via jsonencode([...]) First-class list type. Coder's docs warn that overriding list(string) on the CLI is "tricky" (CSV+JSON quoting) and offer a YAML workaround — exactly the ambiguity this CLI should avoid by defining array semantics up front.
Nix (mkShell) Native lists packages = [ curl git jq ]; First-class; no string parsing.
Gitpod (.gitpod.yml) Native YAML arrays (tasks, ports, vscode.extensions) First-class.
Docker Compose Native YAML arrays (volumes, ports, environment) First-class.
Helm Native YAML arrays in values.yaml, iterated with range First-class.
Terraform list(string), list(any) native variable types First-class.
Dev Containers (this CLI) ❌ No array option type — comma-separated string only Outlier.

Dependencies & unblocking

  • Blocked on spec acceptance: devcontainers/spec#766
    (the RFC defining array option type, Option Resolution for arrays, and array-of-option-objects).
  • Closes the long-standing workaround: spec#57
    (2022) and spec#44.

Once the spec RFC lands, this issue is the implementation tracker for the reference CLI. The type
changes in §1 are the minimal unblocking step; everything else follows from the spec's normative
requirements.


References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions