Skip to content

Commit

Permalink
Merge pull request #216 from ubiquity-os-marketplace/development
Browse files Browse the repository at this point in the history
Merge development into main
  • Loading branch information
gentlementlegen authored Dec 20, 2024
2 parents efcb4be + 3f2fea1 commit 6942c37
Show file tree
Hide file tree
Showing 30 changed files with 585 additions and 467 deletions.
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
dist/** linguist-generated
bun.lockb linguist-generated
1 change: 1 addition & 0 deletions .github/workflows/compute.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ jobs:
PERMIT_TREASURY_GITHUB_USERNAME: ${{ secrets.PERMIT_TREASURY_GITHUB_USERNAME }}
PERMIT_ERC20_TOKENS_NO_FEE_WHITELIST: ${{ secrets.PERMIT_ERC20_TOKENS_NO_FEE_WHITELIST }}
KERNEL_PUBLIC_KEY: ${{ secrets.KERNEL_PUBLIC_KEY }}
LOG_LEVEL: ${{ secrets.LOG_LEVEL }}

steps:
# Note: the checkout could potentially be avoided by calling the workflow on the repo/branch directly.
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,8 @@ with:
delayMs: 10000
incentives:
requirePriceLabel: true
limitRewards: true
collaboratorOnlyPaymentInvocation: true
contentEvaluator:
openAi:
model: "gpt-4o"
Expand Down
4 changes: 2 additions & 2 deletions dist/index.js

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
"description": "Should the rewards of non-assignees be limited to the task reward?",
"type": "boolean"
},
"collaboratorOnlyPaymentInvocation": {
"default": true,
"description": "If false, will allow contributors to generate permits.",
"type": "boolean"
},
"contentEvaluator": {
"default": null,
"anyOf": [
Expand Down
18 changes: 18 additions & 0 deletions src/helpers/checkers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,27 @@ export function nonAssigneeApprovedReviews(data: Readonly<IssueActivity>) {
return false;
}

/*
* Returns true if a given user has admin permission in the specific repo, otherwise checks for admin / billing manager
* within the parent organization.
*/
export async function isAdmin(username: string, context: ContextPlugin): Promise<boolean> {
const octokit = context.octokit;
try {
const permissionLevel = await octokit.rest.repos.getCollaboratorPermissionLevel({
username,
owner: context.payload.repository.owner.login,
repo: context.payload.repository.name,
});
context.logger.debug(`Retrieved collaborator permission level for ${username}.`, {
username,
owner: context.payload.repository.owner.login,
repo: context.payload.repository.name,
isAdmin: permissionLevel.data.user?.permissions?.admin,
});
if (permissionLevel.data.user?.permissions?.admin) {
return true;
}
const userPerms = await octokit.rest.orgs.getMembershipForUser({
org: context.payload.repository.owner.login,
username: username,
Expand Down
15 changes: 15 additions & 0 deletions src/helpers/label-price-extractor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,18 @@ export function getSortedPrices(labels: GitHubIssue["labels"] | undefined) {
}
return sortedPriceLabels;
}

/*
* Returns the associated task reward of the issue, based on the final task reward taking into account any multipliers
* applied. If no task reward is found, falls back to the task price. If no task price is found, returns 0.
*/
export function getTaskReward(issue: GitHubIssue | null) {
if (issue) {
const sortedPriceLabels = getSortedPrices(issue.labels);
if (sortedPriceLabels.length) {
return sortedPriceLabels[0];
}
}

return 0;
}
22 changes: 4 additions & 18 deletions src/parser/content-evaluator-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,19 +84,6 @@ export class ContentEvaluatorModule extends BaseModule {
return result;
}

_getRewardForComment(comment: GithubCommentScore, relevance: number) {
let reward = new Decimal(comment?.score?.reward ?? 0);

if (comment?.score?.formatting && comment.score.multiplier && comment.score.words) {
let totalRegexReward = new Decimal(0);
totalRegexReward = totalRegexReward.add(comment.score.words.result);
totalRegexReward = totalRegexReward.mul(comment.score.multiplier);
const totalRegexRewardWithRelevance = totalRegexReward.mul(relevance);
reward = reward.sub(totalRegexReward).add(totalRegexRewardWithRelevance);
}
return reward;
}

async _processComment(comments: Readonly<GithubCommentScore>[], specificationBody: string, allComments: AllComments) {
const commentsWithScore: GithubCommentScore[] = [...comments];
const { commentsToEvaluate, prCommentsToEvaluate } = this._splitCommentsByPrompt(commentsWithScore);
Expand Down Expand Up @@ -124,15 +111,14 @@ export class ContentEvaluatorModule extends BaseModule {
currentRelevance = relevancesByAi[currentComment.id];
}

const currentReward = this._getRewardForComment(currentComment, currentRelevance).mul(
currentComment.score?.priority ?? 1
);
const currentReward = new Decimal(currentComment.score?.reward ?? 0);
const priority = currentComment.score?.priority ?? 1;

currentComment.score = {
...(currentComment.score || { multiplier: 0 }),
relevance: new Decimal(currentRelevance).toNumber(),
priority: currentComment.score?.priority ?? 1,
reward: currentReward.toNumber(),
priority: priority,
reward: currentReward.mul(currentRelevance).mul(priority).toDecimalPlaces(3).toNumber(),
};
}

Expand Down
25 changes: 12 additions & 13 deletions src/parser/github-comment-module.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,21 @@
import { Value } from "@sinclair/typebox/value";
import { postComment } from "@ubiquity-os/plugin-sdk";
import Decimal from "decimal.js";
import * as fs from "fs";
import { JSDOM } from "jsdom";
import { stringify } from "yaml";
import { CommentAssociation, CommentKind } from "../configuration/comment-types";
import { GithubCommentConfiguration, githubCommentConfigurationType } from "../configuration/github-comment-config";
import { isAdmin, isCollaborative } from "../helpers/checkers";
import { GITHUB_COMMENT_PAYLOAD_LIMIT } from "../helpers/constants";
import { getGithubWorkflowRunUrl } from "../helpers/github";
import { getTaskReward } from "../helpers/label-price-extractor";
import { createStructuredMetadata } from "../helpers/metadata";
import { removeKeyFromObject, typeReplacer } from "../helpers/result-replacer";
import { getErc20TokenSymbol } from "../helpers/web3";
import { IssueActivity } from "../issue-activity";
import { BaseModule } from "../types/module";
import { GithubCommentScore, Result } from "../types/results";
import { postComment } from "@ubiquity-os/plugin-sdk";
import { isAdmin, isCollaborative } from "../helpers/checkers";

interface SortedTasks {
issues: { specification: GithubCommentScore | null; comments: GithubCommentScore[] };
Expand Down Expand Up @@ -43,15 +44,10 @@ export class GithubCommentModule extends BaseModule {
return div.innerHTML;
}

async getBodyContent(result: Result, stripContent = false): Promise<string> {
async getBodyContent(data: Readonly<IssueActivity>, result: Result, stripContent = false): Promise<string> {
const keysToRemove: string[] = [];
const bodyArray: (string | undefined)[] = [];
const taskReward = Object.values(result).reduce((acc, curr) => {
if (curr.task) {
return curr.task.reward * curr.task.multiplier;
}
return acc;
}, 0);
const taskReward = getTaskReward(data.self);

if (stripContent) {
this.context.logger.info("Stripping content due to excessive length.");
Expand Down Expand Up @@ -105,7 +101,7 @@ export class GithubCommentModule extends BaseModule {
if (newBody.length <= GITHUB_COMMENT_PAYLOAD_LIMIT) {
return newBody;
} else {
return this.getBodyContent(result, true);
return this.getBodyContent(data, result, true);
}
}
return body;
Expand All @@ -114,7 +110,7 @@ export class GithubCommentModule extends BaseModule {
async transform(data: Readonly<IssueActivity>, result: Result): Promise<Result> {
const isIssueCollaborative = isCollaborative(data);
const isUserAdmin = data.self?.user ? await isAdmin(data.self.user.login, this.context) : false;
const body = await this.getBodyContent(result);
const body = await this.getBodyContent(data, result);
if (this._configuration?.debug) {
fs.writeFileSync(this._debugFilePath, body);
}
Expand Down Expand Up @@ -292,8 +288,11 @@ export class GithubCommentModule extends BaseModule {
this.context.config.erc20RewardToken
);

const rewardsSum = result.comments?.reduce<number>((acc, curr) => acc + (curr.score?.reward ?? 0), 0) ?? 0;
const isCapped = result.total < rewardsSum;
const rewardsSum =
result.comments?.reduce<Decimal>((acc, curr) => acc.add(curr.score?.reward ?? 0), new Decimal(0)) ??
new Decimal(0);
// The task reward can be 0 if either there is no pricing tag or if there is no assignee
const isCapped = taskReward > 0 && rewardsSum.gt(taskReward);

return `
<details>
Expand Down
18 changes: 7 additions & 11 deletions src/parser/processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import { FormattingEvaluatorModule } from "./formatting-evaluator-module";
import { GithubCommentModule } from "./github-comment-module";
import { PermitGenerationModule } from "./permit-generation-module";
import { UserExtractorModule } from "./user-extractor-module";
import { getTaskReward } from "../helpers/label-price-extractor";
import { GitHubIssue } from "../github-types";

export class Processor {
private _transformers: Module[] = [];
Expand All @@ -34,18 +36,12 @@ export class Processor {
return this;
}

_getRewardsLimit() {
let taskReward = Infinity;
_getRewardsLimit(issue: GitHubIssue | null) {
if (!this._configuration.limitRewards) {
return taskReward;
return Infinity;
}
for (const item of Object.keys(this._result)) {
if (this._result[item].task) {
taskReward = this._result[item].task.reward * this._result[item].task.multiplier;
return taskReward;
}
}
return taskReward;
const priceTagReward = getTaskReward(issue);
return priceTagReward || Infinity;
}

async run(data: Readonly<IssueActivity>) {
Expand All @@ -55,7 +51,7 @@ export class Processor {
}
// Aggregate total result
for (const item of Object.keys(this._result)) {
this._result[item].total = this._sumRewards(this._result[item], this._getRewardsLimit());
this._result[item].total = this._sumRewards(this._result[item], this._getRewardsLimit(data.self));
}
}
return this._result;
Expand Down
21 changes: 21 additions & 0 deletions src/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,21 @@ import { parseGitHubUrl } from "./start";
import { ContextPlugin } from "./types/plugin-input";
import { Result } from "./types/results";

async function isUserAllowedToGeneratePermits(context: ContextPlugin) {
const { octokit, payload } = context;
const username = payload.sender.login;
try {
await octokit.rest.orgs.getMembershipForUser({
org: payload.repository.owner.login,
username,
});
return true;
} catch (e) {
context.logger.debug(`${username} is not a member of ${context.payload.repository.owner.login}`, { e });
return false;
}
}

export async function run(context: ContextPlugin) {
const { eventName, payload, logger, config } = context;

Expand All @@ -25,6 +40,12 @@ export async function run(context: ContextPlugin) {
return result.logMessage.raw;
}

if (config.incentives.collaboratorOnlyPaymentInvocation && !(await isUserAllowedToGeneratePermits(context))) {
const result = logger.error("You are not allowed to generate permits.");
await postComment(context, result);
return result.logMessage.raw;
}

logger.debug("Will use the following configuration:", { config });

if (config.incentives.githubComment?.post) {
Expand Down
4 changes: 4 additions & 0 deletions src/types/plugin-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ export const pluginSettingsSchema = T.Object(
default: true,
description: "Should the rewards of non-assignees be limited to the task reward?",
}),
collaboratorOnlyPaymentInvocation: T.Boolean({
default: true,
description: "If false, will allow contributors to generate permits.",
}),
contentEvaluator: T.Union([contentEvaluatorConfigurationType, T.Null()], { default: null }),
userExtractor: T.Union([userExtractorConfigurationType, T.Null()], { default: null }),
dataPurge: T.Union([dataPurgeConfigurationType, T.Null()], { default: null }),
Expand Down
2 changes: 1 addition & 1 deletion src/web/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { PluginSettings, pluginSettingsSchema, SupportedEvents } from "../../typ
import { getPayload } from "./payload";
import { IssueActivityCache } from "../db/issue-activity-cache";

const baseApp = createPlugin<PluginSettings, EnvConfig, SupportedEvents>(
const baseApp = createPlugin<PluginSettings, EnvConfig, null, SupportedEvents>(
async (context) => {
const { payload, config } = context;
const issue = parseGitHubUrl(payload.issue.html_url);
Expand Down
2 changes: 2 additions & 0 deletions tests/__mocks__/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ export const handlers = [
role: "admin"
}
});
} else if (username === "non-collaborator") {
return HttpResponse.json({}, { status: 404 });
}
return HttpResponse.json({
data: {
Expand Down
Loading

0 comments on commit 6942c37

Please sign in to comment.