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 #21

Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
a5a70e2
chore: update validator.ts
gentlementlegen Sep 20, 2024
ed21a56
chore: update validator.ts
gentlementlegen Sep 20, 2024
1474a79
chore: update validator.ts
gentlementlegen Sep 20, 2024
27cdcae
chore: added logs
gentlementlegen Sep 20, 2024
a76f5dd
chore: added logs
gentlementlegen Sep 20, 2024
7e64c89
chore: added logs
gentlementlegen Sep 20, 2024
f293238
chore: added write all permission
gentlementlegen Sep 20, 2024
d0956fe
chore: added errors
gentlementlegen Sep 20, 2024
f90eca5
chore: added errors
gentlementlegen Sep 20, 2024
7303017
chore: added payload
gentlementlegen Sep 20, 2024
77d2f89
chore: added payload
gentlementlegen Sep 20, 2024
92a5667
chore: added payload
gentlementlegen Sep 21, 2024
ca9c2d9
chore: added payload
gentlementlegen Sep 21, 2024
041f7d0
chore: removed logs
gentlementlegen Sep 21, 2024
622f4fe
chore: added errors
gentlementlegen Sep 23, 2024
49175d7
chore: added errors
gentlementlegen Sep 23, 2024
8b0d115
chore: added errors
gentlementlegen Sep 23, 2024
c902519
chore: added errors
gentlementlegen Sep 23, 2024
7b728e7
chore: added errors
gentlementlegen Sep 23, 2024
4a1e7eb
chore: added errors
gentlementlegen Sep 23, 2024
0a4c3c2
chore: added errors
gentlementlegen Sep 23, 2024
9e57d63
chore: added errors
gentlementlegen Sep 23, 2024
481c1dc
chore: added errors
gentlementlegen Sep 23, 2024
50a2892
chore: added errors
gentlementlegen Sep 23, 2024
df5c64f
chore: added errors
gentlementlegen Sep 23, 2024
6be3f8c
chore: added configuration
gentlementlegen Sep 30, 2024
f722548
chore: added configuration
gentlementlegen Sep 30, 2024
910811d
feat: added configuration generation script
gentlementlegen Oct 2, 2024
242a671
chore: updated generated configuration
github-actions[bot] Oct 2, 2024
c6d3c97
chore: test all in actions
gentlementlegen Oct 2, 2024
59e6c56
chore: test all in actions
gentlementlegen Oct 2, 2024
09279d1
chore: test all in actions
gentlementlegen Oct 2, 2024
59ca697
chore: test all in actions
gentlementlegen Oct 2, 2024
6cb9492
chore: test all in actions
gentlementlegen Oct 2, 2024
dbf3d35
chore: test all in actions
gentlementlegen Oct 2, 2024
3326c7c
chore: test all in actions
gentlementlegen Oct 2, 2024
86f0268
chore: fixed knip
gentlementlegen Oct 2, 2024
62f5cad
chore: fixed tests
gentlementlegen Oct 2, 2024
56a8d33
chore: fixed tests
gentlementlegen Oct 2, 2024
75f2bb6
chore: fixed tests
gentlementlegen Oct 2, 2024
7417f3d
chore: removed logs
gentlementlegen Oct 2, 2024
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
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: |
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 'github-actions[bot]'
git config --global user.email 'github-actions[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 }}
25 changes: 0 additions & 25 deletions .github/workflows/validate-schema.yml

This file was deleted.

