From 223bbcaa6523f9a6b9726124826bccd8ff9215b8 Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Wed, 27 May 2026 10:54:04 -0400 Subject: [PATCH 01/25] fix script --- docs/testdata/gilbert.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/testdata/gilbert.sh b/docs/testdata/gilbert.sh index 8854be2f..39e67fd2 100755 --- a/docs/testdata/gilbert.sh +++ b/docs/testdata/gilbert.sh @@ -1,3 +1,3 @@ #!/usr/bin/env sh set -e -go run ../../cmd/gilbert2 "$@" --log-level=debug +go run ../../cmd/gilbert "$@" --log-level=debug From 7df958ebc6a46402b8f0935ca0ed42f88c9b13ea Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Wed, 27 May 2026 10:54:20 -0400 Subject: [PATCH 02/25] add todo for missing prop --- internal/manifest/loader/yamlloader/inputs.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/internal/manifest/loader/yamlloader/inputs.go b/internal/manifest/loader/yamlloader/inputs.go index 7dfd8088..c172f3dd 100644 --- a/internal/manifest/loader/yamlloader/inputs.go +++ b/internal/manifest/loader/yamlloader/inputs.go @@ -7,11 +7,12 @@ import ( "strings" "time" + "github.com/goccy/go-yaml/ast" + "github.com/go-gilbert/gilbert/internal/manifest" "github.com/go-gilbert/gilbert/pkg/expr" "github.com/go-gilbert/gilbert/pkg/parsetypes" . "github.com/go-gilbert/gilbert/pkg/yamltree" - "github.com/goccy/go-yaml/ast" ) var listTypeSchema = Struct( @@ -159,6 +160,7 @@ var inputDefinitionSchema = Struct( Offset: offset, } }), + // TODO: add delimiter ), ), func(_ context.Context, dst *manifest.InputDefinition, val *manifest.InputBinding) error { From 9b91549522e4af23b7b1c9a25d3a079bf96d20ee Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Wed, 27 May 2026 10:54:28 -0400 Subject: [PATCH 03/25] add schema draft --- docs/spec/syntax/expressions.md | 0 docs/spec/syntax/schema.d.ts | 252 ++++++++++++++++++++++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 docs/spec/syntax/expressions.md create mode 100644 docs/spec/syntax/schema.d.ts diff --git a/docs/spec/syntax/expressions.md b/docs/spec/syntax/expressions.md new file mode 100644 index 00000000..e69de29b diff --git a/docs/spec/syntax/schema.d.ts b/docs/spec/syntax/schema.d.ts new file mode 100644 index 00000000..34c76b31 --- /dev/null +++ b/docs/spec/syntax/schema.d.ts @@ -0,0 +1,252 @@ +/** + * @schema + * This file is a YAML schema description using TypeScript type syntax. + * It is NOT part of the application codebase (which is in Go). + * Do not generate TypeScript code from this file. + * + * Documentation conventions: + * + * - See @./expressions.md for expression language documentation. + * - "@const" annotation states that property should be a scalar value and expressions are not supported. + * - Properties that only allow expressions, use `Expression` type, where `T` is a return type of an expression. + */ + +/** + * A Yaml string type which a value parseable with Go's time.ParseDuration. + */ +type DurationString = string + +/** + * YAML string with expression, execution of which returns a type T. + * + * Expression syntax is described in @./expressions.md + * + * Although most values can be a type of an expression string, + * this type states that a value should be an expression and cannot be a static value. + */ +type Expression = string + +interface JobAction { + /** + * Name of action to be executed. + */ + action: string +} + +interface JobMixin { + /** + * Name of mixin to be executed. + */ + mixin: string +} + +interface JobTask { + /** + * Name of task to be executed. + */ + mixin: string +} + +/** + * Contains fields common to all job types. + */ +interface JobBase { + /** + * Whether to execute job concurrently. + * When true, a next job will start, without waiting current one to finish. + */ + async?: boolean + + /** + * Duration of time to wait before starting a job. + */ + delay?: DurationString + + /** + * Path to a working directory where job will be executed. + */ + ["working-directory"]?: string + + /** + * Job execution timeout duration + */ + deadline?: DurationString + + /** + * Whether to allow job to fail. + */ + ["continue-on-error"]?: boolean + + /** + * Whether to run a job. + * + * If expression returns false - job is skipped. + */ + if?: Expression; + + /** + * Enables matrix strategy. + * Used to define variables whose combinations create separate job runs. + * For example, node: [18, 20] and os: [ubuntu-latest, macos-latest] creates four runs. + * + * NOTE: "@const" annotation means that value cannot be an expression and should be a literal YAML value. + */ + strategy?: { + /** + * Maximum number of matrix jobs to run concurrently. + * + * @const + * @default 1 + */ + ["max-parallel"]?: number + + /** + * Matrix of different job configurations. + * The variables defined in a matrix, become properties in `matrix` context. + * + * Example: + * + * ```yaml + * matrix: + * os: [darwin, linux, windows] + * arch: [amd64, arm64] + * ``` + * + * Matrix will populate `${{matrix.os}}` and `${{matrix.arch}}` properties to every job execution. + * + * @const + */ + matrix: Record | any[]> + + /** + * List of combinations to exclude from job matrix. + * + * @const + */ + exclude?: Record[] + } + + /** + * Input parameters for action, mixin or task. + * + * Tasks and mixins declare expected parameters in "inputs" block. + */ + with: Record + + /** + * Defines a set of jobs to run when certain hook is called. + * Hooks are events produced by certain actions. + * + * For example, the `fs/watch` action has `changed` hook which is called when filesystem contents are changed. + * + * Besides that, runner itself has a builtin `error` hook to handle job execution failures (e.g. resource cleanup). + */ + on?: Record +} + +type JobType = JobAction | JobMixin | JobTask + +/** + * Job is a single execution unit of a task or mixin. + * Job can start an action, run mixin or call a different task. + * + * TODO: improve description. + */ +type Job = JobType & JobBase + +/** + * Defines task or mixin input parameter. + * Parameters are mapped to command-line flags. + * + * Input parameter description can be documented with a comment block below input block. + */ +interface InputDefinition { + /** + * Input value type. + * + * Besides standard scalar values, it supports special types that are parceable from command-line flag or string value: + * - `duration`: time duration is nanoseconds. String value is parsed using Go's `time.ParseDuration` function. + * - `date`: Date and time. Parsed from a format defined in `dateFormat` field. + * + * @const + */ + type: 'string' | 'int' | 'bool' | 'date' | 'duration' | 'float' | 'list' + + /** + * Type of items of an array. + * + * Should only be used when `type` is set to `list`. + * + * @const + */ + items?: 'string' | 'int' | 'bool' | 'date' | 'duration' | 'float' | 'list' + + /** + * Date format used to parse a command-line flag value. + * + * Has effect only when `type` is `date`. + * + * @const + */ + dateFormat?: string + + /** + * Fallback value used when input value not specified. + * + * Value type should correspond to `type`. + * Note: use `optional` to use an empty value by default. + * @const + */ + default?: any + + /** + * Whether a value is not required. + * + * When enabled and input value is unspecified - sets an empty value for input. + * + * Has no effect when `default` is set. + * + * @const + */ + optional?: boolean + + /** + * Binding property controls how input value is parsed from command-line + * flags and environment variables. + * + * Works only for tasks. Has no effect in mixins. + * + * @const + */ + binding?: { + /** + * Name of environment variable. + * + * If input value is unspecified and command-line isn't present, + * value of a given environment variable will be used (if present). + * + * @const + */ + env?: string + + /** + * Overrides a name of command-line flag assigned to input. + * + * By default, each input gets a command-line flag with the same name as an input. + * + * @const + */ + flag?: string + + /** + * Character to be used to split a string into a list. + * + * Used when parsing a value from environment variable or command-line. + * + * Has no effect if input type is not `list`. + * + * @const + */ + delimiter?: string + } +} From 27dc6820f1596587c9c6289cc5a743123d9647e1 Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Wed, 27 May 2026 11:02:35 -0400 Subject: [PATCH 04/25] document inputs --- docs/spec/syntax/schema.d.ts | 45 ++++++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/docs/spec/syntax/schema.d.ts b/docs/spec/syntax/schema.d.ts index 34c76b31..baf65b08 100644 --- a/docs/spec/syntax/schema.d.ts +++ b/docs/spec/syntax/schema.d.ts @@ -154,32 +154,34 @@ type JobType = JobAction | JobMixin | JobTask */ type Job = JobType & JobBase -/** - * Defines task or mixin input parameter. - * Parameters are mapped to command-line flags. - * - * Input parameter description can be documented with a comment block below input block. - */ -interface InputDefinition { +interface ListInputDefinition { /** - * Input value type. + * @const + */ + type: 'list' + + /** + * Defines a type of element of a list. * - * Besides standard scalar values, it supports special types that are parceable from command-line flag or string value: - * - `duration`: time duration is nanoseconds. String value is parsed using Go's `time.ParseDuration` function. - * - `date`: Date and time. Parsed from a format defined in `dateFormat` field. + * Should only be used when `type` is set to `list`. * * @const */ - type: 'string' | 'int' | 'bool' | 'date' | 'duration' | 'float' | 'list' + items: 'string' | 'int' | 'bool' | 'date' | 'duration' | 'float' | 'list' +} +interface ScalarInputDefinition { /** - * Type of items of an array. + * Input value type. * - * Should only be used when `type` is set to `list`. + * Besides standard scalar values, it supports special types that are parceable from command-line flag or string value: + * - `duration`: time duration is nanoseconds. String value is parsed using Go's `time.ParseDuration` function. + * - `date`: Date and time. Parsed from a format defined in `dateFormat` field. * * @const */ - items?: 'string' | 'int' | 'bool' | 'date' | 'duration' | 'float' | 'list' + type: 'string' | 'int' | 'bool' | 'date' | 'duration' | 'float' + items: never /** * Date format used to parse a command-line flag value. @@ -189,15 +191,16 @@ interface InputDefinition { * @const */ dateFormat?: string +} +interface InputDefinitionCommon { /** * Fallback value used when input value not specified. * * Value type should correspond to `type`. * Note: use `optional` to use an empty value by default. - * @const */ - default?: any + default?: Expression | any /** * Whether a value is not required. @@ -250,3 +253,11 @@ interface InputDefinition { delimiter?: string } } + +/** + * Defines task or mixin input parameter. + * Parameters are mapped to command-line flags. + * + * Input parameter description can be documented with a comment block below input block. + */ +type InputDefinition = InputDefinitionCommon & (ListInputDefinition | ScalarInputDefinition) From eb16b0cd1f6df2eb486196db18917e997136ea90 Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Wed, 27 May 2026 11:32:58 -0400 Subject: [PATCH 05/25] document schema --- docs/spec/syntax/schema.d.ts | 138 +++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/docs/spec/syntax/schema.d.ts b/docs/spec/syntax/schema.d.ts index baf65b08..d63229ce 100644 --- a/docs/spec/syntax/schema.d.ts +++ b/docs/spec/syntax/schema.d.ts @@ -18,6 +18,7 @@ type DurationString = string /** * YAML string with expression, execution of which returns a type T. + * The `T` type carries a purely documentation role. * * Expression syntax is described in @./expressions.md * @@ -261,3 +262,140 @@ interface InputDefinitionCommon { * Input parameter description can be documented with a comment block below input block. */ type InputDefinition = InputDefinitionCommon & (ListInputDefinition | ScalarInputDefinition) + +/** + * Mixins allow to decople a set of jobs into a reusable block. + */ +interface Mixin { + /** + * List of input parameters accepted by mixin. + * + * NOTE: unlike tasks, mixins don't support using command-line flags. + * Input parameters for mixins should be explicitly passed via `with:` parameter in a job. + * + * @const + */ + inputs?: Record + + /** + * Sequence of jobs that will be executed when mixin is called. + * + * @const + */ + steps: Job[] + + /** + * Override a working directory for a mixin. + * @const + */ + ["working-directory"]?: string +} + +interface Task { + /** + * List of input parameters accepted by a task. + * + * Input values can be passed to a task via command-line flags. + * + * @const + */ + inputs?: Record + + /** + * Sequence of jobs that will be executed when task is started. + * + * @const + */ + steps: Job[] + + /** + * Override a working directory for a task. + * @const + */ + ["working-directory"]?: string +} + +interface WorkflowFile { + /** + * Workflow file version. Should be `2`. + * @const + */ + version: 2 + + /** + * List of other workflow files to include and merge. + * + * @const + */ + include: string[] + + /** + * Set of plugins to import. + * + * Key is a namespace and value is import URL. + * + * For example, plugin with a following import, will expose its actions via `mydocker/` prefix: + * + * ```yaml + * plugins: + * mydocker: github://go-gilbert/gilbert-contrib/docker + * tasks: + * foo: + * steps: + * - action: mydocker/run + * with: + * ... + * ``` + * + * @const + */ + plugins: Record + + /** + * List of predefined variables to be used in expressions. + * + * Values defined in this block are available as `${{consts.*}}` + * + * Value should not be an expression. + * + * @const + */ + const: Record + + /** + * Global input parameters. + * + * @see [Task.inputs] + * @const + */ + inputs: Record + + /** + * Map of task name and its definition. + * + * Task description can be documented via a comment block below its name: + * + * ```yaml + * tasks: + * # Build the project + * build: + * steps: + * ... + * ``` + * + * Documented task description will be present in `gilbert list` output. + * + * Task can be called by name using `gilbert run `. + * + * Use `gilbert list` to display a list of available tasks. + * Use `gilbert run --help` see task description and its parameters. + */ + tasks: Record + + /** + * Mixins are reusable pieces of pipeline which can accept inputs. + * + * Unlike tasks, they cannot be called from command-line. + */ + mixins: Record +} From 17a74156689b4df4124888d2164fc108d446275f Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Wed, 27 May 2026 12:14:07 -0400 Subject: [PATCH 06/25] wip expression doc --- docs/spec/syntax/expressions.md | 64 +++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/docs/spec/syntax/expressions.md b/docs/spec/syntax/expressions.md index e69de29b..a9c2cc96 100644 --- a/docs/spec/syntax/expressions.md +++ b/docs/spec/syntax/expressions.md @@ -0,0 +1,64 @@ +# Gilbert Expression Syntax + +The Gilbert task runner supports expanding expressions. +Expressions are defined as YAML string values. + +## Expression Types + +### Shell call expression + +Substitutes a `$(...)` section in a string with a result of a shell command inside of it. + +Example: + +- **Expression:** `"$(printf 2+2=%d 4)"` +- **Result:** `"4"` + +### Language Expression + +TODO: find a better name for this type of expression. + +Runs an [Expr language][expr] expression inside `${{...}}` and returns its value. + +[expr]: https://expr-lang.org/ + +#### Examples + +##### Basic Operations + +- **Expression:** `"${{ 2+2*3 }}"` +- **Result:** `8` + +> [!NOTE] +> Unlike shell expressions, this example returns a numeric value. + +##### Accessing Context Variables + +Assume given a following workflow file: + +```yaml +consts: + is_prod: true +``` + +- **Expression:** `"${{consts.is_prod}}"` +- **Result:** `true` + +> [!NOTE] +> See [Available Context Variables](#available-context-variables) for a list of variables available inside expressions. + +### Mixed Expression + +Both shell and language expresssions can be mixed together. +This type of expression always return string. + +Example: + +- **Expression:**: `"$(whoami)'s home dir is ${{env.HOME}}"` +- **Result:**: `"root's home dir is /root"` + +## Available Context Variables + +### `project` + +Contains From 8b3a65dba24638e548aa7b9b53f0eb5cca524aeb Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Wed, 27 May 2026 13:48:41 -0400 Subject: [PATCH 07/25] document expressions --- docs/spec/syntax/eval_context.d.ts | 71 ++++++++++++++++++++++++++++++ docs/spec/syntax/expressions.md | 29 ++++++++++-- 2 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 docs/spec/syntax/eval_context.d.ts diff --git a/docs/spec/syntax/eval_context.d.ts b/docs/spec/syntax/eval_context.d.ts new file mode 100644 index 00000000..88ab400c --- /dev/null +++ b/docs/spec/syntax/eval_context.d.ts @@ -0,0 +1,71 @@ +/** + * @schema + * This file describes a schema of available variables in Expr lang expressions using TypeScript type syntax. + * It is NOT part of the application codebase (which is in Go). + * Do not generate TypeScript code from this file. + * + * See: + * - @./schema.d.ts - workflow file (`gilbert.yaml`) schema. + * - @./expressions.md - expression language documentation. + */ + +/** + * Holds global scope available in `${{...}}` expressions. + * + * Acts like `globalThis`/`window` in JavaScript. + */ +interface Scope { + /** + * Carries values provided to task/mixin inputs. + * + * @see [JobBase.on] in workflow file schema. + */ + inputs?: Record + + /** + * Holds constants defined in `const` section of workflow file. + * + * @see [WorkflowFile.const] + */ + consts?: Record + + project: { + /** + * Current job working directory. + */ + workDir: string + + /** + * Directory where workflow file is located. + */ + workspaceDir: string + + /** + * Path to workflow file. + */ + workflowFile: string + } + + /** + * System environment variables. + */ + env: Record + + /** + * Matrix values populated to a matrix strategy job. + * + * Only available if job has `.strategy.matrix` defined. + * + * @see [JobBase.strategy.matrix] + */ + matrix?: Record + + /** + * Metadata provided with a signal, fired by action. + * + * Only available if job is defined in `.on` block. + * + * @see [JobBase.on] + */ + event?: Record +} diff --git a/docs/spec/syntax/expressions.md b/docs/spec/syntax/expressions.md index a9c2cc96..bcaa77fe 100644 --- a/docs/spec/syntax/expressions.md +++ b/docs/spec/syntax/expressions.md @@ -1,3 +1,7 @@ +--- +agent_note: See eval_context.d.ts for expression context variables. +--- + # Gilbert Expression Syntax The Gilbert task runner supports expanding expressions. @@ -37,7 +41,7 @@ Runs an [Expr language][expr] expression inside `${{...}}` and returns its value Assume given a following workflow file: ```yaml -consts: +const: is_prod: true ``` @@ -54,11 +58,28 @@ This type of expression always return string. Example: -- **Expression:**: `"$(whoami)'s home dir is ${{env.HOME}}"` -- **Result:**: `"root's home dir is /root"` +- **Expression:** `"$(whoami)'s home dir is ${{env.HOME}}"` +- **Result:** `"root's home dir is /root"` ## Available Context Variables +List of variables available in language expressions: + +| Property | Description | Example | +| --------- | ----------------------------------------------------------------------------------------------- | ---------------------- | +| `project` | see [docs below](#project) | `${{project.workDir}}` | +| `consts` | Holds values of constants declared in `const` section of `gilbert.yaml` | `${{consts.MY_CONST}}` | +| `env` | Shell environment variables | `${{env.HOME}}` | +| `inputs` | Holds values for input parameters. | `${{inputs.foobar}}` | +| `matrix` | Matrix values given passed to a job. See `job.strategy.matrix` of `gilbert.yaml` | `${{matrix.goos}}` | +| `event` | Holds event args when action fires on signal. Available only in jobs defined inside `job.on.*`. | `${{event.error}}` | + ### `project` -Contains +Contains current working directory and a location from where task runner was started. + +| Property | Description | +| -------------- | --------------------------------------------------- | +| `workDir` | Path to a current working directory. | +| `workspaceDir` | Path to a directory where `gilbert.yaml` is located | +| `workflowFile` | Path to a current `gilbert.yaml`. | From 606873c203f4dee63ed85fa97d3b054299cd7475 Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Wed, 27 May 2026 15:38:43 -0400 Subject: [PATCH 08/25] add env to schema --- docs/spec/syntax/schema.d.ts | 45 ++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/docs/spec/syntax/schema.d.ts b/docs/spec/syntax/schema.d.ts index d63229ce..8e32d4fe 100644 --- a/docs/spec/syntax/schema.d.ts +++ b/docs/spec/syntax/schema.d.ts @@ -85,6 +85,21 @@ interface JobBase { */ if?: Expression; + /** + * Custom environment variables to set for a job. + * + * Example: + * + * ```yaml + * env: + * FOO: "bar" + * key: "${{inputs.myinput}}" + * ``` + * + * @const + */ + env?: Record + /** * Enables matrix strategy. * Used to define variables whose combinations create separate job runs. @@ -277,6 +292,21 @@ interface Mixin { */ inputs?: Record + /** + * Custom environment variables to use for a mixin. + * + * Example: + * + * ```yaml + * env: + * FOO: "bar" + * key: "${{inputs.myinput}}" + * ``` + * + * @const + */ + env?: Record + /** * Sequence of jobs that will be executed when mixin is called. * @@ -301,6 +331,21 @@ interface Task { */ inputs?: Record + /** + * Custom environment variables to use for a task. + * + * Example: + * + * ```yaml + * env: + * FOO: "bar" + * key: "${{inputs.myinput}}" + * ``` + * + * @const + */ + env?: Record + /** * Sequence of jobs that will be executed when task is started. * From 3ec04f76a7ef85090889897ca3520dd502c2ffc6 Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Wed, 27 May 2026 15:48:22 -0400 Subject: [PATCH 09/25] add example --- docs/spec/syntax/gilbert.yml | 171 +++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 docs/spec/syntax/gilbert.yml diff --git a/docs/spec/syntax/gilbert.yml b/docs/spec/syntax/gilbert.yml new file mode 100644 index 00000000..c614a442 --- /dev/null +++ b/docs/spec/syntax/gilbert.yml @@ -0,0 +1,171 @@ +# This is an example file that accompanies the @./schema.d.ts file. +version: 2 + +include: + - include.yml + +plugins: + githubMod: github://go-gilbert/example-plugin@v1.0.0 + httpMod: http://example.com/plugin.zip + localMod: ./my-plugin + +const: + foo: bar + +inputs: + # Is build a production release or not + release: + default: false + type: bool + + # Examples of other inputs of types + timeout: + type: duration + default: 10s + binding: + env: JOB_TIMEOUT + timestamp: + type: date + optional: true + dateFormat: "2006-01-02 15:04:05" + items: + type: list + items: + type: int + +mixins: + hello: + inputs: + name: + type: string + greeting: + type: string + default: Hello + steps: + - action: debug/echo + with: + message: "${{inputs.greeting}}, ${{inputs.name}}!" + - action: debug/echo + delay: 300ms + with: + message: "Goodbye, ${{inputs.name}}" + +tasks: + test:subtask: + inputs: + message: + type: string + steps: + - task: test:subtask:child + with: + msg: "subtasks with message: ${{inputs.message}}" + + test:subtask:child: + inputs: + msg: + type: string + steps: + - action: debug/echo + with: + message: "${{inputs.msg}}" + + test:mixin: + steps: + - mixin: hello + working-directory: .. + with: + name: Bob + greeting: Howdy + - action: debug/echo + with: + message: "This is the end, ${{inputs.name}}" + + # Task to debug task runner + test: + inputs: + # Test message content + message: + type: string + + # Signal name + signal: + type: string + default: mysignal + + steps: + - action: debug/signal + with: + signal: ${{inputs.signal}} + on: + mysignal: + - action: debug/echo + with: + message: "message: ${{inputs.message}}, data: ${{toJSON(event)}}" + error: + - action: debug/echo + with: + message: Error happened + - action: debug/echo + with: + message: next + + # Build builds application + build: + inputs: + # App version + version: + type: string + default: $(git describe --tags --abbrev=0) + steps: + - action: go/build + if: ${{inputs.version != "invalid"}} + timeout: 30s + continue-on-error: false + env: + FOO: bar + strategy: + max-parallel: 2 + matrix: + os: [windows, linux, darwin] + arch: [amd64, 386, arm64] + exclude: + - os: darwin + arch: 386 + - arch: amd64 + os: darwin + with: + package: . + os: ${{matrix.os}} + arch: ${{matrix.arch}} + cgo: false + buildvcs: false + tags: + - '${{inputs.release ? "release" : "debug"}}' + package_vars: + "github.com/go-gilbert/gilbert/internal/legacy/build.Version": "${{inputs.version}}" + "github.com/go-gilbert/gilbert/internal/legacy/build.Channel": "stable" + + # Starts dev server. + # + # Detailed description comes here. + start: + inputs: + configFile: + type: string + default: "dev.env" + steps: + - action: fs/watch + delay: 1s + async: true + with: + path: "./..." + debounceTime: 100ms + ignore: + - "*.log" + on: + change: + - action: go/run + async: true + with: + package: . + args: "-cfg=${{ inputs.configFile }}" From 7ff64b5b793aee633f497a8287543454addc254e Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Wed, 27 May 2026 16:13:36 -0400 Subject: [PATCH 10/25] add workflow doc --- docs/spec/syntax/gilbert.yml | 1 + docs/spec/syntax/types.md | 24 ++++++++++++++++++ docs/test_jobs.yaml | 48 ------------------------------------ 3 files changed, 25 insertions(+), 48 deletions(-) create mode 100644 docs/spec/syntax/types.md delete mode 100644 docs/test_jobs.yaml diff --git a/docs/spec/syntax/gilbert.yml b/docs/spec/syntax/gilbert.yml index c614a442..5affab5f 100644 --- a/docs/spec/syntax/gilbert.yml +++ b/docs/spec/syntax/gilbert.yml @@ -140,6 +140,7 @@ tasks: cgo: false buildvcs: false tags: + - customtag - '${{inputs.release ? "release" : "debug"}}' package_vars: "github.com/go-gilbert/gilbert/internal/legacy/build.Version": "${{inputs.version}}" diff --git a/docs/spec/syntax/types.md b/docs/spec/syntax/types.md new file mode 100644 index 00000000..a2f36d13 --- /dev/null +++ b/docs/spec/syntax/types.md @@ -0,0 +1,24 @@ +# Workflow File + +Workflow file is a YAML file named `gilbert.yaml` which contains definitions of executable tasks. + +## Schema + +See: @./schema.d.ts + +## Input Types + +Gilbert introduces a set of types available for inputs. Types above are convertable from/to YAML: + +| Name | Description | YAML equivalent | +| ---------- | --------------------------------------------------------------------------------------------- | -------------------------- | +| `string` | String | Strings, including heredoc | +| `int` | signed 64-bit integer | number | +| `float` | floating point number | number | +| `date` | Date and time | string | +| `duration` | Time diration in nanoseconds. String values are parsed using `time.ParseDuration` Go function | string | +| `list` | Array of elements | List block | + +## Expressions + +See @./expressions.md diff --git a/docs/test_jobs.yaml b/docs/test_jobs.yaml deleted file mode 100644 index 2e7ebdd7..00000000 --- a/docs/test_jobs.yaml +++ /dev/null @@ -1,48 +0,0 @@ -version: 1.0 - -#plugins: -# - github://github.com/go-gilbert/gilbert-plugin-example?version=v0.8.5 - -mixins: - async-test: - - action: shell - async: true - params: - command: 'foo' - -tasks: - test-subtask: - - task: '__foo' - vars: - message: 'Hello foo' - - task: 'test-plugins' - __foo: - - action: shell - params: - command: 'echo {{message}}' - test-plugins: - - action: 'example-plugin:hello-world' - params: - message: "i love vodka" - test-deadline-async: - - action: shell - deadline: 2000 - async: true - params: - command: 'sleep 4' - - action: shell - params: - command: 'ping -c 5 localhost' - win-test-async: - - action: shell - params: - command: ping google.com - test-async: - - mixin: async-test - - action: shell - params: - command: 'uname -a' - - action: shell - async: true - params: - command: 'ping -c 5 localhost' From d9487b069f6198779827f1dbed749f7b8b5fc1b6 Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Wed, 27 May 2026 16:14:09 -0400 Subject: [PATCH 11/25] move doc sources --- docs/spec/{syntax => src}/eval_context.d.ts | 0 docs/spec/{syntax => src}/expressions.md | 0 docs/spec/{syntax => src}/gilbert.yml | 0 docs/spec/{syntax => src}/schema.d.ts | 0 docs/spec/{syntax => src}/types.md | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename docs/spec/{syntax => src}/eval_context.d.ts (100%) rename docs/spec/{syntax => src}/expressions.md (100%) rename docs/spec/{syntax => src}/gilbert.yml (100%) rename docs/spec/{syntax => src}/schema.d.ts (100%) rename docs/spec/{syntax => src}/types.md (100%) diff --git a/docs/spec/syntax/eval_context.d.ts b/docs/spec/src/eval_context.d.ts similarity index 100% rename from docs/spec/syntax/eval_context.d.ts rename to docs/spec/src/eval_context.d.ts diff --git a/docs/spec/syntax/expressions.md b/docs/spec/src/expressions.md similarity index 100% rename from docs/spec/syntax/expressions.md rename to docs/spec/src/expressions.md diff --git a/docs/spec/syntax/gilbert.yml b/docs/spec/src/gilbert.yml similarity index 100% rename from docs/spec/syntax/gilbert.yml rename to docs/spec/src/gilbert.yml diff --git a/docs/spec/syntax/schema.d.ts b/docs/spec/src/schema.d.ts similarity index 100% rename from docs/spec/syntax/schema.d.ts rename to docs/spec/src/schema.d.ts diff --git a/docs/spec/syntax/types.md b/docs/spec/src/types.md similarity index 100% rename from docs/spec/syntax/types.md rename to docs/spec/src/types.md From 51e11f00d4896f60f678d649dc2b80dfec3901af Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Wed, 27 May 2026 16:31:29 -0400 Subject: [PATCH 12/25] fix schema --- docs/spec/src/schema.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/spec/src/schema.d.ts b/docs/spec/src/schema.d.ts index 8e32d4fe..04d919ed 100644 --- a/docs/spec/src/schema.d.ts +++ b/docs/spec/src/schema.d.ts @@ -71,7 +71,7 @@ interface JobBase { /** * Job execution timeout duration */ - deadline?: DurationString + timeout?: DurationString /** * Whether to allow job to fail. From e766d99896f16407bf1da175c23753586a00e4e9 Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Wed, 27 May 2026 16:47:00 -0400 Subject: [PATCH 13/25] workflow spec draft --- docs/spec/workflow.md | 376 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 376 insertions(+) create mode 100644 docs/spec/workflow.md diff --git a/docs/spec/workflow.md b/docs/spec/workflow.md new file mode 100644 index 00000000..6fc71279 --- /dev/null +++ b/docs/spec/workflow.md @@ -0,0 +1,376 @@ +# Gilbert Workflow File Specification + +This document specifies the Gilbert workflow file format, the input schema model, and the expression language available inside workflow values. + +The workflow file is a YAML document named `gilbert.yaml` or `gilbert.yml`. It declares executable tasks, reusable mixins, plugins, constants, and input definitions. + +Reference material: + +- `src/schema.d.ts` describes the YAML shape in TypeScript syntax for documentation. +- `src/eval_context.d.ts` describes variables available to expression evaluation. +- `src/expressions.md` and `src/types.md` describe expression and input-type behavior. +- `src/gilbert.yml` is an example workflow. + +## Document Shape + +```yaml +version: "2" +include: [] +plugins: {} +const: {} +inputs: {} +mixins: {} +tasks: {} +``` + +All top-level sections except `version` are optional unless a task needs them. Unknown fields are invalid when the loader validates a structured block. + +| Field | Type | Description | +| --- | --- | --- | +| `version` | string or number | Workflow format version. The supported value is `2`. | +| `include` | list of strings | Other workflow files to load and merge into the current workflow. Paths are resolved relative to the file that declares them. | +| `plugins` | map of string to string | Plugin imports. The map key is the action namespace exposed by the plugin. | +| `const` | map of scalar values | Static values exposed to expressions as `consts.*`. Expressions are not evaluated in this block. | +| `inputs` | map of input definitions | Global input definitions. | +| `mixins` | map of mixin definitions | Reusable job groups that can be called from tasks or other mixins. | +| `tasks` | map of task definitions | Runnable entry points. A task can be executed with `gilbert run `. | + +## Includes + +`include` is a list of YAML file paths: + +```yaml +include: + - ./common.yml +``` + +Each path must point to a file, cannot point to the current file, and is resolved relative to the including file. Included workflows are merged into the current workflow. Duplicate definitions are rejected for sections that require unique names. + +## Plugins + +`plugins` maps a namespace to an import URL: + +```yaml +plugins: + docker: github://go-gilbert/gilbert-contrib/docker + local: ./plugins/local +``` + +Actions exported by the plugin are referenced with the namespace prefix: + +```yaml +tasks: + image: + steps: + - action: docker/build + with: + context: . +``` + +## Constants + +`const` defines scalar values available to `${{ ... }}` expressions through the `consts` context: + +```yaml +const: + release_channel: stable + publish: true +``` + +Constants are literal YAML scalars. Expressions are not expanded inside `const`. + +## Inputs + +Inputs declare values accepted by the workflow, tasks, and mixins. + +```yaml +inputs: + release: + type: bool + default: false + + timeout: + type: duration + default: 10s + binding: + env: JOB_TIMEOUT + flag: timeout +``` + +Task inputs can be set from command-line flags, environment variables, defaults, or explicit `with` values. Mixin inputs are passed explicitly from the calling job through `with`; command-line bindings do not apply to mixins. + +### Input Definition + +| Field | Type | Description | +| --- | --- | --- | +| `type` | input type | Required. One of `string`, `int`, `bool`, `float`, `date`, `duration`, or `list`. | +| `items` | type schema | Required when `type: list`; invalid otherwise. Defines the list element type. | +| `dateFormat` | string | Date parsing layout. Only valid for `date`. Defaults to Go `time.RFC3339`. | +| `default` | value or expression | Fallback value when no value is provided. Must match the input type after expansion. | +| `optional` | bool | Allows an omitted input to resolve to the type's zero value. Has no effect when `default` is set. Boolean inputs are optional by default. | +| `binding.env` | string | Environment variable used when no explicit input or flag is provided. | +| `binding.flag` | string | Command-line flag name. Defaults to the input name when omitted. | +| `binding.delimiter` | string | Intended delimiter for parsing list values from flags or environment variables. Current loader support is not implemented. | + +### Input Types + +| Type | YAML representation | Runtime value | +| --- | --- | --- | +| `string` | string | string | +| `int` | number | signed 64-bit integer | +| `float` | number | 64-bit floating-point number | +| `bool` | boolean | boolean | +| `date` | string | parsed with `dateFormat` or RFC3339 | +| `duration` | string | parsed by Go `time.ParseDuration`, for example `300ms`, `10s`, `5m` | +| `list` | YAML sequence or delimited input value | list of typed elements | + +List inputs use an `items` schema: + +```yaml +inputs: + ports: + type: list + items: + type: int +``` + +Nested complex item types are not supported. A list item type can be scalar, but not another list or dictionary. + +## Tasks And Mixins + +Tasks and mixins share the same job-group shape: + +```yaml +tasks: + build: + inputs: {} + env: {} + working-directory: . + steps: [] + +mixins: + hello: + inputs: {} + env: {} + working-directory: . + steps: [] +``` + +| Field | Type | Description | +| --- | --- | --- | +| `inputs` | map of input definitions | Values accepted by this task or mixin. | +| `env` | map | Environment values scoped to the task or mixin. | +| `working-directory` | string | Working directory for jobs in the group. Relative paths resolve from the current workflow working directory. | +| `steps` | list of jobs | Required. Jobs executed in order unless a job is asynchronous. | + +Tasks are command-line entry points. Mixins are reusable job groups and cannot be run directly from the command line. + +Comments immediately above task, mixin, and input keys may be collected as documentation by tooling. + +## Jobs + +A job is one step in a task, mixin, or event hook. It must target exactly one executable unit. + +```yaml +steps: + - action: debug/echo + with: + message: Hello + + - mixin: hello + with: + name: Bob + + - task: test:subtask + with: + message: nested task call +``` + +### Job Target + +Exactly one target field must be set. + +| Field | Type | Description | +| --- | --- | --- | +| `action` | string | Action name. Built-in and plugin actions use namespace-style names such as `debug/echo` or `go/build`. | +| `mixin` | string | Mixin name to execute. | +| `task` | string | Task name to execute. | + +### Job Fields + +| Field | Type | Description | +| --- | --- | --- | +| `async` | bool | When true, the runner starts the next job without waiting for this job to finish. | +| `delay` | duration string | Time to wait before starting the job. | +| `working-directory` | string | Working directory for this job. | +| `timeout` | duration string | Maximum execution time for this job. | +| `continue-on-error` | bool | When true, task execution continues after this job fails. | +| `if` | expression | Conditional expression. If it evaluates to false, the job is skipped. Static literal values are invalid here. | +| `env` | map | Environment values scoped to this job. | +| `strategy` | strategy object | Matrix execution strategy. | +| `with` | map | Arguments passed to the action, mixin, or task. | +| `on` | map of string to job list | Event hooks fired by this job or by the runner. | + +## Matrix Strategy + +`strategy` expands one job into multiple job runs by computing the Cartesian product of matrix values. + +```yaml +steps: + - action: go/build + strategy: + max-parallel: 2 + matrix: + os: [windows, linux, darwin] + arch: [amd64, arm64] + exclude: + - os: darwin + arch: amd64 + with: + os: ${{ matrix.os }} + arch: ${{ matrix.arch }} +``` + +| Field | Type | Description | +| --- | --- | --- | +| `max-parallel` | positive integer | Maximum number of matrix jobs to run concurrently. Defaults to `1`. | +| `matrix` | ordered map of string to list or expression returning list | Required. Each key becomes available under `matrix.` during each job run. | +| `exclude` | list of scalar maps | Excludes matrix rows that match all specified key/value pairs in an exclude rule. | + +`matrix` values may be literal arrays or expressions that evaluate to arrays. Empty matrix arrays are ignored with a warning. `exclude` values and the referenced matrix values must be scalar and comparable. + +## Event Hooks + +Jobs may define `on` hooks. Each hook maps an event name to jobs that run when the event is emitted. + +```yaml +steps: + - action: debug/signal + with: + signal: changed + on: + changed: + - action: debug/echo + with: + message: "event: ${{ event.name }}" + error: + - action: debug/echo + with: + message: "job failed" +``` + +Actions may define their own hook names. The runner also supports an `error` hook for handling job failures. + +Jobs inside a hook receive the `event` expression context. + +## Expression Language + +Gilbert expands dynamic expressions in YAML string values where the schema allows lazy values. Expressions are parsed before execution and evaluated at runtime. + +There are two expression forms: + +| Form | Example | Result type | +| --- | --- | --- | +| Shell expression | `$(git describe --tags --abbrev=0)` | string | +| Eval expression | `${{ inputs.release ? "release" : "debug" }}` | value returned by Expr | + +When a string contains multiple literal or expression parts, the parts are concatenated and the final value is a string: + +```yaml +message: "$(whoami)'s workspace is ${{ project.workspaceDir }}" +``` + +When a string contains exactly one eval expression, the expression may return a non-string value: + +```yaml +cgo: ${{ inputs.release }} +``` + +### Shell Expressions + +Shell expressions use `$(...)`: + +```yaml +version: + type: string + default: $(git describe --tags --abbrev=0) +``` + +The body is executed by the system shell in the current expression scope. The working directory is `project.workDir`, and the command receives the current environment. Command evaluation has a fixed timeout of 30 seconds. + +Shell expressions can contain eval expressions in their body: + +```yaml +value: $(echo "${{ inputs.name }}") +``` + +### Eval Expressions + +Eval expressions use `${{ ... }}` and are evaluated by the [Expr](https://expr-lang.org/) language: + +```yaml +if: ${{ inputs.version != "invalid" }} +``` + +Expr syntax supports operators, conditionals, function calls, selectors, indexing, and built-ins provided by Expr. Gilbert supplies the evaluation context described below. + +Nested Gilbert expressions are not allowed inside the body of a `${{ ... }}` expression. + +### Expression Context + +| Name | Type | Available when | Description | +| --- | --- | --- | --- | +| `project.workDir` | string | always | Current job working directory. | +| `project.workspaceDir` | string | always | Directory containing the workflow file. | +| `project.workflowFile` | string | always | Path to the current workflow file. | +| `env` | map of string to string | always | System environment variables. | +| `consts` | map | when constants exist | Values from the workflow `const` block. | +| `inputs` | map | inside task, mixin, job, and default evaluation scopes | Resolved input values. | +| `matrix` | map | inside matrix job runs | Current matrix row values. | +| `event` | map | inside jobs declared under `on` hooks | Event metadata emitted by an action or runner hook. | + +Input and constant lookups are inherited through nested scopes. A child scope can see values from its parent scope unless it overrides the same key. + +### Expression Placement + +Expressions are allowed in dynamic value positions such as: + +- input `default` +- job `if` +- job `with` values +- matrix values +- nested arrays and maps inside dynamic values + +Expressions are not evaluated in fields marked as constant by the schema, including: + +- top-level `version`, `include`, `plugins`, and `const` +- input metadata such as `type`, `items`, `dateFormat`, `optional`, and `binding` +- job and job-group structure such as target names, `steps`, hook names, and strategy field names + +## JSON Schema Representation + +The workflow schema can be represented as JSON Schema for structural validation, but not as a complete replacement for the current TypeScript documentation and Go loader rules. + +A JSON Schema can model: + +- top-level sections and required `version` +- task, mixin, job, strategy, and input object shapes +- allowed scalar input type names +- `oneOf` rules for `action`, `mixin`, and `task` targets +- `if`/`then` rules such as requiring `items` when `type` is `list` +- `patternProperties` for name-to-definition maps +- duration and expression fields as strings + +A JSON Schema cannot fully model, without custom keywords or runtime validation: + +- parsing Go duration strings with exact `time.ParseDuration` behavior +- parsing dates with a user-provided Go layout in `dateFormat` +- validating Expr syntax inside `${{ ... }}` +- validating shell command expressions inside `$(...)` +- distinguishing all literal-only fields from expression-capable fields by runtime parser behavior +- enforcing include-file existence, recursive include checks, and merge conflicts +- validating action-specific `with` schemas from plugins +- checking matrix exclude comparability and whether exclude keys are present in the matrix +- collecting comments as task/input documentation + +Recommended approach: generate or maintain a JSON Schema for editor assistance and early structural diagnostics, while keeping the TypeScript schema and Go loader as the normative source for semantic validation. Use custom schema extensions such as `x-gilbert-expression`, `x-gilbert-const`, and `x-gilbert-duration` if tooling needs to preserve Gilbert-specific semantics. From 7130cc2467408e39116103ffa076d39a8ab38e60 Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Wed, 27 May 2026 17:03:54 -0400 Subject: [PATCH 14/25] document mixed expr --- docs/spec/src/expressions.md | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/spec/src/expressions.md b/docs/spec/src/expressions.md index bcaa77fe..e9456b7b 100644 --- a/docs/spec/src/expressions.md +++ b/docs/spec/src/expressions.md @@ -18,7 +18,7 @@ Example: - **Expression:** `"$(printf 2+2=%d 4)"` - **Result:** `"4"` -### Language Expression +### Value Expression TODO: find a better name for this type of expression. @@ -36,6 +36,15 @@ Runs an [Expr language][expr] expression inside `${{...}}` and returns its value > [!NOTE] > Unlike shell expressions, this example returns a numeric value. +### Mixed Expression + +A string which contains both shell, value expression and string literals. + +Returns a string. + +- **Expression:** `"Home of $(whoami) is ${{env.HOME}}"` +- **Result:** `"Home of Alice is /Users/Alice"` + ##### Accessing Context Variables Assume given a following workflow file: From e4307bf13169e68e21135cc32543909666c5e961 Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Wed, 27 May 2026 17:19:49 -0400 Subject: [PATCH 15/25] spec --- docs/spec/workflow.md | 260 ++++++++++++++++++++++++++++++------------ 1 file changed, 188 insertions(+), 72 deletions(-) diff --git a/docs/spec/workflow.md b/docs/spec/workflow.md index 6fc71279..9c9f125a 100644 --- a/docs/spec/workflow.md +++ b/docs/spec/workflow.md @@ -25,15 +25,15 @@ tasks: {} All top-level sections except `version` are optional unless a task needs them. Unknown fields are invalid when the loader validates a structured block. -| Field | Type | Description | -| --- | --- | --- | -| `version` | string or number | Workflow format version. The supported value is `2`. | -| `include` | list of strings | Other workflow files to load and merge into the current workflow. Paths are resolved relative to the file that declares them. | -| `plugins` | map of string to string | Plugin imports. The map key is the action namespace exposed by the plugin. | -| `const` | map of scalar values | Static values exposed to expressions as `consts.*`. Expressions are not evaluated in this block. | -| `inputs` | map of input definitions | Global input definitions. | -| `mixins` | map of mixin definitions | Reusable job groups that can be called from tasks or other mixins. | -| `tasks` | map of task definitions | Runnable entry points. A task can be executed with `gilbert run `. | +| Field | Required | Type | Expressions | Description | +| --- | --- | --- | --- | --- | +| `version` | [x] | string or number | No | Workflow format version. The supported value is `2`. | +| `include` | | list of strings | No | Other workflow files to load and merge into the current workflow. Paths are resolved relative to the file that declares them. | +| `plugins` | | map of string to string | No | Plugin imports. The map key is the action namespace exposed by the plugin. | +| `const` | | map of scalar values | No | Static values exposed to expressions as `consts.*`. | +| `inputs` | | map of [input definitions](#input-definition) | Per definition | Global input definitions. | +| `mixins` | | map of [mixin definitions](#tasks-and-mixins) | Per mixin field | Reusable job groups that can be called from tasks or other mixins. | +| `tasks` | | map of [task definitions](#tasks-and-mixins) | Per task field | Runnable entry points. A task can be executed with `gilbert run `. | ## Includes @@ -101,28 +101,28 @@ Task inputs can be set from command-line flags, environment variables, defaults, ### Input Definition -| Field | Type | Description | -| --- | --- | --- | -| `type` | input type | Required. One of `string`, `int`, `bool`, `float`, `date`, `duration`, or `list`. | -| `items` | type schema | Required when `type: list`; invalid otherwise. Defines the list element type. | -| `dateFormat` | string | Date parsing layout. Only valid for `date`. Defaults to Go `time.RFC3339`. | -| `default` | value or expression | Fallback value when no value is provided. Must match the input type after expansion. | -| `optional` | bool | Allows an omitted input to resolve to the type's zero value. Has no effect when `default` is set. Boolean inputs are optional by default. | -| `binding.env` | string | Environment variable used when no explicit input or flag is provided. | -| `binding.flag` | string | Command-line flag name. Defaults to the input name when omitted. | -| `binding.delimiter` | string | Intended delimiter for parsing list values from flags or environment variables. Current loader support is not implemented. | +| Field | Required | Type | Expressions | Description | +| --- | --- | --- | --- | --- | +| `type` | [x] | [input type](#input-types) | No | One of [`string`](#string), [`int`](#int), [`bool`](#bool), [`float`](#float), [`date`](#date), [`duration`](#duration), or [`list`](#list). | +| `items` | when `type: list` | [type schema](#type-schema) | No | Invalid otherwise. Defines the list element type. | +| `dateFormat` | | string | No | Date parsing layout. Only valid for [`date`](#date). Defaults to Go `time.RFC3339`. | +| `default` | | value or [expression](#expression-language) | Yes | Fallback value when no value is provided. Must match the input type after expansion. | +| `optional` | | bool | No | Allows an omitted input to resolve to the type's zero value. Has no effect when `default` is set. Boolean inputs are optional by default. | +| `binding.env` | | string | No | Environment variable used when no explicit input or flag is provided. | +| `binding.flag` | | string | No | Command-line flag name. Defaults to the input name when omitted. | +| `binding.delimiter` | | string | No | Intended delimiter for parsing list values from flags or environment variables. Current loader support is not implemented. | ### Input Types | Type | YAML representation | Runtime value | | --- | --- | --- | -| `string` | string | string | -| `int` | number | signed 64-bit integer | -| `float` | number | 64-bit floating-point number | -| `bool` | boolean | boolean | -| `date` | string | parsed with `dateFormat` or RFC3339 | -| `duration` | string | parsed by Go `time.ParseDuration`, for example `300ms`, `10s`, `5m` | -| `list` | YAML sequence or delimited input value | list of typed elements | +| [`string`](#string) | string | string | +| [`int`](#int) | number | signed 64-bit integer | +| [`float`](#float) | number | 64-bit floating-point number | +| [`bool`](#bool) | boolean | boolean | +| [`date`](#date) | string | parsed with `dateFormat` or RFC3339 | +| [`duration`](#duration) | string | parsed by Go `time.ParseDuration`, for example `300ms`, `10s`, `5m` | +| [`list`](#list) | YAML sequence or delimited input value | list of typed elements | List inputs use an `items` schema: @@ -136,6 +136,95 @@ inputs: Nested complex item types are not supported. A list item type can be scalar, but not another list or dictionary. +### Type Reference + +#### String + +`string` is text. YAML strings may be plain, quoted, or block scalars. + +```yaml +name: + type: string + default: Gilbert +``` + +#### Int + +`int` is a signed 64-bit integer. + +```yaml +retries: + type: int + default: 3 +``` + +#### Float + +`float` is a 64-bit floating-point number. + +```yaml +threshold: + type: float + default: 0.75 +``` + +#### Bool + +`bool` is a boolean value. Boolean inputs are optional by default. + +```yaml +release: + type: bool + default: false +``` + +#### Date + +`date` is a date/time value parsed from a string. By default, values use Go `time.RFC3339`. A custom Go time layout can be set with `dateFormat`. + +```yaml +publishedAt: + type: date + dateFormat: "2006-01-02 15:04:05" +``` + +#### Duration + +`duration` is a time duration parsed from a string with Go `time.ParseDuration`. Supported examples include `300ms`, `10s`, `5m`, `2h45m`, and `1.5h`. + +Duration values are also used by workflow fields described as a [duration string](#duration), such as job `delay` and `timeout`. + +```yaml +timeout: + type: duration + default: 10s +``` + +#### List + +`list` is an array of typed elements. List inputs must declare an `items` [type schema](#type-schema). + +```yaml +ports: + type: list + items: + type: int +``` + +### Type Schema + +A type schema is the object form used by `items` to describe a list element: + +```yaml +items: + type: int +``` + +| Field | Required | Type | Expressions | Description | +| --- | --- | --- | --- | --- | +| `type` | [x] | [input type](#input-types) | No | Element type. Complex nested types such as [`list`](#list) are not supported. | +| `dateFormat` | | string | No | Date parsing layout. Only valid when `type` is [`date`](#date). | + ## Tasks And Mixins Tasks and mixins share the same job-group shape: @@ -156,12 +245,12 @@ mixins: steps: [] ``` -| Field | Type | Description | -| --- | --- | --- | -| `inputs` | map of input definitions | Values accepted by this task or mixin. | -| `env` | map | Environment values scoped to the task or mixin. | -| `working-directory` | string | Working directory for jobs in the group. Relative paths resolve from the current workflow working directory. | -| `steps` | list of jobs | Required. Jobs executed in order unless a job is asynchronous. | +| Field | Required | Type | Expressions | Description | +| --- | --- | --- | --- | --- | +| `inputs` | | map of [input definitions](#input-definition) | Per definition | Values accepted by this task or mixin. | +| `env` | | map | No | Environment values scoped to the task or mixin. | +| `working-directory` | | string | No | Working directory for jobs in the group. Relative paths resolve from the current workflow working directory. | +| `steps` | [x] | list of [jobs](#jobs) | Per job field | Jobs executed in order unless a job is asynchronous. | Tasks are command-line entry points. Mixins are reusable job groups and cannot be run directly from the command line. @@ -190,26 +279,26 @@ steps: Exactly one target field must be set. -| Field | Type | Description | -| --- | --- | --- | -| `action` | string | Action name. Built-in and plugin actions use namespace-style names such as `debug/echo` or `go/build`. | -| `mixin` | string | Mixin name to execute. | -| `task` | string | Task name to execute. | +| Field | Required | Type | Expressions | Description | +| --- | --- | --- | --- | --- | +| `action` | one target | string | No | Action name. Built-in and plugin actions use namespace-style names such as `debug/echo` or `go/build`. | +| `mixin` | one target | string | No | Mixin name to execute. | +| `task` | one target | string | No | Task name to execute. | ### Job Fields -| Field | Type | Description | -| --- | --- | --- | -| `async` | bool | When true, the runner starts the next job without waiting for this job to finish. | -| `delay` | duration string | Time to wait before starting the job. | -| `working-directory` | string | Working directory for this job. | -| `timeout` | duration string | Maximum execution time for this job. | -| `continue-on-error` | bool | When true, task execution continues after this job fails. | -| `if` | expression | Conditional expression. If it evaluates to false, the job is skipped. Static literal values are invalid here. | -| `env` | map | Environment values scoped to this job. | -| `strategy` | strategy object | Matrix execution strategy. | -| `with` | map | Arguments passed to the action, mixin, or task. | -| `on` | map of string to job list | Event hooks fired by this job or by the runner. | +| Field | Required | Type | Expressions | Description | +| --- | --- | --- | --- | --- | +| `async` | | bool | No | When true, the runner starts the next job without waiting for this job to finish. | +| `delay` | | [duration string](#duration) | No | Time to wait before starting the job. | +| `working-directory` | | string | No | Working directory for this job. | +| `timeout` | | [duration string](#duration) | No | Maximum execution time for this job. | +| `continue-on-error` | | bool | No | When true, task execution continues after this job fails. | +| `if` | | [expression](#expression-language) | Required expression | Conditional expression. If it evaluates to false, the job is skipped. Static literal values are invalid here. | +| `env` | | map | No | Environment values scoped to this job. | +| `strategy` | | [strategy object](#matrix-strategy) | Per strategy field | Matrix execution strategy. | +| `with` | | map | Yes, recursively in values | Arguments passed to the action, mixin, or task. Map keys are literal. | +| `on` | | map of string to job list | Per hook job field | Event hooks fired by this job or by the runner. Hook names are literal. | ## Matrix Strategy @@ -231,11 +320,11 @@ steps: arch: ${{ matrix.arch }} ``` -| Field | Type | Description | -| --- | --- | --- | -| `max-parallel` | positive integer | Maximum number of matrix jobs to run concurrently. Defaults to `1`. | -| `matrix` | ordered map of string to list or expression returning list | Required. Each key becomes available under `matrix.` during each job run. | -| `exclude` | list of scalar maps | Excludes matrix rows that match all specified key/value pairs in an exclude rule. | +| Field | Required | Type | Expressions | Description | +| --- | --- | --- | --- | --- | +| `max-parallel` | | positive integer | No | Maximum number of matrix jobs to run concurrently. Defaults to `1`. | +| `matrix` | [x] | ordered map of string to list or expression returning list | Yes, in values | Each key becomes available under `matrix.` during each job run. Matrix keys are literal. | +| `exclude` | | list of scalar maps | No | Excludes matrix rows that match all specified key/value pairs in an exclude rule. | `matrix` values may be literal arrays or expressions that evaluate to arrays. Empty matrix arrays are ignored with a warning. `exclude` values and the referenced matrix values must be scalar and comparable. @@ -267,25 +356,30 @@ Jobs inside a hook receive the `event` expression context. Gilbert expands dynamic expressions in YAML string values where the schema allows lazy values. Expressions are parsed before execution and evaluated at runtime. -There are two expression forms: +There are three expression forms: | Form | Example | Result type | | --- | --- | --- | | Shell expression | `$(git describe --tags --abbrev=0)` | string | -| Eval expression | `${{ inputs.release ? "release" : "debug" }}` | value returned by Expr | +| Value expression | `${{ inputs.release ? "release" : "debug" }}` | value returned by Expr | +| Mixed expression | `'some string, ${{ "expres" + "sion" }}, and $(echo shell)'` | string | -When a string contains multiple literal or expression parts, the parts are concatenated and the final value is a string: +When a string contains multiple literal or expression parts, it is a mixed expression. The parts are concatenated and the final value is a string: ```yaml message: "$(whoami)'s workspace is ${{ project.workspaceDir }}" ``` -When a string contains exactly one eval expression, the expression may return a non-string value: +Values returned from `${{ ... }}` inside a mixed expression are cast to strings. String, number, and boolean values can be cast. Objects and lists cannot be cast and cause expression evaluation to fail. + +When a string contains exactly one value expression, the expression may return a non-string value: ```yaml cgo: ${{ inputs.release }} ``` +Terminology note: this document uses "value expression" for `${{ ... }}` because the expression can return typed values when it is the whole YAML value. Other reasonable names are "Expr expression", "language expression", and "runtime expression"; "value expression" is the least awkward and describes the behavior most directly. + ### Shell Expressions Shell expressions use `$(...)`: @@ -298,31 +392,41 @@ version: The body is executed by the system shell in the current expression scope. The working directory is `project.workDir`, and the command receives the current environment. Command evaluation has a fixed timeout of 30 seconds. -Shell expressions can contain eval expressions in their body: +Shell expressions can contain value expressions in their body: ```yaml value: $(echo "${{ inputs.name }}") ``` -### Eval Expressions +### Value Expressions -Eval expressions use `${{ ... }}` and are evaluated by the [Expr](https://expr-lang.org/) language: +Value expressions use `${{ ... }}` and are evaluated by the [Expr](https://expr-lang.org/) language: ```yaml if: ${{ inputs.version != "invalid" }} ``` -Expr syntax supports operators, conditionals, function calls, selectors, indexing, and built-ins provided by Expr. Gilbert supplies the evaluation context described below. +Expr syntax supports operators, conditionals, function calls, selectors, indexing, and built-ins provided by Expr. See the Expr [language definition](https://expr-lang.org/docs/language-definition) and [functions](https://expr-lang.org/docs/functions) documentation for the syntax and standard functions available from Expr. + +Gilbert supplies the evaluation context described below. Nested Gilbert expressions are not allowed inside the body of a `${{ ... }}` expression. +### Mixed Expressions + +Mixed expressions combine literal text with one or more shell or value expressions: + +```yaml +message: 'some string, ${{ "expres" + "sion" }}, and $(echo shell)' +``` + +Mixed expressions always produce a string. Shell expression results are already strings. Value expression results are converted to strings when possible; conversion fails for objects and lists. + ### Expression Context | Name | Type | Available when | Description | | --- | --- | --- | --- | -| `project.workDir` | string | always | Current job working directory. | -| `project.workspaceDir` | string | always | Directory containing the workflow file. | -| `project.workflowFile` | string | always | Path to the current workflow file. | +| [`project`](#project-context) | object | always | Project paths and current working directory. | | `env` | map of string to string | always | System environment variables. | | `consts` | map | when constants exist | Values from the workflow `const` block. | | `inputs` | map | inside task, mixin, job, and default evaluation scopes | Resolved input values. | @@ -331,9 +435,27 @@ Nested Gilbert expressions are not allowed inside the body of a `${{ ... }}` exp Input and constant lookups are inherited through nested scopes. A child scope can see values from its parent scope unless it overrides the same key. +### Project Context + +`project` contains paths for the current workflow execution: + +| Name | Type | Description | +| --- | --- | --- | +| `project.workDir` | string | Current job working directory. | +| `project.workspaceDir` | string | Directory containing the workflow file. | +| `project.workflowFile` | string | Path to the current workflow file. | + ### Expression Placement -Expressions are allowed in dynamic value positions such as: +Each schema table includes an `Expressions` column. The general rules are: + +- `No` means the value is literal and expressions are not evaluated. +- `Yes` means the field accepts shell expressions, value expressions, and mixed expressions. +- `Required` means the field must be an expression and cannot be a static literal. +- `Per ... field` means the object itself is structural, and expression support is determined by each child property. +- `recursively in values` means expressions are allowed in nested map and list values, but map keys remain literal. + +Common dynamic value positions include: - input `default` - job `if` @@ -341,12 +463,6 @@ Expressions are allowed in dynamic value positions such as: - matrix values - nested arrays and maps inside dynamic values -Expressions are not evaluated in fields marked as constant by the schema, including: - -- top-level `version`, `include`, `plugins`, and `const` -- input metadata such as `type`, `items`, `dateFormat`, `optional`, and `binding` -- job and job-group structure such as target names, `steps`, hook names, and strategy field names - ## JSON Schema Representation The workflow schema can be represented as JSON Schema for structural validation, but not as a complete replacement for the current TypeScript documentation and Go loader rules. From 4eb413bb038e075b994f1adac2ef32fa95612cc6 Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Wed, 27 May 2026 17:24:47 -0400 Subject: [PATCH 16/25] polish the spec --- docs/spec/workflow.md | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/spec/workflow.md b/docs/spec/workflow.md index 9c9f125a..49973567 100644 --- a/docs/spec/workflow.md +++ b/docs/spec/workflow.md @@ -27,7 +27,7 @@ All top-level sections except `version` are optional unless a task needs them. U | Field | Required | Type | Expressions | Description | | --- | --- | --- | --- | --- | -| `version` | [x] | string or number | No | Workflow format version. The supported value is `2`. | +| `version` | Yes | string or number | No | Workflow format version. The supported value is `2`. | | `include` | | list of strings | No | Other workflow files to load and merge into the current workflow. Paths are resolved relative to the file that declares them. | | `plugins` | | map of string to string | No | Plugin imports. The map key is the action namespace exposed by the plugin. | | `const` | | map of scalar values | No | Static values exposed to expressions as `consts.*`. | @@ -103,7 +103,7 @@ Task inputs can be set from command-line flags, environment variables, defaults, | Field | Required | Type | Expressions | Description | | --- | --- | --- | --- | --- | -| `type` | [x] | [input type](#input-types) | No | One of [`string`](#string), [`int`](#int), [`bool`](#bool), [`float`](#float), [`date`](#date), [`duration`](#duration), or [`list`](#list). | +| `type` | Yes | [input type](#input-types) | No | One of [`string`](#string), [`int`](#int), [`bool`](#bool), [`float`](#float), [`date`](#date), [`duration`](#duration), or [`list`](#list). | | `items` | when `type: list` | [type schema](#type-schema) | No | Invalid otherwise. Defines the list element type. | | `dateFormat` | | string | No | Date parsing layout. Only valid for [`date`](#date). Defaults to Go `time.RFC3339`. | | `default` | | value or [expression](#expression-language) | Yes | Fallback value when no value is provided. Must match the input type after expansion. | @@ -136,8 +136,6 @@ inputs: Nested complex item types are not supported. A list item type can be scalar, but not another list or dictionary. -### Type Reference - #### String `string` is text. YAML strings may be plain, quoted, or block scalars. @@ -222,7 +220,7 @@ items: | Field | Required | Type | Expressions | Description | | --- | --- | --- | --- | --- | -| `type` | [x] | [input type](#input-types) | No | Element type. Complex nested types such as [`list`](#list) are not supported. | +| `type` | Yes | [input type](#input-types) | No | Element type. Complex nested types such as [`list`](#list) are not supported. | | `dateFormat` | | string | No | Date parsing layout. Only valid when `type` is [`date`](#date). | ## Tasks And Mixins @@ -250,7 +248,7 @@ mixins: | `inputs` | | map of [input definitions](#input-definition) | Per definition | Values accepted by this task or mixin. | | `env` | | map | No | Environment values scoped to the task or mixin. | | `working-directory` | | string | No | Working directory for jobs in the group. Relative paths resolve from the current workflow working directory. | -| `steps` | [x] | list of [jobs](#jobs) | Per job field | Jobs executed in order unless a job is asynchronous. | +| `steps` | Yes | list of [jobs](#jobs) | Per job field | Jobs executed in order unless a job is asynchronous. | Tasks are command-line entry points. Mixins are reusable job groups and cannot be run directly from the command line. @@ -323,7 +321,7 @@ steps: | Field | Required | Type | Expressions | Description | | --- | --- | --- | --- | --- | | `max-parallel` | | positive integer | No | Maximum number of matrix jobs to run concurrently. Defaults to `1`. | -| `matrix` | [x] | ordered map of string to list or expression returning list | Yes, in values | Each key becomes available under `matrix.` during each job run. Matrix keys are literal. | +| `matrix` | Yes | ordered map of string to list or expression returning list | Yes, in values | Each key becomes available under `matrix.` during each job run. Matrix keys are literal. | | `exclude` | | list of scalar maps | No | Excludes matrix rows that match all specified key/value pairs in an exclude rule. | `matrix` values may be literal arrays or expressions that evaluate to arrays. Empty matrix arrays are ignored with a warning. `exclude` values and the referenced matrix values must be scalar and comparable. From 628bdbbe3a144d33c959537c30b9d2c949944ba0 Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Wed, 27 May 2026 17:47:37 -0400 Subject: [PATCH 17/25] fix: root file schema --- docs/spec/src/schema.d.ts | 12 ++++++------ docs/todo.md | 5 +++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs/spec/src/schema.d.ts b/docs/spec/src/schema.d.ts index 04d919ed..b79e880b 100644 --- a/docs/spec/src/schema.d.ts +++ b/docs/spec/src/schema.d.ts @@ -372,7 +372,7 @@ interface WorkflowFile { * * @const */ - include: string[] + include?: string[] /** * Set of plugins to import. @@ -394,7 +394,7 @@ interface WorkflowFile { * * @const */ - plugins: Record + plugins?: Record /** * List of predefined variables to be used in expressions. @@ -405,7 +405,7 @@ interface WorkflowFile { * * @const */ - const: Record + const?: Record /** * Global input parameters. @@ -413,7 +413,7 @@ interface WorkflowFile { * @see [Task.inputs] * @const */ - inputs: Record + inputs?: Record /** * Map of task name and its definition. @@ -435,12 +435,12 @@ interface WorkflowFile { * Use `gilbert list` to display a list of available tasks. * Use `gilbert run --help` see task description and its parameters. */ - tasks: Record + tasks?: Record /** * Mixins are reusable pieces of pipeline which can accept inputs. * * Unlike tasks, they cannot be called from command-line. */ - mixins: Record + mixins?: Record } diff --git a/docs/todo.md b/docs/todo.md index f40ac544..390f9a77 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -13,9 +13,10 @@ - input binding delimiter -**cobra** +**schema** -- Fix showing tasks list "Available Tasks" for "gilbert --help" +- wire `InputDefinition.binding.delimiter` to yamlloader +- support setting env **expr** From a0e4508b964b3380d3abce976dc30dcffe1acdfd Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Wed, 27 May 2026 18:03:35 -0400 Subject: [PATCH 18/25] add typespec schema based on typescript type --- .gitignore | 5 +- docs/spec/schema.tsp | 261 ++++++++++++ package-lock.json | 953 +++++++++++++++++++++++++++++++++++++++++++ package.json | 26 ++ 4 files changed, 1244 insertions(+), 1 deletion(-) create mode 100644 docs/spec/schema.tsp create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore index 5737ef7d..301b8de4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,13 @@ *.so *.dylib +# Node +node_modules + # Artifacts *.exe -# Arhives +# Archives *.tar *.zip diff --git a/docs/spec/schema.tsp b/docs/spec/schema.tsp new file mode 100644 index 00000000..b170da19 --- /dev/null +++ b/docs/spec/schema.tsp @@ -0,0 +1,261 @@ +import "@typespec/json-schema"; + +using JsonSchema; + +@jsonSchema +namespace Schemas; + +/** A YAML string parseable by Go's time.ParseDuration. */ +alias DurationString = string; + +/** + * YAML string containing a Gilbert expression. + * + * The type parameter from schema.d.ts is documentation-only, so the JSON + * schema representation is a string. + */ +alias Expression = string; + +/** Any YAML value. */ +alias AnyValue = unknown; + +/** YAML object with string keys and arbitrary YAML values. */ +alias AnyRecord = Record; + +/** + * Contains fields common to all job types. + */ +model JobBase { + /** + * Whether to execute job concurrently. + * When true, the next job will start without waiting for the current one to finish. + */ + async?: boolean; + + /** Duration of time to wait before starting a job. */ + delay?: DurationString; + + /** Path to a working directory where job will be executed. */ + `working-directory`?: string; + + /** Job execution timeout duration. */ + timeout?: DurationString; + + /** Whether to allow job to fail. */ + `continue-on-error`?: boolean; + + /** + * Whether to run a job. + * If expression returns false, the job is skipped. + */ + `if`?: Expression; + + /** Custom environment variables to set for a job. */ + env?: AnyRecord; + + /** Matrix execution strategy. */ + strategy?: Strategy; + + /** Input parameters for action, mixin or task. */ + with: AnyRecord; + + /** + * Defines a set of jobs to run when a certain hook is called. + * Hooks are events produced by certain actions. + */ + on?: Record; +} + +/** Matrix execution strategy. */ +model Strategy { + /** + * Maximum number of matrix jobs to run concurrently. + * + * Default: 1. + */ + `max-parallel`?: int32; + + /** + * Matrix of different job configurations. + * Variables defined in a matrix become properties in the `matrix` expression context. + */ + matrix: Record; + + /** List of combinations to exclude from job matrix. */ + exclude?: AnyRecord[]; +} + +/** Matrix values may be a literal array or an expression returning an array. */ +alias MatrixValues = Expression | AnyValue[]; + +/** Job that executes an action. */ +model ActionJob extends JobBase { + /** Name of action to be executed. */ + action: string; +} + +/** Job that executes a mixin. */ +model MixinJob extends JobBase { + /** Name of mixin to be executed. */ + mixin: string; +} + +/** Job that executes a task. */ +model TaskJob extends JobBase { + /** Name of task to be executed. */ + task: string; +} + +/** + * Job is a single execution unit of a task or mixin. + * Job can start an action, run a mixin, or call a different task. + */ +alias Job = ActionJob | MixinJob | TaskJob; + +/** Input value types supported by task and mixin inputs. */ +alias InputValueType = "string" | "int" | "bool" | "date" | "duration" | "float"; + +/** List item value types supported by list inputs. */ +alias ListItemValueType = InputValueType | "list"; + +/** Command-line and environment binding for an input value. */ +model InputBinding { + /** + * Environment variable name. + * If input value and command-line flag are unspecified, this variable is used when present. + */ + env?: string; + + /** + * Overrides the command-line flag name assigned to an input. + * By default, each input gets a command-line flag with the same name as the input. + */ + flag?: string; + + /** + * Character used to split a string into a list when parsing an environment + * variable or command-line value. + */ + delimiter?: string; +} + +/** Fields common to all input definitions. */ +model InputDefinitionCommon { + /** + * Fallback value used when input value is not specified. + * Value type should correspond to `type`. + */ + default?: AnyValue; + + /** + * Whether a value is not required. + * Has no effect when `default` is set. + */ + optional?: boolean; + + /** + * Controls how input value is parsed from command-line flags and environment variables. + * Works only for tasks. Has no effect in mixins. + */ + binding?: InputBinding; +} + +/** Input definition for list values. */ +model ListInputDefinition extends InputDefinitionCommon { + type: "list"; + + /** + * Defines a type of element of a list. + * Should only be used when `type` is set to `list`. + */ + items: ListItemValueType; +} + +/** Input definition for scalar values. */ +model ScalarInputDefinition extends InputDefinitionCommon { + /** + * Input value type. + * + * Besides standard scalar values, special values are parseable from + * command-line flags or string values. + */ + type: InputValueType; + + /** + * Date format used to parse a command-line flag value. + * Has effect only when `type` is `date`. + */ + dateFormat?: string; +} + +/** Defines task or mixin input parameter. */ +alias InputDefinition = ListInputDefinition | ScalarInputDefinition; + +/** Mixins allow a set of jobs to be reused as a block. */ +model Mixin { + /** + * List of input parameters accepted by mixin. + * Unlike tasks, mixins do not support command-line flags. + */ + inputs?: Record; + + /** Custom environment variables to use for a mixin. */ + env?: AnyRecord; + + /** Sequence of jobs that will be executed when mixin is called. */ + steps: Job[]; + + /** Override a working directory for a mixin. */ + `working-directory`?: string; +} + +/** Task definition. */ +model Task { + /** + * List of input parameters accepted by a task. + * Input values can be passed to a task via command-line flags. + */ + inputs?: Record; + + /** Custom environment variables to use for a task. */ + env?: AnyRecord; + + /** Sequence of jobs that will be executed when task is started. */ + steps: Job[]; + + /** Override a working directory for a task. */ + `working-directory`?: string; +} + +/** Gilbert workflow file. */ +model WorkflowFile { + /** Workflow file version. Should be `2`. */ + version: 2; + + /** List of other workflow files to include and merge. */ + include?: string[]; + + /** + * Set of plugins to import. + * Key is a namespace and value is import URL. + */ + plugins?: Record; + + /** + * Predefined variables available to expressions as `${{ consts.* }}`. + * Values in this block should not be expressions. + */ + `const`?: AnyRecord; + + /** Global input parameters. */ + inputs?: Record; + + /** Map of task name and its definition. */ + tasks?: Record; + + /** + * Mixins are reusable pieces of pipeline which can accept inputs. + * Unlike tasks, they cannot be called from command-line. + */ + mixins?: Record; +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..fe2d69e9 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,953 @@ +{ + "name": "gilbert", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gilbert", + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "@typespec/compiler": "^1.12.0", + "@typespec/json-schema": "^1.12.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.6.tgz", + "integrity": "sha512-I/INw4sHGlVZ/afZOckpLiDP9SmbMl1g/GCqeHjLw1Afw/0PlRs2tRFgTGWmdI0hoNuWZn3y2iHNmG1vyECyQQ==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-5.2.0.tgz", + "integrity": "sha512-1HJt+3fqxblp/GQjdntSyoSHYBc0e3CzXVgjFpKA6qFLd9FHBBqwN8Co0xYH6t2JVUZrtFwZ4bBiwptkiLxyOg==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.6", + "@inquirer/core": "^11.2.0", + "@inquirer/figures": "^2.0.6", + "@inquirer/type": "^4.0.6" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.1.0.tgz", + "integrity": "sha512-USpeB76eqK7yGricDlGAupxWlp4a59qpeZOoNWaxO/nJln7agpJveyNkQ1d5u8YXG6TOqxZtQpKPORQQDrdVsA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.0", + "@inquirer/type": "^4.0.6" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.2.0.tgz", + "integrity": "sha512-joR1YS2sI0us+9d0I8ViqFbrRLONO8CFTuyvBX4ZVBSch+VsZiugUABdrhBXXJR1VyEzvpz5SQCix3keETQ58g==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.6", + "@inquirer/figures": "^2.0.6", + "@inquirer/type": "^4.0.6", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^4.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/editor": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-5.2.0.tgz", + "integrity": "sha512-/m+sgRmzSdK6HDtVnl3PmI6MnZC4O+LLezedoJcrX7mINhTjjb0hlC7aEDGZXkFTB4b5uQ0q59AhYTah88KbNg==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.0", + "@inquirer/external-editor": "^3.0.1", + "@inquirer/type": "^4.0.6" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/expand": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-5.1.0.tgz", + "integrity": "sha512-fR7g4BVnIcs+4NApF6C5byflNM/EULxSxsv/2Jvg+gmop0R6eBIPvZqE6RYnTy1tQTFnf9wyHkwNoQSZbofaGA==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.0", + "@inquirer/type": "^4.0.6" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/external-editor": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@inquirer/external-editor/-/external-editor-3.0.1.tgz", + "integrity": "sha512-tam+Gwjsxg2sx3iUVPkAnhKT/yrk2rd2NAa7XJU/J8OYpU0ifXsnp12xlvzp/DCpWBXVv+vLQsqnpAWwUcWD5Q==", + "license": "MIT", + "dependencies": { + "chardet": "^2.1.1", + "iconv-lite": "^0.7.2" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.6.tgz", + "integrity": "sha512-dsZgQtH2t5Q6ah3aPbZbeEZAxsD9qQu0DXf01AltuEfRTm+NoLN6+rLVbr+4edeEbNCp/wBNM6mALRWtsQpfkw==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + } + }, + "node_modules/@inquirer/input": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-5.1.0.tgz", + "integrity": "sha512-sVZCz6P6e8tW5g2bSFel1oLpa6jK/u7BexFfrgTqR8syIdnHqy+iopnlSbYBZMsCK52chLjhGNBxt0eRqhsghw==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.0", + "@inquirer/type": "^4.0.6" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/number": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-4.1.0.tgz", + "integrity": "sha512-VMXB/XejCbaSTf9Xucl7dqjzzsaGsrs6XwSYXPbGZ2QbSuq/Gz8XamhSi9ClRubNXZlGry9xVg1tKkJdTDgCtQ==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.0", + "@inquirer/type": "^4.0.6" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/password": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-5.1.0.tgz", + "integrity": "sha512-5tqRuKCDIUxdPxTI/CuLnh914kz+WMPmURHKnZgui9gk43ebudEsdu4EwSn1CPSi5R+17YpBG+ba/YqTnRAcJA==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.6", + "@inquirer/core": "^11.2.0", + "@inquirer/type": "^4.0.6" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/prompts": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-8.5.0.tgz", + "integrity": "sha512-pLjXOnY4y3R1mgyHP3pXD/8eXejp+L/dde/0N2NLKgKfMstqhNZrpvs7Wkzbl9FYFQh10LRQ7QZwq+cz9rrhyw==", + "license": "MIT", + "dependencies": { + "@inquirer/checkbox": "^5.2.0", + "@inquirer/confirm": "^6.1.0", + "@inquirer/editor": "^5.2.0", + "@inquirer/expand": "^5.1.0", + "@inquirer/input": "^5.1.0", + "@inquirer/number": "^4.1.0", + "@inquirer/password": "^5.1.0", + "@inquirer/rawlist": "^5.3.0", + "@inquirer/search": "^4.2.0", + "@inquirer/select": "^5.2.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/rawlist": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-5.3.0.tgz", + "integrity": "sha512-p+vAeTAD+cGXjGleP1F5LXrX2ISxNDZm+lqeBpnJausNLSZskZZkcggwhomqP8Igx9oIjnoeOrw98xvdFvdm2w==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.0", + "@inquirer/type": "^4.0.6" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/search": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-4.2.0.tgz", + "integrity": "sha512-ByURoSGIaSl5O5Q0AmYmVmUsXbMUcBGNoA3FRL7TOyiA22IeFHymJKRkuILbOIlJwqnBk7AnPpseodyFUBzg+g==", + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.0", + "@inquirer/figures": "^2.0.6", + "@inquirer/type": "^4.0.6" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/select": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-5.2.0.tgz", + "integrity": "sha512-6IzkcmEbEXfgVbxZ2d1UyJFbCBoc6dTofulFmrYuomIp88HXiVqRbqbg4/mbfZhvnNo6xYmnYo2AEmDof6fQkg==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.6", + "@inquirer/core": "^11.2.0", + "@inquirer/figures": "^2.0.6", + "@inquirer/type": "^4.0.6" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.6.tgz", + "integrity": "sha512-J+9tdxOskuYuGjsvGaq00AamhDgjR7anhEW2dP4QdQpFCMPngCeC/bCYWQ5NsMWZRdsy53is7kAHb/+7cwDk2g==", + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@typespec/asset-emitter": { + "version": "0.79.1", + "resolved": "https://registry.npmjs.org/@typespec/asset-emitter/-/asset-emitter-0.79.1.tgz", + "integrity": "sha512-53s3GLu5BwNkl7Itr/OizfhymTV2u7k5/cwjUOAt03AUDfiKlwbsp+iCIsq1vccJuoDOiXOceJOfL8rAf4/9LQ==", + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@typespec/compiler": "^1.10.0" + } + }, + "node_modules/@typespec/compiler": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@typespec/compiler/-/compiler-1.12.0.tgz", + "integrity": "sha512-hKCkHEEDdCpXFyOU8ln+TzBBwonFMbkeUV0zIc+vBETyO8p/Upui3XvEyLOyB4CpKUReHzGeGm3gcFjNc73ygg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@inquirer/prompts": "^8.4.1", + "ajv": "^8.18.0", + "change-case": "^5.4.4", + "env-paths": "^4.0.0", + "is-unicode-supported": "^2.1.0", + "mustache": "^4.2.0", + "picocolors": "^1.1.1", + "prettier": "^3.8.1", + "semver": "^7.7.4", + "tar": "^7.5.13", + "temporal-polyfill": "^0.3.2", + "vscode-languageserver": "^9.0.1", + "vscode-languageserver-textdocument": "^1.0.12", + "yaml": "^2.8.3", + "yargs": "^18.0.0" + }, + "bin": { + "tsp": "cmd/tsp.js", + "tsp-server": "cmd/tsp-server.js" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/@typespec/json-schema": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@typespec/json-schema/-/json-schema-1.12.0.tgz", + "integrity": "sha512-e/hxD6q0ThpCmIXOt4wseC30dv1lWnmQJaDhsn6MJySNVpcfTw9xVgj40Oeygc3TC1nO5X0cICtkOqDV/FMFAw==", + "license": "MIT", + "dependencies": { + "@typespec/asset-emitter": "^0.79.1", + "yaml": "^2.8.3" + }, + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "@typespec/compiler": "^1.12.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "license": "MIT" + }, + "node_modules/chardet": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-2.1.1.tgz", + "integrity": "sha512-PsezH1rqdV9VvyNhxxOW32/d75r01NY7TQCmOqomRo15ZSOKbpTFVsfjghxo6JloQUCGnH4k1LGu0R4yCLlWQQ==", + "license": "MIT" + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "license": "MIT" + }, + "node_modules/env-paths": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-4.0.0.tgz", + "integrity": "sha512-pxP8eL2SwwaTRi/KHYwLYXinDs7gL3jxFcBYmEdYfZmZXbaVDvdppd0XBU8qVz03rDfKZMXg1omHCbsJjZrMsw==", + "license": "MIT", + "dependencies": { + "is-safe-filename": "^0.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/is-safe-filename": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-safe-filename/-/is-safe-filename-0.1.1.tgz", + "integrity": "sha512-4SrR7AdnY11LHfDKTZY1u6Ga3RuxZdl3YKWWShO5iyuG5h8QS4GD2tOb04peBJ5I7pXbR+CGBNEhTcwK+FzN3g==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/mute-stream": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-4.0.0.tgz", + "integrity": "sha512-gSrprq0fJ3EiOErzjdIZrjysVVmJ4uu1QWfCDss5LypA5OXvrMje5Ym5z6V6RLyJ2eF87lasX7t6a0AnFvZblg==", + "license": "ISC", + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/prettier": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/tar": { + "version": "7.5.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.15.tgz", + "integrity": "sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/temporal-polyfill": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/temporal-polyfill/-/temporal-polyfill-0.3.2.tgz", + "integrity": "sha512-TzHthD/heRK947GNiSu3Y5gSPpeUDH34+LESnfsq8bqpFhsB79HFBX8+Z834IVX68P3EUyRPZK5bL/1fh437Eg==", + "license": "MIT", + "dependencies": { + "temporal-spec": "0.3.1" + } + }, + "node_modules/temporal-spec": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/temporal-spec/-/temporal-spec-0.3.1.tgz", + "integrity": "sha512-B4TUhezh9knfSIMwt7RVggApDRJZo73uZdj8AacL2mZ8RP5KtLianh2MXxL06GN9ESYiIsiuoLQhgVfwe55Yhw==", + "license": "ISC" + }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz", + "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..80726774 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "gilbert", + "version": "2.0.0", + "description": "

