Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
47 changes: 47 additions & 0 deletions .codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
comment:
behavior: "default"
layout: "condensed_header, condensed_files, condensed_footer"
hide_project_coverage: true
require_changes: false
show_carryforward_flags: true
coverage:
status:
project:
default:
target: auto
patch:
default:
target: auto
flags:
dash-num:
paths:
- pkgs/num/
dash-script:
paths:
- pkgs/script/
dash-p2p-core:
paths:
- pkgs/p2p_core/
dash-params:
paths:
- pkgs/params/
dash-pkc:
paths:
- pkgs/pkc/
dash-pow:
paths:
- pkgs/pow/
dash-primitives:
paths:
- pkgs/primitives/
dash-types:
paths:
- pkgs/types/
- pkgs/types/marker/
flag_management:
default_rules:
carryforward: true
ignore:
- "**/bench/**"
- "**/corpus/**"
- "**/tests/**"
55 changes: 55 additions & 0 deletions .coderabbit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

early_access: false
enable_free_tier: true
inheritance: false
chat:
allow_non_org_members: false
auto_reply: true
art: false
issue_enrichment:
auto_enrich:
enabled: false
planning:
enabled: false
labeling:
enabled: false
knowledge_base:
code_guidelines:
enabled: true
filePatterns:
- "AGENTS.md"
- "docs/dev/guide_rust.md"
language: en-US
reviews:
auto_apply_labels: false
auto_assign_reviewers: false
auto_review:
enabled: true
base_branches:
- "develop"
drafts: false
changed_files_summary: false
collapse_walkthrough: true
estimate_code_review_effort: false
finishing_touches:
docstrings:
enabled: false
unit_tests:
enabled: false
high_level_summary_in_walkthrough: true
in_progress_fortune: false
path_filters:
- "!**/*.json"
- "!**/*.json5"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- "!**/*.lock"
poem: false
pre_merge_checks:
docstrings:
mode: "off"
profile: chill
related_issues: false
related_prs: false
request_changes_workflow: false
sequence_diagrams: true
suggested_labels: false
3 changes: 3 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ indent_size = 4

[*.md]
trim_trailing_whitespace = false

[Justfile]
indent_size = 4
21 changes: 21 additions & 0 deletions .github/board.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"projects": {
"defaults": {
"owner": "dashpay",
"field": "Status"
},
"allow": [
{ "number": 13 },
{ "number": 14 }
]
},
"topics": [
"Build/CI",
"Codec",
"Crypto",
"Documentation",
"FFI",
"P2P",
"Primitives"
]
}
232 changes: 232 additions & 0 deletions .github/scripts/pr_board.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
/*!
* Copyright (c) 2026-present, The Dash Core developers
* SPDX-License-Identifier: MIT
* See the accompanying file LICENSE or https://opensource.org/license/MIT
*/

// @ts-check

// Syncs pull request topic labels with the project board buckets, with the
// board as the source of truth. Boards and topics come from `board.json`.

const fs = require("node:fs");
const path = require("node:path");

const { listOpenPulls } = require("./util");

const CONFIG_PATH = path.join(__dirname, "..", "board.json");

// Color of newly created topic labels.
const TOPIC_COLOR = "1d76db";

const BOARD_QUERY = `
query($owner: String!, $number: Int!, $field: String!, $cursor: String) {
organization(login: $owner) {
projectV2(number: $number) {
id
field(name: $field) {
... on ProjectV2SingleSelectField { id options { id name } }
}
items(first: 100, after: $cursor) {
pageInfo { hasNextPage endCursor }
nodes {
id
fieldValueByName(name: $field) {
... on ProjectV2ItemFieldSingleSelectValue { name }
}
content {
... on PullRequest {
number
repository { nameWithOwner }
labels(first: 50) { nodes { name } }
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
}
}
}
}
}
`;

const SET_TOPIC = `
mutation($project: ID!, $item: ID!, $field: ID!, $option: String!) {
updateProjectV2ItemFieldValue(input: {
projectId: $project, itemId: $item, fieldId: $field,
value: { singleSelectOptionId: $option }
}) { projectV2Item { id } }
}
`;

// `github` always sends the workflow token, which cannot reach organization
// projects, so board queries use `PROJECT_TOKEN` directly.
async function graphql(token, query, variables) {
const response = await fetch("https://api.github.com/graphql", {
method: "POST",
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
body: JSON.stringify({ query, variables }),
});
const body = await response.json().catch(() => ({}));
if (body.errors?.length) {
throw new Error(body.errors.map((e) => e.message).join("; "));
}
if (!response.ok) {
throw new Error(`board refused the query with ${response.status}`);
}
return body.data;
}

