Skip to content
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
8b3bc47
feat: generate and upload json schema
Kiryous Jun 24, 2025
3ef2022
ci: allow manually release schema
Kiryous Jun 24, 2025
f3e82ee
fix: update console_provider to trigger a workflow
Kiryous Jun 24, 2025
52d1d68
ci: also run on pull-requests
Kiryous Jun 24, 2025
27fecf9
ci: update the token and add release-step
Kiryous Jun 24, 2025
f57440f
Merge branch 'main' into feat/5087-workflow-yaml-json-schema
Kiryous Jun 24, 2025
51f4303
Update release-workflow-schema.yml
Kiryous Jun 24, 2025
30cf1f3
Merge branch 'main' into feat/5087-workflow-yaml-json-schema
Kiryous Jun 25, 2025
5ad05ae
fix: mock console provider notify
Kiryous Jun 26, 2025
4e36315
tests: print current active element of e2e failures
Kiryous Jun 26, 2025
97f7087
fix: fill the input for more robust e2e test involving react-select
Kiryous Jun 26, 2025
12fd0e6
fix: update reference to modal
Kiryous Jun 26, 2025
f732169
fix: select combobox input with retry
Kiryous Jun 27, 2025
4270a5a
Merge branch 'main' into feat/5087-workflow-yaml-json-schema
Kiryous Jun 27, 2025
27793f0
fix: update select reference
Kiryous Jun 27, 2025
df92c1a
Merge branch 'feat/5087-workflow-yaml-json-schema' of github.com:keep…
Kiryous Jun 27, 2025
34b9f2f
fix: schema name
Kiryous Jun 27, 2025
5776359
fix: specify repo for release
Kiryous Jun 27, 2025
fbe5cf7
fix: only create tag for `main`
Kiryous Jun 27, 2025
8e9d7f7
fix: remove root visited check, rely on const
Kiryous Jun 27, 2025
b9a99b0
Revert "fix: update console_provider to trigger a workflow"
Kiryous Jun 30, 2025
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
153 changes: 153 additions & 0 deletions .github/workflows/release-workflow-schema.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
name: Release JSON Schema

on:
push:
branches:
- main
paths:
- "pyproject.toml"
- "keep/providers/**"
- "keep-ui/entities/workflows/model/yaml.schema.ts"
pull_request:
paths:
- "pyproject.toml"
- "keep/providers/**"
- "keep-ui/entities/workflows/model/yaml.schema.ts"
workflow_dispatch:

env:
PYTHON_VERSION: 3.11
STORAGE_MANAGER_DIRECTORY: /tmp/storage-manager
SCHEMA_REPO_NAME: keephq/keep-workflow-schema

jobs:
release-schema:
runs-on: ubuntu-latest
permissions:
contents: read

steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Extract version from pyproject.toml
id: get_version
run: |
VERSION=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/')
echo "version=$VERSION" >> $GITHUB_OUTPUT

- name: Set up Python ${{ env.PYTHON_VERSION }}
uses: actions/setup-python@v4
with:
python-version: "3.11"

- name: Install Poetry
uses: snok/install-poetry@v1
with:
virtualenvs-create: true
virtualenvs-in-project: true

- name: Cache dependencies
id: cache-deps
uses: actions/cache@v4.2.0
with:
path: .venv
key: pydeps-${{ hashFiles('**/poetry.lock') }}

- name: Install dependencies using poetry
run: poetry install --no-interaction --no-root --with dev

- name: Save providers list
run: |
PYTHONPATH="${{ github.workspace }}" poetry run python ./scripts/save_providers_list.py

- name: Set up Node.js 20
uses: actions/setup-node@v3
with:
node-version: 20
cache: "npm"
cache-dependency-path: keep-ui/package-lock.json

- name: Install Node dependencies
working-directory: keep-ui
run: npm ci

- name: Generate JSON Schema
working-directory: keep-ui
run: npm run build:workflow-yaml-json-schema

- name: Checkout schema repository
uses: actions/checkout@v4
with:
repository: ${{ env.SCHEMA_REPO_NAME }}
token: ${{ secrets.SCHEMA_REPO_PAT }}
path: schema-repo

- name: Set target branch variable
id: set_branch
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "branch=${{ github.head_ref }}" >> $GITHUB_OUTPUT
else
echo "branch=${{ github.ref_name }}" >> $GITHUB_OUTPUT
fi

- name: Create or switch to target branch in schema repo
working-directory: schema-repo
run: |
git fetch origin
if git show-ref --verify --quiet refs/heads/${{ steps.set_branch.outputs.branch }}; then
git checkout ${{ steps.set_branch.outputs.branch }}
else
git checkout -b ${{ steps.set_branch.outputs.branch }}
fi

- name: Copy schema to target repository
run: |
cp workflow-yaml-json-schema.json schema-repo/schema.json