86 changes: 84 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,87 @@
{
"name": "Automated merging",
"description": "Automatically merge pull-requests.",
"ubiquity:listeners": ["push", "issue_comment.created"]
}
"ubiquity:listeners": [
"push",
"issue_comment.created"
],
"configuration": {
0x4007 marked this conversation as resolved.
Show resolved Hide resolved
"type": "object",
"properties": {
"approvalsRequired": {
"default": {},
"type": "object",
"properties": {
"collaborator": {
"default": 1,
"minimum": 1,
"type": "number"
},
"contributor": {
"default": 2,
"minimum": 1,
"type": "number"
}
},
"required": [
"collaborator",
"contributor"
]
},
"mergeTimeout": {
"default": {},
"type": "object",
"properties": {
"collaborator": {
"default": "3.5 days",
"type": "string"
},
"contributor": {
"default": "7 days",
"type": "string"
}
},
"required": [
"collaborator",
"contributor"
]
},
"repos": {
"default": {},
"type": "object",
"properties": {
"monitor": {
"default": [],
"type": "array",
"items": {
"minLength": 1,
"type": "string"
}
},
"ignore": {
"default": [],
"type": "array",
"items": {
"type": "string"
}
}
},
"required": [
"monitor",
"ignore"
]
},
"allowedReviewerRoles": {
"default": [
"COLLABORATOR",
"MEMBER",
"OWNER"
],
"type": "array",
"items": {
"type": "string"
}
}
}
}
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@
"npm-run-all": "4.1.5",
"prettier": "3.3.2",
"ts-jest": "29.1.5",
"typescript": "5.4.5",
"typescript-eslint": "7.13.1"
"typescript": "5.6.2",
"typescript-eslint": "8.8.0"
},
"lint-staged": {
"*.ts": [
Expand Down
10 changes: 5 additions & 5 deletions src/action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,27 +11,27 @@ export async function run() {
const payload = github.context.payload.inputs;

payload.env = { ...(payload.env || {}), workflowName: github.context.workflow };
const { envDecoded, settingsDecoded } = validateAndDecodeSchemas(payload.env, JSON.parse(payload.settings));
const { decodedSettings, decodedEnv } = validateAndDecodeSchemas(payload.env, JSON.parse(payload.settings));
const inputs: PluginInputs = {
stateId: payload.stateId,
eventName: payload.eventName,
eventPayload: JSON.parse(payload.eventPayload),
settings: settingsDecoded,
settings: decodedSettings,
authToken: payload.authToken,
ref: payload.ref,
};

await plugin(inputs, envDecoded);
await plugin(inputs, decodedEnv);

return returnDataToKernel(process.env.GITHUB_TOKEN, inputs.stateId, {});
}

async function returnDataToKernel(repoToken: string, stateId: string, output: object) {
export async function returnDataToKernel(repoToken: string, stateId: string, output: object, eventType = "return_data_to_ubiquibot_kernel") {
const octokit = new Octokit({ auth: repoToken });
return octokit.repos.createDispatchEvent({
owner: github.context.repo.owner,
repo: github.context.repo.repo,
event_type: "return_data_to_ubiquibot_kernel",
event_type: eventType,
client_payload: {
state_id: stateId,
output: JSON.stringify(output),
Expand Down
6 changes: 3 additions & 3 deletions src/handlers/summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface ResultInfo {

function generateGitHubSummary(context: Context, urls: ResultInfo[]): string {
const target = `https://github.com/${context.payload.repository.owner?.login}`;
const output: string[] = ["## Merge report\n\n"];
const output: (string | undefined)[] = ["## Merge report\n\n"];
0x4007 marked this conversation as resolved.
Show resolved Hide resolved
output.push("<samp>\n");
output.push("| Merged | ID |");
output.push("|---|---|");
Expand All @@ -29,9 +29,9 @@ function generateGitHubSummary(context: Context, urls: ResultInfo[]): string {
output.push("\n</samp>\n");
output.push("## Configuration\n\n");
output.push("### Watching Repositories\n\n");
output.push(context.config.repos.monitor.map((o) => `- [${o}](${target}/${o})`).join("\n"));
output.push(context.config.repos?.monitor.map((o) => `- [${o}](${target}/${o})`).join("\n"));
0x4007 marked this conversation as resolved.
Show resolved Hide resolved
output.push("### Ignored Repositories\n\n");
output.push(context.config.repos.ignore.map((o) => `- [${o}](${target}/${o})`).join("\n"));
output.push(context.config.repos?.ignore.map((o) => `- [${o}](${target}/${o})`).join("\n"));
return output.join("\n");
}

Expand Down
18 changes: 10 additions & 8 deletions src/helpers/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,22 @@ export interface Requirements {
* Gets the merge timeout depending on the status of the assignee. If there are multiple assignees with different
* statuses, the longest timeout is chosen.
*/
export async function getMergeTimeoutAndApprovalRequiredCount(context: Context, authorAssociation: string): Promise<Requirements> {
const { config: { mergeTimeout, approvalsRequired } } = context;
export async function getMergeTimeoutAndApprovalRequiredCount(context: Context, authorAssociation: string) {
const {
config: { mergeTimeout, approvalsRequired },
} = context;
const timeoutCollaborator = {
mergeTimeout: mergeTimeout.collaborator,
requiredApprovalCount: approvalsRequired.collaborator,
mergeTimeout: mergeTimeout?.collaborator,
requiredApprovalCount: approvalsRequired?.collaborator,
};
const timeoutContributor = {
mergeTimeout: mergeTimeout.contributor,
requiredApprovalCount: approvalsRequired.contributor,
mergeTimeout: mergeTimeout?.contributor,
requiredApprovalCount: approvalsRequired?.contributor,
};

/**
* Hardcoded roles here because we need to determine the timeouts
* separate from `allowedReviewerRoles` which introduces
* separate from `allowedReviewerRoles` which introduces
* potential unintended user errors and logic issues.
*/
return ["COLLABORATOR", "MEMBER", "OWNER"].includes(authorAssociation) ? timeoutCollaborator : timeoutContributor;
Expand All @@ -49,7 +51,7 @@ export async function getApprovalCount({ octokit, logger, config: { allowedRevie
repo,
pull_number: pullNumber,
});
return reviews.filter((review) => allowedReviewerRoles.includes(review.author_association)).filter((review) => review.state === "APPROVED").length;
return reviews.filter((review) => allowedReviewerRoles?.includes(review.author_association)).filter((review) => review.state === "APPROVED").length;
} catch (e) {
logger.error(`Error fetching reviews' approvals: ${e}`);
return 0;
Expand Down
16 changes: 11 additions & 5 deletions src/helpers/update-pull-requests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { RestEndpointMethodTypes } from "@octokit/rest";
import ms from "ms";
import { getAllTimelineEvents } from "../handlers/github-events";
import { generateSummary, ResultInfo } from "../handlers/summary";
import { Context } from "../types";
import { Context, ReposWatchSettings } from "../types";
import {
getApprovalCount,
getMergeTimeoutAndApprovalRequiredCount,
Expand Down Expand Up @@ -30,7 +30,7 @@ export async function updatePullRequests(context: Context) {
const { logger } = context;
const results: ResultInfo[] = [];

if (!context.config.repos.monitor.length) {
if (!context.config.repos?.monitor.length) {
const owner = context.payload.repository.owner;
if (owner) {
logger.info(`No organizations or repo have been specified, will default to the organization owner: ${owner.login}.`);
Expand All @@ -39,7 +39,7 @@ export async function updatePullRequests(context: Context) {
}
}

const pullRequests = await getOpenPullRequests(context, context.config.repos);
const pullRequests = await getOpenPullRequests(context, context.config.repos as ReposWatchSettings);

if (!pullRequests?.length) {
return logger.info("Nothing to do.");
Expand Down Expand Up @@ -74,8 +74,14 @@ export async function updatePullRequests(context: Context) {
);
if (isNaN(lastActivityDate.getTime())) {
logger.info(`PR ${html_url} does not seem to have any activity, nothing to do.`);
} else if (isPastOffset(lastActivityDate, requirements.mergeTimeout)) {
isMerged = await attemptMerging(context, { gitHubUrl, htmlUrl: html_url, requirements, lastActivityDate, pullRequestDetails });
} else if (requirements?.mergeTimeout && isPastOffset(lastActivityDate, requirements?.mergeTimeout)) {
isMerged = await attemptMerging(context, {
gitHubUrl,
htmlUrl: html_url,
requirements: requirements as Requirements,
lastActivityDate,
pullRequestDetails,
});
} else {
logger.info(`PR ${html_url} has activity up until (${lastActivityDate}), nothing to do.`);
}
Expand Down
41 changes: 25 additions & 16 deletions src/helpers/validator.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,39 @@
import { Value } from "@sinclair/typebox/value";
import { envSchema, envValidator, PluginSettings, pluginSettingsSchema, pluginSettingsValidator } from "../types";
import { TransformDecodeCheckError, TransformDecodeError, Value, ValueError } from "@sinclair/typebox/value";
import { Env, envSchema, envValidator, PluginSettings, pluginSettingsSchema, pluginSettingsValidator } from "../types";

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

console.log(rawEnv, rawSettings);
const env = Value.Default(envSchema, rawEnv) as Env;
if (!envValidator.test(env)) {
const errors: object[] = [];
for (const error of envValidator.errors(env)) {
const errorMessage = { path: error.path, message: error.message, value: error.value };
console.error(errorMessage);
errors.push(errorMessage);
console.error(error);
errors.push(error);
}
throw new Error(`Invalid environment provided. ${errors}`);
}
const envDecoded = Value.Decode(envSchema, env || {});

const settings = Value.Default(pluginSettingsSchema, rawSettings) as PluginSettings;
if (!pluginSettingsValidator.test(settings)) {
const errors: object[] = [];
for (const error of pluginSettingsValidator.errors(settings)) {
const errorMessage = { path: error.path, message: error.message, value: error.value };
console.error(errorMessage);
errors.push(errorMessage);
console.error(error);
gentlementlegen marked this conversation as resolved.
Show resolved Hide resolved
errors.push(error);
}
throw new Error(`Invalid settings provided. ${errors}`);
}

const settingsDecoded = Value.Decode(pluginSettingsSchema, settings);
if (errors.length) {
throw { errors };
}

return { envDecoded, settingsDecoded };
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;
}
}
Loading