diff --git a/README.md b/README.md
index 2436c06..b3dca74 100644
--- a/README.md
+++ b/README.md
@@ -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 }}
+```
diff --git a/dist/index.js b/dist/index.js
index 04584e1..6c67150 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -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}`, `${match.url}`]);
+ 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;
/***/ }),
@@ -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,
@@ -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;
@@ -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;
diff --git a/src/constants.ts b/src/constants.ts
index bfcd002..5885199 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -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";
diff --git a/src/cron.test.ts b/src/cron.test.ts
new file mode 100644
index 0000000..9467bb2
--- /dev/null
+++ b/src/cron.test.ts
@@ -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);
+});
diff --git a/src/cron.ts b/src/cron.ts
new file mode 100644
index 0000000..cc7039c
--- /dev/null
+++ b/src/cron.ts
@@ -0,0 +1,171 @@
+import { debug, error, info, warning } from "@actions/core";
+import { summary, SummaryTableRow } from "@actions/core/lib/summary";
+import { ApiPromise, WsProvider } from "@polkadot/api";
+
+import { PROVIDER_URL } from "./constants";
+import { extractRfcResult } from "./parse-RFC";
+import { ActionLogger, OctokitInstance } from "./types";
+
+const logger: ActionLogger = {
+ info,
+ debug,
+ warn: warning,
+ error,
+};
+
+/** Gets the date of a block */
+const getBlockDate = async (blockNr: number, api: ApiPromise): Promise => {
+ const hash = await api.rpc.chain.getBlockHash(blockNr);
+ const timestamp = await api.query.timestamp.now.at(hash);
+ return new Date(timestamp.toPrimitive() as string);
+};
+
+export const getAllPRs = async (
+ octokit: OctokitInstance,
+ repo: { owner: string; repo: string },
+): Promise<[number, string][]> => {
+ const prs = await octokit.paginate(octokit.rest.pulls.list, repo);
+
+ logger.info(`Found ${prs.length} open PRs`);
+
+ const prRemarks: [number, string][] = [];
+
+ 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 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;
+};
+
+export const getAllRFCRemarks = async (startDate: Date): Promise<{ url: string; hash: string }[]> => {
+ const wsProvider = new WsProvider(PROVIDER_URL);
+ try {
+ const api = await 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: { url: string; hash: string }[] = [];
+ 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() as { ongoing?: OnGoing };
+
+ 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();
+ }
+};
+
+export const cron = async (startDate: Date, owner: string, repo: string, octokit: OctokitInstance): Promise => {
+ const remarks = await getAllRFCRemarks(startDate);
+ if (remarks.length === 0) {
+ logger.warn("No ongoing RFCs made from pull requests. Shuting down");
+ await 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 getAllPRs(octokit, { owner, repo });
+ logger.debug(`Found all PR remarks ${JSON.stringify(prRemarks)}`);
+
+ const rows: SummaryTableRow[] = [
+ [
+ { data: "PR", header: true },
+ { data: "Referenda", header: true },
+ ],
+ ];
+
+ const wsProvider = new WsProvider(PROVIDER_URL);
+ try {
+ const api = await 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}`, `${match.url}`]);
+ await octokit.rest.issues.createComment({ owner, repo, issue_number: pr, body: msg });
+ }
+ }
+ } catch (e) {
+ logger.error(e as Error);
+ throw new Error("There was a problem during the commenting");
+ } finally {
+ await wsProvider.disconnect();
+ }
+
+ await summary
+ .addHeading("Referenda search", 3)
+ .addHeading(`Found ${rows.length - 1} PRs matching ongoing referendas`, 5)
+ .addTable(rows)
+ .write();
+
+ logger.info("Finished run");
+};
+
+interface OnGoing {
+ track: number;
+ origin: { fellowshipOrigins: string };
+ proposal: { lookup?: { hash: string }; inline?: string };
+ enactment: { after: number };
+ submitted: number;
+ submissionDeposit: {
+ who: string;
+ amount: number;
+ };
+ decisionDeposit: {
+ who: string;
+ amount: number;
+ };
+ deciding: { since: number; confirming: null };
+ tally: Record;
+ inQueue: boolean;
+}
diff --git a/src/index.ts b/src/index.ts
index ac11c40..81750b5 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -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 { cron } from "./cron";
import { handleCommand } from "./handle-command";
import { GithubReactionType } from "./types";
export async function run(): Promise {
+ 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) =>
@@ -46,7 +53,7 @@ export async function run(): Promise {
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}).`,
);
diff --git a/src/parse-RFC.ts b/src/parse-RFC.ts
index d35bc48..baa0e6e 100644
--- a/src/parse-RFC.ts
+++ b/src/parse-RFC.ts
@@ -1,6 +1,6 @@
import fetch from "node-fetch";
-import { RequestResultFailed, RequestState } from "./types";
+import { OctokitInstance, RequestResultFailed, RequestState } from "./types";
import { hashProposal, userProcessError } from "./util";
export type ParseRFCResult = {
@@ -10,52 +10,75 @@ export type ParseRFCResult = {
rfcFileRawUrl: string;
};
-/**
- * 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)
- */
-export const parseRFC = async (requestState: RequestState): Promise => {
- const { octokitInstance, event } = requestState;
+export const extractRfcResult = async (
+ octokit: OctokitInstance,
+ pr: { owner: string; repo: string; number: number },
+): Promise<{ success: true; result: ParseRFCResult } | { success: false; error: string }> => {
+ const { owner, repo, number } = pr;
const addedMarkdownFiles = (
- await octokitInstance.rest.pulls.listFiles({
- repo: event.repository.name,
- owner: event.repository.owner.login,
- pull_number: event.issue.number,
+ 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 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 userProcessError(
- requestState,
- `The system can only parse **one** markdown file but more than one were found: ${addedMarkdownFiles
+ 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 fetch(rfcFile.raw_url)).text();
const rfcNumber: string | undefined = rfcFile.filename.split("text/")[1].split("-")[0];
if (rfcNumber === undefined) {
- return 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: getApproveRemarkText(rfcNumber, rawText),
- rejectRemarkText: getRejectRemarkText(rfcNumber, rawText),
- rfcFileRawUrl: rfcFile.raw_url,
- rfcNumber,
+ success: true,
+ result: {
+ approveRemarkText: getApproveRemarkText(rfcNumber, rawText),
+ rejectRemarkText: getRejectRemarkText(rfcNumber, rawText),
+ rfcFileRawUrl: rfcFile.raw_url,
+ rfcNumber,
+ },
};
};
+/**
+ * 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)
+ */
+export const parseRFC = async (requestState: RequestState): Promise => {
+ const { octokitInstance, event } = requestState;
+
+ const result = await extractRfcResult(octokitInstance, {
+ repo: event.repository.name,
+ owner: event.repository.owner.login,
+ number: event.issue.number,
+ });
+ if (!result.success) {
+ return userProcessError(requestState, result.error);
+ }
+ return result.result;
+};
+
export const getApproveRemarkText = (rfcNumber: string, rawProposalText: string): string =>
`RFC_APPROVE(${rfcNumber},${hashProposal(rawProposalText)})`;
export const getRejectRemarkText = (rfcNumber: string, rawProposalText: string): string =>
diff --git a/src/types.ts b/src/types.ts
index 0be3e80..92538f3 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -7,8 +7,17 @@ export type GithubReactionType = "+1" | "-1" | "laugh" | "confused" | "heart" |
export type RequestState = {
event: IssueCommentCreatedEvent;
requester: string;
- octokitInstance: ReturnType<(typeof githubActions)["getOctokit"]>;
+ octokitInstance: OctokitInstance;
};
+export type OctokitInstance = ReturnType<(typeof githubActions)["getOctokit"]>;
+
export type RequestResult = { success: true; message: string } | { success: false; errorMessage: string };
export type RequestResultFailed = RequestResult & { success: false };
+
+export interface ActionLogger {
+ debug(message: string): void;
+ info(message: string): void;
+ warn(message: string | Error): void;
+ error(message: string | Error): void;
+}