diff --git a/.gitattributes b/.gitattributes index 78a8e2963d8..9313f457c6f 100644 --- a/.gitattributes +++ b/.gitattributes @@ -41,3 +41,4 @@ site/src/api/typesGenerated.ts linguist-generated=true # AI Gateway prices coderd/aibridge/prices/data/prices.json linguist-generated=true site/src/pages/AgentsPage/components/ChatModelAdminPanel/knownModels/knownModelsGenerated.json linguist-generated=true +docs/experiments.json linguist-generated=true diff --git a/Makefile b/Makefile index 4070f892410..3dae55a2819 100644 --- a/Makefile +++ b/Makefile @@ -144,6 +144,10 @@ _gen/bin/auditdocgen: $(wildcard scripts/auditdocgen/*.go) $(wildcard enterprise @mkdir -p _gen/bin go build -o $@ ./scripts/auditdocgen +_gen/bin/experimentsdocgen: $(wildcard scripts/experimentsdocgen/*.go) codersdk/deployment.go | _gen + @mkdir -p _gen/bin + go build -o $@ ./scripts/experimentsdocgen + _gen/bin/check-scopes: $(wildcard scripts/check-scopes/*.go) $(RBAC_GO_FILES) | _gen @mkdir -p _gen/bin go build -o $@ ./scripts/check-scopes @@ -1017,6 +1021,7 @@ GEN_FILES := \ docs/admin/integrations/prometheus.md \ docs/reference/cli/index.md \ docs/admin/security/audit-logs.md \ + docs/experiments.json \ docs/install/releases/feature-stages.md \ docs/admin/setup/configuration-reference.md \ coderd/apidoc/swagger.json \ @@ -1116,6 +1121,7 @@ gen/mark-fresh: docs/admin/integrations/prometheus.md \ docs/reference/cli/index.md \ docs/admin/security/audit-logs.md \ + docs/experiments.json \ docs/install/releases/feature-stages.md \ docs/admin/setup/configuration-reference.md \ coderd/apidoc/swagger.json \ @@ -1353,10 +1359,17 @@ docs/admin/security/audit-logs.md: node_modules/.installed coderd/database/queri pnpm exec markdown-table-formatter "$$tmpfile" && \ mv "$$tmpfile" "$@" && rm -rf "$$tmpdir" +# Every experiment this version knows about, with its display name, description, +# and whether --experiments=* enables it. The feature-stages page renders its +# experiments table from this file, and the docs engine validates experimental +# content markers against it. +docs/experiments.json: codersdk/deployment.go $(wildcard scripts/experimentsdocgen/*.go) | _gen _gen/bin/experimentsdocgen + _gen/bin/experimentsdocgen --source codersdk/deployment.go --out $@ + docs/install/releases/feature-stages.md: \ node_modules/.installed \ scripts/release/docs_update_feature_stages.sh \ - codersdk/deployment.go \ + docs/experiments.json \ docs/manifest.json | _gen tmpdir=$$(mktemp -d -p _gen) && tmpfile=$$(realpath "$$tmpdir")/$(notdir $@) && cp "$@" "$$tmpfile" && \ ./scripts/release/docs_update_feature_stages.sh "$$tmpfile" && \ diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go index 452e9329096..d0aa7f30f52 100644 --- a/coderd/apidoc/docs.go +++ b/coderd/apidoc/docs.go @@ -23396,7 +23396,7 @@ const docTemplate = `{ "x-enum-comments": { "ExperimentAIGatewaySeatExclusion": "Excludes AI Gateway (AI Bridge) usage from AI Governance seat consumption.", "ExperimentAgentLifecycleHooks": "Enables chat lifecycle hook webhooks for agent chats.", - "ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.", + "ExperimentAutoFillParameters": "Pre-fills a new workspace's parameter form with the values from the user's most recent build of that template.", "ExperimentChatAdvisor": "Enables the advisor tool for root agent chats.", "ExperimentChatVirtualDesktop": "Enables virtual desktop and computer use provider for agents.", "ExperimentExample": "This isn't used for anything.", @@ -23411,7 +23411,7 @@ const docTemplate = `{ }, "x-enum-descriptions": [ "This isn't used for anything.", - "This should not be taken out of experiments until we have redesigned the feature.", + "Pre-fills a new workspace's parameter form with the values from the user's most recent build of that template.", "Sends notifications via SMTP and webhooks following certain events.", "Enables the new workspace usage tracking.", "Enables OAuth2 provider functionality.", diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json index ad9119be53b..fb153094ed7 100644 --- a/coderd/apidoc/swagger.json +++ b/coderd/apidoc/swagger.json @@ -21300,7 +21300,7 @@ "x-enum-comments": { "ExperimentAIGatewaySeatExclusion": "Excludes AI Gateway (AI Bridge) usage from AI Governance seat consumption.", "ExperimentAgentLifecycleHooks": "Enables chat lifecycle hook webhooks for agent chats.", - "ExperimentAutoFillParameters": "This should not be taken out of experiments until we have redesigned the feature.", + "ExperimentAutoFillParameters": "Pre-fills a new workspace's parameter form with the values from the user's most recent build of that template.", "ExperimentChatAdvisor": "Enables the advisor tool for root agent chats.", "ExperimentChatVirtualDesktop": "Enables virtual desktop and computer use provider for agents.", "ExperimentExample": "This isn't used for anything.", @@ -21315,7 +21315,7 @@ }, "x-enum-descriptions": [ "This isn't used for anything.", - "This should not be taken out of experiments until we have redesigned the feature.", + "Pre-fills a new workspace's parameter form with the values from the user's most recent build of that template.", "Sends notifications via SMTP and webhooks following certain events.", "Enables the new workspace usage tracking.", "Enables OAuth2 provider functionality.", diff --git a/codersdk/deployment.go b/codersdk/deployment.go index e79b94774e9..a886972bac3 100644 --- a/codersdk/deployment.go +++ b/codersdk/deployment.go @@ -5472,7 +5472,7 @@ type Experiment string const ( // Add new experiments here! ExperimentExample Experiment = "example" // This isn't used for anything. - ExperimentAutoFillParameters Experiment = "auto-fill-parameters" // This should not be taken out of experiments until we have redesigned the feature. + ExperimentAutoFillParameters Experiment = "auto-fill-parameters" // Pre-fills a new workspace's parameter form with the values from the user's most recent build of that template. ExperimentNotifications Experiment = "notifications" // Sends notifications via SMTP and webhooks following certain events. ExperimentWorkspaceUsage Experiment = "workspace-usage" // Enables the new workspace usage tracking. ExperimentOAuth2 Experiment = "oauth2" // Enables OAuth2 provider functionality. @@ -5501,6 +5501,8 @@ func (e Experiment) DisplayName() string { return "OAuth2 Provider Functionality" case ExperimentMCPServerHTTP: return "MCP HTTP Server Functionality" + case ExperimentMCPToolSearch: + return "MCP Tool Search" case ExperimentWorkspaceBuildUpdates: return "Workspace Build Updates Channel" case ExperimentNATSPubsub: diff --git a/docs/experiments.json b/docs/experiments.json new file mode 100644 index 00000000000..81d5337f13f --- /dev/null +++ b/docs/experiments.json @@ -0,0 +1,83 @@ +{ + "schemaVersion": 1, + "experiments": [ + { + "id": "agent-lifecycle-hooks", + "displayName": "Agent Lifecycle Hooks", + "description": "Enables chat lifecycle hook webhooks for agent chats.", + "safe": false + }, + { + "id": "ai-gateway-seat-exclusion", + "displayName": "AI Gateway Seat Exclusion", + "description": "Excludes AI Gateway (AI Bridge) usage from AI Governance seat consumption.", + "safe": false + }, + { + "id": "auto-fill-parameters", + "displayName": "Auto-fill Template Parameters", + "description": "Pre-fills a new workspace's parameter form with the values from the user's most recent build of that template.", + "safe": false + }, + { + "id": "chat-advisor", + "displayName": "Chat Advisor", + "description": "Enables the advisor tool for root agent chats.", + "safe": false + }, + { + "id": "chat-virtual-desktop", + "displayName": "Chat Virtual Desktop", + "description": "Enables virtual desktop and computer use provider for agents.", + "safe": false + }, + { + "id": "mcp-server-http", + "displayName": "MCP HTTP Server Functionality", + "description": "Enables the MCP HTTP server functionality.", + "safe": false + }, + { + "id": "mcp-tool-search", + "displayName": "MCP Tool Search", + "description": "Defers MCP tool schemas behind a searchable catalog in agent chats.", + "safe": false + }, + { + "id": "nats_pubsub", + "displayName": "NATS Pubsub", + "description": "Enables embedded NATS pubsub.", + "safe": false + }, + { + "id": "notifications", + "displayName": "SMTP and Webhook Notifications", + "description": "Sends notifications via SMTP and webhooks following certain events.", + "safe": false + }, + { + "id": "oauth2", + "displayName": "OAuth2 Provider Functionality", + "description": "Enables OAuth2 provider functionality.", + "safe": false + }, + { + "id": "workspace-build-updates", + "displayName": "Workspace Build Updates Channel", + "description": "Enables publishing workspace build updates to the all builds pubsub channel.", + "safe": false + }, + { + "id": "workspace-capable-licensing", + "displayName": "Workspace-Capable Licensing", + "description": "Counts only users holding the workspace-create permission toward the license seat limit.", + "safe": false + }, + { + "id": "workspace-usage", + "displayName": "Workspace Usage Tracking", + "description": "Enables the new workspace usage tracking.", + "safe": false + } + ] +} diff --git a/docs/install/releases/feature-stages.md b/docs/install/releases/feature-stages.md index 57ccea3121d..9000f324c42 100644 --- a/docs/install/releases/feature-stages.md +++ b/docs/install/releases/feature-stages.md @@ -38,33 +38,61 @@ staging deployment.
To enable early access features: -Use the [Coder CLI](../../install/cli.md) `--experiments` flag to enable early -access features: +Early access features are experiments, and each one has a flag. +Coder marks each experiment as **safe** or **unsafe** for opting in; the Safety column in the table below shows which is which for this version. -- Enable all early access features: +- A safe experiment is ready for opt-in testing. + `--experiments=*` enables every safe experiment in this version and nothing else. +- An unsafe experiment is not ready for general use and must be named exactly; the wildcard never enables it. + Enable unsafe experiments only on a staging deployment. + +Use the [Coder CLI](../../install/cli.md) `--experiments` flag on `coder server`: + +- Enable every safe experiment: ```sh coder server --experiments=* ``` -- Enable multiple early access features: +- Enable specific experiments by flag, safe or unsafe: ```sh - coder server --experiments=feature1,feature2 + coder server --experiments=flag-one,flag-two ``` -You can also use the `CODER_EXPERIMENTS` -[environment variable](../../admin/setup/index.md). +- Enable every safe experiment plus named unsafe ones in one flag: + + ```sh + coder server --experiments=*,unsafe-flag-one,unsafe-flag-two + ``` -You can opt-out of a feature after you've enabled it. +You can also set the `CODER_EXPERIMENTS` [environment variable](../../admin/setup/index.md) to the same value. +You can opt out of a feature after you've enabled it by removing its flag and restarting the server.
### Available early access features +This table lists every experiment this version of Coder knows about. +It is generated from the code, so it always matches the flags this version accepts. + -Currently no experimental features are available. +| Feature | Flag | Description | Safety | +|---------------------------------|-------------------------------|----------------------------------------------------------------------------------------------------------------|--------| +| Agent Lifecycle Hooks | `agent-lifecycle-hooks` | Enables chat lifecycle hook webhooks for agent chats. | Unsafe | +| AI Gateway Seat Exclusion | `ai-gateway-seat-exclusion` | Excludes AI Gateway (AI Bridge) usage from AI Governance seat consumption. | Unsafe | +| Auto-fill Template Parameters | `auto-fill-parameters` | Pre-fills a new workspace's parameter form with the values from the user's most recent build of that template. | Unsafe | +| Chat Advisor | `chat-advisor` | Enables the advisor tool for root agent chats. | Unsafe | +| Chat Virtual Desktop | `chat-virtual-desktop` | Enables virtual desktop and computer use provider for agents. | Unsafe | +| MCP HTTP Server Functionality | `mcp-server-http` | Enables the MCP HTTP server functionality. | Unsafe | +| MCP Tool Search | `mcp-tool-search` | Defers MCP tool schemas behind a searchable catalog in agent chats. | Unsafe | +| NATS Pubsub | `nats_pubsub` | Enables embedded NATS pubsub. | Unsafe | +| SMTP and Webhook Notifications | `notifications` | Sends notifications via SMTP and webhooks following certain events. | Unsafe | +| OAuth2 Provider Functionality | `oauth2` | Enables OAuth2 provider functionality. | Unsafe | +| Workspace Build Updates Channel | `workspace-build-updates` | Enables publishing workspace build updates to the all builds pubsub channel. | Unsafe | +| Workspace-Capable Licensing | `workspace-capable-licensing` | Counts only users holding the workspace-create permission toward the license seat limit. | Unsafe | +| Workspace Usage Tracking | `workspace-usage` | Enables the new workspace usage tracking. | Unsafe | ## Beta diff --git a/scripts/experimentsdocgen/main.go b/scripts/experimentsdocgen/main.go new file mode 100644 index 00000000000..62155abf588 --- /dev/null +++ b/scripts/experimentsdocgen/main.go @@ -0,0 +1,171 @@ +// experimentsdocgen writes docs/experiments.json: every user-facing experiment +// this version of Coder knows about (codersdk.ExperimentsKnown minus the +// placeholders in notDocumented), its display name, +// the description from the constant's comment in codersdk/deployment.go, and +// whether `--experiments=*` enables it (membership in codersdk.ExperimentsSafe). +// +// The documentation reads the file to list experiments per version and to +// label experimental content, so the list is generated from the code rather +// than maintained by hand. `make gen` runs it; CI fails on a stale copy. +package main + +import ( + "encoding/json" + "flag" + "go/ast" + "go/parser" + "go/token" + "log" + "os" + "sort" + "strconv" + "strings" + + "golang.org/x/xerrors" + + "github.com/coder/coder/v2/codersdk" + "github.com/coder/coder/v2/scripts/atomicwrite" +) + +// experimentsDoc is the on-disk shape of docs/experiments.json. +type experimentsDoc struct { + SchemaVersion int `json:"schemaVersion"` + Experiments []experimentEntry `json:"experiments"` +} + +type experimentEntry struct { + // ID is the flag value passed to --experiments. + ID string `json:"id"` + // DisplayName is codersdk.Experiment.DisplayName() for the flag. + DisplayName string `json:"displayName"` + // Description is the trailing comment on the constant, or empty. + Description string `json:"description"` + // Safe reports whether `--experiments=*` enables the flag. + Safe bool `json:"safe"` +} + +func main() { + var ( + source string + out string + dryRun bool + ) + flag.StringVar(&source, "source", "codersdk/deployment.go", "Go file declaring the Experiment constants") + flag.StringVar(&out, "out", "docs/experiments.json", "Path of the JSON file to write") + flag.BoolVar(&dryRun, "dry-run", false, "Print the JSON instead of writing it") + flag.Parse() + + src, err := os.ReadFile(source) + if err != nil { + log.Fatalf("read %s: %v", source, err) + } + descriptions, err := parseExperimentDescriptions(src) + if err != nil { + log.Fatalf("parse %s: %v", source, err) + } + + doc := buildExperimentsDoc(documented(codersdk.ExperimentsKnown), codersdk.ExperimentsSafe, descriptions, codersdk.Experiment.DisplayName) + data, err := json.MarshalIndent(doc, "", " ") + if err != nil { + log.Fatalf("encode: %v", err) + } + data = append(data, '\n') + + if dryRun { + _, _ = os.Stdout.Write(data) + return + } + if err := atomicwrite.File(out, data); err != nil { + log.Fatalf("write %s: %v", out, err) + } +} + +// notDocumented lists experiments that exist for the code's own purposes and +// never describe a user-facing feature, so the documentation omits them. +var notDocumented = map[codersdk.Experiment]bool{ + codersdk.ExperimentExample: true, // A placeholder kept for tests. +} + +// documented filters `known` down to the experiments the docs should list. +func documented(known codersdk.Experiments) codersdk.Experiments { + out := make(codersdk.Experiments, 0, len(known)) + for _, experiment := range known { + if !notDocumented[experiment] { + out = append(out, experiment) + } + } + return out +} + +// parseExperimentDescriptions maps each `Experiment` constant's string value to +// the comment beside it (`ExperimentFoo Experiment = "foo" // Enables foo.`), +// falling back to a doc comment above the constant. Constants of any other +// type are ignored, so the file may declare whatever else it likes. +func parseExperimentDescriptions(src []byte) (map[string]string, error) { + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "deployment.go", src, parser.ParseComments) + if err != nil { + return nil, xerrors.Errorf("parse: %w", err) + } + + descriptions := map[string]string{} + for _, decl := range file.Decls { + gen, ok := decl.(*ast.GenDecl) + if !ok || gen.Tok != token.CONST { + continue + } + for _, spec := range gen.Specs { + value, ok := spec.(*ast.ValueSpec) + if !ok || len(value.Names) != 1 || len(value.Values) != 1 { + continue + } + typeName, ok := value.Type.(*ast.Ident) + if !ok || typeName.Name != "Experiment" { + continue + } + lit, ok := value.Values[0].(*ast.BasicLit) + if !ok || lit.Kind != token.STRING { + continue + } + id, err := strconv.Unquote(lit.Value) + if err != nil { + return nil, xerrors.Errorf("unquote %s: %w", lit.Value, err) + } + description := "" + switch { + case value.Comment != nil: + description = value.Comment.Text() + case value.Doc != nil: + description = value.Doc.Text() + } + descriptions[id] = strings.TrimSpace(description) + } + } + return descriptions, nil +} + +// buildExperimentsDoc assembles the document for `known`, sorted by id, marking +// each entry safe when it also appears in `safe`. Descriptions missing from the +// map are left empty rather than failing: a flag with no comment is still a flag. +func buildExperimentsDoc( + known codersdk.Experiments, + safe codersdk.Experiments, + descriptions map[string]string, + displayName func(codersdk.Experiment) string, +) experimentsDoc { + safeSet := make(map[codersdk.Experiment]bool, len(safe)) + for _, experiment := range safe { + safeSet[experiment] = true + } + entries := make([]experimentEntry, 0, len(known)) + for _, experiment := range known { + entries = append(entries, experimentEntry{ + ID: string(experiment), + DisplayName: displayName(experiment), + Description: descriptions[string(experiment)], + Safe: safeSet[experiment], + }) + } + sort.Slice(entries, func(i, j int) bool { return entries[i].ID < entries[j].ID }) + return experimentsDoc{SchemaVersion: 1, Experiments: entries} +} diff --git a/scripts/experimentsdocgen/main_test.go b/scripts/experimentsdocgen/main_test.go new file mode 100644 index 00000000000..b409e506844 --- /dev/null +++ b/scripts/experimentsdocgen/main_test.go @@ -0,0 +1,83 @@ +package main + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/coder/coder/v2/codersdk" +) + +func TestParseExperimentDescriptions(t *testing.T) { + t.Parallel() + + src := []byte(`package codersdk + +type Experiment string + +const ( + ExperimentAlpha Experiment = "alpha" // Enables alpha. + // Beta has a doc comment instead of a trailing one. + ExperimentBeta Experiment = "beta" + ExperimentGamma Experiment = "gamma" + NotAnExperiment string = "ignored" // Different type. +) + +const Unrelated = 3 +`) + + descriptions, err := parseExperimentDescriptions(src) + require.NoError(t, err) + require.Equal(t, map[string]string{ + "alpha": "Enables alpha.", + "beta": "Beta has a doc comment instead of a trailing one.", + "gamma": "", + }, descriptions) +} + +func TestParseExperimentDescriptionsRejectsInvalidGo(t *testing.T) { + t.Parallel() + + _, err := parseExperimentDescriptions([]byte("package codersdk\nconst (")) + require.Error(t, err) +} + +func TestBuildExperimentsDoc(t *testing.T) { + t.Parallel() + + known := codersdk.Experiments{"zeta", "alpha", "mid"} + safe := codersdk.Experiments{"mid"} + descriptions := map[string]string{"alpha": "First.", "zeta": "Last."} + displayName := func(e codersdk.Experiment) string { return "Name of " + string(e) } + + doc := buildExperimentsDoc(known, safe, descriptions, displayName) + + require.Equal(t, 1, doc.SchemaVersion) + require.Equal(t, []experimentEntry{ + {ID: "alpha", DisplayName: "Name of alpha", Description: "First.", Safe: false}, + {ID: "mid", DisplayName: "Name of mid", Description: "", Safe: true}, + {ID: "zeta", DisplayName: "Name of zeta", Description: "Last.", Safe: false}, + }, doc.Experiments) +} + +// The real constants file must parse and every known experiment must have a +// display name, so a new experiment cannot land without documentation metadata. +func TestRealDeploymentFile(t *testing.T) { + t.Parallel() + + src, err := os.ReadFile("../../codersdk/deployment.go") + require.NoError(t, err) + descriptions, err := parseExperimentDescriptions(src) + require.NoError(t, err) + + known := documented(codersdk.ExperimentsKnown) + require.NotContains(t, known, codersdk.ExperimentExample, "the example experiment is never documented") + require.Len(t, known, len(codersdk.ExperimentsKnown)-1) + doc := buildExperimentsDoc(known, codersdk.ExperimentsSafe, descriptions, codersdk.Experiment.DisplayName) + require.Len(t, doc.Experiments, len(known)) + for _, entry := range doc.Experiments { + require.NotEmpty(t, entry.DisplayName, "experiment %q has no display name", entry.ID) + require.Contains(t, descriptions, entry.ID, "experiment %q is not declared as a constant", entry.ID) + } +} diff --git a/scripts/release/docs_update_feature_stages.sh b/scripts/release/docs_update_feature_stages.sh index 4251ea8dbc3..2b13a99248c 100755 --- a/scripts/release/docs_update_feature_stages.sh +++ b/scripts/release/docs_update_feature_stages.sh @@ -17,36 +17,6 @@ if isdarwin; then awk() { gawk "$@"; } fi -parse_all_experiments() { - # Try ExperimentsSafe first, then fall back to ExperimentsAll if needed. - experiments_var="ExperimentsSafe" - experiments_output=$(go doc -all ./codersdk "${experiments_var}" 2>/dev/null || true) - - if [[ -z "${experiments_output}" ]]; then - experiments_var="ExperimentsAll" - experiments_output=$(go doc -all ./codersdk "${experiments_var}" 2>/dev/null || true) - - if [[ -z "${experiments_output}" ]]; then - log "Warning: Neither ExperimentsSafe nor ExperimentsAll found in ./codersdk" - return - fi - fi - - echo "${experiments_output}" | - tr -d $'\n\t ' | - grep -E -o "${experiments_var}=Experiments\{[^}]*\}" | - sed -e 's/.*{\(.*\)}.*/\1/' | - tr ',' '\n' -} - -parse_experiments() { - go doc -all ./codersdk Experiment | - sed \ - -e 's/\t\(Experiment[^ ]*\)\ \ *Experiment = "\([^"]*\)"\(.*\/\/ \(.*\)\)\?/\1|\2|\4/' \ - -e 's/\t\/\/ \(.*\)/||\1/' | - grep '|' -} - parse_beta_features() { jq -r ' # Collect paths that live under any beta-marked subtree. We exclude @@ -74,57 +44,24 @@ dest=${1:-docs/install/releases/feature-stages.md} log "Updating generated feature-stages sections in ${dest}" -# Collect experiments from the current codersdk package. -declare -A experiments=() -declare -A all_experiments=() -all_experiments_out="$(parse_all_experiments)" -if [[ -n "${all_experiments_out}" ]]; then - readarray -t all_experiments_tmp <<<"${all_experiments_out}" - for exp in "${all_experiments_tmp[@]}"; do - all_experiments[$exp]=1 - done -fi - -maybe_desc= -while read -r line; do - line=${line//$'\n'/} - readarray -d '|' -t parts <<<"$line" - - if [[ -z ${parts[0]} ]]; then - maybe_desc+="${parts[2]//$'\n'/ }" - continue - fi - - var="${parts[0]}" - key="${parts[1]}" - desc="${parts[2]}" - desc=${desc//$'\n'/} - - if [[ -z "${desc}" ]]; then - desc="${maybe_desc% }" - fi - maybe_desc= - - if [[ ! -v all_experiments[$var] ]]; then - log "Skipping ${var}, not listed in experiments list" - continue - fi - - experiments[$key]="$desc" -done < <(parse_experiments) +# The experiments table comes from docs/experiments.json, which +# scripts/experimentsdocgen generates from codersdk.ExperimentsKnown (every +# experiment this version knows about) and codersdk.ExperimentsSafe (the ones +# `--experiments=*` enables). Reading the generated file keeps the table and +# the machine-readable list identical by construction. +experiments_json="${EXPERIMENTS_JSON:-${PROJECT_ROOT}/docs/experiments.json}" table="$( - if [[ "${#experiments[@]}" -eq 0 ]]; then + if [[ ! -f "${experiments_json}" ]] || [[ "$(jq '.experiments | length' "${experiments_json}")" -eq 0 ]]; then echo "Currently no experimental features are available." exit 0 fi - echo "| Feature | Description |" - echo "| ------- | ----------- |" - for key in "${!experiments[@]}"; do - desc=${experiments[$key]} - echo "| \`$key\` | $desc |" - done + # Safety is decided per version in codersdk.ExperimentsSafe: a safe + # experiment is enabled by `--experiments=*`, an unsafe one only by name. + echo "| Feature | Flag | Description | Safety |" + echo "| ------- | ---- | ----------- | ------ |" + jq -r '.experiments[] | "| \(.displayName) | `\(.id)` | \(.description) | \(if .safe then "Safe" else "Unsafe" end) |"' "${experiments_json}" )" # Collect beta features from the current docs/manifest.json. Keying on the