", + "main": "index.js", + "directories": { + "doc": "docs" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/go-gilbert/gilbert.git" + }, + "author": "", + "license": "MIT", + "bugs": { + "url": "https://github.com/go-gilbert/gilbert/issues" + }, + "homepage": "https://github.com/go-gilbert/gilbert#readme", + "dependencies": { + "@typespec/compiler": "^1.12.0", + "@typespec/json-schema": "^1.12.0" + } +} From 348fcea4304469d05da010dfaacfac3482768a7b Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Thu, 28 May 2026 00:01:39 -0400 Subject: [PATCH 19/25] feat: add schema generation instructions --- docs/man/schema_build.md | 84 +++++++++++++++++++++++++++++++++++ docs/spec/schema.tsp | 3 +- package.json | 6 +-- tools/post-process-schema.mjs | 47 ++++++++++++++++++++ 4 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 docs/man/schema_build.md create mode 100644 tools/post-process-schema.mjs diff --git a/docs/man/schema_build.md b/docs/man/schema_build.md new file mode 100644 index 00000000..51838eb3 --- /dev/null +++ b/docs/man/schema_build.md @@ -0,0 +1,84 @@ +# JSON Schema Build + +`docs/spec/schema.tsp` is the TypeSpec source used to generate the editor-facing JSON Schema for `gilbert.yml`. + +## Generate Schema + +Run: + +```sh +npx tsp compile docs/spec/schema.tsp \ + --emit=@typespec/json-schema \ + --option @typespec/json-schema.file-type=json \ + --list-files +``` + +This emits: + +```text +tsp-output/@typespec/json-schema/WorkflowFile.json +``` + +The generated file can be renamed to: + +```text +tsp-output/@typespec/json-schema/gilbert.schema.json +``` + +To write the generated schema into a custom directory, set the JSON Schema emitter output directory: + +```sh +npx tsp compile docs/spec/schema.tsp \ + --emit=@typespec/json-schema \ + --option @typespec/json-schema.file-type=json \ + --option @typespec/json-schema.emitter-output-dir=./docs/spec/json-schema \ + --list-files +``` + +With this option, the generated schema is written to: + +```text +docs/spec/json-schema/WorkflowFile.json +``` + +`WorkflowFile` is the only TypeSpec model decorated with `@jsonSchema`, so helper types are emitted into the same JSON Schema document under `$defs`. This keeps the schema unified and avoids external `*.json` references. + +## YAML Language Server Compatibility + +TypeSpec emits map-like records with `unevaluatedProperties`: + +```json +{ + "type": "object", + "unevaluatedProperties": { + "$ref": "#/$defs/Task" + } +} +``` + +This is valid JSON Schema 2020-12, but YAML language server support is weaker for this keyword. In practice, it may show hover information for top-level properties like `tasks` and `inputs`, but fail to provide hover/completion for arbitrary nested keys inside those maps. + +YAML language server handles `additionalProperties` better for map-like objects: + +```json +{ + "type": "object", + "additionalProperties": { + "$ref": "#/$defs/Task" + } +} +``` + +After generating the schema, copy each `unevaluatedProperties` value to `additionalProperties`. Keeping both fields preserves the original 2020-12 semantics while making the schema more useful to YAML language server. + +Example post-processing command: + +```sh +node -e 'const fs=require("fs"); const p="tsp-output/@typespec/json-schema/gilbert.schema.json"; const doc=JSON.parse(fs.readFileSync(p,"utf8")); function walk(x){ if(!x||typeof x!=="object") return; if(Object.prototype.hasOwnProperty.call(x,"unevaluatedProperties") && !Object.prototype.hasOwnProperty.call(x,"additionalProperties")) x.additionalProperties=x.unevaluatedProperties; for(const v of Object.values(x)) walk(v); } walk(doc); fs.writeFileSync(p, JSON.stringify(doc,null,4)+"\n");' +``` + +Then reference it from YAML: + +```yaml +# yaml-language-server: $schema=http://localhost:8000/json-schema/gilbert.schema.json +``` diff --git a/docs/spec/schema.tsp b/docs/spec/schema.tsp index b170da19..a50b6433 100644 --- a/docs/spec/schema.tsp +++ b/docs/spec/schema.tsp @@ -2,7 +2,6 @@ import "@typespec/json-schema"; using JsonSchema; -@jsonSchema namespace Schemas; /** A YAML string parseable by Go's time.ParseDuration. */ @@ -228,6 +227,8 @@ model Task { } /** Gilbert workflow file. */ +@jsonSchema +@id("gilbert.schema.json") model WorkflowFile { /** Workflow file version. Should be `2`. */ version: 2; diff --git a/package.json b/package.json index 80726774..db1d8b25 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,12 @@ { "name": "gilbert", "version": "2.0.0", - "description": "

