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

added cron job to comment ongoing referenda on PRs #28

Merged
merged 36 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
6811f85
created testing environment
Bullrich Dec 29, 2023
2b38241
imrpved logic to list all rfc and filter valid ones
Bullrich Dec 29, 2023
b258ec1
fixed index
Bullrich Jan 3, 2024
7718400
added ability to fetch the remark
Bullrich Jan 4, 2024
9bb5256
abstracted the logic for extract RFC from PR
Bullrich Jan 12, 2024
9a035a3
isolated method to fetch all RFCs
Bullrich Jan 12, 2024
fe78464
improved cron class to be used from outside
Bullrich Jan 16, 2024
480d45d
called cron class from index
Bullrich Jan 16, 2024
15eb4ed
added ability to comment
Bullrich Jan 16, 2024
eb47d0c
added a basic readme for the cron job
Bullrich Jan 16, 2024
f83c850
added a working example to readme
Bullrich Jan 17, 2024
a7451c7
yarn fix
Bullrich Jan 17, 2024
ce56aed
rebuilt dist directory
Bullrich Jan 17, 2024
299b48c
moved cron logic to a higher position
Bullrich Jan 17, 2024
e60617b
returned void method
Bullrich Jan 17, 2024
2e1615f
added summary to action
Bullrich Jan 17, 2024
8cece16
rebuilt library
Bullrich Jan 17, 2024
7c1030c
updated readme with working example
Bullrich Jan 17, 2024
2dcdd81
implemented constants for env variables
Bullrich Jan 17, 2024
0726b80
converted logs to core logger
Bullrich Jan 17, 2024
c1085ac
converted types to one of two
Bullrich Jan 17, 2024
572cec2
updated tests to evaluate that they run
Bullrich Jan 17, 2024
5e4d169
renamed rfc-cron to cron
Bullrich Jan 17, 2024
c456af9
rebuilt the dist directory
Bullrich Jan 17, 2024
ee37a69
fixed import order
Bullrich Jan 17, 2024
fb82be6
modified code to compare with RPC node
Bullrich Jan 19, 2024
146dbed
rebuilt the dist directory
Bullrich Jan 19, 2024
c02d90f
removed test files
Bullrich Jan 19, 2024
c5f3f51
fixed typo
Bullrich Jan 19, 2024
0a4488b
fixed ws never disconnecting
Bullrich Jan 19, 2024
12c0cb0
fixed more typos
Bullrich Jan 19, 2024
159b04c
changed gh cli command to get successful workflows
Bullrich Jan 19, 2024
9a43f35
replaced `with` for `env` in readme
Bullrich Jan 19, 2024
1ec75db
added debug log
Bullrich Jan 19, 2024
ba1c685
enhanced logging
Bullrich Jan 19, 2024
831ceb5
improved action code in readme
Bullrich Jan 19, 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
30 changes: 30 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,33 @@ The built-in `secrets.GITHUB_TOKEN` can be used, as long as it has the necessary
The `PROVIDER_URL` variable can be specified to override the default public endpoint to the Collectives parachain.

A full archive node is needed to process the confirmed referenda.

## Notification job

You can set the GitHub action to also run notifying on a PR when a referenda is available for voting.

It will look for new referendas available since the last time the action was run, so it won't generate duplicated messages.

