Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: schema validation #23

Merged
2 changes: 1 addition & 1 deletion .github/knip.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const config: KnipConfig = {
ignore: ["src/types/config.ts", "**/__mocks__/**", "**/__fixtures__/**"],
ignoreExportsUsedInFile: true,
// eslint can also be safely ignored as per the docs: https://knip.dev/guides/handling-issues#eslint--jest
ignoreDependencies: ["eslint-config-prettier", "eslint-plugin-prettier", "@mswjs/data"],
ignoreDependencies: ["eslint-config-prettier", "eslint-plugin-prettier"],
eslint: true,
};

Expand Down
58 changes: 58 additions & 0 deletions .github/workflows/update-configuration.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
name: "Update Configuration"

on:
workflow_dispatch:
push:

jobs:
update:
name: "Update Configuration in manifest.json"
runs-on: ubuntu-latest
permissions: write-all

steps:
- uses: actions/checkout@v4

- name: Setup node
uses: actions/setup-node@v4
with:
node-version: "20.10.0"

- name: Install deps and run configuration update
run: |
yarn install --immutable --immutable-cache --check-cache
yarn tsc --noCheck --project tsconfig.json

- name: Update manifest configuration using GitHub Script
uses: actions/github-script@v7
with:
script: |
gentlementlegen marked this conversation as resolved.
Show resolved Hide resolved
const fs = require('fs');
const path = require('path');

const { pluginSettingsSchema } = require('./src/types');

const manifestPath = path.resolve("${{ github.workspace }}", './manifest.json');
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));

const configuration = JSON.stringify(pluginSettingsSchema);

manifest["configuration"] = JSON.parse(configuration);

const updatedManifest = JSON.stringify(manifest, null, 2)
console.log('Updated manifest:', updatedManifest);
fs.writeFileSync(manifestPath, updatedManifest);

