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 all 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
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,35 @@ 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: |
last=$(gh run list -w "$WORKFLOW" --json startedAt,status -q 'map(select(.status == "completed"))[0].startedAt')
echo "last=$last" >> "$GITHUB_OUTPUT"
id: date
env:
GH_TOKEN: ${{ github.token }}
WORKFLOW: ${{ github.workflow }}
GH_REPO: "${{ github.repository_owner }}/${{ github.event.repository.name }}"
- uses: paritytech/rfc-action@main
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PROVIDER_URL: "wss://polkadot-collectives-rpc.polkadot.io" # Optional.
START_DATE: ${{ steps.date.outputs.last }}
```
239 changes: 206 additions & 33 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -67473,9 +67473,153 @@ function socketOnError() {
"use strict";

Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.POLKADOT_APPS_URL = exports.PROVIDER_URL = void 0;
exports.START_DATE = exports.POLKADOT_APPS_URL = exports.PROVIDER_URL = void 0;
exports.PROVIDER_URL = process.env.PROVIDER_URL || "wss://polkadot-collectives-rpc.polkadot.io";
exports.POLKADOT_APPS_URL = `https://polkadot.js.org/apps/?rpc=${encodeURIComponent(exports.PROVIDER_URL)}#/`;
exports.START_DATE = process.env.START_DATE || "0";


/***/ }),

/***/ 59866:
/***/ ((__unused_webpack_module, exports, __nccwpck_require__) => {

"use strict";

Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.cron = exports.getAllRFCRemarks = exports.getAllPRs = void 0;
const core_1 = __nccwpck_require__(42186);
const summary_1 = __nccwpck_require__(81327);
const api_1 = __nccwpck_require__(47196);
const constants_1 = __nccwpck_require__(69042);
const parse_RFC_1 = __nccwpck_require__(58542);
const logger = {
info: core_1.info,
debug: core_1.debug,
warn: core_1.warning,
error: core_1.error,
};
/** 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);
logger.info(`Found ${prs.length} open PRs`);
const prRemarks = [];
for (const pr of prs) {
const { owner, name } = pr.base.repo;
logger.info(`Extracting from PR: #${pr.number} in ${owner.login}/${name}`);
const rfcResult = await (0, parse_RFC_1.extractRfcResult)(octokit, { ...repo, number: pr.number });
if (rfcResult.success) {
logger.info(`RFC Result for #${pr.number} is ${rfcResult.result.approveRemarkText}`);
prRemarks.push([pr.number, rfcResult.result?.approveRemarkText]);
}
else {
logger.warn(`Had an error while creating RFC for #${pr.number}: ${rfcResult.error}`);
}
}
return prRemarks;
};
exports.getAllPRs = getAllPRs;
const getAllRFCRemarks = async (startDate) => {
const wsProvider = new api_1.WsProvider(constants_1.PROVIDER_URL);
try {
const api = await api_1.ApiPromise.create({ provider: wsProvider });
// We fetch all the available referendas
const query = (await api.query.fellowshipReferenda.referendumCount()).toPrimitive();
if (typeof query !== "number") {
throw new Error(`Query result is not a number: ${typeof query}`);
}
logger.info(`Available referendas: ${query}`);
const hashes = [];
for (const index of Array.from(Array(query).keys())) {
logger.info(`Fetching elements ${index + 1}/${query}`);
const refQuery = (await api.query.fellowshipReferenda.referendumInfoFor(index)).toJSON();
if (refQuery.ongoing) {
logger.info(`Found ongoing request: ${JSON.stringify(refQuery)}`);
const blockNr = refQuery.ongoing.submitted;
const blockDate = await getBlockDate(blockNr, api);
logger.debug(`Checking if the startDate (${startDate.toString()}) is newer than the block date (${blockDate.toString()})`);
// Skip referendas that have been interacted with last time
if (startDate > blockDate) {
logger.info(`Referenda #${index} is older than previous check. Ignoring.`);
continue;
}
const { proposal } = refQuery.ongoing;
const hash = proposal?.lookup?.hash ?? proposal?.inline;
if (hash) {
hashes.push({ hash, url: `https://collectives.polkassembly.io/referenda/${index}` });
}
else {
logger.warn(`Found no lookup hash nor inline hash for https://collectives.polkassembly.io/referenda/${index}`);
continue;
}
}
else {
logger.debug(`Reference query is not ongoing: ${JSON.stringify(refQuery)}`);
}
}
logger.info(`Found ${hashes.length} ongoing requests`);
return hashes;
}
catch (err) {
logger.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) {
logger.warn("No ongoing RFCs made from pull requests. Shuting down");
await summary_1.summary.addHeading("Referenda search", 3).addHeading("Found no matching referenda to open PRs", 5).write();
return;
}
logger.debug(`Found remarks ${JSON.stringify(remarks)}`);
const prRemarks = await (0, exports.getAllPRs)(octokit, { owner, repo });
logger.debug(`Found all PR remarks ${JSON.stringify(prRemarks)}`);
const rows = [
[
{ data: "PR", header: true },
{ data: "Referenda", header: true },
],
];
const wsProvider = new api_1.WsProvider(constants_1.PROVIDER_URL);
try {
const api = await api_1.ApiPromise.create({ provider: wsProvider });
for (const [pr, remark] of prRemarks) {
// We compare the hash to see if there is a match
const tx = api.tx.system.remark(remark);
const match = remarks.find(({ hash }) => hash === tx.method.hash.toHex() || hash === tx.method.toHex());
if (match) {
logger.info(`Found existing referenda for PR #${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 });
}
}
}
catch (e) {
logger.error(e);
throw new Error("There was a problem during the commenting");
}
finally {
await wsProvider.disconnect();
}
await summary_1.summary
.addHeading("Referenda search", 3)
.addHeading(`Found ${rows.length - 1} PRs matching ongoing referendas`, 5)
.addTable(rows)
.write();
logger.info("Finished run");
};
exports.cron = cron;


/***/ }),
Expand Down Expand Up @@ -67613,21 +67757,28 @@ exports.run = void 0;
const core = __importStar(__nccwpck_require__(42186));
const githubActions = __importStar(__nccwpck_require__(95438));
const js_1 = __nccwpck_require__(49246);
const constants_1 = __nccwpck_require__(69042);
const cron_1 = __nccwpck_require__(59866);
const handle_command_1 = __nccwpck_require__(51534);
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;
return await (0, cron_1.cron)(new Date(constants_1.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.");
}
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 +67802,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 +67829,65 @@ 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 { success: false, 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 {
success: false,
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 {
success: false,
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,
success: true,
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 = await (0, exports.extractRfcResult)(octokitInstance, {
repo: event.repository.name,
owner: event.repository.owner.login,
number: event.issue.number,
});
if (!result.success) {
return (0, util_1.userProcessError)(requestState, result.error);
}
return result.result;
};
exports.parseRFC = parseRFC;
const getApproveRemarkText = (rfcNumber, rawProposalText) => `RFC_APPROVE(${rfcNumber},${(0, util_1.hashProposal)(rawProposalText)})`;
exports.getApproveRemarkText = getApproveRemarkText;
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";
8 changes: 8 additions & 0 deletions src/cron.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { getAllRFCRemarks } from "./cron";

describe("RFC Listing test", () => {
test("Should not return any remark with future date", async () => {
const remarks = await getAllRFCRemarks(new Date());
expect(remarks).toHaveLength(0);
}, 60_000);
});
Loading