```yml
on:
workflow_dispatch:
schedule:
- cron: '0 12 * * *'

jobs:
notify_referendas:
runs-on: ubuntu-latest
steps:
- name: Get last run
run: echo "last=$(gh run list -w "$WORKFLOW" --json startedAt -q '.[0].startedAt')" >> "$GITHUB_OUTPUT"
Bullrich marked this conversation as resolved.
Show resolved Hide resolved
id: date
env:
GH_TOKEN: ${{ github.token }}
WORKFLOW: ${{ github.workflow }}
GH_REPO: "${{ github.repository_owner }}/${{ github.event.repository.name }}"
- uses: paritytech/rfc-action@main
with:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PROVIDER_URL: "wss://polkadot-collectives-rpc.polkadot.io" # Optional.
START_DATE: ${{ steps.date.outputs.last }}
```
232 changes: 200 additions & 32 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -67614,20 +67614,27 @@ const core = __importStar(__nccwpck_require__(42186));
const githubActions = __importStar(__nccwpck_require__(95438));
const js_1 = __nccwpck_require__(49246);
const handle_command_1 = __nccwpck_require__(51534);
const rfc_cron_1 = __nccwpck_require__(74569);
async function run() {
const { context } = githubActions;
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const [_, command, ...args] = githubActions.context.payload.comment?.body.split(" ");
const respondParams = {
owner: githubActions.context.repo.owner,
repo: githubActions.context.repo.repo,
issue_number: githubActions.context.issue.number,
};
const octokitInstance = githubActions.getOctokit((0, js_1.envVar)("GH_TOKEN"));
if (githubActions.context.eventName !== "issue_comment") {
if (context.eventName === "schedule" || context.eventName === "workflow_dispatch") {
const { owner, repo } = context.repo;
const startDate = core.getInput("start-date") ?? "0";
return await (0, rfc_cron_1.cron)(new Date(startDate), owner, repo, octokitInstance);
}
else if (context.eventName !== "issue_comment") {
throw new Error("The action is expected to be run on 'issue_comment' events only.");
}
const event = githubActions.context.payload;
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const [_, command, ...args] = context.payload.comment?.body.split(" ");
const respondParams = {
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
};
const event = context.payload;
const requester = event.comment.user.login;
const githubComment = async (body) => await octokitInstance.rest.issues.createComment({
...respondParams,
Expand All @@ -67651,7 +67658,7 @@ async function run() {
}
}
catch (e) {
const logs = `${githubActions.context.serverUrl}/${githubActions.context.repo.owner}/${githubActions.context.repo.repo}/actions/runs/${githubActions.context.runId}`;
const logs = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
await githubComment(`@${requester} Handling the RFC command failed :(\nYou can open an issue [here](https://github.com/paritytech/rfc-propose/issues/new).\nSee the logs [here](${logs}).`);
await githubEmojiReaction("confused");
throw e;
Expand All @@ -67678,43 +67685,66 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.getRejectRemarkText = exports.getApproveRemarkText = exports.parseRFC = void 0;
exports.getRejectRemarkText = exports.getApproveRemarkText = exports.parseRFC = exports.extractRfcResult = void 0;
const node_fetch_1 = __importDefault(__nccwpck_require__(80467));
const util_1 = __nccwpck_require__(92629);
/**
* Parses the RFC details contained in the PR.
* The details include the RFC number,
* a link to the RFC text on GitHub,
* and the remark text, e.g. RFC_APPROVE(1234,hash)
*/
const parseRFC = async (requestState) => {
const { octokitInstance, event } = requestState;
const addedMarkdownFiles = (await octokitInstance.rest.pulls.listFiles({
repo: event.repository.name,
owner: event.repository.owner.login,
pull_number: event.issue.number,
const extractRfcResult = async (octokit, pr) => {
const { owner, repo, number } = pr;
const addedMarkdownFiles = (await octokit.rest.pulls.listFiles({
owner,
repo,
pull_number: number,
})).data.filter((file) => file.status === "added" && file.filename.startsWith("text/") && file.filename.includes(".md"));
if (addedMarkdownFiles.length < 1) {
return (0, util_1.userProcessError)(requestState, "RFC markdown file was not found in the PR.");
return { error: "RFC markdown file was not found in the PR." };
}
if (addedMarkdownFiles.length > 1) {
return (0, util_1.userProcessError)(requestState, `The system can only parse **one** markdown file but more than one were found: ${addedMarkdownFiles
.map((file) => file.filename)
.join(",")}. Please, reduce the number of files to **one file** for the system to work.`);
return {
error: `The system can only parse **one** markdown file but more than one were found: ${addedMarkdownFiles
.map((file) => file.filename)
.join(",")}. Please, reduce the number of files to **one file** for the system to work.`,
};
}
const [rfcFile] = addedMarkdownFiles;
const rawText = await (await (0, node_fetch_1.default)(rfcFile.raw_url)).text();
const rfcNumber = rfcFile.filename.split("text/")[1].split("-")[0];
if (rfcNumber === undefined) {
return (0, util_1.userProcessError)(requestState, "Failed to read the RFC number from the filename. Please follow the format: `NNNN-name.md`. Example: `0001-example-proposal.md`");
return {
error: "Failed to read the RFC number from the filename. Please follow the format: `NNNN-name.md`. Example: `0001-example-proposal.md`",
};
}
return {
approveRemarkText: (0, exports.getApproveRemarkText)(rfcNumber, rawText),
rejectRemarkText: (0, exports.getRejectRemarkText)(rfcNumber, rawText),
rfcFileRawUrl: rfcFile.raw_url,
rfcNumber,
result: {
approveRemarkText: (0, exports.getApproveRemarkText)(rfcNumber, rawText),
rejectRemarkText: (0, exports.getRejectRemarkText)(rfcNumber, rawText),
rfcFileRawUrl: rfcFile.raw_url,
rfcNumber,
},
};
};
exports.extractRfcResult = extractRfcResult;
/**
* Parses the RFC details contained in the PR.
* The details include the RFC number,
* a link to the RFC text on GitHub,
* and the remark text, e.g. RFC_APPROVE(1234,hash)
*/
const parseRFC = async (requestState) => {
const { octokitInstance, event } = requestState;
const { result, error } = await (0, exports.extractRfcResult)(octokitInstance, {
repo: event.repository.name,
owner: event.repository.owner.login,
number: event.issue.number,
});
if (error) {
return (0, util_1.userProcessError)(requestState, error);
}
else if (result) {
return result;
}
// TODO: Fix this logic and use an union type
throw new Error("Should not arrive here");
};
exports.parseRFC = parseRFC;
const getApproveRemarkText = (rfcNumber, rawProposalText) => `RFC_APPROVE(${rfcNumber},${(0, util_1.hashProposal)(rawProposalText)})`;
exports.getApproveRemarkText = getApproveRemarkText;
Expand Down Expand Up @@ -67912,6 +67942,144 @@ const createReferendumTx = async (opts) => {
exports.createReferendumTx = createReferendumTx;


/***/ }),

/***/ 74569:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {

"use strict";

var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.cron = exports.getAllRFCRemarks = exports.getAllPRs = void 0;
const summary_1 = __nccwpck_require__(81327);
const api_1 = __nccwpck_require__(47196);
const node_fetch_1 = __importDefault(__nccwpck_require__(80467));
const parse_RFC_1 = __nccwpck_require__(58542);
const getReferendaData = async (track) => {
const url = `https://collectives.subsquare.io/api/fellowship/referenda/${track}.json`;
const call = await (0, node_fetch_1.default)(url);
const data = (await call.json());
console.log("Parsed data is", data);
return data;
};
const hexToString = (hex) => {
let str = "";
for (let i = 0; i < hex.length; i += 2) {
const hexValue = hex.substr(i, 2);
const decimalValue = parseInt(hexValue, 16);
str += String.fromCharCode(decimalValue);
}
return str;
};
/** Gets the date of a block */
const getBlockDate = async (blockNr, api) => {
const hash = await api.rpc.chain.getBlockHash(blockNr);
const timestamp = await api.query.timestamp.now.at(hash);
return new Date(timestamp.toPrimitive());
};
const getAllPRs = async (octokit, repo) => {
const prs = await octokit.paginate(octokit.rest.pulls.list, repo);
console.log("PRs", prs.length);
const prRemarks = [];
for (const pr of prs) {
const { owner, name } = pr.base.repo;
console.log("Extracting from PR: #%s in %s/%s", pr.number, owner.login, name);
const rfcResult = await (0, parse_RFC_1.extractRfcResult)(octokit, { ...repo, number: pr.number });
if (rfcResult.result) {
console.log("RFC Result for #%s is", pr.number, rfcResult.result.approveRemarkText);
prRemarks.push([pr.number, rfcResult.result?.approveRemarkText]);
}
else {
console.log("Had an error while creating RFC for #%s", pr.number, rfcResult.error);
}
}
return prRemarks;
};
exports.getAllPRs = getAllPRs;
const getAllRFCRemarks = async (startDate) => {
const wsProvider = new api_1.WsProvider("wss://polkadot-collectives-rpc.polkadot.io");
try {
const api = await api_1.ApiPromise.create({ provider: wsProvider });
// We fetch all the members
const query = (await api.query.fellowshipReferenda.referendumCount()).toPrimitive();
console.log("referendumCount", query);
if (typeof query !== "number") {
throw new Error(`Query result is not a number: ${typeof query}`);
}
const ongoing = [];
const remarks = [];
for (const index of Array.from(Array(query).keys())) {
console.log("Fetching element %s/%s", index + 1, query);
const refQuery = (await api.query.fellowshipReferenda.referendumInfoFor(index)).toJSON();
console.log("Reference query", refQuery);
if (refQuery.ongoing) {
const blockNr = refQuery.ongoing.submitted;
const blockDate = await getBlockDate(blockNr, api);
console.warn("date", blockDate);
if (startDate > blockDate) {
console.log("Referenda is older than previous check. Ignoring.");
}
ongoing.push(refQuery.ongoing);
const referendaData = await getReferendaData(refQuery.ongoing.track);
if (referendaData.onchainData?.inlineCall?.call?.args &&
referendaData.onchainData?.inlineCall?.call?.args[0].name == "remark") {
const [call] = referendaData.onchainData.inlineCall.call.args;
const remark = hexToString(call.value);
remarks.push({
remark,
url: `https://collectives.polkassembly.io/referenda/${referendaData.polkassemblyId}`,
});
}
}
}
console.log(`Found ${ongoing.length} ongoing requests`, ongoing);
return remarks;
}
catch (err) {
console.error("Error during exectuion");
throw err;
}
finally {
await wsProvider.disconnect();
}
};
exports.getAllRFCRemarks = getAllRFCRemarks;
const cron = async (startDate, owner, repo, octokit) => {
const remarks = await (0, exports.getAllRFCRemarks)(startDate);
if (remarks.length === 0) {
console.warn("No ongoing RFCs made from pull requesting. Shuting down");
return;
}
console.log("Found remarks", remarks);
const prRemarks = await (0, exports.getAllPRs)(octokit, { owner, repo });
console.log("Found all PR remarks", prRemarks);
const rows = [
[
{ data: "PR", header: true },
{ data: "Referenda", header: true },
],
];
for (const [pr, remark] of prRemarks) {
const match = remarks.find((r) => r.remark === remark);
if (match) {
console.log("Found existing referenda for PR #%s", pr);
const msg = `Voting for this referenda is **ongoing**.\n\nVote for it [here]${match.url}`;
rows.push([`${owner}/${repo}#${pr}`, `<a href="${match.url}">${match.url}</a>`]);
await octokit.rest.issues.createComment({ owner, repo, issue_number: pr, body: msg });
}
}
await summary_1.summary
.addHeading("Referenda search", 3)
.addHeading(`Found ${rows.length - 1} ongoing referendas`, 5)
.addTable(rows)
.write();
};
exports.cron = cron;


/***/ }),

/***/ 92629:
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export const PROVIDER_URL = process.env.PROVIDER_URL || "wss://polkadot-collectives-rpc.polkadot.io";
export const POLKADOT_APPS_URL = `https://polkadot.js.org/apps/?rpc=${encodeURIComponent(PROVIDER_URL)}#/`;
export const START_DATE = process.env.START_DATE || "0";
27 changes: 17 additions & 10 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,31 @@ import * as githubActions from "@actions/github";
import { envVar } from "@eng-automation/js";
import type { IssueCommentCreatedEvent } from "@octokit/webhooks-types";

import { START_DATE } from "./constants";
import { handleCommand } from "./handle-command";
import { cron } from "./rfc-cron";
import { GithubReactionType } from "./types";

export async function run(): Promise<void> {
const { context } = githubActions;
try {
const octokitInstance = githubActions.getOctokit(envVar("GH_TOKEN"));
if (context.eventName === "schedule" || context.eventName === "workflow_dispatch") {
const { owner, repo } = context.repo;
return await cron(new Date(START_DATE), owner, repo, octokitInstance);
} else if (context.eventName !== "issue_comment") {
throw new Error("The action is expected to be run on 'issue_comment' events only.");
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-call
const [_, command, ...args] = githubActions.context.payload.comment?.body.split(" ") as (string | undefined)[];
const [_, command, ...args] = context.payload.comment?.body.split(" ") as (string | undefined)[];
const respondParams = {
owner: githubActions.context.repo.owner,
repo: githubActions.context.repo.repo,
issue_number: githubActions.context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
};

const octokitInstance = githubActions.getOctokit(envVar("GH_TOKEN"));
if (githubActions.context.eventName !== "issue_comment") {
throw new Error("The action is expected to be run on 'issue_comment' events only.");
}
const event: IssueCommentCreatedEvent = githubActions.context.payload as IssueCommentCreatedEvent;
const event: IssueCommentCreatedEvent = context.payload as IssueCommentCreatedEvent;
const requester = event.comment.user.login;

const githubComment = async (body: string) =>
Expand All @@ -46,7 +53,7 @@ export async function run(): Promise<void> {
await githubEmojiReaction("confused");
}
} catch (e) {
const logs = `${githubActions.context.serverUrl}/${githubActions.context.repo.owner}/${githubActions.context.repo.repo}/actions/runs/${githubActions.context.runId}`;
const logs = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
await githubComment(
`@${requester} Handling the RFC command failed :(\nYou can open an issue [here](https://github.com/paritytech/rfc-propose/issues/new).\nSee the logs [here](${logs}).`,
);
Expand Down
Loading
Loading