", + "description": "", "main": "index.js", "directories": { "doc": "docs" }, - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, + "scripts": {}, "repository": { "type": "git", "url": "git+https://github.com/go-gilbert/gilbert.git" diff --git a/tools/post-process-schema.mjs b/tools/post-process-schema.mjs new file mode 100644 index 00000000..d77af020 --- /dev/null +++ b/tools/post-process-schema.mjs @@ -0,0 +1,47 @@ +/** + * Post-processes a JSON Schema file emitted by TypeSpec's `@typespec/json-schema` emitter. + * Replaces all occurrences of `unevaluatedProperties` with `additionalProperties` to + * improve compatibility with YAML language servers. + */ + +import { readFileSync, writeFileSync } from "fs"; + +const [inputFile, outputFile] = process.argv.slice(2); + +if (!inputFile) { + console.error("Usage: node post-process-schema.mjs [outputFile]"); + process.exit(1); +} + +let data; +try { + data = JSON.parse(readFileSync(inputFile, "utf8")); +} catch (err) { + console.error(`Error: Failed to parse '${inputFile}' as JSON: ${err.message}`); + process.exit(1); +} + +function renameKeys(obj) { + if (Array.isArray(obj)) { + return obj.map(renameKeys); + } + if (obj !== null && typeof obj === "object") { + const result = {}; + for (const [key, value] of Object.entries(obj)) { + result[key === "unevaluatedProperties" ? "additionalProperties" : key] = + renameKeys(value); + } + return result; + } + return obj; +} + +const result = renameKeys(data); +const output = JSON.stringify(result, null, 2) + "\n"; + +if (outputFile) { + writeFileSync(outputFile, output); + console.log(`Patched: ${outputFile}`); +} else { + process.stdout.write(output); +} From f2b24707127221ede7896c79e46b5f83c7230892 Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Thu, 28 May 2026 00:03:31 -0400 Subject: [PATCH 20/25] add todo --- docs/todo.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/todo.md b/docs/todo.md index 390f9a77..e267a565 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -17,6 +17,7 @@ - wire `InputDefinition.binding.delimiter` to yamlloader - support setting env +- support binding to file **expr** From 9db950c4414db041a9e132660571e885e75d1896 Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Thu, 28 May 2026 00:16:36 -0400 Subject: [PATCH 21/25] update schema build doc --- docs/man/schema_build.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/man/schema_build.md b/docs/man/schema_build.md index 51838eb3..3ca102bb 100644 --- a/docs/man/schema_build.md +++ b/docs/man/schema_build.md @@ -31,7 +31,7 @@ To write the generated schema into a custom directory, set the JSON Schema emitt npx tsp compile docs/spec/schema.tsp \ --emit=@typespec/json-schema \ --option @typespec/json-schema.file-type=json \ - --option @typespec/json-schema.emitter-output-dir=./docs/spec/json-schema \ + --option @typespec/json-schema.emitter-output-dir='{cwd}/docs/spec/json-schema' \ --list-files ``` From 94d02ecf5faa63fd041bd88c380129705f84a570 Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Thu, 28 May 2026 00:23:30 -0400 Subject: [PATCH 22/25] add JSON schema --- docs/man/schema_build.md | 28 +- docs/spec/.gitignore | 3 + docs/spec/gilbert.schema.json | 472 ++++++++++++++++++++++++++++++++++ gilbert.yml | 1 + 4 files changed, 485 insertions(+), 19 deletions(-) create mode 100644 docs/spec/.gitignore create mode 100644 docs/spec/gilbert.schema.json diff --git a/docs/man/schema_build.md b/docs/man/schema_build.md index 3ca102bb..4f169f20 100644 --- a/docs/man/schema_build.md +++ b/docs/man/schema_build.md @@ -43,7 +43,7 @@ docs/spec/json-schema/WorkflowFile.json `WorkflowFile` is the only TypeSpec model decorated with `@jsonSchema`, so helper types are emitted into the same JSON Schema document under `$defs`. This keeps the schema unified and avoids external `*.json` references. -## YAML Language Server Compatibility +## Post-Processing TypeSpec emits map-like records with `unevaluatedProperties`: @@ -56,29 +56,19 @@ TypeSpec emits map-like records with `unevaluatedProperties`: } ``` -This is valid JSON Schema 2020-12, but YAML language server support is weaker for this keyword. In practice, it may show hover information for top-level properties like `tasks` and `inputs`, but fail to provide hover/completion for arbitrary nested keys inside those maps. +This is valid JSON Schema 2020-12, but YAML language servers have weak support for `unevaluatedProperties` and fail to provide hover/completion for arbitrary nested keys inside those maps. The `additionalProperties` keyword is handled better by YAML language servers. -YAML language server handles `additionalProperties` better for map-like objects: - -```json -{ - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/Task" - } -} -``` - -After generating the schema, copy each `unevaluatedProperties` value to `additionalProperties`. Keeping both fields preserves the original 2020-12 semantics while making the schema more useful to YAML language server. - -Example post-processing command: +To fix this, run the post-processing script to replace `unevaluatedProperties` with `additionalProperties` throughout the schema: ```sh -node -e 'const fs=require("fs"); const p="tsp-output/@typespec/json-schema/gilbert.schema.json"; const doc=JSON.parse(fs.readFileSync(p,"utf8")); function walk(x){ if(!x||typeof x!=="object") return; if(Object.prototype.hasOwnProperty.call(x,"unevaluatedProperties") && !Object.prototype.hasOwnProperty.call(x,"additionalProperties")) x.additionalProperties=x.unevaluatedProperties; for(const v of Object.values(x)) walk(v); } walk(doc); fs.writeFileSync(p, JSON.stringify(doc,null,4)+"\n");' +node tools/post-process-schema.mjs [outputFile] ``` -Then reference it from YAML: +- ``: the generated JSON Schema file +- `[outputFile]`: optional output path; if omitted, writes to stdout + +After generating and patching the schema, reference it from YAML: ```yaml # yaml-language-server: $schema=http://localhost:8000/json-schema/gilbert.schema.json -``` +``` \ No newline at end of file diff --git a/docs/spec/.gitignore b/docs/spec/.gitignore new file mode 100644 index 00000000..9e338757 --- /dev/null +++ b/docs/spec/.gitignore @@ -0,0 +1,3 @@ +# Ignore json schema file produced by TypeSpec. +# It is replaced by a schema patcher script with 'gilbert.schema.json' file. +WorkflowFile.json diff --git a/docs/spec/gilbert.schema.json b/docs/spec/gilbert.schema.json new file mode 100644 index 00000000..fa2e8ce0 --- /dev/null +++ b/docs/spec/gilbert.schema.json @@ -0,0 +1,472 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "gilbert.schema.json", + "type": "object", + "properties": { + "version": { + "type": "number", + "const": 2, + "description": "Workflow file version. Should be `2`." + }, + "include": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of other workflow files to include and merge." + }, + "plugins": { + "$ref": "#/$defs/RecordString", + "description": "Set of plugins to import.\nKey is a namespace and value is import URL." + }, + "const": { + "$ref": "#/$defs/RecordUnknown", + "description": "Predefined variables available to expressions as `${{ consts.* }}`.\nValues in this block should not be expressions." + }, + "inputs": { + "type": "object", + "properties": {}, + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/ListInputDefinition" + }, + { + "$ref": "#/$defs/ScalarInputDefinition" + } + ] + }, + "description": "Global input parameters." + }, + "tasks": { + "$ref": "#/$defs/RecordTask", + "description": "Map of task name and its definition." + }, + "mixins": { + "$ref": "#/$defs/RecordMixin", + "description": "Mixins are reusable pieces of pipeline which can accept inputs.\nUnlike tasks, they cannot be called from command-line." + } + }, + "required": [ + "version" + ], + "description": "Gilbert workflow file.", + "$defs": { + "RecordString": { + "type": "object", + "properties": {}, + "additionalProperties": { + "type": "string" + } + }, + "RecordUnknown": { + "type": "object", + "properties": {}, + "additionalProperties": {} + }, + "ListInputDefinition": { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "list" + }, + "items": { + "anyOf": [ + { + "type": "string", + "const": "string" + }, + { + "type": "string", + "const": "int" + }, + { + "type": "string", + "const": "bool" + }, + { + "type": "string", + "const": "date" + }, + { + "type": "string", + "const": "duration" + }, + { + "type": "string", + "const": "float" + }, + { + "type": "string", + "const": "list" + } + ], + "description": "Defines a type of element of a list.\nShould only be used when `type` is set to `list`." + } + }, + "required": [ + "type", + "items" + ], + "allOf": [ + { + "$ref": "#/$defs/InputDefinitionCommon" + } + ], + "description": "Input definition for list values." + }, + "ScalarInputDefinition": { + "type": "object", + "properties": { + "type": { + "anyOf": [ + { + "type": "string", + "const": "string" + }, + { + "type": "string", + "const": "int" + }, + { + "type": "string", + "const": "bool" + }, + { + "type": "string", + "const": "date" + }, + { + "type": "string", + "const": "duration" + }, + { + "type": "string", + "const": "float" + } + ], + "description": "Input value type.\n\nBesides standard scalar values, special values are parseable from\ncommand-line flags or string values." + }, + "dateFormat": { + "type": "string", + "description": "Date format used to parse a command-line flag value.\nHas effect only when `type` is `date`." + } + }, + "required": [ + "type" + ], + "allOf": [ + { + "$ref": "#/$defs/InputDefinitionCommon" + } + ], + "description": "Input definition for scalar values." + }, + "RecordTask": { + "type": "object", + "properties": {}, + "additionalProperties": { + "$ref": "#/$defs/Task" + } + }, + "RecordMixin": { + "type": "object", + "properties": {}, + "additionalProperties": { + "$ref": "#/$defs/Mixin" + } + }, + "InputDefinitionCommon": { + "type": "object", + "properties": { + "default": { + "description": "Fallback value used when input value is not specified.\nValue type should correspond to `type`." + }, + "optional": { + "type": "boolean", + "description": "Whether a value is not required.\nHas no effect when `default` is set." + }, + "binding": { + "$ref": "#/$defs/InputBinding", + "description": "Controls how input value is parsed from command-line flags and environment variables.\nWorks only for tasks. Has no effect in mixins." + } + }, + "description": "Fields common to all input definitions." + }, + "Task": { + "type": "object", + "properties": { + "inputs": { + "type": "object", + "properties": {}, + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/ListInputDefinition" + }, + { + "$ref": "#/$defs/ScalarInputDefinition" + } + ] + }, + "description": "List of input parameters accepted by a task.\nInput values can be passed to a task via command-line flags." + }, + "env": { + "$ref": "#/$defs/RecordUnknown", + "description": "Custom environment variables to use for a task." + }, + "steps": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/$defs/ActionJob" + }, + { + "$ref": "#/$defs/MixinJob" + }, + { + "$ref": "#/$defs/TaskJob" + } + ] + }, + "description": "Sequence of jobs that will be executed when task is started." + }, + "working-directory": { + "type": "string", + "description": "Override a working directory for a task." + } + }, + "required": [ + "steps" + ], + "description": "Task definition." + }, + "Mixin": { + "type": "object", + "properties": { + "inputs": { + "type": "object", + "properties": {}, + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/ListInputDefinition" + }, + { + "$ref": "#/$defs/ScalarInputDefinition" + } + ] + }, + "description": "List of input parameters accepted by mixin.\nUnlike tasks, mixins do not support command-line flags." + }, + "env": { + "$ref": "#/$defs/RecordUnknown", + "description": "Custom environment variables to use for a mixin." + }, + "steps": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/$defs/ActionJob" + }, + { + "$ref": "#/$defs/MixinJob" + }, + { + "$ref": "#/$defs/TaskJob" + } + ] + }, + "description": "Sequence of jobs that will be executed when mixin is called." + }, + "working-directory": { + "type": "string", + "description": "Override a working directory for a mixin." + } + }, + "required": [ + "steps" + ], + "description": "Mixins allow a set of jobs to be reused as a block." + }, + "InputBinding": { + "type": "object", + "properties": { + "env": { + "type": "string", + "description": "Environment variable name.\nIf input value and command-line flag are unspecified, this variable is used when present." + }, + "flag": { + "type": "string", + "description": "Overrides the command-line flag name assigned to an input.\nBy default, each input gets a command-line flag with the same name as the input." + }, + "delimiter": { + "type": "string", + "description": "Character used to split a string into a list when parsing an environment\nvariable or command-line value." + } + }, + "description": "Command-line and environment binding for an input value." + }, + "ActionJob": { + "type": "object", + "properties": { + "action": { + "type": "string", + "description": "Name of action to be executed." + } + }, + "required": [ + "action" + ], + "allOf": [ + { + "$ref": "#/$defs/JobBase" + } + ], + "description": "Job that executes an action." + }, + "MixinJob": { + "type": "object", + "properties": { + "mixin": { + "type": "string", + "description": "Name of mixin to be executed." + } + }, + "required": [ + "mixin" + ], + "allOf": [ + { + "$ref": "#/$defs/JobBase" + } + ], + "description": "Job that executes a mixin." + }, + "TaskJob": { + "type": "object", + "properties": { + "task": { + "type": "string", + "description": "Name of task to be executed." + } + }, + "required": [ + "task" + ], + "allOf": [ + { + "$ref": "#/$defs/JobBase" + } + ], + "description": "Job that executes a task." + }, + "JobBase": { + "type": "object", + "properties": { + "async": { + "type": "boolean", + "description": "Whether to execute job concurrently.\nWhen true, the next job will start without waiting for the current one to finish." + }, + "delay": { + "type": "string", + "description": "Duration of time to wait before starting a job." + }, + "working-directory": { + "type": "string", + "description": "Path to a working directory where job will be executed." + }, + "timeout": { + "type": "string", + "description": "Job execution timeout duration." + }, + "continue-on-error": { + "type": "boolean", + "description": "Whether to allow job to fail." + }, + "if": { + "type": "string", + "description": "Whether to run a job.\nIf expression returns false, the job is skipped." + }, + "env": { + "$ref": "#/$defs/RecordUnknown", + "description": "Custom environment variables to set for a job." + }, + "strategy": { + "$ref": "#/$defs/Strategy", + "description": "Matrix execution strategy." + }, + "with": { + "$ref": "#/$defs/RecordUnknown", + "description": "Input parameters for action, mixin or task." + }, + "on": { + "type": "object", + "properties": {}, + "additionalProperties": { + "type": "array", + "items": { + "anyOf": [ + { + "$ref": "#/$defs/ActionJob" + }, + { + "$ref": "#/$defs/MixinJob" + }, + { + "$ref": "#/$defs/TaskJob" + } + ] + } + }, + "description": "Defines a set of jobs to run when a certain hook is called.\nHooks are events produced by certain actions." + } + }, + "required": [ + "with" + ], + "description": "Contains fields common to all job types." + }, + "Strategy": { + "type": "object", + "properties": { + "max-parallel": { + "type": "integer", + "minimum": -2147483648, + "maximum": 2147483647, + "description": "Maximum number of matrix jobs to run concurrently.\n\nDefault: 1." + }, + "matrix": { + "type": "object", + "properties": {}, + "additionalProperties": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": {} + } + ] + }, + "description": "Matrix of different job configurations.\nVariables defined in a matrix become properties in the `matrix` expression context." + }, + "exclude": { + "type": "array", + "items": { + "$ref": "#/$defs/RecordUnknown" + }, + "description": "List of combinations to exclude from job matrix." + } + }, + "required": [ + "matrix" + ], + "description": "Matrix execution strategy." + } + } +} diff --git a/gilbert.yml b/gilbert.yml index 7f095f21..fea0c316 100644 --- a/gilbert.yml +++ b/gilbert.yml @@ -1,3 +1,4 @@ +# yaml-language-server: $schema=./docs/spec/gilbert.schema.json version: 2 const: From 37c002c9adce631268a284139914e55fd45954b4 Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Thu, 28 May 2026 00:52:35 -0400 Subject: [PATCH 23/25] ignore artifacts --- docs/spec/.gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/spec/.gitignore b/docs/spec/.gitignore index 9e338757..0db07c74 100644 --- a/docs/spec/.gitignore +++ b/docs/spec/.gitignore @@ -1,3 +1,6 @@ # Ignore json schema file produced by TypeSpec. # It is replaced by a schema patcher script with 'gilbert.schema.json' file. WorkflowFile.json + +# AI brainstorming artifacts +prompts From 3618392697de8f2f55cc75a70afbed4b8a93f130 Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Thu, 28 May 2026 00:59:46 -0400 Subject: [PATCH 24/25] update schema --- docs/spec/gilbert.schema.json | 61 +++++++-------- docs/spec/schema.tsp | 140 +++++++++++++++++++++++++++++----- 2 files changed, 148 insertions(+), 53 deletions(-) diff --git a/docs/spec/gilbert.schema.json b/docs/spec/gilbert.schema.json index fa2e8ce0..9fd46323 100644 --- a/docs/spec/gilbert.schema.json +++ b/docs/spec/gilbert.schema.json @@ -6,22 +6,22 @@ "version": { "type": "number", "const": 2, - "description": "Workflow file version. Should be `2`." + "description": "Workflow file version. `2` is the only supported value; other values are invalid." }, "include": { "type": "array", "items": { "type": "string" }, - "description": "List of other workflow files to include and merge." + "description": "List of other workflow files to include and merge.\nPaths resolve relative to the including file. A file cannot include\nitself. Duplicate task, mixin, and input names across included files are\nrejected." }, "plugins": { "$ref": "#/$defs/RecordString", - "description": "Set of plugins to import.\nKey is a namespace and value is import URL." + "description": "Set of plugins to import.\nKey is a namespace and value is import URL.\n\nThe namespace becomes the action prefix. For example, a plugin declared as\n`docker:` exposes actions such as `docker/build` and `docker/run`." }, "const": { "$ref": "#/$defs/RecordUnknown", - "description": "Predefined variables available to expressions as `${{ consts.* }}`.\nValues in this block should not be expressions." + "description": "Predefined variables available to expressions as `${{ consts.* }}`.\nExpressions are not expanded inside this block. Values are literal YAML\nscalars." }, "inputs": { "type": "object", @@ -40,11 +40,11 @@ }, "tasks": { "$ref": "#/$defs/RecordTask", - "description": "Map of task name and its definition." + "description": "Map of task name and its definition.\nTasks are command-line entry points and can be run with\n`gilbert run `. Comments immediately above a task key are collected\nas the task description for list/help output." }, "mixins": { "$ref": "#/$defs/RecordMixin", - "description": "Mixins are reusable pieces of pipeline which can accept inputs.\nUnlike tasks, they cannot be called from command-line." + "description": "Mixins are reusable pieces of pipeline which can accept inputs.\nUnlike tasks, they cannot be called from command-line. Mixins can be\ncalled from both tasks and other mixins." } }, "required": [ @@ -69,7 +69,8 @@ "properties": { "type": { "type": "string", - "const": "list" + "const": "list", + "description": "Declares this input as a list value." }, "items": { "anyOf": [ @@ -96,13 +97,9 @@ { "type": "string", "const": "float" - }, - { - "type": "string", - "const": "list" } ], - "description": "Defines a type of element of a list.\nShould only be used when `type` is set to `list`." + "description": "Defines a type of element of a list.\nShould only be used when `type` is set to `list`.\n\nList item types must be scalar. Nested lists and dictionaries are not\nsupported." } }, "required": [ @@ -146,11 +143,11 @@ "const": "float" } ], - "description": "Input value type.\n\nBesides standard scalar values, special values are parseable from\ncommand-line flags or string values." + "description": "Input value type.\n\nBesides standard scalar values, special values are parseable from\ncommand-line flags or string values. `duration` values are parsed with\nGo's time.ParseDuration. `date` values are parsed with the Go time layout\nfrom `dateFormat`, or RFC3339 when `dateFormat` is omitted." }, "dateFormat": { "type": "string", - "description": "Date format used to parse a command-line flag value.\nHas effect only when `type` is `date`." + "description": "Date format used to parse a command-line flag value.\nHas effect only when `type` is `date`.\nDefaults to Go time.RFC3339 when omitted." } }, "required": [ @@ -181,11 +178,11 @@ "type": "object", "properties": { "default": { - "description": "Fallback value used when input value is not specified.\nValue type should correspond to `type`." + "description": "Fallback value used when input value is not specified.\nThis value may be an expression. After expression expansion, the value\nmust match the declared `type`." }, "optional": { "type": "boolean", - "description": "Whether a value is not required.\nHas no effect when `default` is set." + "description": "Whether a value is not required.\nHas no effect when `default` is set.\n\nWhen `optional` is true and there is no default, the input resolves to the\ndeclared type's zero value. Boolean inputs are optional by default." }, "binding": { "$ref": "#/$defs/InputBinding", @@ -210,7 +207,7 @@ } ] }, - "description": "List of input parameters accepted by a task.\nInput values can be passed to a task via command-line flags." + "description": "List of input parameters accepted by a task.\nInput values can come from explicit `with` values, command-line flags,\nenvironment variables, or defaults." }, "env": { "$ref": "#/$defs/RecordUnknown", @@ -259,7 +256,7 @@ } ] }, - "description": "List of input parameters accepted by mixin.\nUnlike tasks, mixins do not support command-line flags." + "description": "List of input parameters accepted by mixin.\nUnlike tasks, mixins do not support command-line flags. Mixins can be\ncalled from tasks and from other mixins, and their inputs should be passed\nexplicitly via `with`." }, "env": { "$ref": "#/$defs/RecordUnknown", @@ -290,14 +287,14 @@ "required": [ "steps" ], - "description": "Mixins allow a set of jobs to be reused as a block." + "description": "Mixins allow a set of jobs to be reused as a block.\nMixin inputs are set only through `with` from a calling task or mixin;\ncommand-line flag and environment bindings are ignored for mixins." }, "InputBinding": { "type": "object", "properties": { "env": { "type": "string", - "description": "Environment variable name.\nIf input value and command-line flag are unspecified, this variable is used when present." + "description": "Environment variable name.\nIf input value and command-line flag are unspecified, this variable is used when present.\n\nTask input resolution order is: explicit `with` value, command-line flag,\nenvironment variable, then `default`. Bindings apply only to task inputs,\nnot mixin inputs." }, "flag": { "type": "string", @@ -305,7 +302,7 @@ }, "delimiter": { "type": "string", - "description": "Character used to split a string into a list when parsing an environment\nvariable or command-line value." + "description": "Character used to split a string into a list when parsing an environment\nvariable or command-line value.\n\nCurrent loader support is not implemented." } }, "description": "Command-line and environment binding for an input value." @@ -315,7 +312,7 @@ "properties": { "action": { "type": "string", - "description": "Name of action to be executed." + "description": "Name of action to be executed.\nBuilt-in actions use `namespace/name` names such as `debug/echo` and\n`go/build`. Plugin actions use the namespace declared in `plugins`, for\nexample `docker/build`." } }, "required": [ @@ -333,7 +330,7 @@ "properties": { "mixin": { "type": "string", - "description": "Name of mixin to be executed." + "description": "Name of mixin to be executed.\nMixin inputs must be passed explicitly with `with`; command-line input\nbindings do not apply to mixin calls." } }, "required": [ @@ -351,7 +348,7 @@ "properties": { "task": { "type": "string", - "description": "Name of task to be executed." + "description": "Name of task to be executed.\nCalls another task from the current workflow, enabling nested task calls." } }, "required": [ @@ -377,7 +374,7 @@ }, "working-directory": { "type": "string", - "description": "Path to a working directory where job will be executed." + "description": "Path to a working directory where job will be executed.\nRelative paths resolve from the current workflow working directory." }, "timeout": { "type": "string", @@ -389,19 +386,19 @@ }, "if": { "type": "string", - "description": "Whether to run a job.\nIf expression returns false, the job is skipped." + "description": "Whether to run a job.\nIf expression returns false, the job is skipped.\n\nThis field must be an expression. Static literal values are invalid." }, "env": { "$ref": "#/$defs/RecordUnknown", - "description": "Custom environment variables to set for a job." + "description": "Custom environment variables to set for a job.\n\nThis block is literal. Expressions are not evaluated in keys or values." }, "strategy": { "$ref": "#/$defs/Strategy", - "description": "Matrix execution strategy." + "description": "Matrix execution strategy.\nExpands this job into the Cartesian product of matrix values." }, "with": { "$ref": "#/$defs/RecordUnknown", - "description": "Input parameters for action, mixin or task." + "description": "Input parameters for action, mixin or task.\nMap keys are literal. Expressions are allowed recursively in values." }, "on": { "type": "object", @@ -422,7 +419,7 @@ ] } }, - "description": "Defines a set of jobs to run when a certain hook is called.\nHooks are events produced by certain actions." + "description": "Defines a set of jobs to run when a certain hook is called.\nHooks are events produced by certain actions.\n\nThe runner also provides a built-in `error` hook for handling job\nfailures. Jobs inside hook lists receive the `event` expression context." } }, "required": [ @@ -453,14 +450,14 @@ } ] }, - "description": "Matrix of different job configurations.\nVariables defined in a matrix become properties in the `matrix` expression context." + "description": "Matrix of different job configurations.\nVariables defined in a matrix become properties in the `matrix` expression context.\n\nEach key becomes available as `matrix.` during each matrix job run.\nValues may be literal arrays or expressions that evaluate to arrays.\nEmpty arrays are ignored with a warning." }, "exclude": { "type": "array", "items": { "$ref": "#/$defs/RecordUnknown" }, - "description": "List of combinations to exclude from job matrix." + "description": "List of combinations to exclude from job matrix.\nA rule matches only when all key/value pairs in the rule match the matrix\nrow. Exclude values and referenced matrix values must be scalar and\ncomparable." } }, "required": [ diff --git a/docs/spec/schema.tsp b/docs/spec/schema.tsp index a50b6433..1b41d9fd 100644 --- a/docs/spec/schema.tsp +++ b/docs/spec/schema.tsp @@ -4,12 +4,28 @@ using JsonSchema; namespace Schemas; -/** A YAML string parseable by Go's time.ParseDuration. */ +/** + * A YAML string parseable by Go's time.ParseDuration. + * + * Examples: `300ms`, `10s`, `5m`, `2h45m`, `1.5h`. + */ alias DurationString = string; /** * YAML string containing a Gilbert expression. * + * Gilbert supports three expression forms: + * + * - Shell expression: `$(...)`. The body is executed by the system shell, + * has a fixed 30 second timeout, and runs in `project.workDir`. + * - Value expression: `${{ ... }}`. The body is evaluated by the Expr + * language. When this is the entire YAML value, it can return a typed value + * such as a boolean, number, string, list, or object. + * - Mixed expression: literal text combined with one or more shell or value + * expressions. Mixed expressions always produce a string. Value-expression + * results inside mixed expressions are cast to strings; objects and lists + * cannot be cast. + * * The type parameter from schema.d.ts is documentation-only, so the JSON * schema representation is a string. */ @@ -34,7 +50,10 @@ model JobBase { /** Duration of time to wait before starting a job. */ delay?: DurationString; - /** Path to a working directory where job will be executed. */ + /** + * Path to a working directory where job will be executed. + * Relative paths resolve from the current workflow working directory. + */ `working-directory`?: string; /** Job execution timeout duration. */ @@ -46,21 +65,36 @@ model JobBase { /** * Whether to run a job. * If expression returns false, the job is skipped. + * + * This field must be an expression. Static literal values are invalid. */ `if`?: Expression; - /** Custom environment variables to set for a job. */ + /** + * Custom environment variables to set for a job. + * + * This block is literal. Expressions are not evaluated in keys or values. + */ env?: AnyRecord; - /** Matrix execution strategy. */ + /** + * Matrix execution strategy. + * Expands this job into the Cartesian product of matrix values. + */ strategy?: Strategy; - /** Input parameters for action, mixin or task. */ + /** + * Input parameters for action, mixin or task. + * Map keys are literal. Expressions are allowed recursively in values. + */ with: AnyRecord; /** * Defines a set of jobs to run when a certain hook is called. * Hooks are events produced by certain actions. + * + * The runner also provides a built-in `error` hook for handling job + * failures. Jobs inside hook lists receive the `event` expression context. */ on?: Record; } @@ -77,10 +111,19 @@ model Strategy { /** * Matrix of different job configurations. * Variables defined in a matrix become properties in the `matrix` expression context. + * + * Each key becomes available as `matrix.` during each matrix job run. + * Values may be literal arrays or expressions that evaluate to arrays. + * Empty arrays are ignored with a warning. */ matrix: Record; - /** List of combinations to exclude from job matrix. */ + /** + * List of combinations to exclude from job matrix. + * A rule matches only when all key/value pairs in the rule match the matrix + * row. Exclude values and referenced matrix values must be scalar and + * comparable. + */ exclude?: AnyRecord[]; } @@ -89,19 +132,31 @@ alias MatrixValues = Expression | AnyValue[]; /** Job that executes an action. */ model ActionJob extends JobBase { - /** Name of action to be executed. */ + /** + * Name of action to be executed. + * Built-in actions use `namespace/name` names such as `debug/echo` and + * `go/build`. Plugin actions use the namespace declared in `plugins`, for + * example `docker/build`. + */ action: string; } /** Job that executes a mixin. */ model MixinJob extends JobBase { - /** Name of mixin to be executed. */ + /** + * Name of mixin to be executed. + * Mixin inputs must be passed explicitly with `with`; command-line input + * bindings do not apply to mixin calls. + */ mixin: string; } /** Job that executes a task. */ model TaskJob extends JobBase { - /** Name of task to be executed. */ + /** + * Name of task to be executed. + * Calls another task from the current workflow, enabling nested task calls. + */ task: string; } @@ -114,14 +169,22 @@ alias Job = ActionJob | MixinJob | TaskJob; /** Input value types supported by task and mixin inputs. */ alias InputValueType = "string" | "int" | "bool" | "date" | "duration" | "float"; -/** List item value types supported by list inputs. */ -alias ListItemValueType = InputValueType | "list"; +/** + * List item value types supported by list inputs. + * Nested complex types are not supported: a list item can be scalar, but not + * another list or dictionary. + */ +alias ListItemValueType = InputValueType; /** Command-line and environment binding for an input value. */ model InputBinding { /** * Environment variable name. * If input value and command-line flag are unspecified, this variable is used when present. + * + * Task input resolution order is: explicit `with` value, command-line flag, + * environment variable, then `default`. Bindings apply only to task inputs, + * not mixin inputs. */ env?: string; @@ -134,6 +197,8 @@ model InputBinding { /** * Character used to split a string into a list when parsing an environment * variable or command-line value. + * + * Current loader support is not implemented. */ delimiter?: string; } @@ -142,13 +207,17 @@ model InputBinding { model InputDefinitionCommon { /** * Fallback value used when input value is not specified. - * Value type should correspond to `type`. + * This value may be an expression. After expression expansion, the value + * must match the declared `type`. */ default?: AnyValue; /** * Whether a value is not required. * Has no effect when `default` is set. + * + * When `optional` is true and there is no default, the input resolves to the + * declared type's zero value. Boolean inputs are optional by default. */ optional?: boolean; @@ -161,11 +230,15 @@ model InputDefinitionCommon { /** Input definition for list values. */ model ListInputDefinition extends InputDefinitionCommon { + /** Declares this input as a list value. */ type: "list"; /** * Defines a type of element of a list. * Should only be used when `type` is set to `list`. + * + * List item types must be scalar. Nested lists and dictionaries are not + * supported. */ items: ListItemValueType; } @@ -176,13 +249,16 @@ model ScalarInputDefinition extends InputDefinitionCommon { * Input value type. * * Besides standard scalar values, special values are parseable from - * command-line flags or string values. + * command-line flags or string values. `duration` values are parsed with + * Go's time.ParseDuration. `date` values are parsed with the Go time layout + * from `dateFormat`, or RFC3339 when `dateFormat` is omitted. */ type: InputValueType; /** * Date format used to parse a command-line flag value. * Has effect only when `type` is `date`. + * Defaults to Go time.RFC3339 when omitted. */ dateFormat?: string; } @@ -190,11 +266,17 @@ model ScalarInputDefinition extends InputDefinitionCommon { /** Defines task or mixin input parameter. */ alias InputDefinition = ListInputDefinition | ScalarInputDefinition; -/** Mixins allow a set of jobs to be reused as a block. */ +/** + * Mixins allow a set of jobs to be reused as a block. + * Mixin inputs are set only through `with` from a calling task or mixin; + * command-line flag and environment bindings are ignored for mixins. + */ model Mixin { /** * List of input parameters accepted by mixin. - * Unlike tasks, mixins do not support command-line flags. + * Unlike tasks, mixins do not support command-line flags. Mixins can be + * called from tasks and from other mixins, and their inputs should be passed + * explicitly via `with`. */ inputs?: Record; @@ -212,7 +294,8 @@ model Mixin { model Task { /** * List of input parameters accepted by a task. - * Input values can be passed to a task via command-line flags. + * Input values can come from explicit `with` values, command-line flags, + * environment variables, or defaults. */ inputs?: Record; @@ -230,33 +313,48 @@ model Task { @jsonSchema @id("gilbert.schema.json") model WorkflowFile { - /** Workflow file version. Should be `2`. */ + /** Workflow file version. `2` is the only supported value; other values are invalid. */ version: 2; - /** List of other workflow files to include and merge. */ + /** + * List of other workflow files to include and merge. + * Paths resolve relative to the including file. A file cannot include + * itself. Duplicate task, mixin, and input names across included files are + * rejected. + */ include?: string[]; /** * Set of plugins to import. * Key is a namespace and value is import URL. + * + * The namespace becomes the action prefix. For example, a plugin declared as + * `docker:` exposes actions such as `docker/build` and `docker/run`. */ plugins?: Record; /** * Predefined variables available to expressions as `${{ consts.* }}`. - * Values in this block should not be expressions. + * Expressions are not expanded inside this block. Values are literal YAML + * scalars. */ `const`?: AnyRecord; /** Global input parameters. */ inputs?: Record; - /** Map of task name and its definition. */ + /** + * Map of task name and its definition. + * Tasks are command-line entry points and can be run with + * `gilbert run `. Comments immediately above a task key are collected + * as the task description for list/help output. + */ tasks?: Record; /** * Mixins are reusable pieces of pipeline which can accept inputs. - * Unlike tasks, they cannot be called from command-line. + * Unlike tasks, they cannot be called from command-line. Mixins can be + * called from both tasks and other mixins. */ mixins?: Record; } From b63daf60598b6899d2a08714230c7a135fa90398 Mon Sep 17 00:00:00 2001 From: x1unix <9203548+x1unix@users.noreply.github.com> Date: Thu, 28 May 2026 01:01:34 -0400 Subject: [PATCH 25/25] update todos --- docs/todo.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/todo.md b/docs/todo.md index e267a565..967bf3bb 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -19,7 +19,7 @@ - support setting env - support binding to file -**expr** +**actions** -- Support indents in DocumentInfo -- Consider cel-go as it might support dynamic envs: https://github.com/google/cel-go +- Add `node/npx`, will be used to run typespec schema gen. +- Add `fs/watch` and other.