Skip to content

Commit

Permalink
Merge pull request ubiquity-os-marketplace#209 from gentlementlegen/f…
Browse files Browse the repository at this point in the history
…ix/contributor-generation

fix: contributor generation
  • Loading branch information
gentlementlegen authored Dec 12, 2024
2 parents 5aaeaea + 60835ec commit 8f5d852
Show file tree
Hide file tree
Showing 21 changed files with 187 additions and 76 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
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: false
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": false,
"description": "If true, will allow contributors to generate permits.",
"type": "boolean"
},
"contentEvaluator": {
"default": null,
"anyOf": [
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;
}
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: false,
description: "If true, 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
4 changes: 2 additions & 2 deletions tests/__mocks__/results/github-comment-results.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion tests/__mocks__/results/output.html

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion tests/__mocks__/results/reward-split.json
Original file line number Diff line number Diff line change
Expand Up @@ -273,4 +273,4 @@
"total": 32.743,
"userId": 9807008
}
}
}
3 changes: 3 additions & 0 deletions tests/action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,9 @@ describe("Action tests", () => {
login: "ubiquity-os",
},
},
sender: {
login: "0x4007",
},
},
config: cfg,
logger: new Logs("debug"),
Expand Down
3 changes: 2 additions & 1 deletion tests/fees.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
import { IssueActivity } from "../src/issue-activity";
import { ContextPlugin } from "../src/types/plugin-input";
import { Result } from "../src/types/results";
import cfg from "./__mocks__/results/valid-configuration.json";
Expand Down Expand Up @@ -66,7 +67,7 @@ describe("GithubCommentModule Fee Tests", () => {

jest.spyOn(githubCommentModule, "_generateHtml");

const bodyContent = await githubCommentModule.getBodyContent(result);
const bodyContent = await githubCommentModule.getBodyContent({} as unknown as IssueActivity, result);

expect(bodyContent).toEqual(
'<details><summary><b><h3>&nbsp;<a href="https://pay.ubq.fi" target="_blank" rel="noopener">[ 100 WXDAI ]</a>&nbsp;</h3><h6>@ubiquity-os</h6></b></summary><h6>⚠️ 20% fee rate has been applied. Consider using the&nbsp;<a href="https://dao.ubq.fi/dollar" target="_blank" rel="noopener">Ubiquity Dollar</a>&nbsp;for no fees.</h6><h6>Contributions Overview</h6><table><thead><tr><th>View</th><th>Contribution</th><th>Count</th><th>Reward</th></tr></thead><tbody><tr><td>Issue</td><td>Task</td><td>1.5</td><td>50</td></tr></tbody></table><h6>Conversation Incentives</h6><table><thead><tr><th>Comment</th><th>Formatting</th><th>Relevance</th><th>Priority</th><th>Reward</th></tr></thead><tbody></tbody></table></details>\n' +
Expand Down
2 changes: 1 addition & 1 deletion tests/helpers/web3.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,6 @@ describe("web3.ts", () => {
const tokenAddress = "0xe91D153E0b41518A2Ce8Dd3D7944Fa863463a97d"; // WXDAI
const tokenSymbol = await getErc20TokenSymbol(networkId, tokenAddress);
expect(tokenSymbol).toEqual("WXDAI");
}, 60000);
}, 120000);
});
});
114 changes: 75 additions & 39 deletions tests/pre-check.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,45 +22,6 @@ jest.unstable_mockModule("@actions/github", () => ({
},
}));

jest.unstable_mockModule("../src/data-collection/collect-linked-pulls", () => ({
collectLinkedMergedPulls: jest.fn(() => [
{
id: "PR_kwDOKzVPS85zXUoj",
title: "fix: add state to sorting manager for bottom and top",
number: 70,
url: "https://github.com/ubiquity/work.ubq.fi/pull/70",
state: "OPEN",
author: {
login: "0x4007",
id: 4975670,
},
repository: {
owner: {
login: "ubiquity",
},
name: "work.ubq.fi",
},
},
{
id: "PR_kwDOKzVPS85zXUok",
title: "fix: add state to sorting manager for bottom and top 2",
number: 71,
url: "https://github.com/ubiquity/work.ubq.fi/pull/71",
state: "MERGED",
author: {
login: "0x4007",
id: 4975670,
},
repository: {
owner: {
login: "ubiquity",
},
name: "work.ubq.fi",
},
},
]),
}));

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Expand All @@ -74,9 +35,49 @@ describe("Pre-check tests", () => {
db[tableName].create(row);
}
}
jest.resetModules();
jest.resetAllMocks();
});

it("Should reopen the issue and not generate rewards if linked pull-requests are still open", async () => {
jest.unstable_mockModule("../src/data-collection/collect-linked-pulls", () => ({
collectLinkedMergedPulls: jest.fn(() => [
{
id: "PR_kwDOKzVPS85zXUoj",
title: "fix: add state to sorting manager for bottom and top",
number: 70,
url: "https://github.com/ubiquity/work.ubq.fi/pull/70",
state: "OPEN",
author: {
login: "0x4007",
id: 4975670,
},
repository: {
owner: {
login: "ubiquity",
},
name: "work.ubq.fi",
},
},
{
id: "PR_kwDOKzVPS85zXUok",
title: "fix: add state to sorting manager for bottom and top 2",
number: 71,
url: "https://github.com/ubiquity/work.ubq.fi/pull/71",
state: "MERGED",
author: {
login: "0x4007",
id: 4975670,
},
repository: {
owner: {
login: "ubiquity",
},
name: "work.ubq.fi",
},
},
]),
}));
const patchMock = jest.fn(() => HttpResponse.json({}));
server.use(http.patch("https://api.github.com/repos/ubiquity/work.ubq.fi/issues/69", patchMock, { once: true }));
const { run } = await import("../src/run");
Expand All @@ -103,4 +104,39 @@ describe("Pre-check tests", () => {
expect(result).toEqual("All linked pull requests must be closed to generate rewards.");
expect(patchMock).toHaveBeenCalled();
});

it("Should not generate a permit if non-collaborator user is merging / closing the issue", async () => {
jest.unstable_mockModule("../src/data-collection/collect-linked-pulls", () => ({
collectLinkedMergedPulls: jest.fn(() => []),
}));
const patchMock = jest.fn(() => HttpResponse.json({}));
server.use(http.patch("https://api.github.com/repos/ubiquity/work.ubq.fi/issues/69", patchMock, { once: true }));
const { run } = await import("../src/run");

const result = await run({
eventName: "issues.closed",
payload: {
issue: {
html_url: issueUrl,
number: 1,
state_reason: "completed",
},
repository: {
name: "conversation-rewards",
owner: {
login: "ubiquity-os",
id: 76412717,
},
},
sender: {
login: "non-collaborator",
},
},
config: cfg,
logger: new Logs("debug"),
octokit: new Octokit({ auth: process.env.GITHUB_TOKEN }),
} as unknown as ContextPlugin);

expect(result).toEqual("You are not allowed to generate permits.");
});
});
Loading

0 comments on commit 8f5d852

Please sign in to comment.