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
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/02-beginner-issue.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Beginner Issue Template
description: Create a beginner issue for contributors ready to learn the codebase and own a small implementation.
labels: ["beginner"]
labels: ["skill: beginner"]
assignees: []
body:
- type: markdown
Expand Down Expand Up @@ -227,7 +227,7 @@ body:
- [Windows Setup Guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/setup_windows.md) — platform-specific installation steps
- [Pylance guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/pylance.md) — inline type checking during development
- [Running examples](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/examples.md)
- [Browse closed beginner PRs](https://github.com/hiero-ledger/hiero-sdk-python/pulls?q=is%3Apr+is%3Amerged+label%3Abeginner) — see how others did it
- [Browse closed beginner PRs](https://github.com/hiero-ledger/hiero-sdk-python/pulls?q=is%3Apr+is%3Amerged+label%3A%22skill%3A+beginner%22) — see how others did it
Comment thread
DeathGun44 marked this conversation as resolved.

**References:**
- [Hedera Protobufs](https://github.com/hashgraph/hedera-protobufs)
Expand Down
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/03-intermediate-issue.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Intermediate Issue Template
description: Create a well-documented issue for contributors ready to own larger tasks independently
labels: ["intermediate"]
labels: ["skill: intermediate"]
assignees: []
body:
- type: markdown
Expand Down Expand Up @@ -248,7 +248,7 @@ body:
**Project references:**
- [Project structure](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/setup/project_structure.md)
- [CONTRIBUTING.md](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/CONTRIBUTING.md)
- [Browse closed intermediate PRs](https://github.com/hiero-ledger/hiero-sdk-python/pulls?q=is%3Apr+is%3Amerged+label%3Aintermediate) — see how others did it
- [Browse closed intermediate PRs](https://github.com/hiero-ledger/hiero-sdk-python/pulls?q=is%3Apr+is%3Amerged+label%3A%22skill%3A+intermediate%22) — see how others did it
Comment thread
DeathGun44 marked this conversation as resolved.
- [Pylance guide](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/pylance.md)

**Protobuf references:**
Expand Down
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/04-advanced-issue.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Advanced Issue Template
description: For expert contributors tackling architectural, multi-module, or core-logic changes.
labels: ["advanced"]
labels: ["skill: advanced"]
assignees: []

body:
Expand Down Expand Up @@ -145,7 +145,7 @@ body:
- [Transaction Lifecycle](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/transaction_lifecycle.md)
- [Executable Architecture](https://github.com/hiero-ledger/hiero-sdk-python/blob/main/docs/sdk_developers/training/executable.md)
- [Hedera Protobufs (services)](https://github.com/hashgraph/hedera-protobufs/tree/main/services)
- [Browse closed advanced PRs](https://github.com/hiero-ledger/hiero-sdk-python/pulls?q=is%3Apr+is%3Amerged+label%3Aadvanced) — see how others did it
- [Browse closed advanced PRs](https://github.com/hiero-ledger/hiero-sdk-python/pulls?q=is%3Apr+is%3Amerged+label%3A%22skill%3A+advanced%22) — see how others did it
Comment thread
DeathGun44 marked this conversation as resolved.

**🆘 Stuck?**
- [Community Calls](https://zoom-lfx.platform.linuxfoundation.org/meetings/hiero?view=week)
Expand Down
14 changes: 9 additions & 5 deletions .github/scripts/bot-advanced-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ log() {
# Configuration
#######################################
REQUIRED_INTERMEDIATE_COUNT=1
INTERMEDIATE_LABEL="${INTERMEDIATE_LABEL:-skill: intermediate}"
Comment thread
exploreriii marked this conversation as resolved.

#######################################
# Validate required environment variables
Expand Down Expand Up @@ -67,20 +68,21 @@ get_intermediate_count() {

gh api graphql \
-f query='
query($owner: String!, $name: String!, $user: String!) {
query($owner: String!, $name: String!, $user: String!, $labels: [String!]!) {
repository(owner: $owner, name: $name) {
intermediate: issues(
first: 1
states: CLOSED
filterBy: { assignee: $user, labels: ["intermediate"] }
filterBy: { assignee: $user, labels: $labels }
) {
totalCount
}
}
}' \
-f owner="$OWNER" \
-f name="$NAME" \
-f user="$user"
-f user="$user" \
-f "labels[]=$INTERMEDIATE_LABEL"
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

#######################################
Expand Down Expand Up @@ -187,15 +189,17 @@ check_user() {
###################################
log "User @$user NOT qualified."

SUGGESTION="[intermediate issues](https://github.com/$REPO/labels/intermediate)"
# URL-encode: jq @uri handles colons, spaces, and all special chars
ENCODED_LABEL=$(printf '%s' "$INTERMEDIATE_LABEL" | jq -Rr @uri)
SUGGESTION="[$INTERMEDIATE_LABEL issues](https://github.com/$REPO/labels/$ENCODED_LABEL)"

MSG="Hi @$user, I cannot assign you to this issue yet.

**Why?**
Advanced issues involve high-risk changes to the core codebase and require prior experience in this repository.

**Requirement:**
- Complete at least **$REQUIRED_INTERMEDIATE_COUNT** 'intermediate' issue (You have: **$INT_COUNT**)
- Complete at least **$REQUIRED_INTERMEDIATE_COUNT** '$INTERMEDIATE_LABEL' issue (You have: **$INT_COUNT**)

Please check out our **$SUGGESTION** to build your experience first!

Expand Down
8 changes: 5 additions & 3 deletions .github/scripts/bot-beginner-assign-on-comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ Parameters:
*/

const fs = require("fs");
const { BEGINNER_LABEL } = require('./shared/labels.js');

const SPAM_LIST_PATH = ".github/spam-list.txt";
const REQUIRED_GFI_COUNT = 1;
Expand Down Expand Up @@ -115,9 +116,10 @@ module.exports = async ({ github, context }) => {
}

// 2. Label Check (Fix 2: Defensive Check)
const hasBeginnerLabel = Array.isArray(issue.labels) && issue.labels.some((label) => label.name === "beginner");
const beginnerLabel = BEGINNER_LABEL.toLowerCase();
const hasBeginnerLabel = Array.isArray(issue.labels) && issue.labels.some((label) => label.name?.toLowerCase() === beginnerLabel);
if (!hasBeginnerLabel) {
console.log(`[Beginner Bot] Issue #${issue.number} does not have 'beginner' label. Exiting.`);
console.log(`[Beginner Bot] Issue #${issue.number} does not have '${BEGINNER_LABEL}' label. Exiting.`);
return;
}

Expand Down Expand Up @@ -261,7 +263,7 @@ Please try a GFI first, then come back — we’ll be happy to assign this! 😊
owner: repo.owner.login,
repo: repo.name,
issue_number: issue.number,
body: `👋 Hi @${commenter}, thanks for your interest! This issue is already assigned to @${currentAssignee}, but we'd love your help on another one. You can find more "beginner" issues [here](https://github.com/${repo.owner.login}/${repo.name}/issues?q=is%3Aissue+is%3Aopen+label%3Abeginner+no%3Aassignee).`,
body: `👋 Hi @${commenter}, thanks for your interest! This issue is already assigned to @${currentAssignee}, but we'd love your help on another one. You can find more "${BEGINNER_LABEL}" issues [here](https://github.com/${repo.owner.login}/${repo.name}/issues?q=is%3Aissue+is%3Aopen+label%3A%22${encodeURIComponent(BEGINNER_LABEL)}%22+no%3Aassignee).`,
});
} catch (error) {
console.error("[Beginner Bot] Failed to post already-assigned message:", {
Expand Down
4 changes: 2 additions & 2 deletions .github/scripts/bot-gfi-assign-on-comment.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@

const fs = require('fs'); // For spam list

const GOOD_FIRST_ISSUE_LABEL = 'Good First Issue';
const { GOOD_FIRST_ISSUE_LABEL } = require('./shared/labels.js');
const UNASSIGNED_GFI_SEARCH_URL =
'https://github.com/hiero-ledger/hiero-sdk-python/issues?q=is%3Aissue%20state%3Aopen%20label%3A%22Good%20First%20Issue%22%20no%3Aassignee';
`https://github.com/hiero-ledger/hiero-sdk-python/issues?q=${encodeURIComponent(`is:issue state:open label:"${GOOD_FIRST_ISSUE_LABEL}" no:assignee`)}`;

const SPAM_LIST_PATH = '.github/spam-list.txt';

Expand Down
3 changes: 2 additions & 1 deletion .github/scripts/bot-gfi-candidate-notification.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Script to notify the team when a Good First Issue Candidate is created.

const { GOOD_FIRST_ISSUE_CANDIDATE_LABEL } = require('./shared/labels.js');
const marker = '<!-- GFI Candidate Notification -->';
const TEAM_ALIAS = '@hiero-ledger/hiero-sdk-good-first-issue-support';

Expand Down Expand Up @@ -53,7 +54,7 @@ module.exports = async ({ github, context }) => {
}

// Only handle Good First Issue Candidate
if (label.name.toLowerCase() !== 'good first issue candidate') {
if (label.name.toLowerCase() !== GOOD_FIRST_ISSUE_CANDIDATE_LABEL.toLowerCase()) {
return;
}

Expand Down
9 changes: 5 additions & 4 deletions .github/scripts/bot-intermediate-assignment.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const shared = require('./shared/labels.js');
const COMMENT_MARKER = process.env.INTERMEDIATE_COMMENT_MARKER || '<!-- Intermediate Issue Guard -->';
const INTERMEDIATE_LABEL = process.env.INTERMEDIATE_LABEL?.trim() || 'intermediate';
const BEGINNER_LABEL = process.env.BEGINNER_LABEL?.trim() || 'beginner';
const INTERMEDIATE_LABEL = process.env.INTERMEDIATE_LABEL?.trim() || shared.INTERMEDIATE_LABEL;
const BEGINNER_LABEL = process.env.BEGINNER_LABEL?.trim() || shared.BEGINNER_LABEL;
const EXEMPT_PERMISSION_LEVELS = (process.env.INTERMEDIATE_EXEMPT_PERMISSIONS || 'admin,maintain,write,triage')
.split(',')
.map((entry) => entry.trim().toLowerCase())
Expand Down Expand Up @@ -61,7 +62,7 @@ async function countCompletedBeginnerIssues(github, owner, repo, username) {
!isSafeSearchToken(owner) ||
!isSafeSearchToken(repo) ||
!isSafeSearchToken(username) ||
!isSafeSearchToken(BEGINNER_LABEL)
!shared.isSafeLabel(BEGINNER_LABEL)
) {
console.log('Invalid search inputs', {
owner,
Expand All @@ -75,7 +76,7 @@ async function countCompletedBeginnerIssues(github, owner, repo, username) {
try {
const searchQuery = [
`repo:${owner}/${repo}`,
`label:${BEGINNER_LABEL}`,
`label:"${BEGINNER_LABEL}"`,
Comment thread
DeathGun44 marked this conversation as resolved.
'is:issue',
'is:closed',
`assignee:${username}`,
Expand Down
36 changes: 25 additions & 11 deletions .github/scripts/bot-next-issue-recommendation.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
const {
GOOD_FIRST_ISSUE_LABEL,
BEGINNER_LABEL,
INTERMEDIATE_LABEL,
ADVANCED_LABEL,
isSafeLabel,
} = require('./shared/labels.js');

for (const label of [GOOD_FIRST_ISSUE_LABEL, BEGINNER_LABEL, INTERMEDIATE_LABEL, ADVANCED_LABEL]) {
if (!isSafeLabel(label)) {
throw new Error(`Invalid configured label: ${label}`);
}
}

const SUPPORTED_GFI_REPOS = [
'hiero-sdk-cpp',
'hiero-sdk-swift',
Expand Down Expand Up @@ -58,10 +72,10 @@ module.exports = async ({ github, context, core }) => {

// Determine issue difficulty level
const difficultyLevels = {
beginner: labelSet.has('beginner'),
goodFirstIssue: labelSet.has('good first issue'),
intermediate: labelSet.has('intermediate'),
advanced: labelSet.has('advanced'),
beginner: labelSet.has(BEGINNER_LABEL.toLowerCase()),
goodFirstIssue: labelSet.has(GOOD_FIRST_ISSUE_LABEL.toLowerCase()),
intermediate: labelSet.has(INTERMEDIATE_LABEL.toLowerCase()),
advanced: labelSet.has(ADVANCED_LABEL.toLowerCase()),
};
Comment thread
coderabbitai[bot] marked this conversation as resolved.

// Skip if intermediate or advanced
Expand All @@ -80,22 +94,22 @@ module.exports = async ({ github, context, core }) => {
let recommendedLabel = null;
let isFallback = false;

recommendedIssues = await searchIssues(github, core, repoOwner, repoName, 'beginner');
recommendedLabel = 'Beginner';
recommendedIssues = await searchIssues(github, core, repoOwner, repoName, BEGINNER_LABEL);
recommendedLabel = BEGINNER_LABEL;

if (recommendedIssues.length === 0) {
isFallback = true;
recommendedIssues = await searchIssues(github, core, repoOwner, repoName, 'good first issue');
recommendedLabel = 'Good First Issue';
recommendedIssues = await searchIssues(github, core, repoOwner, repoName, GOOD_FIRST_ISSUE_LABEL);
recommendedLabel = GOOD_FIRST_ISSUE_LABEL;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.


// Remove the issue they just solved
recommendedIssues = recommendedIssues.filter(i => i.number !== issueNumber);

// Generate and post comment
const completedLabel = difficultyLevels.goodFirstIssue ? 'Good First Issue' : 'Beginner';
const completedLabelText = completedLabel === 'Beginner' ? 'Beginner issue' : completedLabel;
const completedLabel = difficultyLevels.goodFirstIssue ? GOOD_FIRST_ISSUE_LABEL : BEGINNER_LABEL;
const completedLabelText = completedLabel === BEGINNER_LABEL ? `${BEGINNER_LABEL} issue` : completedLabel;
const recommendationMeta = {
completedLabelText,
recommendedLabel,
Expand Down Expand Up @@ -170,7 +184,7 @@ async function generateAndPostComment(github, context, core, prNumber, recommend
`org:${context.repo.owner}`,
'archived:false',
'no:assignee',
'(label:"good first issue" OR label:"skill: good first issue")',
`label:"${GOOD_FIRST_ISSUE_LABEL}"`,
`(${repoQuery})`,
].join(' ');

Expand Down
15 changes: 10 additions & 5 deletions .github/scripts/coderabbit_plan_trigger.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Script to trigger CodeRabbit plan for intermediate and advanced issues

const CODERABBIT_MARKER = '<!-- CodeRabbit Plan Trigger -->';
const { DIFFICULTY_LABELS, GOOD_FIRST_ISSUE_LABEL } = require('./shared/labels.js');

async function triggerCodeRabbitPlan(github, owner, repo, issue, marker = CODERABBIT_MARKER) {
const comment = `${marker} @coderabbitai plan`;
Expand All @@ -27,13 +28,17 @@ async function triggerCodeRabbitPlan(github, owner, repo, issue, marker = CODERA
}

function hasBeginnerOrHigherLabel(issue, label) {
// Check if issue has beginner, intermediate or advanced label (case-insensitive)
const allowed = ['beginner', 'intermediate', 'advanced'];
// Only beginner+ labels qualify here; GFI gets its own CodeRabbit plan
// trigger via the assignment bot chain (bot-gfi-assign-on-comment.js).
const beginnerPlus = DIFFICULTY_LABELS
.filter(d => d !== GOOD_FIRST_ISSUE_LABEL)
.map(d => d.toLowerCase());
const allowed = new Set(beginnerPlus);

const hasAllowedLabel = issue.labels?.some(l => allowed.includes(l?.name?.toLowerCase()));
const hasAllowedLabel = issue.labels?.some(l => allowed.has(l?.name?.toLowerCase()));

// Also check if newly added label is beginner/intermediate/advanced
const isNewLabelAllowed = allowed.includes(label?.name?.toLowerCase());
// Also check if newly added label is a difficulty label
const isNewLabelAllowed = allowed.has(label?.name?.toLowerCase());

return hasAllowedLabel || isNewLabelAllowed;
}
Expand Down
7 changes: 6 additions & 1 deletion .github/scripts/cron-admin-update-spam-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@

const fs = require('fs').promises;
const { LABEL_AUTOMATED } = require('./labels.js');
const { GOOD_FIRST_ISSUE_LABEL, isSafeLabel } = require('./shared/labels.js');

if (!isSafeLabel(GOOD_FIRST_ISSUE_LABEL)) {
throw new Error(`Invalid GOOD_FIRST_ISSUE_LABEL: ${GOOD_FIRST_ISSUE_LABEL}`);
}

const SPAM_LIST_PATH = '.github/spam-list.txt';
const dryRun = (process.env.DRY_RUN || 'false').toString().toLowerCase() === 'true';
Expand Down Expand Up @@ -146,7 +151,7 @@ module.exports = async ({github, context, core}) => {
},
{
name: 'rehabilitated PRs',
query: `repo:${owner}/${repo} is:pr is:merged label:"Good First Issue"`,
query: `repo:${owner}/${repo} is:pr is:merged label:"${GOOD_FIRST_ISSUE_LABEL}"`,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
process: async (pr) => {
if (!pr.user?.login) {
console.log(`Skipping PR #${pr.number}: user account unavailable`);
Expand Down
59 changes: 59 additions & 0 deletions .github/scripts/shared/labels.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/**
* Centralized label configuration — single source of truth for difficulty labels.
*
* All JavaScript scripts that reference difficulty labels should import
* constants from this module instead of hardcoding strings.
*
* To rename labels in the future, update the defaults below and the
* corresponding workflow YAML `if:` conditions (which cannot import JS).
*
* Environment variable overrides are supported for per-workflow customization:
* GOOD_FIRST_ISSUE_LABEL, GOOD_FIRST_ISSUE_CANDIDATE_LABEL,
* BEGINNER_LABEL, INTERMEDIATE_LABEL, ADVANCED_LABEL
*/

const GOOD_FIRST_ISSUE_LABEL = process.env.GOOD_FIRST_ISSUE_LABEL?.trim() || 'Good First Issue';
const GOOD_FIRST_ISSUE_CANDIDATE_LABEL = process.env.GOOD_FIRST_ISSUE_CANDIDATE_LABEL?.trim() || 'Good First Issue Candidate';
const BEGINNER_LABEL = process.env.BEGINNER_LABEL?.trim() || 'skill: beginner';
const INTERMEDIATE_LABEL = process.env.INTERMEDIATE_LABEL?.trim() || 'skill: intermediate';
const ADVANCED_LABEL = process.env.ADVANCED_LABEL?.trim() || 'skill: advanced';

/**
* All difficulty labels as an array, ordered by ascending difficulty.
*
* Scripts that need to check whether an issue has *any* difficulty label
* (e.g. coderabbit_plan_trigger) should test against this array.
*/
const DIFFICULTY_LABELS = [
GOOD_FIRST_ISSUE_LABEL,
BEGINNER_LABEL,
INTERMEDIATE_LABEL,
ADVANCED_LABEL,
];

/**
* Validates a label string for safe use in API queries.
*
* Allows letters, digits, and the characters: . _ / : space -
* This is intentionally looser than `isSafeSearchToken` (which rejects
* colons and spaces) because labels like "skill: beginner" are valid.
*
* The hyphen is escaped (\-) to prevent accidental ASCII range creation
* if a future developer appends characters to the character class.
*
* @param {string} value - The label string to validate.
* @returns {boolean} True if the value is safe for use in queries.
*/
function isSafeLabel(value) {
return typeof value === 'string' && value.trim().length > 0 && /^[a-zA-Z0-9._/: \-]+$/.test(value);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

module.exports = {
GOOD_FIRST_ISSUE_LABEL,
GOOD_FIRST_ISSUE_CANDIDATE_LABEL,
BEGINNER_LABEL,
INTERMEDIATE_LABEL,
ADVANCED_LABEL,
DIFFICULTY_LABELS,
isSafeLabel,
};
Loading
Loading