Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 151 additions & 0 deletions .github/workflows/schema-publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
name: Publish schema

on:
push:
tags: ['v*']
workflow_dispatch:
inputs:
release_tag:
description: Existing stable release tag (vX.Y.Z)
required: true
type: string
pull_request:
paths:
- 'packages/schema/**'
- 'packages/sdk/src/**'
- 'packages/sdk/package-lock.json'
- 'scripts/generate-json-schema.mjs'
- 'scripts/schema-*.mjs'
- '.github/workflows/schema-publish.yml'

permissions:
contents: read

concurrency:
group: schema-${{ github.event_name == 'pull_request' && github.ref || 'publish' }}
cancel-in-progress: false

jobs:
validate:
runs-on: ubuntu-24.04
timeout-minutes: 10
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.release_tag || github.ref }}
- uses: actions/setup-node@v4
with:
node-version: '22'
- uses: oven-sh/setup-bun@v2
with:
bun-version: '1.4.0'
- name: Install generator and test dependencies
run: npm ci --prefix packages/sdk --ignore-scripts
- name: Regenerate and check committed schema
run: |
node scripts/generate-json-schema.mjs
git diff --exit-code -- packages/schema/flows.schema.json
cp packages/schema/flows.schema.json /tmp/flows.schema.first.json
node scripts/generate-json-schema.mjs
diff -q /tmp/flows.schema.first.json packages/schema/flows.schema.json
- name: Schema parity and smoke
working-directory: packages/schema
run: bun run test
- name: Version package from stable release tag
if: github.event_name != 'pull_request'
id: version
env:
RELEASE_TAG: ${{ inputs.release_tag || github.ref_name }}
run: |
if [[ ! "$RELEASE_TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
echo 'Schema publication requires a stable vX.Y.Z tag.' >&2
exit 1
fi
test "$(git rev-parse HEAD)" = "$(git rev-parse "refs/tags/$RELEASE_TAG^{commit}")"
version="${RELEASE_TAG#v}"
npm version --prefix packages/schema "$version" --no-git-tag-version --allow-same-version
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Pack data-only artifact
if: github.event_name != 'pull_request'
run: |
mkdir -p dist/schema
npm pack ./packages/schema --pack-destination dist/schema --ignore-scripts
cp packages/schema/flows.schema.json dist/schema/
- uses: actions/upload-artifact@v4
if: github.event_name != 'pull_request'
with:
name: schema-release
path: dist/schema/
if-no-files-found: error

npm:
if: github.event_name != 'pull_request'
needs: validate
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/setup-node@v4
with:
node-version: '22'
registry-url: https://registry.npmjs.org
- uses: actions/download-artifact@v4
with:
name: schema-release
path: release
- name: Publish data package
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
RELEASE_VERSION: ${{ needs.validate.outputs.version }}
run: npm publish "release/relayflows-schema-${RELEASE_VERSION}.tgz" --access public --ignore-scripts

pages:
if: github.event_name != 'pull_request'
needs: [validate, npm]
runs-on: ubuntu-24.04
timeout-minutes: 10
permissions:
contents: write
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: schema-release
path: release
- name: Assemble versioned schema paths
env:
RELEASE_VERSION: ${{ needs.validate.outputs.version }}
run: |
node --input-type=module <<'NODE'
import { readFileSync, mkdirSync, copyFileSync } from 'node:fs';
const schema = JSON.parse(readFileSync('release/flows.schema.json', 'utf8'));
const dialect = new URL(schema.$id).pathname.split('/')[1];
if (!/^v\d+\.\d+$/.test(dialect)) throw new Error('Unexpected schema dialect path');
for (const version of [dialect, `v${process.env.RELEASE_VERSION}`]) {
mkdirSync(`public/${version}`, { recursive: true });
copyFileSync('release/flows.schema.json', `public/${version}/flows.schema.json`);
}
NODE
# Retain old versions in gh-pages; deploy the complete retained tree with
# the official Pages API (a GITHUB_TOKEN push alone need not build Pages).
- uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: public
keep_files: true
- uses: actions/checkout@v4
with:
ref: gh-pages
path: retained-pages
- uses: actions/configure-pages@v5
- uses: actions/upload-pages-artifact@v3
with:
path: retained-pages
- uses: actions/deploy-pages@v4
id: deployment
126 changes: 126 additions & 0 deletions docs/EDITOR.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,129 @@ families remain follow-up work and are not advertised as shipped:
Those rules need their own SDK parity fixtures and source spans. YAML, runtime
probes, quick fixes, and a full language server remain outside this package's
scope. This slice changes no SDK, surface, or kernel semantics.
# YAML and JSON editor validation

Register the published draft 2020-12 schema for the canonical declarative
`FlowSpec` dialect (`version`, `steps`, and explicit `type` fields). The schema
provides completion, hover documentation, and structural diagnostics using your
editor's YAML/JSON support; no Relayflows extension, server, or npm install is
needed. VS Code and Cursor need their usual YAML language support (Red Hat YAML
if it is not already installed). JetBrains includes schema-backed YAML support.

The versioned schema URL is reserved for publication:
`https://schema.relayflows.dev/v0.1/flows.schema.json`. Until that hosting is
configured, use the committed `packages/schema/flows.schema.json` locally.
The npm package also ships this exact file, with zero runtime dependencies.

## VS Code / Cursor

Add to `.vscode/settings.json` (or user settings):

```json
{
"yaml.validate": true,
"yaml.schemas": {
"https://schema.relayflows.dev/v0.1/flows.schema.json": ["**/*.flow.yaml", "**/*.flow.yml"]
},
"json.schemas": [{
"fileMatch": ["**/*.flow.json"],
"url": "https://schema.relayflows.dev/v0.1/flows.schema.json"
}]
}
```

For offline use, replace the URL with `./packages/schema/flows.schema.json`
(relative to the workspace) or `./node_modules/@relayflows/schema/flows.schema.json`
if installed with `npm install --save-dev @relayflows/schema`.
The modeline and mapping behavior is documented by
[yaml-language-server](https://github.com/redhat-developer/yaml-language-server#language-server-settings).

## JetBrains

1. Open Settings / Preferences → Languages & Frameworks → Schemas and DTDs →
JSON Schema Mappings.
2. Add a mapping named **Relayflows**, select JSON Schema version **2020-12**,
and choose the URL above or the local `flows.schema.json` file.
3. Add file path patterns `*.flow.yaml`, `*.flow.yml`, and `*.flow.json`.
4. Open a flow and verify that the status-bar schema selector says **Relayflows**.

See JetBrains' [YAML schema support](https://www.jetbrains.com/help/idea/yaml.html)
and [JSON schema mappings](https://www.jetbrains.com/help/idea/json.html).

## Neovim

With `yaml-language-server` on PATH, Neovim 0.11+ can start it directly:

```lua
vim.lsp.config('relayflows_yaml', {
cmd = { 'yaml-language-server', '--stdio' },
filetypes = { 'yaml' },
root_markers = { '.git', 'flows.json' },
settings = {
yaml = {
validate = true,
schemas = {
['https://schema.relayflows.dev/v0.1/flows.schema.json'] = {
'**/*.flow.yaml', '**/*.flow.yml',
},
},
},
},
})
vim.lsp.enable('relayflows_yaml')
```

If you already use `nvim-lspconfig`'s `yamlls`, merge the `settings.yaml` block
into that configuration instead of starting a second server. Local absolute
schema paths also work.

## Per-file fallback

Put this on the first line of a YAML flow:

```yaml
# yaml-language-server: $schema=https://schema.relayflows.dev/v0.1/flows.schema.json
version: '0.1.0'
steps:
- id: greet
type: deterministic
command: echo hello
```

A relative local URL is resolved from the YAML file: for the repository's
`testdata/hello-deterministic.flow.yaml`, use
`# yaml-language-server: $schema=../packages/schema/flows.schema.json`.
The language server modeline takes precedence over settings. `flows check`
emits `editor_schema_missing` as a warning when a `.flow.yaml` file lacks this
first-line comment, including when a settings mapping is already configured.
It never changes the file or refuses a valid flow because of that warning.

## Validation scope

The schema follows `packages/sdk/src/spec.ts`, with structural constraints from
runtime validation: closed objects, discriminated step types, required fields,
value bounds, named-agent declaration shapes, input selectors, and output
schema keyword shapes for SDK-supported drafts. The generated file bundles its
meta-schemas and needs no network after the file itself has loaded.

`flows check` remains necessary for unique step/trigger IDs, dependency cycles,
named-agent and input-source lookup, declared output-path lookup, JSON Schema
reference resolution/termination and regex compilation, model allowlists, CLI
authentication, and executor readiness. JSON Schema cannot express comparisons
against arbitrary values elsewhere in the flow. Embedded schemas may use
unknown annotation keywords, just as the SDK permits. Future surface shorthand
such as `run:` and headers such as `identity:` are not canonical `FlowSpec`
fields today; both `identity:` and its typo `identitty:` are rejected here.
Compiled snake_case kernel JSON is a separate dialect and is not the editor
schema's entry point, even though `flows check` can also ingest it.

## Editor smoke procedure

1. Register the local schema and open `testdata/hello-deterministic.flow.yaml`.
2. Add `identitty: chief` at the root, save, and observe an unknown-property
squiggle. Remove the whole added line, save, and confirm it clears.
3. Change a deterministic step's `command` to `commmand`; confirm the unknown
field and missing required `command` diagnostics. Undo and save.
4. Hover `timeoutMs` and inspect completion after `type: agent`.

Do not correct `identitty` to `identity` in this dialect: neither is supported.
Loading
Loading