- name: Commit and Push generated types
run: |
git config --global user.name 'ubiquity-os[bot]'
git config --global user.email 'ubiquity-os[bot]@users.noreply.github.com'
git add ./manifest.json
if [ -n "$(git diff-index --cached --name-only HEAD)" ]; then
git commit -m "chore: updated generated configuration" || echo "Lint-staged check failed"
git push origin HEAD:${{ github.ref_name }}
else
echo "No changes to commit"
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1 change: 1 addition & 0 deletions .github/workflows/worker-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jobs:
id: wrangler_deploy
uses: cloudflare/wrangler-action@v3
with:
wranglerVersion: "3.79.0"
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
secrets: |
Expand Down
25 changes: 22 additions & 3 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,30 @@
{
"name": "ts-template",
"description": "ts-template for Ubiquibot plugins.",
"ubiquity:listeners": ["issue_comment.created"],
"description": "ts-template for UbiquityOS plugins.",
"ubiquity:listeners": [
"issue_comment.created"
],
"commands": {
"command1": {
"ubiquity:example": "/command1 argument",
"description": "Command 1 with an argument."
}
},
"configuration": {
"default": {
"configurableResponse": "Hello, world!"
},
"type": "object",
"properties": {
"configurableResponse": {
"type": "string"
},
"customStringsUrl": {
"type": "string"
}
},
"required": [
"configurableResponse"
]
}
}
}
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"node": ">=20.10.0"
},
"scripts": {
"build": "tsc --project tsconfig.json",
"format": "run-p format:*",
"format:lint": "eslint --fix .",
"format:prettier": "prettier --write .",
Expand Down Expand Up @@ -63,9 +64,9 @@
"prettier": "3.3.2",
"ts-jest": "29.1.5",
"tsx": "4.15.6",
"typescript": "5.4.5",
"typescript": "5.6.2",
"typescript-eslint": "7.13.1",
"wrangler": "3.60.3"
"wrangler": "3.79.0"
},
"lint-staged": {
"*.ts": [
Expand Down
53 changes: 53 additions & 0 deletions src/helpers/validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import * as github from "@actions/github";
import { Octokit } from "@octokit/rest";
import { TransformDecodeCheckError, TransformDecodeError, Value, ValueError } from "@sinclair/typebox/value";
import { Env, envSchema, envValidator, PluginSettings, pluginSettingsSchema, pluginSettingsValidator } from "../types";

export async function returnDataToKernel(repoToken: string, stateId: string, output: object, eventType = "return-data-to-ubiquity-os-kernel") {
gentlementlegen marked this conversation as resolved.
Show resolved Hide resolved
const octokit = new Octokit({ auth: repoToken });
return octokit.repos.createDispatchEvent({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
event_type: eventType,
client_payload: {
state_id: stateId,
output: JSON.stringify(output),
},
});
}

export function validateAndDecodeSchemas(rawEnv: object, rawSettings: object) {
const errors: ValueError[] = [];

const env = Value.Default(envSchema, rawEnv) as Env;
if (!envValidator.test(env)) {
for (const error of envValidator.errors(env)) {
console.error(error);
errors.push(error);
}
}

const settings = Value.Default(pluginSettingsSchema, rawSettings) as PluginSettings;
if (!pluginSettingsValidator.test(settings)) {
for (const error of pluginSettingsValidator.errors(settings)) {
console.error(error);
errors.push(error);
}
}

if (errors.length) {
throw { errors };
}

try {
const decodedSettings = Value.Decode(pluginSettingsSchema, settings);
const decodedEnv = Value.Decode(envSchema, rawEnv || {});
return { decodedEnv, decodedSettings };
} catch (e) {
console.error("validateAndDecodeSchemas", e);
if (e instanceof TransformDecodeCheckError || e instanceof TransformDecodeError) {
throw { errors: [e.error] };
}
throw e;
}
}
4 changes: 3 additions & 1 deletion src/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Octokit } from "@octokit/rest";
import { returnDataToKernel } from "./helpers/validator";
import { Env, PluginInputs } from "./types";
import { Context } from "./types";
import { isIssueCommentEvent } from "./types/typeguards";
Expand Down Expand Up @@ -44,5 +45,6 @@ export async function plugin(inputs: PluginInputs, env: Env) {
* context.adapters = createAdapters(supabase, context);
*/

return runPlugin(context);
await runPlugin(context);
return returnDataToKernel(process.env.GITHUB_TOKEN, inputs.stateId, {});
}
9 changes: 9 additions & 0 deletions src/types/process-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
declare global {
namespace NodeJS {
interface ProcessEnv {
GITHUB_TOKEN: string;
}
}
}

export {};
47 changes: 15 additions & 32 deletions src/worker.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { Value } from "@sinclair/typebox/value";
import { plugin } from "./plugin";
import { Env, envValidator, pluginSettingsSchema, pluginSettingsValidator } from "./types";
import manifest from "../manifest.json";
import { validateAndDecodeSchemas } from "./helpers/validator";
import { plugin } from "./plugin";
import { Env } from "./types";

export default {
async fetch(request: Request, env: Env): Promise<Response> {
try {
if (request.method === "GET") {
const url = new URL(request.url);
if (url.pathname === "/manifest.json") {
const url = new URL(request.url);
if (url.pathname === "/manifest") {
if (request.method === "GET") {
return new Response(JSON.stringify(manifest), {
headers: { "content-type": "application/json" },
});
} else if (request.method === "POST") {
const webhookPayload = await request.json();

validateAndDecodeSchemas(env, webhookPayload.settings);
return new Response(JSON.stringify({ message: "Schema is valid" }), { status: 200, headers: { "content-type": "application/json" } });
}
}
if (request.method !== "POST") {
Expand All @@ -29,33 +34,11 @@ export default {
}

const webhookPayload = await request.json();
const settings = Value.Decode(pluginSettingsSchema, Value.Default(pluginSettingsSchema, webhookPayload.settings));

if (!pluginSettingsValidator.test(settings)) {
const errors: string[] = [];
for (const error of pluginSettingsValidator.errors(settings)) {
console.error(error);
errors.push(`${error.path}: ${error.message}`);
}
return new Response(JSON.stringify({ error: `Error: "Invalid settings provided. ${errors.join("; ")}"` }), {
status: 400,
headers: { "content-type": "application/json" },
});
}
if (!envValidator.test(env)) {
const errors: string[] = [];
for (const error of envValidator.errors(env)) {
console.error(error);
errors.push(`${error.path}: ${error.message}`);
}
return new Response(JSON.stringify({ error: `Error: "Invalid environment provided. ${errors.join("; ")}"` }), {
status: 400,
headers: { "content-type": "application/json" },
});
}
const { decodedSettings, decodedEnv } = validateAndDecodeSchemas(env, webhookPayload.settings);

webhookPayload.settings = settings;
await plugin(webhookPayload, env);
webhookPayload.env = decodedEnv;
webhookPayload.settings = decodedSettings;
await plugin(webhookPayload, decodedEnv);
return new Response(JSON.stringify("OK"), { status: 200, headers: { "content-type": "application/json" } });
} catch (error) {
return handleUncaughtError(error);
Expand Down
2 changes: 1 addition & 1 deletion tests/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ describe("Plugin tests", () => {

it("Should serve the manifest file", async () => {
const worker = (await import("../src/worker")).default;
const response = await worker.fetch(new Request("http://localhost/manifest.json"), {});
const response = await worker.fetch(new Request("http://localhost/manifest"), {});
const content = await response.json();
expect(content).toEqual(manifest);
});
Expand Down
5 changes: 4 additions & 1 deletion wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ compatibility_date = "2024-05-23"
node_compat = true

[env.dev]
[env.prod]
[env.prod]

[observability]
enabled = true
Loading