# Update schema with version info
jq --arg version "${{ steps.get_version.outputs.version }}" \
--arg id "https://raw.githubusercontent.com/${{ env.SCHEMA_REPO_NAME }}/v${{ steps.get_version.outputs.version }}/schema.json" \
'. + {version: $version, "$id": $id}' \
schema-repo/schema.json > schema-repo/schema.tmp.json

mv schema-repo/schema.tmp.json schema-repo/schema.json

- name: Check if schema changed
id: check_changes
working-directory: schema-repo
run: |
if git diff --quiet schema.json; then
echo "changed=false" >> $GITHUB_OUTPUT
else
echo "changed=true" >> $GITHUB_OUTPUT
fi

- name: Commit and push schema
if: steps.check_changes.outputs.changed == 'true'
working-directory: schema-repo
run: |
git config user.name "Keep Schema Bot"
git config user.email "no-reply@keephq.dev"
git add schema.json
git commit -m "Release schema v${{ steps.get_version.outputs.version }}"
git push origin ${{ steps.set_branch.outputs.branch }}
if [ "${{ steps.set_branch.outputs.branch }}" = "main" ]; then
git tag "v${{ steps.get_version.outputs.version }}"
git push origin "v${{ steps.get_version.outputs.version }}"
fi

- name: Create GitHub Release
if: steps.check_changes.outputs.changed == 'true' && steps.set_branch.outputs.branch == 'main'
uses: softprops/action-gh-release@v1
with:
repository: ${{ env.SCHEMA_REPO_NAME }}
tag_name: v${{ steps.get_version.outputs.version }}
name: Release v${{ steps.get_version.outputs.version }}
body: |
Automated release of schema version v${{ steps.get_version.outputs.version }}.
env:
GITHUB_TOKEN: ${{ secrets.SCHEMA_REPO_PAT }}
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ scripts/keep_slack_bot.py
*.db
providers_cache.json
providers_list.json
workflow-yaml-json-schema.json

