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

Migrated fellows to use People chain and PAPI #131

Merged
merged 9 commits into from
Jul 29, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
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
3 changes: 3 additions & 0 deletions .papi/descriptors/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
*
!.gitignore
!package.json
24 changes: 24 additions & 0 deletions .papi/descriptors/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"version": "0.1.0-autogenerated.14257783844949306470",
"name": "@polkadot-api/descriptors",
"files": [
"dist"
],
"exports": {
".": {
"module": "./dist/index.mjs",
"import": "./dist/index.mjs",
"require": "./dist/index.js",
"default": "./dist/index.js"
},
"./package.json": "./package.json"
},
"main": "./dist/index.js",
"module": "./dist/index.mjs",
"browser": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"sideEffects": false,
"peerDependencies": {
"polkadot-api": "*"
}
}
Binary file added .papi/metadata/collectives.scale
Binary file not shown.
Binary file added .papi/metadata/people.scale
Binary file not shown.
14 changes: 14 additions & 0 deletions .papi/polkadot-api.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"version": 0,
"descriptorPath": ".papi/descriptors",
"entries": {
"collectives": {
"wsUrl": "wss://polkadot-collectives-rpc.polkadot.io",
"metadata": ".papi/metadata/collectives.scale"
},
"people": {
"wsUrl": "wss://polkadot-people-rpc.polkadot.io",
"metadata": ".papi/metadata/people.scale"
}
}
}
7 changes: 2 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,12 @@ WORKDIR /action

COPY .yarn/ ./.yarn/
COPY package.json yarn.lock .yarnrc.yml ./
COPY .papi ./.papi

RUN yarn install --immutable

COPY . .

RUN yarn run build

FROM node:22-slim

COPY --from=Builder /action/dist /action

ENTRYPOINT ["node", "/action/index.js"]
ENTRYPOINT ["node", "/action/dist/index.js"]
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ outputs:

runs:
using: 'docker'
image: 'docker://ghcr.io/paritytech/review-bot/action:2.5.0'
image: 'docker://ghcr.io/paritytech/review-bot/action:2.6.0'
14 changes: 10 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "review-bot",
"version": "2.5.0",
"version": "2.6.0",
"description": "Have custom review rules for PRs with auto assignment",
"main": "src/index.ts",
"scripts": {
Expand All @@ -9,7 +9,8 @@
"cli": "ncc build src/cli.ts -o dist-cli && node dist-cli",
"test": "jest",
"fix": "npx eslint --fix 'src/**/*.ts' && npx prettier --write 'src/**/*.{ts,yml}'",
"lint": "npx eslint 'src/**/*.ts' && npx prettier --check 'src/**/*.{ts,yml}'"
"lint": "npx eslint 'src/**/*.ts' && npx prettier --check 'src/**/*.{ts,yml}'",
"postinstall": "papi"
},
"engines": {
"node": ">=22.0.0"
Expand Down Expand Up @@ -38,9 +39,14 @@
"@actions/core": "^1.10.1",
"@actions/github": "^6.0.0",
"@eng-automation/js": "^2.2.0",
"@polkadot/api": "^11.3.1",
"@polkadot-api/descriptors": "file:.papi/descriptors",
"joi": "^17.13.1",
"polkadot-api": "^0.12.0",
"smoldot": "^2.0.29",
"yaml": "^2.3.4"
},
"packageManager": "yarn@4.3.0"
"packageManager": "yarn@4.3.0",
"resolutions": {
"@polkadot-api/descriptors": "portal:./.papi/descriptors"
}
}
175 changes: 109 additions & 66 deletions src/polkadot/fellows.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { ApiPromise, WsProvider } from "@polkadot/api";
import { collectives, people } from "@polkadot-api/descriptors";
import { createClient, SS58String } from "polkadot-api";
import { chainSpec as polkadotChainSpec } from "polkadot-api/chains/polkadot";
import { chainSpec as collectivesChainSpec } from "polkadot-api/chains/polkadot_collectives";
import { chainSpec as peopleChainSpec } from "polkadot-api/chains/polkadot_people";
import { getSmProvider } from "polkadot-api/sm-provider";
import { start } from "smoldot";

import { ActionLogger, TeamApi } from "../github/types";