// Returns topic option ids and the *slug* pull requests on the board by number.
// Throws if the board lacks a topic option.
async function readBoard(token, { owner, number, field }, topics, slug, core) {
const where = `${owner}/${number}`;
const options = new Map();
const items = new Map();
let board;
let cursor = null;
do {
const data = await graphql(token, BOARD_QUERY, { owner, number, field, cursor });
board = data.organization?.projectV2;
if (!board) {
throw new Error(`${owner} states no project ${number}`);
}
if (!board.field?.id) {
throw new Error(`${field} is not a single-select field on ${where}`);
}
for (const { id, fieldValueByName, content } of board.items.nodes) {
// Skip issues, drafts and other repositories.
if (content?.number && content.repository.nameWithOwner === slug) {
items.set(content.number, {
itemId: id,
bucket: fieldValueByName?.name ?? null,
labels: content.labels.nodes.map((l) => l.name),
});
}
}
const { hasNextPage, endCursor } = board.items.pageInfo;
cursor = hasNextPage ? endCursor : null;
} while (cursor);

for (const { id, name } of board.field.options) {
if (topics.has(name)) {
options.set(name, id);
} else {
core.notice(`${where}: option ${name} is not a topic`);
}
}
for (const topic of topics) {
if (!options.has(topic)) {
throw new Error(`${where}: topic ${topic} is not offered by ${field}`);
}
}
return { where, id: board.id, fieldId: board.field.id, options, items };
}

// Runs a write, returning false when the target is no longer available
async function settle(write, core, what) {
try {
await write;
return true;
} catch (error) {
if (error.status !== 404) {
throw error;
}
core.info(`${what} no longer exists`);
return false;
}
}

// Creates missing topic labels. GitHub label names are case-insensitive, so a
// label differing only in case is renamed to the topic.
async function mintLabels(github, owner, repo, topics, core) {
const labels = await github.paginate(github.rest.issues.listLabelsForRepo, { owner, repo, per_page: 100 });
const held = new Map(labels.map((l) => [l.name.toLowerCase(), l.name]));
for (const name of topics) {
const present = held.get(name.toLowerCase());
if (present === name) {
continue;
}
if (present === undefined) {
const description = `Pull requests concerning ${name}`;
await github.rest.issues.createLabel({ owner, repo, name, color: TOPIC_COLOR, description });
core.info(`minted label ${name}`);
} else {
const call = github.rest.issues.updateLabel({ owner, repo, name: present, new_name: name });
if (await settle(call, core, `label ${present}`)) {
core.info(`renamed label ${present} to ${name}`);
}
}
}
}

/**
* @param {{ github: any, context: import("@actions/github").context, core: any }} params
*/
module.exports = async ({ github, context, core }) => {
const token = process.env.PROJECT_TOKEN;
if (!token) {
throw new Error("PROJECT_TOKEN reaches the boards; it is unset");
}
const { owner, repo } = context.repo;

// Allowed order breaks ties between boards.
const config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
const topics = new Set(config.topics);
const boards = [];
for (const entry of config.projects.allow) {
const project = { ...config.projects.defaults, ...entry };
boards.push(await readBoard(token, project, topics, `${owner}/${repo}`, core));
}

await mintLabels(github, owner, repo, topics, core);

// Apply board buckets to labels for all pull requests, open or not.
const settled = new Map();
for (const { where, items } of boards) {
for (const [number, { bucket, labels }] of items) {
if (!topics.has(bucket)) {
continue;
}
const prior = settled.get(number);
if (prior !== undefined) {
if (prior !== bucket) {
core.warning(`PR #${number}: ${where} buckets ${bucket}, an earlier board ${prior}`);
}
continue;
}
settled.set(number, bucket);
if (!labels.includes(bucket)) {
const call = github.rest.issues.addLabels({ owner, repo, issue_number: number, labels: [bucket] });
if (await settle(call, core, `PR #${number}`)) {
core.info(`PR #${number}: labelled ${bucket}`);
}
}
for (const name of labels) {
if (name !== bucket && topics.has(name)) {
const call = github.rest.issues.removeLabel({ owner, repo, issue_number: number, name });
if (await settle(call, core, `PR #${number} label ${name}`)) {
core.info(`PR #${number}: pruned ${name}`);
}
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}

// Bucket unbucketed board items from their label. Never add items to a board.
for (const pr of await listOpenPulls({ github, owner, repo })) {
const unbucketed = boards.filter((b) => b.items.get(pr.number)?.bucket === null);
if (unbucketed.length === 0) {
continue;
}
const held = pr.labels.map((l) => l.name).filter((n) => topics.has(n));
const topic = settled.get(pr.number) ?? (held.length === 1 ? held[0] : undefined);
if (topic === undefined) {
core.warning(`PR #${pr.number} is unbucketed and names ${held.length} topics`);
continue;
}
for (const { where, id, fieldId, options, items } of unbucketed) {
const item = items.get(pr.number).itemId;
await graphql(token, SET_TOPIC, { project: id, item, field: fieldId, option: options.get(topic) });
core.info(`PR #${pr.number}: bucketed ${topic} on ${where}`);
}
}
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.
5 changes: 2 additions & 3 deletions .github/scripts/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,14 @@ function sleep(ms) {
* @param {{ github: any, owner: string, repo: string }} params
* @returns {Promise<any[]>}
*/
async function listOpenPulls({ github, owner, repo }) {
const { data: pulls } = await github.rest.pulls.list({
function listOpenPulls({ github, owner, repo }) {
return github.paginate(github.rest.pulls.list, {
owner,
repo,
state: "open",
base: BASE_BRANCH,
per_page: 100,
});
return pulls;
}

/**
Expand Down
Loading
Loading