tests/provision/*
!tests/provision/workflows*
Expand Down
46 changes: 46 additions & 0 deletions keep-ui/entities/workflows/lib/generateWorkflowYamlJsonSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { ZodSchema } from "zod";
import zodToJsonSchema, { PostProcessCallback } from "zod-to-json-schema";

const schemaName = "KeepWorkflowSchema";
const rootPath = `#/definitions/${schemaName}/properties/workflow`;

const makeRequiredEitherStepsOrActions: PostProcessCallback = (
// The original output produced by the package itself:
jsonSchema,
// The ZodSchema def used to produce the original schema:
def,
// The refs object containing the current path, passed options, etc.
refs
) => {
const path = refs.currentPath.join("/");
if (jsonSchema && path === rootPath) {
// @ts-ignore
jsonSchema.required = jsonSchema.required.filter(
(r: string) => r !== "steps"
);
// @ts-ignore
jsonSchema.anyOf = [
{
required: ["steps"],
properties: {
steps: { minItems: 1 },
},
},
{
required: ["actions"],
properties: {
actions: { minItems: 1 },
},
},
];
}
return jsonSchema;
};

export function generateWorkflowYamlJsonSchema(zodSchema: ZodSchema) {
return zodToJsonSchema(zodSchema, {
name: schemaName,
// Make workflow valid if it has either actions or steps
postProcess: makeRequiredEitherStepsOrActions,
});
}
51 changes: 5 additions & 46 deletions keep-ui/entities/workflows/lib/useWorkflowJsonSchema.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,17 @@
import { useProviders } from "@/utils/hooks/useProviders";
import { getYamlWorkflowDefinitionSchema } from "../model/yaml.schema";
import { useMemo } from "react";
import zodToJsonSchema, { PostProcessCallback } from "zod-to-json-schema";
import { YamlWorkflowDefinitionSchema } from "../model/yaml.schema";

const makeRequiredEitherStepsOrActions: PostProcessCallback = (
// The original output produced by the package itself:
jsonSchema,
// The ZodSchema def used to produce the original schema:
def,
// The refs object containing the current path, passed options, etc.
refs
) => {
const path = refs.currentPath.join("/");
if (
jsonSchema &&
path === "#/definitions/WorkflowSchema/properties/workflow"
) {
// @ts-ignore
jsonSchema.required = jsonSchema.required.filter(
(r: string) => r !== "steps"
);
// @ts-ignore
jsonSchema.anyOf = [
{
required: ["steps"],
properties: {
steps: { minItems: 1 },
},
},
{
required: ["actions"],
properties: {
actions: { minItems: 1 },
},
},
];
}
return jsonSchema;
};
import { generateWorkflowYamlJsonSchema } from "./generateWorkflowYamlJsonSchema";

export function useWorkflowJsonSchema() {
const { data: { providers } = {} } = useProviders();
return useMemo(() => {
if (!providers) {
return zodToJsonSchema(YamlWorkflowDefinitionSchema, {
name: "WorkflowSchema",
postProcess: makeRequiredEitherStepsOrActions,
});
return generateWorkflowYamlJsonSchema(YamlWorkflowDefinitionSchema);
}
return zodToJsonSchema(getYamlWorkflowDefinitionSchema(providers), {
name: "WorkflowSchema",
// Make workflow valid if it has either actions or steps
postProcess: makeRequiredEitherStepsOrActions,
});
return generateWorkflowYamlJsonSchema(
getYamlWorkflowDefinitionSchema(providers)
);
}, [providers]);
}
1 change: 1 addition & 0 deletions keep-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"scripts": {
"build-monaco-workers": "node scripts/build-monaco-workers-turbopack.js",
"build": "./next_build.sh",
"build:workflow-yaml-json-schema": "ts-node -P tsconfig.scripts.json scripts/generate-workflow-yaml-json-schema.ts",
"dev": "npm run build-monaco-workers && next dev --turbopack -p 3000",
"dev:webpack": "next dev -p 3000",
"lint": "next lint",
Expand Down
23 changes: 23 additions & 0 deletions keep-ui/scripts/generate-workflow-yaml-json-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { getYamlWorkflowDefinitionSchema } from "../entities/workflows/model/yaml.schema";
import fs from "fs";
import path from "path";
import { generateWorkflowYamlJsonSchema } from "../entities/workflows/lib/generateWorkflowYamlJsonSchema";

function saveWorkflowYamlJsonSchema() {
console.log("Loading providers list");
// providers_list.json should be generated with "python3 scripts/save_providers_list.py" from the root of the repo
const providers = JSON.parse(
fs.readFileSync(path.join(__dirname, "../../providers_list.json"), "utf8")
) as any[];
console.log(`Providers list loaded, ${providers.length} providers found`);
const zodSchema = getYamlWorkflowDefinitionSchema(providers);
console.log(`Zod schema loaded`);
const jsonSchema = generateWorkflowYamlJsonSchema(zodSchema);
fs.writeFileSync(
path.join(__dirname, "../../workflow-yaml-json-schema.json"),
JSON.stringify(jsonSchema, null, 2)
);
console.log("JSON schema generated");
}

saveWorkflowYamlJsonSchema();
14 changes: 11 additions & 3 deletions keep/providers/console_provider/console_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,18 @@ def dispose(self):
# No need to dispose of anything, so just do nothing.
pass

def _query(self, message: str = "", logger: bool = False, severity: str = "info"):
return self._notify(message, logger, severity)
def _query(
self, message: str = "", logger: bool = False, severity: str = "info", **kwargs
):
return self._notify(message, logger, severity, **kwargs)

def _notify(self, message: str = "", logger: bool = False, severity: str = "info"):
def _notify(
self,
message: str = "",
logger: bool = False,
severity: str = "info",
**kwargs, # `enrich_alert` is not removed from kwargs, so we need to allow as part of kwargs
):
"""
Output alert message simply using the print method.

Expand Down
7 changes: 7 additions & 0 deletions scripts/workflow_yaml_generate_json_schema.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/bin/bash

# Save providers list to providers_list.json
python3 ./scripts/save_providers_list.py

# Generate JSON schema from providers list
cd keep-ui && npm run build:workflow-yaml-json-schema
11 changes: 5 additions & 6 deletions tests/e2e_tests/test_end_to_end.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from tests.e2e_tests.utils import (
assert_connected_provider_count,
assert_scope_text_count,
choose_combobox_option_with_retry,
delete_provider,
init_e2e_test,
install_webhook_provider,
Expand Down Expand Up @@ -823,10 +824,8 @@ def test_run_workflow_from_alert_and_incident(
page.wait_for_timeout(200)
expect(modal).to_be_visible()
page.wait_for_timeout(200)
modal.get_by_test_id("manual-run-workflow-select-control").click()
modal.get_by_role(
"option", name=re.compile(r"Log every incident")
).first.click()
select = modal.get_by_test_id("manual-run-workflow-select-control")
choose_combobox_option_with_retry(page, select, "Log every incident")
modal.get_by_role("button", name="Run").click()
expect(page.get_by_text("Workflow started successfully")).to_be_visible()
# Run workflow from alert
Expand All @@ -841,8 +840,8 @@ def test_run_workflow_from_alert_and_incident(
"button", name="Run workflow"
).click()
modal = page.get_by_test_id("manual-run-workflow-modal")
modal.get_by_test_id("manual-run-workflow-select-control").click()
modal.get_by_role("option", name=re.compile(r"Log every alert")).click()
select = modal.get_by_test_id("manual-run-workflow-select-control")
choose_combobox_option_with_retry(page, select, "Log every alert")
modal.get_by_role("button", name="Run").click()
expect(page.get_by_text("Workflow started successfully")).to_be_visible()
except Exception:
Expand Down
Loading