diff --git a/.github/workflows/update-configuration.yml b/.github/workflows/update-configuration.yml new file mode 100644 index 0000000..ef75268 --- /dev/null +++ b/.github/workflows/update-configuration.yml @@ -0,0 +1,59 @@ +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: | + (async () => { + const fs = await import('fs/promises'); + const path = await import('path'); + + const { pluginSettingsSchema } = await import("${{ github.workspace }}/src/types/plugin-inputs.js"); + + const manifestPath = path.resolve("${{ github.workspace }}", './manifest.json'); + const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8')); + + const configuration = JSON.stringify(pluginSettingsSchema); + manifest["configuration"] = JSON.parse(configuration); + + const updatedManifest = JSON.stringify(manifest, null, 2); + console.log('Updated manifest:', updatedManifest); + await fs.writeFile(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 }} diff --git a/eslint.config.js b/eslint.config.cjs similarity index 100% rename from eslint.config.js rename to eslint.config.cjs diff --git a/manifest.json b/manifest.json index 7731d05..3477e3d 100644 --- a/manifest.json +++ b/manifest.json @@ -1,5 +1,41 @@ { "name": "User activity watcher", "description": "Watches user activity on issues, sends reminders on deadlines, and unassign inactive users.", - "ubiquity:listeners": ["pull_request_review_comment.created", "issue_comment.created", "push"] -} + "ubiquity:listeners": [ + "pull_request_review_comment.created", + "issue_comment.created", + "push" + ], + "configuration": { + "type": "object", + "properties": { + "warning": { + "default": "3.5 days", + "type": "string" + }, + "watch": { + "type": "object", + "properties": { + "optOut": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "optOut" + ] + }, + "disqualification": { + "default": "7 days", + "type": "string" + } + }, + "required": [ + "warning", + "watch", + "disqualification" + ] + } +} \ No newline at end of file diff --git a/package.json b/package.json index 297b164..45c75be 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "user-activity-watcher", "version": "1.0.0", "description": "Watches user activity on issues, sends reminders on deadlines, and unassign inactive users.", - "main": "src/worker.ts", + "main": "src/index.ts", "author": "Ubiquity DAO", "license": "MIT", "type": "module", @@ -40,7 +40,8 @@ "dotenv": "16.4.5", "luxon": "3.4.4", "ms": "2.1.3", - "tsx": "4.11.2" + "tsx": "4.11.2", + "typebox-validators": "0.3.5" }, "devDependencies": { "@commitlint/cli": "19.3.0", @@ -72,7 +73,7 @@ "prettier": "3.3.0", "supabase": "1.176.9", "ts-jest": "29.1.4", - "typescript": "^5.4.5" + "typescript": "5.6.2" }, "lint-staged": { "*.ts": [ diff --git a/src/helpers/validator.ts b/src/helpers/validator.ts new file mode 100644 index 0000000..feeb043 --- /dev/null +++ b/src/helpers/validator.ts @@ -0,0 +1,50 @@ +import * as github from "@actions/github"; +import { Octokit } from "@octokit/rest"; +import { TransformDecodeCheckError, TransformDecodeError, Value, ValueError } from "@sinclair/typebox/value"; +import { Env, envSchema, envValidator, pluginSettingsValidator, PluginSettings, pluginSettingsSchema } from "../types/plugin-inputs"; + +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)) { + errors.push(error); + } + } + + const settings = Value.Default(pluginSettingsSchema, rawSettings) as PluginSettings; + if (!pluginSettingsValidator.test(settings)) { + for (const error of pluginSettingsValidator.errors(settings)) { + 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) { + if (e instanceof TransformDecodeCheckError || e instanceof TransformDecodeError) { + throw { errors: [e.error] }; + } + throw e; + } +} + +export async function returnDataToKernel(repoToken: string, stateId: string, output: object, eventType = "return-data-to-ubiquity-os-kernel") { + 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), + }, + }); +} diff --git a/src/parser/payload.ts b/src/parser/payload.ts index 19b2150..a715569 100644 --- a/src/parser/payload.ts +++ b/src/parser/payload.ts @@ -1,12 +1,12 @@ import * as github from "@actions/github"; -import { Value } from "@sinclair/typebox/value"; import { config } from "dotenv"; -import { PluginInputs, userActivityWatcherSettingsSchema } from "../types/plugin-inputs"; +import { validateAndDecodeSchemas } from "../helpers/validator"; +import { PluginInputs } from "../types/plugin-inputs"; config(); const webhookPayload = github.context.payload.inputs; -const settings = Value.Decode(userActivityWatcherSettingsSchema, Value.Default(userActivityWatcherSettingsSchema, JSON.parse(webhookPayload.settings))); +const { decodedSettings } = validateAndDecodeSchemas(JSON.parse(webhookPayload.settings), process.env); const program: PluginInputs = { stateId: webhookPayload.stateId, @@ -14,7 +14,7 @@ const program: PluginInputs = { authToken: webhookPayload.authToken, ref: webhookPayload.ref, eventPayload: JSON.parse(webhookPayload.eventPayload), - settings, + settings: decodedSettings, }; export default program; diff --git a/src/run.ts b/src/run.ts index 7c48a1b..d6b263a 100644 --- a/src/run.ts +++ b/src/run.ts @@ -1,4 +1,5 @@ import { Octokit } from "@octokit/rest"; +import { returnDataToKernel } from "./helpers/validator"; import { Context } from "./types/context"; import { PluginInputs } from "./types/plugin-inputs"; import { Logs } from "@ubiquity-dao/ubiquibot-logger"; @@ -14,7 +15,7 @@ export async function run(inputs: PluginInputs) { logger: new Logs("verbose"), }; await runPlugin(context); - return JSON.stringify({ status: 200 }); + return returnDataToKernel(process.env.GITHUB_TOKEN, inputs.stateId, {}); } export async function runPlugin(context: Context) { diff --git a/src/types/context.ts b/src/types/context.ts index 0ff5ce5..cb6fc06 100644 --- a/src/types/context.ts +++ b/src/types/context.ts @@ -1,12 +1,12 @@ import { EmitterWebhookEvent as WebhookEvent, EmitterWebhookEventName as WebhookEventName } from "@octokit/webhooks"; import { Octokit } from "@octokit/rest"; -import { SupportedEvents, UserActivityWatcherSettings } from "./plugin-inputs"; +import { SupportedEvents, PluginSettings } from "./plugin-inputs"; import { Logs } from "@ubiquity-dao/ubiquibot-logger"; export interface Context { eventName: T; payload: WebhookEvent["payload"]; octokit: InstanceType; - config: UserActivityWatcherSettings; + config: PluginSettings; logger: Logs; } diff --git a/src/types/plugin-inputs.ts b/src/types/plugin-inputs.ts index 1c78ac5..b3a9b98 100644 --- a/src/types/plugin-inputs.ts +++ b/src/types/plugin-inputs.ts @@ -1,6 +1,7 @@ import { EmitterWebhookEvent as WebhookEvent, EmitterWebhookEventName as WebhookEventName } from "@octokit/webhooks"; import { StaticDecode, StringOptions, Type as T, TypeBoxError } from "@sinclair/typebox"; import ms from "ms"; +import { StandardValidator } from "typebox-validators"; export type SupportedEvents = "pull_request_review_comment.created" | "issue_comment.created" | "push"; @@ -8,7 +9,7 @@ export interface PluginInputs { stateId: string; eventName: T; eventPayload: WebhookEvent["payload"]; - settings: UserActivityWatcherSettings; + settings: PluginSettings; authToken: string; ref: string; } @@ -31,6 +32,7 @@ function thresholdType(options?: StringOptions) { }); } +<<<<<<< HEAD const eventWhitelist = [ "pull_request.review_requested", "pull_request.ready_for_review", @@ -120,5 +122,34 @@ export const userActivityWatcherSettingsSchema = T.Object( }, { default: {} } ); +======= +export const pluginSettingsSchema = T.Object({ + /** + * Delay to send reminders. 0 means disabled. Any other value is counted in days, e.g. 1,5 days + */ + warning: thresholdType({ default: "3.5 days" }), + /** + * By default all repositories are watched. Use this option to opt-out from watching specific repositories + * within your organization. The value is an array of repository names. + */ + watch: T.Object({ + optOut: T.Array(T.String()), + }), + /** + * Delay to unassign users. 0 means disabled. Any other value is counted in days, e.g. 7 days + */ + disqualification: thresholdType({ + default: "7 days", + }), +}); +>>>>>>> development -export type UserActivityWatcherSettings = StaticDecode; +export const pluginSettingsValidator = new StandardValidator(pluginSettingsSchema); + +export type PluginSettings = StaticDecode; + +export const envSchema = T.Object({}); + +export const envValidator = new StandardValidator(envSchema); + +export type Env = StaticDecode; diff --git a/src/types/process-env.d.ts b/src/types/process-env.d.ts new file mode 100644 index 0000000..a2bb98f --- /dev/null +++ b/src/types/process-env.d.ts @@ -0,0 +1,9 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + GITHUB_TOKEN: string; + } + } +} + +export {}; diff --git a/tests/main.test.ts b/tests/main.test.ts index 841e4d3..328f457 100644 --- a/tests/main.test.ts +++ b/tests/main.test.ts @@ -1,7 +1,11 @@ import { drop } from "@mswjs/data"; import { TransformDecodeError, Value } from "@sinclair/typebox/value"; import { runPlugin } from "../src/run"; +<<<<<<< HEAD import { UserActivityWatcherSettings, userActivityWatcherSettingsSchema } from "../src/types/plugin-inputs"; +======= +import { pluginSettingsSchema } from "../src/types/plugin-inputs"; +>>>>>>> development import { db } from "./__mocks__/db"; import { server } from "./__mocks__/node"; import cfg from "./__mocks__/results/valid-configuration.json"; @@ -46,6 +50,7 @@ describe("User start/stop", () => { ).toThrow(TypeBoxError); }); it("Should parse thresholds", async () => { +<<<<<<< HEAD const settings = Value.Decode(userActivityWatcherSettingsSchema, Value.Default(userActivityWatcherSettingsSchema, cfg)); expect(settings).toEqual({ warning: 302400000, @@ -53,10 +58,14 @@ describe("User start/stop", () => { watch: { optOut: [STRINGS.PRIVATE_REPO_NAME] }, eventWhitelist: ["review_requested", "ready_for_review", "commented", "committed"], }); +======= + const settings = Value.Decode(pluginSettingsSchema, Value.Default(pluginSettingsSchema, cfg)); + expect(settings).toEqual({ warning: 302400000, disqualification: 604800000, watch: { optOut: [STRINGS.PRIVATE_REPO_NAME] } }); +>>>>>>> development expect(() => Value.Decode( - userActivityWatcherSettingsSchema, - Value.Default(userActivityWatcherSettingsSchema, { + pluginSettingsSchema, + Value.Default(pluginSettingsSchema, { warning: "12 foobars", disqualification: "2 days", watch: { optOut: [STRINGS.PRIVATE_REPO_NAME] }, diff --git a/tsconfig.json b/tsconfig.json index c6d3097..7f76784 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,7 @@ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ /* Language and Environment */ - "target": "es2016" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "target": "ES2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ // "jsx": "preserve", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */ @@ -25,9 +25,9 @@ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ /* Modules */ - "module": "commonjs" /* Specify what module code is generated. */, + "module": "ESNext" /* Specify what module code is generated. */, // "rootDir": "./", /* Specify the root folder within your source files. */ - // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */ + "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */, // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ diff --git a/yarn.lock b/yarn.lock index 6005b1a..fd2aa9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5189,10 +5189,15 @@ type-fest@^4.9.0: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.18.3.tgz#5249f96e7c2c3f0f1561625f54050e343f1c8f68" integrity sha512-Q08/0IrpvM+NMY9PA2rti9Jb+JejTddwmwmVQGskAlhtcrw1wsRzoR6ode6mR+OAabNa75w/dxedSUY2mlphaQ== -typescript@^5.4.5: - version "5.5.4" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.5.4.tgz#d9852d6c82bad2d2eda4fd74a5762a8f5909e9ba" - integrity sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q== +typebox-validators@0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/typebox-validators/-/typebox-validators-0.3.5.tgz#b913bad0a87571ffe0edd01d2b6090a268e1ecc9" + integrity sha512-FXrmSUAN6bSGxDANResNCZQ8VRRLr5bSyy73/HyqSXGdiVuogppGAoRocy7NTVZY4Wc2sWUofmWwwIXE6OxS6Q== + +typescript@5.6.2: + version "5.6.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0" + integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw== undici-types@~5.26.4: version "5.26.5"