Expand All @@ -10,83 +15,121 @@ export class PolkadotFellows implements TeamApi {

constructor(private readonly logger: ActionLogger) {}

private async fetchAllFellows(): Promise<Map<string, number>> {
let api: ApiPromise;
this.logger.debug("Connecting to collective parachain");
// we connect to the collective rpc node
const wsProvider = new WsProvider("wss://polkadot-collectives-rpc.polkadot.io");
api = await ApiPromise.create({ provider: wsProvider });
private async fetchAllFellows(logger: ActionLogger): Promise<Map<string, number>> {
logger.info("Initializing smoldot");
const smoldot = start();

try {
// We fetch all the members
const membersObj = await api.query.fellowshipCollective.members.entries();

// We iterate over the fellow data and convert them into usable values
const fellows: FellowData[] = [];
for (const [key, rank] of membersObj) {
// @ts-ignore
const [address]: [string] = key.toHuman();
fellows.push({ address, ...(rank.toHuman() as object) } as FellowData);
}
this.logger.debug(JSON.stringify(fellows));

// Once we obtained this information, we disconnect this api.
await api.disconnect();

this.logger.debug("Connecting to relay parachain.");
// We connect to the relay chain
api = await ApiPromise.create({ provider: new WsProvider("wss://rpc.polkadot.io") });

// We iterate over the different members and extract their data
const users: Map<string, number> = new Map<string, number>();
for (const fellow of fellows) {
this.logger.debug(`Fetching identity of '${fellow.address}', rank: ${fellow.rank}`);
const identityQuery = await api.query.identity.identityOf(fellow.address);
// If the identity is null, we check if there is a super identity.
if (identityQuery.isEmpty) {
this.logger.debug("Identity is null. Checking for super identity");
const superIdentity = (await api.query.identity.superOf(fellow.address)).toHuman() as
| [string, { Raw: string }]
| undefined;
if (superIdentity && superIdentity[0]) {
this.logger.debug(`${fellow.address} has a super identity: ${superIdentity[0]}. Adding it to the array`);
fellows.push({ address: superIdentity[0], rank: fellow.rank });
// Create smoldot chain with Polkadot Relay Chain
const smoldotRelayChain = await smoldot.addChain({
chainSpec: polkadotChainSpec,
});

// Add the people chain to smoldot
const peopleRelayChain = await smoldot.addChain({
chainSpec: peopleChainSpec,
potentialRelayChains: [smoldotRelayChain],
});

// Initialize the smoldot provider
const jsonRpcProvider = getSmProvider(peopleRelayChain);
logger.info("Initializing the people client");
const peopleClient = createClient(jsonRpcProvider);

// Get the types for the people client
const peopleApi = peopleClient.getTypedApi(people);

const getGhHandle = async (address: SS58String): Promise<string | undefined> => {
logger.debug(`Fetching identity of '${address}'`);
const identityOf = await peopleApi.query.Identity.IdentityOf.getValue(address);

if (identityOf) {
const [identity] = identityOf;
const github = identity.info.github.value;

if (!github) {
logger.debug(`'${address}' does not have an additional field named 'github'`);
return;
}

const handle = github.asText().replace("@", "") as string;

if (handle) {
logger.info(`Found github handle for '${address}': '${handle}'`);
} else {
this.logger.debug("No super identity found. Skipping");
logger.debug(`'${address}' does not have a GitHub handle`);
return;
}
continue;
return handle;
}

const [fellowData] = identityQuery.toHuman() as [Record<string, unknown>, unknown];
logger.debug(`Identity of '${address}' is null. Checking for super identity`);

// @ts-ignore
const additional = fellowData.info?.additional as [{ Raw: string }, { Raw: string }][] | undefined;
const superIdentityAddress = (await peopleApi.query.Identity.SuperOf.getValue(address))?.[0];

// If it does not have additional data (GitHub handle goes here) we ignore it
if (!additional || additional.length < 1) {
this.logger.debug("Additional data is null. Skipping");
continue;
if (superIdentityAddress) {
logger.debug(`'${address}' has a super identity: '${superIdentityAddress}'. Fetching that identity`);
return await getGhHandle(superIdentityAddress);
} else {
logger.debug(`No superidentity for ${address} found.`);
return undefined;
}

for (const additionalData of additional) {
const [key, value] = additionalData;
// We verify that they have an additional data of the key "github"
// If it has a handle defined, we push it into the array
if (key?.Raw && key?.Raw === "github" && value?.Raw && value?.Raw.length > 0) {
this.logger.debug(`Found handles: '${value.Raw}'`);
// We add it to the array and remove the @ if they add it to the handle
users.set(value.Raw.replace("@", ""), fellow.rank);
}
};

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we move this function outside please? It got too nested so I didn't get where the function ends at first 😅

logger.info("Initializing the collectives client");

const collectiveRelayChain = await smoldot.addChain({
chainSpec: collectivesChainSpec,
potentialRelayChains: [smoldotRelayChain],
});
const collectiveJsonRpcProvider = getSmProvider(collectiveRelayChain);
logger.info("Initializing the relay client");
const collectivesClient = createClient(collectiveJsonRpcProvider);
const collectivesApi = collectivesClient.getTypedApi(collectives);

// Pull the members of the FellowshipCollective
const memberEntries = await collectivesApi.query.FellowshipCollective.Members.getEntries();

// We no longer need the collective client, so let's destroy it
collectivesClient.destroy();

// Build the Array of FellowData and filter out candidates (zero rank members)
const fellows: FellowData[] = memberEntries
.map(({ keyArgs: [address], value: rank }) => {
return { address, rank };
})
.filter(({ rank }) => rank > 0);
logger.debug(JSON.stringify(fellows));

// Let's now pull the GH handles of the fellows
const users = await Promise.all(
fellows.map(async ({ address, rank }) => {
return {
address,
rank,
githubHandle: await getGhHandle(address),
};
}),
);
logger.info(`Found users: ${JSON.stringify(Array.from(users.entries()))}`);

const userMap: Map<string, number> = new Map<string, number>();

for (const { githubHandle, rank } of users) {
if (githubHandle) {
userMap.set(githubHandle, rank);
}
}

this.logger.info(`Found users: ${JSON.stringify(Array.from(users.entries()))}`);
// We are now done with the relay client
peopleClient.destroy();

return users;
return userMap;
} catch (error) {
this.logger.error(error as Error);
logger.error(error as Error);
throw error;
} finally {
await api.disconnect();
await smoldot.terminate();
}
}

Expand All @@ -96,7 +139,7 @@ export class PolkadotFellows implements TeamApi {

if (this.fellowsCache.size < 1) {
this.logger.debug("Cache not found. Fetching fellows.");
this.fellowsCache = await this.fetchAllFellows();
this.fellowsCache = await this.fetchAllFellows(this.logger);
}

return Array.from(this.fellowsCache.entries());
Expand All @@ -108,7 +151,7 @@ export class PolkadotFellows implements TeamApi {

if (this.fellowsCache.size < 1) {
this.logger.debug("Cache not found. Fetching fellows.");
this.fellowsCache = await this.fetchAllFellows();
this.fellowsCache = await this.fetchAllFellows(this.logger);
}
const users: string[] = [];
for (const [user, rank] of this.fellowsCache) {
Expand Down
8 changes: 5 additions & 3 deletions src/test/fellows.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { mock, mockClear, MockProxy } from "jest-mock-extended";
import { ActionLogger, TeamApi } from "../github/types";
import { PolkadotFellows } from "../polkadot/fellows";

const timeout = 25_000;
const timeout = 60_000;

describe("CAPI test", () => {
let fellows: TeamApi;
Expand All @@ -29,11 +29,13 @@ describe("CAPI test", () => {
async () => {
const members = await fellows.getTeamMembers("2");
expect(members.length).toBeGreaterThan(0);
expect(logger.debug).toHaveBeenCalledWith("Connecting to collective parachain");
expect(logger.info).toHaveBeenCalledWith("Initializing the people client");
expect(logger.info).toHaveBeenCalledWith("Initializing the collectives client");
mockClear(logger);
const members2 = await fellows.getTeamMembers("2");
expect(members2.length).toBeGreaterThan(0);
expect(logger.debug).not.toHaveBeenCalledWith("Connecting to collective parachain");
expect(logger.info).not.toHaveBeenCalledWith("Initializing the people client");
expect(logger.info).not.toHaveBeenCalledWith("Initializing the collectives client");
},
timeout,
);
Expand Down
Loading
Loading