From d0ed592ee2aa6ea6cf6c57d38679f488729d5696 Mon Sep 17 00:00:00 2001 From: Rob Walworth Date: Wed, 25 Mar 2026 16:33:44 -0400 Subject: [PATCH 1/5] feat: add finalize command and new issue templates Signed-off-by: Rob Walworth --- .../01_good_first_issue_candidate.yml | 222 ------- .../ISSUE_TEMPLATE/02_good_first_issue.yml | 224 ------- .github/ISSUE_TEMPLATE/03_beginner_issue.yml | 217 ------- .../ISSUE_TEMPLATE/04_intermediate_issue.yml | 221 ------- .github/ISSUE_TEMPLATE/05_advanced_issue.yml | 212 ------- .github/ISSUE_TEMPLATE/bug.yml | 185 ++++-- .github/ISSUE_TEMPLATE/feature.yml | 142 ++++- .github/ISSUE_TEMPLATE/task.yml | 132 ++++ .github/scripts/bot-on-comment.js | 17 +- .github/scripts/commands/finalize-comments.js | 448 ++++++++++++++ .github/scripts/commands/finalize.js | 335 ++++++++++ .github/scripts/helpers/constants.js | 1 + .github/scripts/tests/test-finalize-bot.js | 578 ++++++++++++++++++ CONTRIBUTING.md | 76 ++- docs/contributing/issue-types.md | 139 +++++ docs/maintainers/guidelines-triage.md | 150 +++++ 16 files changed, 2094 insertions(+), 1205 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/01_good_first_issue_candidate.yml delete mode 100644 .github/ISSUE_TEMPLATE/02_good_first_issue.yml delete mode 100644 .github/ISSUE_TEMPLATE/03_beginner_issue.yml delete mode 100644 .github/ISSUE_TEMPLATE/04_intermediate_issue.yml delete mode 100644 .github/ISSUE_TEMPLATE/05_advanced_issue.yml create mode 100644 .github/ISSUE_TEMPLATE/task.yml create mode 100644 .github/scripts/commands/finalize-comments.js create mode 100644 .github/scripts/commands/finalize.js create mode 100644 .github/scripts/tests/test-finalize-bot.js create mode 100644 docs/contributing/issue-types.md create mode 100644 docs/maintainers/guidelines-triage.md diff --git a/.github/ISSUE_TEMPLATE/01_good_first_issue_candidate.yml b/.github/ISSUE_TEMPLATE/01_good_first_issue_candidate.yml deleted file mode 100644 index e9e702d36..000000000 --- a/.github/ISSUE_TEMPLATE/01_good_first_issue_candidate.yml +++ /dev/null @@ -1,222 +0,0 @@ -name: Good First Issue Candidate Template -description: Propose a potential Good First Issue (Candidate) -title: "[Good First Issue Candidate]: " -labels: ["Good First Issue Candidate"] -assignees: [] - -body: - - type: textarea - id: intro-gfi-candidate - attributes: - label: ⚠️ Good First Issue β€” Candidate - value: | - > This issue is not yet a confirmed Good First Issue. - > It is being evaluated for suitability and may require - > clarification or refinement before it is ready to be picked up. - > - > Please wait for maintainer confirmation before starting work, so we can make sure the issue is fully ready. - > - > Maintainers can read more about: - > - [Good First Issues](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/docs/maintainers/guidelines-good-first-issues.md) - > - [Good First Issue Candidates](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/docs/maintainers/guidelines-good-first-issue-candidates.md) - - - type: textarea - id: intro - attributes: - label: πŸ†•πŸ₯ First-Time Friendly - description: Who is this issue for? - value: | - This issue is especially welcoming for people who are new to contributing to the **Hiero C++ SDK**. - - We know that opening your first pull request can feel like a big step. - Issues labeled **Good First Issue** are designed to make that experience easier, clearer, and more comfortable. - - No prior knowledge of Hiero, Hedera, or distributed ledger technology is required. - Just a basic familiarity with C++ and Git is more than enough to get started. - validations: - required: false - - - type: markdown - attributes: - value: | - > [!IMPORTANT] - > ### πŸ“‹ About Good First Issues - > - > Good First Issues are designed to make getting started as smooth and stress-free - > as possible. - > - > They usually focus on: - > - Small, clearly scoped changes - > - Straightforward updates to existing code or docs - > - Simple refactors or clarity improvements - > - > Other kinds of contributions β€” like larger features, deeper technical changes, - > or design-focused work β€” are just as valuable and often use the beginner, intermediate, or advanced labels. - - - type: textarea - id: issue - attributes: - label: πŸ‘Ύ Description of the Issue - description: | - Describe the issue in a way that’s easy for new contributors to understand. - - Aim to keep the explanation clear and accessible for people who are new to the codebase and Hiero. - Links to relevant files or examples are always helpful. - value: | - Edit here. An example is provided below. - validations: - required: true - - - type: markdown - attributes: - value: | - - ## πŸ‘Ύ Description of the Issue β€” Example (C++) - - The file: - - ```src/sdk/main/src/TokenAssociateTransaction.cc``` - - contains several short methods that are part of the - `TokenAssociateTransaction` implementation. - - Some of these methods lack brief comments explaining their purpose, - which can make the file harder to understand for new contributors - who are unfamiliar with the SDK. - - For example, methods like `submitRequest()` and `addToBody()` are - correctly implemented, but their intent is not immediately obvious - without reading surrounding code. - - The behavior is correct, but adding small, clear comments would - make the file easier to read and maintain. - - - type: textarea - id: solution - attributes: - label: πŸ’‘ Proposed Solution - description: | - Describe the solution at a high level. - Focus on *what* should be done, not *why* or alternative approaches. - value: | - Edit here. Example provided below. - validations: - required: true - - - type: markdown - attributes: - value: | - - ## πŸ’‘ Proposed Solution β€” Example - - Add short, descriptive comments above selected methods in - `TokenAssociateTransaction.cc` explaining what each method does - and when it is used. - - The changes should: - - Not modify any existing logic - - Not change behavior or public APIs - - Be limited to comments only - - Help new contributors understand the flow of the file - - - type: textarea - id: implementation - attributes: - label: πŸ‘©β€πŸ’» Implementation Steps (End-to-End) - description: | - Please write out the end-to-end process for completing this issue from start to finish. - - The steps should clearly describe: - - Where to begin - - Which files to open - - What to change - - How to make the change (e.g. what code to add, remove, or update) - - What the final result should look like - - Clear, connected instructions help make the contribution experience smoother - and more enjoyable for everyone. - value: | - Edit here. An example is provided below. - validations: - required: true - - - type: markdown - attributes: - value: | - - ### πŸ‘©β€πŸ’» Implementation β€” Example - - To complete this change: - - - [ ] Open `src/sdk/main/src/TokenAssociateTransaction.cc` - - [ ] Locate the following methods that need comments: - - `submitRequest()` - - `addToBody()` - - [ ] Add **concise, single-line comments** above each method that explain: - - What the method does - - When it is used in the transaction flow - - **Comment guidelines:** - - Use standard C++ `//` comments - - Limit each comment to **one line (β‰ˆ80 characters max)** - - Example: - ```cpp - // Builds and submits the token association request to the network. - ``` - - - [ ] Run existing tests or CI locally **if already set up** - (CI will also run automatically when you open a pull request) - - This change should only affect comments and readability. - - - type: textarea - id: acceptance-criteria - attributes: - label: βœ… Acceptance Criteria - description: Checklist required for completing this issue - value: | - To merge a pull request for this issue: - - - [ ] **Scope:** Changes are limited to this issue - - [ ] **Behavior:** No SDK behavior or public API changes - - [ ] **Tests:** Existing CI checks pass - - [ ] **Review:** All code review feedback addressed - validations: - required: true - - - type: textarea - id: contribution_steps - attributes: - label: πŸ“‹ Step-by-Step Contribution Guide - description: Contribution workflow for new contributors - value: | - To help your first contribution go as smoothly as possible, we recommend following these steps: - - - [ ] Comment `/assign` to request the issue - - [ ] Wait for assignment - - [ ] Fork the repository and create a branch - - [ ] Set up the project by following the instructions in `README.md` - - [ ] Make the requested changes - - [ ] Sign each commit using `-s -S` - - [ ] Push your branch and open a pull request - - Read [Workflow Guide](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/docs/training/workflow.md) for step-by-step workflow guidance. - Read [README.md](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/README.md) for setup instructions. - - ❗ Pull requests **cannot be merged** without `S` and `s` signed commits. - See the [Signing Guide](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/docs/training/signing.md). - validations: - required: true - - - type: textarea - id: information - attributes: - label: πŸ€” Additional Information - description: Extra context or resources for contributors - value: | - If you need help, reach out to the @hiero-ledger/hiero-sdk-good-first-issue-support team. - - You can also join our community on Discord: - [Hiero-SDK-C++](https://discord.com/channels/905194001349627914/1337424839761465364) - - Maintainers are happy to help first-time contributors succeed! \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/02_good_first_issue.yml b/.github/ISSUE_TEMPLATE/02_good_first_issue.yml deleted file mode 100644 index 86f76e11b..000000000 --- a/.github/ISSUE_TEMPLATE/02_good_first_issue.yml +++ /dev/null @@ -1,224 +0,0 @@ -name: Good First Issue Template -description: Create a Good First Issue for new contributors (C++) -title: "[Good First Issue]: " -labels: ["Good First Issue"] -assignees: [] - -body: - - type: markdown - attributes: - value: | - --- - ## **Thanks for contributing!** 😊 - - We truly appreciate your time and effort. If this is your first open-source contribution, welcome! - - This template is designed to help you create a **Good First Issue (GFI)** β€” - a small, well-scoped task that helps new contributors learn the - **Hiero C++ SDK** codebase and workflow. - - > Maintainers can read more about: - > - [Good First Issues](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/docs/maintainers/guidelines-good-first-issues.md) - > - [Good First Issue Candidates](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/docs/maintainers/guidelines-good-first-issue-candidates.md) - --- - - - type: textarea - id: intro - attributes: - label: πŸ†•πŸ₯ First-Time Friendly - description: Who is this issue for? - value: | - This issue is especially welcoming for people who are new to contributing to the **Hiero C++ SDK**. - - We know that opening your first pull request can feel like a big step. - Issues labeled **Good First Issue** are designed to make that experience easier, clearer, and more comfortable. - - No prior knowledge of Hiero, Hedera, or distributed ledger technology is required. - Just a basic familiarity with C++ and Git is more than enough to get started. - validations: - required: false - - - type: markdown - attributes: - value: | - > [!IMPORTANT] - > ### πŸ“‹ About Good First Issues - > - > Good First Issues are designed to make getting started as smooth and stress-free - > as possible. - > - > They usually focus on: - > - Small, clearly scoped changes - > - Straightforward updates to existing code or docs - > - Simple refactors or clarity improvements - > - > Other kinds of contributions β€” like larger features, deeper technical changes, - > or design-focused work β€” are just as valuable and often use the beginner, intermediate, or advanced labels. - - - type: textarea - id: issue - attributes: - label: πŸ‘Ύ Description of the Issue - description: | - Describe the issue in a way that’s easy for new contributors to understand. - - Aim to keep the explanation clear and accessible for people who are new to the codebase and Hiero. - Links to relevant files or examples are always helpful. - value: | - Edit here. An example is provided below. - validations: - required: true - - - type: markdown - attributes: - value: | - - ## πŸ‘Ύ Description of the Issue β€” Example (C++) - - The file: - - ```src/sdk/main/src/TokenAssociateTransaction.cc``` - - contains several short methods that are part of the - `TokenAssociateTransaction` implementation. - - Some of these methods lack brief comments explaining their purpose, - which can make the file harder to understand for new contributors - who are unfamiliar with the SDK. - - For example, methods like `submitRequest()` and `addToBody()` are - correctly implemented, but their intent is not immediately obvious - without reading surrounding code. - - The behavior is correct, but adding small, clear comments would - make the file easier to read and maintain. - - - type: textarea - id: solution - attributes: - label: πŸ’‘ Proposed Solution - description: | - Describe the solution at a high level. - Focus on what should be done, keeping the solution clear and straightforward. - value: | - Edit here. Example provided below. - validations: - required: true - - - type: markdown - attributes: - value: | - - ## πŸ’‘ Proposed Solution β€” Example - - Add short, descriptive comments above selected methods in - `TokenAssociateTransaction.cc` explaining what each method does - and when it is used. - - The changes should: - - Not modify any existing logic - - Not change behavior or public APIs - - Be limited to comments only - - Improve readability for new contributors - - - type: textarea - id: implementation - attributes: - label: πŸ‘©β€πŸ’» Implementation Steps (End-to-End) - description: | - Please write out the end-to-end process for completing this issue from start to finish. - - The steps should clearly describe: - - Where to begin - - Which files to open - - What to change - - How to make the change (e.g. what code to add, remove, or update) - - What the final result should look like - - Clear, connected instructions help make the contribution experience smoother - and more enjoyable for everyone. - value: | - Edit here. An example is provided below. - validations: - required: true - - - type: markdown - attributes: - value: | - - ### πŸ‘©β€πŸ’» Implementation β€” Example - - To complete this change: - - - [ ] Open `src/sdk/main/src/TokenAssociateTransaction.cc` - - [ ] Locate the following methods that need comments: - - `submitRequest()` - - `addToBody()` - - [ ] Add **concise, single-line comments** above each method that explain: - - What the method does - - When it is used in the transaction flow - - **Comment guidelines:** - - Use standard C++ `//` comments - - Limit each comment to **one line (β‰ˆ80 characters max)** - - Example: - ```cpp - // Builds and submits the token association request to the network. - ``` - - - [ ] Run existing tests or CI locally **if already set up** - (CI will also run automatically when you open a pull request) - - This change should only affect comments and readability. - - - type: textarea - id: acceptance-criteria - attributes: - label: βœ… Acceptance Criteria - description: Checklist required for completing this issue - value: | - To merge a pull request for this issue: - - - [ ] **Scope:** Changes are limited to this issue - - [ ] **Behavior:** No SDK behavior or public API changes - - [ ] **Tests:** Existing CI checks pass - - [ ] **Review:** All code review feedback addressed - validations: - required: true - - - type: textarea - id: contribution_steps - attributes: - label: πŸ“‹ Step-by-Step Contribution Guide - description: Contribution workflow for new contributors - value: | - To help your first contribution go as smoothly as possible, we recommend following these steps: - - - [ ] Comment `/assign` to request the issue - - [ ] Wait for assignment - - [ ] Fork the repository and create a branch - - [ ] Set up the project by following the instructions in `README.md` - - [ ] Make the requested changes - - [ ] Sign each commit using `-s -S` - - [ ] Push your branch and open a pull request - - Read [Workflow Guide](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/docs/training/workflow.md) for step-by-step workflow guidance. - Read [README.md](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/README.md) for setup instructions. - - ❗ Pull requests **cannot be merged** without `S` and `s` signed commits. - See the [Signing Guide](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/docs/training/signing.md). - validations: - required: true - - - type: textarea - id: information - attributes: - label: πŸ€” Additional Information - description: Extra context or resources for contributors - value: | - If you need help, reach out to the @hiero-ledger/hiero-sdk-good-first-issue-support team. - - You can also join our community on Discord: - [Hiero-SDK-C++](https://discord.com/channels/905194001349627914/1337424839761465364) - - Maintainers are happy to help first-time contributors succeed! \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/03_beginner_issue.yml b/.github/ISSUE_TEMPLATE/03_beginner_issue.yml deleted file mode 100644 index b206d6df9..000000000 --- a/.github/ISSUE_TEMPLATE/03_beginner_issue.yml +++ /dev/null @@ -1,217 +0,0 @@ -name: Beginner Issue Template -description: Create a Beginner Issue for fairly new contributors (C++) -title: "[Beginner]: " -labels: ["beginner"] -assignees: [] - -body: - - type: markdown - attributes: - value: | - --- - ## **Thanks for contributing!** 😊 - - We truly appreciate your time and effort. - - This template is designed to help you create a **Beginner-friendly issue** β€” - a moderately easy, well-scoped task that helps contributors build confidence - and deepen their understanding of the **Hiero C++ SDK** codebase. - - > Maintainers can read more about: - > [Beginner Issues](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/docs/maintainers/guidelines-beginner-issues.md) - --- - - - type: textarea - id: intro - attributes: - label: πŸ₯ Beginner Friendly - description: Who is this issue for? - value: | - This issue is a great fit for contributors who are ready to explore the Hiero C++ - codebase a little more and take on slightly more independent work. - - Beginner Issues often involve reading existing C++ code, understanding how - different parts of the SDK fit together, and making small, thoughtful updates - that follow established patterns. - - The goal is to support skill growth while keeping the experience approachable, - well-scoped, and enjoyable. - validations: - required: false - - - type: markdown - attributes: - value: | - > [!IMPORTANT] - > ### πŸ₯ About Beginner Issues - > - > Beginner Issues are a great next step for contributors who feel comfortable - > with the basic project workflow and want to explore the codebase a little more. - > - > These issues often involve: - > - Reading existing C++ code - > - Understanding how different parts of the SDK fit together - > - Making small, thoughtful updates that follow established patterns - > - > You’ll usually see Beginner Issues focused on things like: - > - Small, well-scoped improvements to existing tests - > - Narrow updates to `src` functionality (e.g. refining helpers or improving readability) - > - Documentation or comment clarity - > - Enhancements to existing examples - > - > Other types of contributions β€” such as brand-new features, broader system changes, - > or deeper technical work β€” are just as valuable and may use different labels. - - - type: textarea - id: issue - attributes: - label: πŸ‘Ύ Description of the Issue - description: | - Describe the issue in a clear, approachable way. - - Aim to explain the context, what needs to change, and the expected outcome - without assuming deep knowledge of the codebase. The goal is to make the - task feel understandable, well-scoped, and encouraging to take on. - value: | - Edit here. Example provided below. - validations: - required: true - - - type: markdown - attributes: - value: | - - ## πŸ‘Ύ Description of the Issue β€” Example (C++) - - The `AccountId` class located in: - - ``` - src/sdk/main/include/hiero/account/AccountId.h - src/sdk/main/src/account/AccountId.cc - ``` - - currently provides a `toString()` method, but when `AccountId` objects are - logged or inspected during debugging, the output is not very descriptive. - - Improving the readability of this output would make debugging and logging - easier for developers using the SDK. - - - type: textarea - id: solution - attributes: - label: πŸ’‘ Proposed Solution - description: | - Describe the approach for addressing this issue in a clear and focused way. - - Explain what will be changed and what the end result should look like, - keeping the scope straightforward and easy to follow. - value: | - Edit here. An example is provided below. - validations: - required: true - - - type: markdown - attributes: - value: | - - ## πŸ’‘ Proposed Solution β€” Example (C++) - - Update the existing `toString()` method in `AccountId` to produce a more - descriptive and readable string representation that clearly shows the - shard, realm, and account number. - - The change should: - - Preserve existing behavior - - Avoid public API changes - - Follow formatting patterns used by other ID classes - - Example output: - - ``` - AccountId{shard=0, realm=0, num=1234} - ``` - - - type: textarea - id: implementation - attributes: - label: πŸ‘©β€πŸ’» Implementation Steps - description: | - Walk through how to implement the solution in clear, easy-to-follow steps. - - Beginner Issues often leave a little more room for exploration than Good First - Issues, so it’s helpful to describe the overall approach, key files to look at, - what to change, and how to check the result. - value: | - Edit here. Example provided below. - validations: - required: true - - - type: markdown - attributes: - value: | - - ### πŸ‘©β€πŸ’» Implementation β€” Example (C++) - - To implement this improvement: - - - [ ] Locate the `AccountId` class in the `src/` directory - - [ ] Review the existing `toString()` implementation - - [ ] Review how other ID classes format their string output - - [ ] Update `toString()` to: - - Clearly display `shard`, `realm`, and `num` - - Use a consistent, readable format - - [ ] Update or extend existing unit tests to validate the new output - - [ ] Build the SDK and ensure all tests pass - - - type: textarea - id: acceptance-criteria - attributes: - label: βœ… Acceptance Criteria - description: Checklist required for completing this issue - value: | - To help get this change merged smoothly: - - - [ ] **Scope:** Changes are limited to this issue - - [ ] **Behavior:** No other SDK behavior or API changes - - [ ] **Tests:** Existing and any new tests pass - - [ ] **Review:** All code review feedback addressed - validations: - required: true - - - type: textarea - id: contribution_steps - attributes: - label: πŸ“‹ Step-by-Step Contribution Guide - description: Contribution workflow for contributors - value: | - To help keep contributions consistent and easy to review, we recommend following these steps: - - - [ ] Comment `/assign` to request the issue - - [ ] Wait for assignment - - [ ] Fork the repository and create a branch - - [ ] Set up the project using the instructions in `README.md` - - [ ] Make the requested changes - - [ ] Sign each commit using `-s -S` - - [ ] Push your branch and open a pull request - - Read [Workflow Guide](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/docs/training/workflow.md) for step-by-step workflow guidance. - Read [README.md](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/README.md) for setup instructions. - - ❗ Pull requests **cannot be merged** without `S` and `s` signed commits. - See the [Signing Guide](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/docs/training/signing.md). - validations: - required: true - - - type: textarea - id: information - attributes: - label: πŸ€” Additional Information - description: Extra context or resources for contributors - value: | - If you have questions while working on this issue, feel free to ask! - - You can reach the community and maintainers here: - [Hiero-SDK-C++ Discord](https://discord.com/channels/905194001349627914/1337424839761465364) - - Whether you need help finding the right file, understanding existing code, - or confirming your approach β€” we’re happy to help. \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/04_intermediate_issue.yml b/.github/ISSUE_TEMPLATE/04_intermediate_issue.yml deleted file mode 100644 index 98655c4ad..000000000 --- a/.github/ISSUE_TEMPLATE/04_intermediate_issue.yml +++ /dev/null @@ -1,221 +0,0 @@ -name: Intermediate Issue Template -description: Create a well-documented issue for contributors with some familiarity with the codebase (C++) -title: "[Intermediate]: " -labels: ["intermediate"] -assignees: [] - -body: - - type: markdown - attributes: - value: | - --- - ## **Thanks for contributing!** 😊 - - We truly appreciate your time and effort. - - This template is here to help you create an **Intermediate-level issue** for - contributors who are comfortable with the basics and ready to take on a bit - more ownership in the **Hiero C++ SDK** codebase. - - These issues usually involve a mix of exploration, reasoning, and implementation, - while still having clear goals and boundaries. - - > Maintainers can read more about: - > [Intermediate Issues](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/docs/maintainers/guidelines-intermediate-issues.md) - --- - - - type: textarea - id: intro - attributes: - label: 🧩 Intermediate Friendly - description: Who is this issue for? - value: | - This issue is a good fit for contributors who are already familiar with the - Hiero C++ SDK and feel comfortable navigating the codebase. - - Intermediate Issues often involve: - - Exploring existing implementations - - Understanding how different components work together - - Making thoughtful changes that follow established patterns - - The goal is to support deeper problem-solving while keeping the task clear, - focused, and enjoyable to work on. - validations: - required: false - - - type: markdown - attributes: - value: | - > [!IMPORTANT] - > ### 🧭 About Intermediate Issues - > - > Intermediate Issues are a great next step for contributors who enjoy - > digging into the codebase and reasoning about how things work. - > - > These issues often: - > - Involve multiple related files or components - > - Encourage investigation and understanding of existing behavior - > - Leave room for thoughtful implementation choices - > - Stay focused on a clearly defined goal - > - > Other kinds of contributions β€” from beginner-friendly tasks to large - > system-level changes β€” are just as valuable and use different labels. - - - type: textarea - id: problem - attributes: - label: 🐞 Problem Description - description: | - Describe the problem clearly and precisely. - - Try to explain: - - What is missing, unclear, or not working as expected - - Where it lives in the codebase - - Why it matters to users or contributors - value: | - Describe the problem here. - validations: - required: true - - - type: markdown - attributes: - value: | - - ## 🐞 Problem – Example (C++) - - Several query classes in the SDK expose similar configuration methods, - but `TransactionReceiptQuery` is missing validation that is present - in other query implementations. - - For example, other query classes validate that a transaction ID has - been set before executing the query, but `TransactionReceiptQuery` - does not currently perform this check. - - This can lead to unclear runtime errors when the query is executed - without a transaction ID. - - Relevant files: - - `src/sdk/main/include/hiero/query/TransactionReceiptQuery.h` - - `src/sdk/main/src/query/TransactionReceiptQuery.cc` - - - type: textarea - id: solution - attributes: - label: πŸ’‘ Expected Outcome - description: | - Describe what a good solution would look like. - - While contributors can make reasonable implementation choices, this section - should clearly explain the desired behavior and constraints, including: - - What should change - - What should stay the same - - Any important constraints, conventions, or goals - value: | - Describe the expected outcome here. - validations: - required: true - - - type: markdown - attributes: - value: | - - ## πŸ’‘ Expected Outcome – Example (C++) - - Ensure that `TransactionReceiptQuery` validates that a transaction ID - has been set before execution. - - The change should: - - Match the validation behavior used in similar query classes - (for example, `TransactionRecordQuery`) - - Produce a **clear and actionable error message**, meaning: - - The error explains *what is missing* - - The error helps the user understand *how to fix it* - - Avoid changing the public API - - Not affect successful execution paths - - - type: textarea - id: implementation - attributes: - label: 🧠 Implementation Notes - description: | - Share helpful technical context to guide implementation. - - This section is optional but recommended for Intermediate issues. - - For example: - - Which files or modules are likely involved - - Similar patterns elsewhere in the codebase - - Things to be careful about - - Known edge cases or constraints - value: | - Add implementation notes here. - validations: - required: false - - - type: markdown - attributes: - value: | - - ## 🧠 Implementation Notes – Example (C++) - - Suggested approach: - - - Review other query classes that require a transaction ID - (for example, `TransactionRecordQuery`) - - Identify where validation is performed before execution - - Add a similar validation step to `TransactionReceiptQuery` - - Ensure the error message format matches existing patterns - - Add or update a unit test to cover the missing-transaction-ID case - - - type: textarea - id: acceptance-criteria - attributes: - label: βœ… Acceptance Criteria - description: Define what β€œdone” looks like for this issue - value: | - To help get this change merged smoothly: - - - [ ] Address the problem described above - - [ ] Follow existing project conventions - - [ ] Avoid breaking public APIs - - [ ] Include tests and/or example updates where appropriate - - [ ] Pass all CI checks - validations: - required: true - - - type: textarea - id: contribution_steps - attributes: - label: πŸ“‹ Contribution Guide - description: Contribution workflow for intermediate contributors - value: | - To help your contribution go as smoothly as possible, we recommend following these steps: - - - [ ] Comment `/assign` to request the issue - - [ ] Wait for assignment - - [ ] Fork the repository and create a branch - - [ ] Set up the project using the instructions in `README.md` - - [ ] Make the requested changes - - [ ] Sign each commit using `-s -S` - - [ ] Push your branch and open a pull request - - Read [Workflow Guide](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/docs/training/workflow.md) for step-by-step workflow guidance. - Read [README.md](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/README.md) for setup instructions. - - ❗ Pull requests **cannot be merged** without `S` and `s` signed commits. - See the [Signing Guide](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/docs/training/signing.md). - validations: - required: true - - - type: textarea - id: additional-info - attributes: - label: πŸ“š Additional Context or Resources - description: | - Add any links, references, or extra notes that may help. - value: | - If you have questions, the community is happy to help: - - https://discord.com/channels/905194001349627914/1337424839761465364 - validations: - required: false \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/05_advanced_issue.yml b/.github/ISSUE_TEMPLATE/05_advanced_issue.yml deleted file mode 100644 index 6103107f2..000000000 --- a/.github/ISSUE_TEMPLATE/05_advanced_issue.yml +++ /dev/null @@ -1,212 +0,0 @@ -name: Advanced Issue Template -description: Create a high-impact issue for experienced contributors with deep familiarity with the codebase (C++) -title: "[Advanced]: " -labels: ["advanced"] -assignees: [] - -body: - - type: markdown - attributes: - value: | - --- - ## **Thanks for contributing!** πŸš€ - - We really appreciate your interest in working on an **Advanced issue**. - - This template is for larger, more impactful tasks that shape how the - **Hiero C++ SDK** evolves over time. These issues often involve deeper - design thinking, broader context, and long-term considerations. - - They’re a great fit for contributors who enjoy tackling complex problems - and helping guide the direction of the project. - - > Maintainers can read more about: - > [Advanced Issues](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/docs/maintainers/guidelines-advanced-issues.md) - --- - - - type: textarea - id: intro - attributes: - label: 🧠 Advanced - description: Who is this issue for? - value: | - This issue is well-suited for contributors who are very familiar with the - Hiero C++ SDK and enjoy working with its core abstractions and design patterns. - - Advanced Issues often involve: - - Exploring and shaping SDK architecture - - Reasoning about trade-offs and long-term impact - - Working across multiple modules or systems - - Updating tests, examples, and documentation alongside code - - The goal is to support thoughtful, high-impact contributions in a clear - and collaborative way. - validations: - required: false - - - type: markdown - attributes: - value: | - > [!IMPORTANT] - > ### 🧭 About Advanced Issues - > - > Advanced Issues usually focus on larger changes that influence how the - > SDK works as a whole. - > - > These issues often: - > - Touch core abstractions or shared utilities - > - Span multiple parts of the codebase - > - Involve design decisions and trade-offs - > - Consider long-term maintainability and compatibility - > - > Smaller fixes, focused refactors, and onboarding-friendly tasks are just - > as valuable and often use different labels. - - - type: textarea - id: problem - attributes: - label: 🐞 Problem Description - description: | - Describe the problem in detail. - - Try to explain: - - What the current behavior is - - Why it could be improved - - Which parts of the SDK are involved - - Any important context or background - value: | - Describe the problem here. - validations: - required: true - - - type: markdown - attributes: - value: | - - ## 🐞 Problem – Example (C++) - - The current transaction execution flow in the C++ SDK tightly couples - transaction submission, receipt polling, record retrieval, and retry logic - into a single execution path. - - This coupling makes it difficult to: - - customize retry behavior - - extend execution semantics for scheduled transactions - - introduce mirror-node-backed execution strategies - - test individual execution stages in isolation - - Several planned SDK enhancements would benefit from a clearer - separation of concerns in this area. - - Relevant areas: - - `src/sdk/main/src/transaction/` - - `src/sdk/main/src/execution/` - - `src/sdk/main/src/client/` - - - type: textarea - id: solution - attributes: - label: πŸ’‘ Proposed / Expected Outcome - description: | - Describe the intended direction or design. - - Helpful things to include: - - The overall approach - - Any new or updated abstractions - - Important constraints (e.g. compatibility, performance) - - Alternatives considered, if relevant - value: | - Describe the proposed solution here. - validations: - required: true - - - type: markdown - attributes: - value: | - - ## πŸ’‘ Proposed Solution – Example (C++) - - Introduce a dedicated **transaction execution pipeline** abstraction - that separates the following concerns: - - - transaction submission - - receipt polling - - record retrieval - - retry and timeout logic - - The new design should: - - preserve existing public APIs - - allow advanced users to override or extend execution behavior - - make individual execution stages independently testable - - Existing transaction execution logic should be refactored - to use the new pipeline internally. - - - type: textarea - id: implementation - attributes: - label: 🧠 Implementation & Design Notes - description: | - Share detailed technical guidance to support implementation. - - You might include: - - Key modules or classes involved - - Suggested refactoring approach - - Migration or compatibility considerations - - Testing strategy - - Performance, concurrency, or security notes - value: | - Add detailed implementation notes here. - validations: - required: false - - - type: markdown - attributes: - value: | - - ## 🧠 Implementation Notes – Example (C++) - - Suggested approach: - - - Introduce a new `ExecutionPipeline` abstraction under: - `src/sdk/main/src/execution/` - - Refactor existing transaction execution code to delegate - to this pipeline - - Preserve existing public APIs and default behavior - - Add focused unit tests for each pipeline stage - - Update at least one example to demonstrate extensibility - - Care must be taken to preserve timeout semantics and retry behavior - relied upon by existing SDK users. - - - type: textarea - id: acceptance-criteria - attributes: - label: βœ… Acceptance Criteria - description: Define what β€œdone” looks like for this issue - value: | - A pull request for this issue should: - - - [ ] Address the problem and goals described above - - [ ] Maintain backwards compatibility unless discussed otherwise - - [ ] Follow existing architectural and C++ conventions - - [ ] Include comprehensive tests - - [ ] Update relevant examples and documentation - - [ ] Pass all CI checks - validations: - required: true - - - type: textarea - id: additional-info - attributes: - label: πŸ“š Additional Context, Links, or Prior Art - description: | - Add any references that may help: - - design documents - - prior discussions - - related issues or pull requests - - external resources - value: | - Optional. - validations: - required: false diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 90feb626f..c5df02a5d 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -1,73 +1,164 @@ -name: Bug report -description: Create a report to help us improve -labels: [ bug ] +name: Bug Report +description: Report a bug or unexpected behavior in the Hiero C++ SDK +labels: ['status: awaiting triage'] type: Bug body: - type: markdown attributes: value: | - ## Thanks for submitting a bug report! + --- + ## Thanks for submitting a bug report! πŸ› + + We appreciate your help in making the Hiero C++ SDK better. Before submitting: - 1. Try searching the existing issues and discussions to see if your issue has already been reported - 2. Try asking on [Discord's hiero-general channel](https://discord.gg/hyperledger) - 3. If you're reporting a security vulnerability, please disclose responsibly via our [bug bounty program](https://hedera.com/bounty) + - Search [existing issues](https://github.com/hiero-ledger/hiero-sdk-cpp/issues) to check if this has been reported before + - For security vulnerabilities, please follow our [responsible disclosure process](https://hedera.com/bounty) instead of opening a public issue + + A detailed bug report helps us identify and fix the problem faster. Please be as specific as possible. + --- + - type: textarea id: description attributes: - label: Description - description: What happened and what you did you expect to happen? + label: πŸ‘Ύ Description of the Issue + description: | + Briefly describe what is wrong. What did you observe, and what did you expect instead? + Include relevant file paths, class names, or method names if you know them. + value: | + Describe the bug here. validations: required: true + + - type: markdown + attributes: + value: | + + ## πŸ‘Ύ Description β€” Example (C++) + + When calling `client.setDefaultMaxTransactionFee()` with a `Hbar` value of `0`, the SDK + silently ignores the value and continues using the previously configured fee. The method + should either accept `0` as a valid fee cap or throw a clear error. + + Relevant files: + ``` + src/sdk/main/include/Client.h + src/sdk/main/src/Client.cc + ``` + - type: textarea id: steps attributes: - label: Steps to reproduce - description: Steps to reproduce the behavior - placeholder: | - 1. Run the program - 2. Click on '...' - 3. Scroll down to '...' + label: πŸ” Steps to Reproduce + description: | + Provide clear, numbered steps that consistently reproduce the issue. + Include any relevant code, command-line invocations, or configuration. + value: | + 1. + 2. + 3. validations: required: true + + - type: markdown + attributes: + value: | + + ## πŸ” Steps to Reproduce β€” Example (C++) + + ```cpp + auto client = Client::forTestnet(); + client.setOperator(MY_ACCOUNT_ID, MY_PRIVATE_KEY); + client.setDefaultMaxTransactionFee(Hbar(0)); + + // Expect this to fail due to zero max fee, but it succeeds + auto response = TransferTransaction() + .addHbarTransfer(MY_ACCOUNT_ID, Hbar(-1)) + .addHbarTransfer(RECIPIENT_ID, Hbar(1)) + .execute(client); + ``` + + Running the above always succeeds, even though a max fee of `0` should prohibit it. + - type: textarea - id: context + id: expected attributes: - label: Additional context - description: Attach any logs or screenshots relevent to the problem. - placeholder: | - ![Screenshot](bug.png) + label: βœ… Expected Behavior + description: What should have happened? + value: | + Describe the expected behavior here. + validations: + required: true - ```bash - 2021-06-29T13:50:45.008-0600 INFO thread-1 Some logs + - type: markdown + attributes: + value: | + + ## βœ… Expected Behavior β€” Example + + `client.setDefaultMaxTransactionFee(Hbar(0))` should either: + - Accept `0` as a valid cap and reject any transaction whose actual fee exceeds `0` + - Throw a `std::invalid_argument` with a descriptive message explaining that `0` is not valid + + - type: textarea + id: actual + attributes: + label: ❌ Actual Behavior + description: | + What actually happened? Include error messages, stack traces, or unexpected output. + value: | + Describe the actual behavior here. + validations: + required: true + + - type: markdown + attributes: + value: | + + ## ❌ Actual Behavior β€” Example + + The fee cap is silently ignored. No error is thrown, and the transaction executes + as if `setDefaultMaxTransactionFee` was never called. + + Console output: ``` - - type: dropdown - id: network + [INFO] Transaction submitted: 0.0.1234@1700000000.000000000 + [INFO] Receipt status: SUCCESS + ``` + + - type: textarea + id: environment attributes: - description: Which network(s) did the issue occur on? - label: Hedera network - multiple: true - options: - - mainnet - - testnet - - previewnet - - other - - type: input - id: version + label: 🌐 Environment + description: Provide details about the environment where the bug occurred. + value: | + - SDK version: + - Operating system: + - Compiler and version (e.g. GCC 13.2, Clang 17, MSVC 19.38): + - Hiero network (mainnet / testnet / previewnet / solo): + validations: + required: true + + - type: textarea + id: acceptance-criteria attributes: - description: What version of the software are you using? - label: Version - placeholder: v0.1.0 + label: βœ”οΈ Acceptance Criteria + description: What needs to be true for this bug to be considered fixed? + value: | + - [ ] The described behavior is no longer reproducible + - [ ] A regression test is added that fails before the fix and passes after + - [ ] No unrelated behavior or API changes are introduced + - [ ] All existing tests continue to pass validations: required: true - - type: dropdown - id: os + + - type: textarea + id: additional attributes: - description: Which OS did the issue occur on? - label: Operating system - options: - - Linux - - macOS - - Windows - - Other - \ No newline at end of file + label: πŸ€” Additional Information + description: | + Anything else that might help: logs, screenshots, related issues, bisected commits, etc. + value: | + Optional. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index 1801a830f..8635e2784 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -1,39 +1,143 @@ -name: Feature -description: Suggest an idea for this project -labels: [ enhancement ] +name: Feature Request +description: Propose a new capability for the Hiero C++ SDK +labels: ['status: awaiting triage'] type: Feature body: - type: markdown attributes: value: | - ## Thanks for submitting a feature request! + --- + ## Thanks for proposing a feature! πŸ’‘ + + We appreciate ideas that help make the Hiero C++ SDK more capable and useful. Before submitting: - * Try searching the existing issues and discussions to see if something similar has been discussed previously - * Try opening a GitHub discussion first to gather feedback and reach consensus - * If this is a major enhancement, please open a [Hiero Improvement Proposal](https://github.com/hiero-ledger/hiero-improvement-proposals) + - Search [existing issues](https://github.com/hiero-ledger/hiero-sdk-cpp/issues) to see if this has been proposed before + - For large or protocol-level changes, consider opening a [Hiero Improvement Proposal](https://github.com/hiero-ledger/hiero-improvement-proposals) or starting a [GitHub Discussion](https://github.com/hiero-ledger/hiero-sdk-cpp/discussions) first + + A well-described feature request makes it much easier for maintainers to evaluate, plan, and implement. + --- + - type: textarea id: problem attributes: - label: Problem - description: What is the problem you are trying to solve? - placeholder: I'm always frustrated when ... + label: πŸ‘Ύ Problem Description + description: | + What problem are you trying to solve? Describe the need or gap clearly. + Focus on the problem itself β€” the proposed solution comes next. + value: | + Describe the problem here. validations: required: true + + - type: markdown + attributes: + value: | + + ## πŸ‘Ύ Problem β€” Example (C++) + + The Hiero C++ SDK doesn't provide a way to batch multiple queries into a single network + round-trip. Applications that need to fetch balances and transaction records for many + accounts must send individual queries, leading to high latency and unnecessary network + overhead in production environments. + - type: textarea id: solution attributes: - label: Solution - description: What solution do you propose to solve the problem? - placeholder: | - - Add a config property - - Change the schema - - ... + label: πŸ’‘ Proposed Solution + description: | + Describe the feature you'd like to see. What would the API or behavior look like? + Include examples, pseudocode, or sketches that help illustrate the idea. + value: | + Describe the proposed solution here. validations: required: true + + - type: markdown + attributes: + value: | + + ## πŸ’‘ Proposed Solution β€” Example (C++) + + Add a `BatchQuery` class that accepts multiple query objects and executes them in parallel, + returning all results when the last one completes: + + ```cpp + auto results = BatchQuery() + .add(AccountBalanceQuery().setAccountId(id1)) + .add(AccountBalanceQuery().setAccountId(id2)) + .add(AccountInfoQuery().setAccountId(id3)) + .execute(client); + + // Access results by index + auto balance1 = results.get(0); + auto info3 = results.get(2); + ``` + + Each result would be accessible by index. The implementation should preserve existing + query behavior and stay compatible with the current `Query` hierarchy. + - type: textarea id: alternatives attributes: - label: Alternatives - description: What alternative solutions have you considered? - \ No newline at end of file + label: πŸ”„ Alternatives Considered + description: | + What other approaches did you consider? Why is the proposed solution preferred? + Write "None" if no alternatives were considered. + value: | + Describe alternatives here, or write "None" if not applicable. + validations: + required: false + + - type: textarea + id: implementation-notes + attributes: + label: πŸ‘©β€πŸ’» Implementation Notes + description: | + Any hints about where or how this might be implemented in the SDK? + This is optional β€” maintainers will fill in detailed implementation guidance during triage. + value: | + Optional. Add any hints here. + validations: + required: false + + - type: markdown + attributes: + value: | + + ## πŸ‘©β€πŸ’» Implementation Notes β€” Example (C++) + + This would likely build on the existing `Query` base class in: + ``` + src/sdk/main/include/Query.h + src/sdk/main/src/Query.cc + ``` + + The `BatchQuery` class could use `std::async` or the existing gRPC async infrastructure + to parallelize the requests, similar to how the current execution layer works. + + - type: textarea + id: acceptance-criteria + attributes: + label: βœ”οΈ Acceptance Criteria + description: What does "done" look like for this feature? + value: | + - [ ] The feature works as described in the proposal + - [ ] Unit and/or integration tests are added + - [ ] At least one example is added or updated to demonstrate the feature + - [ ] Public API documentation (header comments) is complete + - [ ] All existing tests continue to pass + validations: + required: true + + - type: textarea + id: additional + attributes: + label: πŸ€” Additional Information + description: | + Links to prior art, related HIPs, discussions, or any other context that might + be helpful for evaluating this request. + value: | + Optional. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/task.yml b/.github/ISSUE_TEMPLATE/task.yml new file mode 100644 index 000000000..4e7e9f508 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/task.yml @@ -0,0 +1,132 @@ +name: Task +description: Propose a maintenance, improvement, or operational task for the Hiero C++ SDK +labels: ['status: awaiting triage'] +type: Task +body: + - type: markdown + attributes: + value: | + --- + ## Thanks for contributing! πŸ”§ + + Tasks cover maintenance, quality, tooling, and improvement work that keeps the + Hiero C++ SDK healthy and easy to contribute to. + + Examples of tasks: + - Refactoring or improving existing code + - Updating dependencies or build tooling + - Improving test coverage or CI pipelines + - Clarifying documentation or code comments + - Enhancements to existing features (without adding new public API) + + Before submitting, search [existing issues](https://github.com/hiero-ledger/hiero-sdk-cpp/issues) + to avoid duplicating work in progress. + --- + + - type: textarea + id: description + attributes: + label: πŸ‘Ύ Description of the Task + description: | + What needs to be done and why? Describe the current state and what should change. + Include relevant file paths, class names, or modules if you know them. + value: | + Describe the task here. + validations: + required: true + + - type: markdown + attributes: + value: | + + ## πŸ‘Ύ Description β€” Example (C++) + + The `vcpkg.json` manifest currently pins `nlohmann-json` to version `3.10.5`. Version + `3.11.3` is now available and includes important bug fixes for edge cases in JSON + serialization that affect how we handle network node address book responses. + + Updating to `3.11.3` will improve correctness and keep our dependencies current. + + Relevant files: + ``` + vcpkg.json + ``` + + - type: textarea + id: approach + attributes: + label: πŸ’‘ Proposed Approach + description: | + How should this task be accomplished? Describe the intended strategy and any + constraints or tradeoffs to be aware of. + value: | + Describe the proposed approach here. + validations: + required: true + + - type: markdown + attributes: + value: | + + ## πŸ’‘ Proposed Approach β€” Example + + Update the `nlohmann-json` version constraint in `vcpkg.json` from `3.10.5` to `3.11.3`, + rebuild the SDK, and verify that all existing tests continue to pass. Review the library's + changelog for any breaking changes that might require code updates. + + - type: textarea + id: implementation + attributes: + label: πŸ‘©β€πŸ’» Implementation Steps + description: | + Walk through how to complete this task step by step. + Use checkboxes for actionable items. + value: | + - [ ] Step 1 + - [ ] Step 2 + - [ ] Step 3 + validations: + required: true + + - type: markdown + attributes: + value: | + + ## πŸ‘©β€πŸ’» Implementation Steps β€” Example + + - [ ] Review the [nlohmann-json changelog](https://github.com/nlohmann/json/releases) for v3.10.5 β†’ v3.11.3 + - [ ] Update the `nlohmann-json` version in `vcpkg.json` + - [ ] Rebuild the SDK: + ```bash + cmake --preset linux-x64-debug && cmake --build --preset linux-x64-debug + ``` + - [ ] Run the full test suite: + ```bash + ctest -j6 -C Debug --test-dir build/linux-x64-debug + ``` + - [ ] Address any compilation or test failures caused by the update + - [ ] Open a pull request with the change + + - type: textarea + id: acceptance-criteria + attributes: + label: βœ”οΈ Acceptance Criteria + description: What does "done" look like for this task? + value: | + - [ ] The task is completed as described + - [ ] All existing tests continue to pass + - [ ] No unrelated behavior or API changes are introduced + - [ ] Relevant documentation is updated if applicable + validations: + required: true + + - type: textarea + id: additional + attributes: + label: πŸ€” Additional Information + description: | + Links to changelogs, related issues, PRs, or any other relevant context. + value: | + Optional. + validations: + required: false diff --git a/.github/scripts/bot-on-comment.js b/.github/scripts/bot-on-comment.js index 3d88dfe5f..73427f83f 100644 --- a/.github/scripts/bot-on-comment.js +++ b/.github/scripts/bot-on-comment.js @@ -3,14 +3,17 @@ // bot-on-comment.js // // Handles issue comment events: reads the comment body, parses commands, and dispatches -// to the appropriate handler. Implemented commands: /assign, /unassign. +// to the appropriate handler. Implemented commands: /assign, /unassign, /finalize. // -// /assign: see commands/assign.js (skill levels, assignment limits, required labels). +// /assign: see commands/assign.js (skill levels, assignment limits, required labels). // /unassign: see commands/unassign.js (authorization, label reversion). +// /finalize: see commands/finalize.js (triage permission required; validates labels, +// updates issue title/body with skill-level format, swaps status labels). const { createLogger, buildBotContext } = require('./helpers'); const { handleAssign } = require('./commands/assign'); -const { handleUnassign } = require('./commands/unassign'); +const { handleUnassign } = require('./commands/unassign'); +const { handleFinalize } = require('./commands/finalize'); let logger = createLogger('on-comment'); @@ -33,10 +36,14 @@ function parseComment(body) { logger.log('parseComment: detected /assign'); return { commands: ['assign'] }; } - if (/^\s*\/unassign\s*$/i.test(body)) { + if (/^\s*\/unassign\s*$/i.test(body)) { logger.log('parseComment: detected /unassign'); return { commands: ['unassign'] }; } + if (/^\s*\/finalize\s*$/i.test(body)) { + logger.log('parseComment: detected /finalize'); + return { commands: ['finalize'] }; + } logger.log('parseComment: no known command', { body: body.substring(0, 80) }); return { commands: [] }; } @@ -78,6 +85,8 @@ module.exports = async ({ github, context }) => { await handleAssign(botContext); } else if (command === 'unassign') { await handleUnassign(botContext); + } else if (command === 'finalize') { + await handleFinalize(botContext); } else { logger.log('Unknown command:', command); } diff --git a/.github/scripts/commands/finalize-comments.js b/.github/scripts/commands/finalize-comments.js new file mode 100644 index 000000000..e6aa97f27 --- /dev/null +++ b/.github/scripts/commands/finalize-comments.js @@ -0,0 +1,448 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// commands/finalize-comments.js +// +// Comment builders and per-skill-level boilerplate for the /finalize command. +// Pure formatting functions separated from finalize logic for readability and testability. + +const { MAINTAINER_TEAM, LABELS } = require('../helpers'); + +// ============================================================================= +// TITLE PREFIX MAP +// ============================================================================= + +/** + * Maps skill-level label constants to their title prefix strings. + * Used by /finalize to update the issue title. + * @type {Object} + */ +const SKILL_TITLE_PREFIXES = { + [LABELS.GOOD_FIRST_ISSUE]: '[Good First Issue]: ', + [LABELS.BEGINNER]: '[Beginner]: ', + [LABELS.INTERMEDIATE]: '[Intermediate]: ', + [LABELS.ADVANCED]: '[Advanced]: ', +}; + +// ============================================================================= +// SKILL-LEVEL BOILERPLATE BLOCKS +// ============================================================================= +// Each entry mirrors the intro textarea and [!IMPORTANT] markdown block from +// the original skill-level issue YAML templates. Used by /finalize to prepend +// the appropriate skill context to the issue body. + +/** + * Per-skill-level boilerplate for body reconstruction. + * - introLabel: the H3 header label for the intro section + * - introContent: the body of the intro textarea + * - importantBlock: the [!IMPORTANT] GitHub callout markdown + * + * @type {Object} + */ +const SKILL_BOILERPLATE = { + [LABELS.GOOD_FIRST_ISSUE]: { + introLabel: 'πŸ†•πŸ₯ First-Time Friendly', + introContent: [ + 'This issue is especially welcoming for people who are new to contributing to the **Hiero C++ SDK**.', + '', + 'We know that opening your first pull request can feel like a big step. Issues labeled **Good First Issue** are designed to make that experience easier, clearer, and more comfortable.', + '', + 'No prior knowledge of Hiero, Hedera, or distributed ledger technology is required - just a basic familiarity with C++ and Git is more than enough to get started.', + ].join('\n'), + importantBlock: [ + '> [!IMPORTANT]', + '> ### πŸ“‹ About Good First Issues', + '>', + '> Good First Issues are designed to make getting started as smooth and stress-free as possible.', + '>', + '> They usually focus on:', + '> - Small, clearly scoped changes ', + '> - Straightforward updates to existing code or docs ', + '> - Simple refactors or clarity improvements ', + '>', + '> Other kinds of contributions β€” like larger features, deeper technical changes, or design-focused work β€” are just as valuable and often use the beginner, intermediate, or advanced labels.', + ].join('\n'), + }, + + [LABELS.BEGINNER]: { + introLabel: 'πŸ₯ Beginner Friendly', + introContent: [ + 'This issue is a great fit for contributors who are ready to explore the Hiero C++ codebase a little more and take on slightly more independent work.', + '', + 'Beginner Issues often involve reading existing C++ code, understanding how different parts of the SDK fit together, and making small, thoughtful updates that follow established patterns.', + '', + 'The goal is to support skill growth while keeping the experience approachable, well-scoped, and enjoyable.', + ].join('\n'), + importantBlock: [ + '> [!IMPORTANT]', + '> ### πŸ₯ About Beginner Issues', + '>', + '> Beginner Issues are a great next step for contributors who feel comfortable with the basic project workflow and want to explore the codebase a little more.', + '>', + '> These issues often involve:', + '> - Reading existing C++ code ', + '> - Understanding how different parts of the SDK fit together ', + '> - Making small, thoughtful updates that follow established patterns ', + '>', + '> You\'ll usually see Beginner Issues focused on things like:', + '> - Small, well-scoped improvements to existing tests ', + '> - Narrow updates to `src` functionality (e.g. refining helpers or improving readability) ', + '> - Documentation or comment clarity ', + '> - Enhancements to existing examples ', + '>', + '> Other types of contributions β€” such as brand-new features, broader system changes, or deeper technical work β€” are just as valuable and may use different labels.', + ].join('\n'), + }, + + [LABELS.INTERMEDIATE]: { + introLabel: '🧩 Intermediate Friendly', + introContent: [ + 'This issue is a good fit for contributors who are already familiar with the Hiero C++ SDK and feel comfortable navigating the codebase.', + '', + 'Intermediate Issues often involve:', + '- Exploring existing implementations ', + '- Understanding how different components work together ', + '- Making thoughtful changes that follow established patterns ', + '', + 'The goal is to support deeper problem-solving while keeping the task clear, focused, and enjoyable to work on.', + ].join('\n'), + importantBlock: [ + '> [!IMPORTANT]', + '> ### 🧭 About Intermediate Issues', + '>', + '> Intermediate Issues are a great next step for contributors who enjoy digging into the codebase and reasoning about how things work.', + '>', + '> These issues often:', + '> - Involve multiple related files or components ', + '> - Encourage investigation and understanding of existing behavior ', + '> - Leave room for thoughtful implementation choices ', + '> - Stay focused on a clearly defined goal ', + '>', + '> Other kinds of contributions β€” from beginner-friendly tasks to large system-level changes β€” are just as valuable and use different labels.', + ].join('\n'), + }, + + [LABELS.ADVANCED]: { + introLabel: '🧠 Advanced', + introContent: [ + 'This issue is well-suited for contributors who are very familiar with the Hiero C++ SDK and enjoy working with its core abstractions and design patterns.', + '', + 'Advanced Issues often involve:', + '- Exploring and shaping SDK architecture ', + '- Reasoning about trade-offs and long-term impact ', + '- Working across multiple modules or systems ', + '- Updating tests, examples, and documentation alongside code ', + '', + 'The goal is to support thoughtful, high-impact contributions in a clear and collaborative way.', + ].join('\n'), + importantBlock: [ + '> [!IMPORTANT]', + '> ### 🧭 About Advanced Issues', + '>', + '> Advanced Issues usually focus on larger changes that influence how the SDK works as a whole.', + '>', + '> These issues often:', + '> - Touch core abstractions or shared utilities ', + '> - Span multiple parts of the codebase ', + '> - Involve design decisions and trade-offs ', + '> - Consider long-term maintainability and compatibility ', + '>', + '> Smaller fixes, focused refactors, and onboarding-friendly tasks are just as valuable and often use different labels.', + ].join('\n'), + }, +}; + +// ============================================================================= +// CONTRIBUTION GUIDE BOILERPLATE +// ============================================================================= + +/** + * Standard contribution guide section appended to all finalized issue bodies. + * Mirrors the "Step-by-Step Contribution Guide" textarea from the skill-level templates. + */ +const CONTRIBUTION_GUIDE_LABEL = 'πŸ“‹ Step-by-Step Contribution Guide'; + +const CONTRIBUTION_GUIDE_CONTENT = [ + 'To help keep contributions consistent and easy to review, we recommend following these steps:', + '', + '- [ ] Comment `/assign` to request the issue', + '- [ ] Wait for assignment', + '- [ ] Fork the repository and create a branch', + '- [ ] Set up the project using the instructions in `README.md`', + '- [ ] Make the requested changes', + '- [ ] Sign each commit using `-s -S`', + '- [ ] Push your branch and open a pull request', + '', + 'Read [Workflow Guide](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/docs/training/workflow.md) for step-by-step workflow guidance.', + 'Read [README.md](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/README.md) for setup instructions.', + '', + '❗ Pull requests **cannot be merged** without `S` and `s` signed commits.', + 'See the [Signing Guide](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/docs/training/signing.md).', +].join('\n'); + +// ============================================================================= +// DEFAULT ADDITIONAL INFORMATION CONTENT +// ============================================================================= + +/** + * Default additional information content used when the submitter left the field + * empty or with the default placeholder ("Optional."). + */ +const DEFAULT_ADDITIONAL_INFO_LABEL = 'πŸ€” Additional Information'; + +const DEFAULT_ADDITIONAL_INFO_CONTENT = [ + 'If you have questions while working on this issue, feel free to ask!', + '', + 'You can reach the community and maintainers here: [Hiero-SDK-C++ Discord](https://discord.com/channels/905194001349627914/1337424839761465364)', + '', + 'Whether you need help finding the right file, understanding existing code, or confirming your approach β€” we\'re happy to help.', +].join('\n'); + +// ============================================================================= +// BODY PARSING & RECONSTRUCTION +// ============================================================================= + +/** + * Parses an issue body into sections by splitting on H3 (###) markdown headers. + * GitHub form templates render each field's `label` as a `### ` header in the + * submitted issue body, making this a reliable split point. + * + * @param {string} body - The raw issue body string. + * @returns {Array<{ header: string|null, content: string }>} Ordered section list. + * The first entry may have `header: null` if content appears before the first header. + */ +function parseSections(body) { + if (!body || typeof body !== 'string') return []; + + const lines = body.split('\n'); + const sections = []; + let currentHeader = null; + let currentLines = []; + + for (const line of lines) { + const match = line.match(/^### (.+)$/); + if (match) { + if (currentHeader !== null || currentLines.some((l) => l.trim())) { + sections.push({ header: currentHeader, content: currentLines.join('\n').trim() }); + } + currentHeader = match[1].trim(); + currentLines = []; + } else { + currentLines.push(line); + } + } + + // Flush last section + if (currentHeader !== null || currentLines.some((l) => l.trim())) { + sections.push({ header: currentHeader, content: currentLines.join('\n').trim() }); + } + + return sections; +} + +/** + * Determines whether the content of a section is non-trivial (i.e., the user + * actually filled it in rather than leaving the default placeholder). + * + * @param {string|undefined} content - The section content string. + * @returns {boolean} True if the content is meaningful. + */ +function isMeaningfulContent(content) { + if (!content || typeof content !== 'string') return false; + const trimmed = content.trim(); + return trimmed.length > 0 && trimmed !== 'Optional.' && trimmed !== '_No response_'; +} + +/** + * Reconstructs the issue body in the skill-level template format. The existing + * body is parsed into sections; skill-level boilerplate is prepended; the + * Contribution Guide boilerplate is appended; Additional Information is moved + * to the very end. + * + * @param {string} existingBody - The current issue body (from the submitted template). + * @param {string} skillLevel - A LABELS skill-level constant (e.g. LABELS.BEGINNER). + * @returns {string} The fully reconstructed issue body. + */ +function reconstructBody(existingBody, skillLevel) { + const boilerplate = SKILL_BOILERPLATE[skillLevel]; + const sections = parseSections(existingBody); + + // Extract "Additional Information" section so it can be placed last + const additionalInfoIdx = sections.findIndex( + (s) => s.header && s.header.toLowerCase().replace(/[^\w\s]/g, '').trim().includes('additional information') + ); + let additionalInfoContent = null; + if (additionalInfoIdx !== -1) { + const [aiSection] = sections.splice(additionalInfoIdx, 1); + if (isMeaningfulContent(aiSection.content)) { + additionalInfoContent = aiSection.content; + } + } + + const parts = []; + + // 1. Skill-level intro block + parts.push(`### ${boilerplate.introLabel}`); + parts.push(''); + parts.push(boilerplate.introContent); + parts.push(''); + parts.push(boilerplate.importantBlock); + parts.push(''); + + // 2. User's sections in original order (skip null-header leading content) + for (const section of sections) { + if (!section.header) continue; + parts.push(`### ${section.header}`); + if (section.content) { + parts.push(''); + parts.push(section.content); + } + parts.push(''); + } + + // 3. Contribution guide boilerplate + parts.push('---'); + parts.push(''); + parts.push(`### ${CONTRIBUTION_GUIDE_LABEL}`); + parts.push(''); + parts.push(CONTRIBUTION_GUIDE_CONTENT); + parts.push(''); + + // 4. Additional information (user-provided or default) + parts.push(`### ${DEFAULT_ADDITIONAL_INFO_LABEL}`); + parts.push(''); + parts.push(additionalInfoContent !== null ? additionalInfoContent : DEFAULT_ADDITIONAL_INFO_CONTENT); + + return parts.join('\n').trimEnd(); +} + +// ============================================================================= +// COMMENT BUILDERS +// ============================================================================= + +/** + * Builds the comment posted when the commenter does not have triage-or-above + * permissions. Lists the required permission level. + * + * @param {string} username - The GitHub username who commented /finalize. + * @returns {string} The formatted Markdown comment body. + */ +function buildUnauthorizedComment(username) { + return [ + `πŸ‘‹ Hi @${username}! The \`/finalize\` command is reserved for maintainers and contributors with **triage** (or higher) repository permissions.`, + '', + 'If you believe you should have access, please reach out to a maintainer.', + ].join('\n'); +} + +/** + * Builds the comment posted when one or more label validation rules are violated. + * Lists every violation found so the reviewer can fix them all in one pass. + * + * @param {string} username - The GitHub username who commented /finalize. + * @param {string[]} errors - Array of human-readable error strings (one per violation). + * @returns {string} The formatted Markdown comment body. + */ +function buildValidationErrorComment(username, errors) { + const errorList = errors.map((e) => `- ${e}`).join('\n'); + return [ + `πŸ‘‹ Hi @${username}! The issue isn't quite ready to finalize yet. Please fix the following labeling issue(s) and then comment \`/finalize\` again:`, + '', + errorList, + '', + 'If you have questions about which labels to apply, see the maintainer documentation or ask in the team channel.', + ].join('\n'); +} + +/** + * Builds the comment posted when the GitHub API call to update the issue + * (title or body) fails. Tags the maintainer team for manual intervention. + * + * @param {string} username - The GitHub username who commented /finalize. + * @param {string} error - The error message from the failed API call. + * @returns {string} The formatted Markdown comment body. + */ +function buildUpdateFailureComment(username, error) { + return [ + `⚠️ Hi @${username}! I encountered an error while trying to update the issue title or body.`, + '', + `${MAINTAINER_TEAM} β€” could you please complete the finalization manually?`, + '', + `Error details: ${error}`, + ].join('\n'); +} + +/** + * Builds the comment posted when the label swap after a successful update + * fails. Tags the maintainer team with explicit manual steps. + * + * @param {string} username - The GitHub username who commented /finalize. + * @param {string} error - The error message(s) from the failed label operations. + * @returns {string} The formatted Markdown comment body. + */ +function buildLabelSwapFailureComment(username, error) { + return [ + `⚠️ The issue was updated successfully, but I encountered an error swapping the status labels.`, + '', + `${MAINTAINER_TEAM} β€” please manually:`, + `- Remove the \`${LABELS.AWAITING_TRIAGE}\` label`, + `- Add the \`${LABELS.READY_FOR_DEV}\` label`, + '', + `Error details: ${error}`, + ].join('\n'); +} + +/** + * Builds the comment posted when the permission check API call itself fails. + * Tags the maintainer team for manual assistance. + * + * @param {string} username - The GitHub username who commented /finalize. + * @returns {string} The formatted Markdown comment body. + */ +function buildPermissionCheckErrorComment(username) { + return [ + `πŸ‘‹ Hi @${username}! I encountered an error while trying to verify your permissions.`, + '', + `${MAINTAINER_TEAM} β€” could you please verify @${username}'s permissions and complete the finalization manually if appropriate?`, + '', + 'Sorry for the inconvenience!', + ].join('\n'); +} + +/** + * Builds the success comment posted after a successful /finalize run. + * + * @param {string} username - The GitHub username who ran /finalize. + * @param {string} skillLevel - The skill-level label that was applied (a LABELS constant). + * @param {string} priorityLabel - The priority label on the issue (e.g. 'priority: medium'). + * @returns {string} The formatted Markdown comment body. + */ +function buildSuccessComment(username, skillLevel, priorityLabel) { + const prefix = SKILL_TITLE_PREFIXES[skillLevel] || ''; + const displayLevel = prefix.replace(/[\[\]:]/g, '').trim(); + return [ + `βœ… Issue finalized by @${username}!`, + '', + `**Skill level:** \`${displayLevel}\``, + `**Priority:** \`${priorityLabel}\``, + '', + 'The issue body has been updated with the appropriate skill-level context and contribution guide. This issue is now ready for contributors to pick up via `/assign`.', + ].join('\n'); +} + +module.exports = { + SKILL_TITLE_PREFIXES, + SKILL_BOILERPLATE, + CONTRIBUTION_GUIDE_LABEL, + CONTRIBUTION_GUIDE_CONTENT, + DEFAULT_ADDITIONAL_INFO_LABEL, + DEFAULT_ADDITIONAL_INFO_CONTENT, + parseSections, + isMeaningfulContent, + reconstructBody, + buildUnauthorizedComment, + buildValidationErrorComment, + buildUpdateFailureComment, + buildLabelSwapFailureComment, + buildPermissionCheckErrorComment, + buildSuccessComment, +}; diff --git a/.github/scripts/commands/finalize.js b/.github/scripts/commands/finalize.js new file mode 100644 index 000000000..a71cb9f54 --- /dev/null +++ b/.github/scripts/commands/finalize.js @@ -0,0 +1,335 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// commands/finalize.js +// +// /finalize command: transitions a triaged issue from "awaiting triage" to +// "ready for dev". Validates all required labels, updates the issue title with +// the appropriate skill-level prefix, reconstructs the body in the skill-level +// template format, and swaps status labels. +// +// Only contributors with triage-or-above repository permissions may run this command. + +const { + LABELS, + getLogger, + hasLabel, + addLabels, + removeLabel, + postComment, + acknowledgeComment, +} = require('../helpers'); + +const { + SKILL_TITLE_PREFIXES, + SKILL_BOILERPLATE, + reconstructBody, + buildUnauthorizedComment, + buildValidationErrorComment, + buildUpdateFailureComment, + buildLabelSwapFailureComment, + buildPermissionCheckErrorComment, + buildSuccessComment, +} = require('./finalize-comments'); + +// Delegate to the active logger set by the dispatcher (bot-on-comment.js). +const logger = { + log: (...args) => getLogger().log(...args), + error: (...args) => getLogger().error(...args), +}; + +// Permission levels that are allowed to run /finalize (triage and above). +const ALLOWED_ROLE_NAMES = new Set(['triage', 'write', 'maintain', 'admin']); + +// Recognized GitHub issue type names set by our three templates. +const KNOWN_ISSUE_TYPES = new Set(['Bug', 'Feature', 'Task']); + +// ============================================================================= +// PERMISSION CHECK +// ============================================================================= + +/** + * Checks whether the commenter has triage-or-above repository permissions. + * Uses the GitHub REST API `getCollaboratorPermissionLevel` endpoint, which + * returns a `role_name` of 'read' | 'triage' | 'write' | 'maintain' | 'admin'. + * Non-collaborators (HTTP 404) are treated as unauthorized. + * + * @param {object} botContext - Bot context from buildBotContext. + * @param {string} username - GitHub username to check. + * @returns {Promise<'authorized'|'unauthorized'|'error'>} + */ +async function checkPermission(botContext, username) { + try { + const { data } = await botContext.github.rest.repos.getCollaboratorPermissionLevel({ + owner: botContext.owner, + repo: botContext.repo, + username, + }); + const roleName = data?.role_name; + logger.log(`[finalize] Permission check for @${username}: role_name="${roleName}"`); + if (ALLOWED_ROLE_NAMES.has(roleName)) return 'authorized'; + return 'unauthorized'; + } catch (error) { + const status = error?.status ?? error?.response?.status; + if (status === 404) { + logger.log(`[finalize] @${username} is not a collaborator (404) β€” unauthorized`); + return 'unauthorized'; + } + logger.error(`[finalize] Permission check failed for @${username}:`, error.message); + return 'error'; + } +} + +// ============================================================================= +// LABEL VALIDATION +// ============================================================================= + +/** + * Returns the GitHub issue type name ('Bug', 'Feature', 'Task') from the issue + * payload, or null if not set or unrecognized. + * + * @param {object} issue - The issue object from the GitHub payload. + * @returns {string|null} + */ +function getIssueTypeName(issue) { + const name = issue?.type?.name; + if (typeof name === 'string' && KNOWN_ISSUE_TYPES.has(name)) return name; + return null; +} + +/** + * Returns all label names on the issue that start with the given prefix. + * + * @param {object} issue - The issue object. + * @param {string} prefix - Label group prefix (e.g. 'skill:'). + * @returns {string[]} + */ +function getLabelsByPrefix(issue, prefix) { + return (issue.labels || []) + .map((l) => (typeof l === 'string' ? l : l?.name || '')) + .filter((name) => name.toLowerCase().startsWith(prefix.toLowerCase())); +} + +/** + * Collects all label validation violations for /finalize. Returns an empty + * array when everything is valid. + * + * Rules: + * - status: awaiting triage must be present + * - exactly 1 skill: label + * - exactly 1 priority: label + * - Bug / Task: exactly 1 kind: label + * - Feature: 0 kind: labels + * - issue type must be Bug, Feature, or Task + * + * @param {object} issue - The issue object from the GitHub payload. + * @returns {string[]} Array of human-readable error strings (one per violation). + */ +function collectLabelViolations(issue) { + const errors = []; + + const skillLabels = getLabelsByPrefix(issue, 'skill:'); + const priorityLabels = getLabelsByPrefix(issue, 'priority:'); + const kindLabels = getLabelsByPrefix(issue, 'kind:'); + const issueTypeName = getIssueTypeName(issue); + + // 1. status: awaiting triage must be present + if (!hasLabel(issue, LABELS.AWAITING_TRIAGE)) { + const statusLabels = getLabelsByPrefix(issue, 'status:'); + const currentStatus = statusLabels.length > 0 ? statusLabels.map((l) => `\`${l}\``).join(', ') : 'none'; + errors.push( + `The \`${LABELS.AWAITING_TRIAGE}\` label must be present to run \`/finalize\`. Current status label(s): ${currentStatus}.` + ); + } + + // 2. Exactly 1 skill: label + if (skillLabels.length === 0) { + errors.push( + `Exactly one \`skill:\` label is required (e.g. \`skill: beginner\`). None found. Choose from: \`${LABELS.GOOD_FIRST_ISSUE}\`, \`${LABELS.BEGINNER}\`, \`${LABELS.INTERMEDIATE}\`, \`${LABELS.ADVANCED}\`.` + ); + } else if (skillLabels.length > 1) { + errors.push( + `Exactly one \`skill:\` label is required. Found ${skillLabels.length}: ${skillLabels.map((l) => `\`${l}\``).join(', ')}. Please remove all but one.` + ); + } + + // 3. Exactly 1 priority: label + if (priorityLabels.length === 0) { + errors.push( + `Exactly one \`priority:\` label is required (e.g. \`priority: medium\`). None found.` + ); + } else if (priorityLabels.length > 1) { + errors.push( + `Exactly one \`priority:\` label is required. Found ${priorityLabels.length}: ${priorityLabels.map((l) => `\`${l}\``).join(', ')}. Please remove all but one.` + ); + } + + // 4. Issue type + kind: label enforcement + if (!issueTypeName) { + errors.push( + `The issue type (Bug, Feature, or Task) could not be determined. Ensure the issue was submitted using one of the official issue templates.` + ); + } else if (issueTypeName === 'Feature') { + if (kindLabels.length > 0) { + errors.push( + `Feature issues should not have a \`kind:\` label. Found: ${kindLabels.map((l) => `\`${l}\``).join(', ')}. Please remove it.` + ); + } + } else { + // Bug or Task: exactly 1 kind: label required + if (kindLabels.length === 0) { + errors.push( + `${issueTypeName} issues require exactly one \`kind:\` label (e.g. \`kind: maintenance\`). None found.` + ); + } else if (kindLabels.length > 1) { + errors.push( + `${issueTypeName} issues require exactly one \`kind:\` label. Found ${kindLabels.length}: ${kindLabels.map((l) => `\`${l}\``).join(', ')}. Please remove all but one.` + ); + } + } + + return errors; +} + +// ============================================================================= +// TITLE UPDATE +// ============================================================================= + +/** + * Builds the new issue title by stripping any existing skill-level prefix and + * prepending the correct one for the given skill label. + * + * @param {string} currentTitle - The current issue title. + * @param {string} skillLevel - A LABELS skill-level constant. + * @returns {string} The updated title. + */ +function buildNewTitle(currentTitle, skillLevel) { + const strippedTitle = currentTitle + .replace(/^\[(Good First Issue|Beginner|Intermediate|Advanced)\]:\s*/i, '') + .trim(); + const prefix = SKILL_TITLE_PREFIXES[skillLevel] || ''; + return `${prefix}${strippedTitle}`; +} + +// ============================================================================= +// LABEL SWAP +// ============================================================================= + +/** + * Swaps `status: awaiting triage` β†’ `status: ready for dev` after a successful + * issue update. If either label operation fails, posts a comment tagging + * maintainers with manual instructions. + * + * @param {object} botContext - Bot context from buildBotContext. + * @param {string} finalizerUsername - Username who ran /finalize (for the failure comment). + * @returns {Promise} + */ +async function swapTriageLabels(botContext, finalizerUsername) { + let failed = false; + let errorDetails = ''; + + const removeResult = await removeLabel(botContext, LABELS.AWAITING_TRIAGE); + if (!removeResult.success) { + failed = true; + errorDetails += `Failed to remove \`${LABELS.AWAITING_TRIAGE}\`: ${removeResult.error}`; + } + + const addResult = await addLabels(botContext, [LABELS.READY_FOR_DEV]); + if (!addResult.success) { + failed = true; + errorDetails += (errorDetails ? '; ' : '') + `Failed to add \`${LABELS.READY_FOR_DEV}\`: ${addResult.error}`; + } + + if (failed) { + await postComment(botContext, buildLabelSwapFailureComment(finalizerUsername, errorDetails)); + logger.log('[finalize] Posted label swap failure comment, tagged maintainers'); + } +} + +// ============================================================================= +// MAIN HANDLER +// ============================================================================= + +/** + * Main handler for the /finalize command. Runs the following steps in order, + * posting an informative comment and returning early if any step fails: + * + * 1. Acknowledge the comment with a thumbs-up reaction. + * 2. Check commenter has triage+ permissions β†’ unauthorized / API error comment. + * 3. Collect all label violations β†’ validation error comment listing all issues. + * 4. Determine skill level and build updated title + body. + * 5. Update the issue via the GitHub API β†’ update failure comment on error. + * 6. Swap status labels: awaiting triage β†’ ready for dev. + * 7. Post success comment. + * + * @param {{ github: object, owner: string, repo: string, number: number, + * issue: object, comment: { id: number, user: { login: string } } }} botContext + * Bot context from buildBotContext (issue_comment event). + * @returns {Promise} + */ +async function handleFinalize(botContext) { + const finalizerUsername = botContext.comment.user.login; + + // STEP 1: Acknowledge + await acknowledgeComment(botContext, botContext.comment.id); + + // STEP 2: Permission check + const permResult = await checkPermission(botContext, finalizerUsername); + if (permResult === 'error') { + logger.log('[finalize] Exit: permission check API error'); + await postComment(botContext, buildPermissionCheckErrorComment(finalizerUsername)); + return; + } + if (permResult === 'unauthorized') { + logger.log(`[finalize] Exit: @${finalizerUsername} is not authorized`); + await postComment(botContext, buildUnauthorizedComment(finalizerUsername)); + return; + } + + // STEP 3: Label validation β€” collect ALL violations before posting + const violations = collectLabelViolations(botContext.issue); + if (violations.length > 0) { + logger.log(`[finalize] Exit: ${violations.length} label violation(s) found`); + await postComment(botContext, buildValidationErrorComment(finalizerUsername, violations)); + return; + } + + // STEP 4: Determine skill level + build new title and body + const skillLabels = getLabelsByPrefix(botContext.issue, 'skill:'); + const skillLevel = skillLabels[0]; // validated above: exactly 1 exists + + const newTitle = buildNewTitle(botContext.issue.title, skillLevel); + const newBody = reconstructBody(botContext.issue.body || '', skillLevel); + + logger.log(`[finalize] Updating issue #${botContext.number}: title="${newTitle}", skillLevel="${skillLevel}"`); + + // STEP 5: Update issue title and body + let updateError = null; + try { + await botContext.github.rest.issues.update({ + owner: botContext.owner, + repo: botContext.repo, + issue_number: botContext.number, + title: newTitle, + body: newBody, + }); + logger.log('[finalize] Issue updated successfully'); + } catch (error) { + updateError = error instanceof Error ? error.message : String(error); + logger.error('[finalize] Issue update failed:', updateError); + } + + if (updateError) { + await postComment(botContext, buildUpdateFailureComment(finalizerUsername, updateError)); + return; + } + + // STEP 6: Swap status labels + await swapTriageLabels(botContext, finalizerUsername); + + // STEP 7: Post success comment + const priorityLabel = getLabelsByPrefix(botContext.issue, 'priority:')[0]; + await postComment(botContext, buildSuccessComment(finalizerUsername, skillLevel, priorityLabel)); + logger.log('[finalize] Finalize flow completed successfully'); +} + +module.exports = { handleFinalize }; diff --git a/.github/scripts/helpers/constants.js b/.github/scripts/helpers/constants.js index 7c4ad481f..91fa216c5 100644 --- a/.github/scripts/helpers/constants.js +++ b/.github/scripts/helpers/constants.js @@ -14,6 +14,7 @@ const MAINTAINER_TEAM = '@hiero-ledger/hiero-sdk-cpp-maintainers'; */ const LABELS = Object.freeze({ // Status labels + AWAITING_TRIAGE: 'status: awaiting triage', READY_FOR_DEV: 'status: ready for dev', IN_PROGRESS: 'status: in progress', BLOCKED: 'status: blocked', diff --git a/.github/scripts/tests/test-finalize-bot.js b/.github/scripts/tests/test-finalize-bot.js new file mode 100644 index 000000000..58b338aaa --- /dev/null +++ b/.github/scripts/tests/test-finalize-bot.js @@ -0,0 +1,578 @@ +// SPDX-License-Identifier: Apache-2.0 +// +// tests/test-finalize-bot.js +// +// Local test script for the /finalize command (bot-on-comment.js β†’ commands/finalize.js). +// Run with: node .github/scripts/tests/test-finalize-bot.js +// +// Mocks the GitHub API and runs scenarios to verify /finalize behaves correctly +// without making real API calls. + +const { LABELS } = require('../helpers'); +const script = require('../bot-on-comment.js'); + +// ============================================================================= +// MOCK GITHUB API +// ============================================================================= + +function createMockGithub(options = {}) { + const { + roleName = 'triage', // role_name returned by getCollaboratorPermissionLevel + permissionShouldFail = false, // throw HTTP 500 on permission check + permissionNotFound = false, // throw HTTP 404 (non-collaborator) + updateShouldFail = false, // throw on issues.update + removeLabelShouldFail = false, + addLabelShouldFail = false, + } = options; + + const calls = { + comments: [], + reactions: [], + labelsAdded: [], + labelsRemoved: [], + issueUpdates: [], + permissionChecks: [], + }; + + return { + calls, + rest: { + reactions: { + createForIssueComment: async (params) => { + calls.reactions.push({ commentId: params.comment_id, content: params.content }); + console.log(`\nπŸ‘ REACTION ADDED: ${params.content}`); + }, + }, + repos: { + getCollaboratorPermissionLevel: async (params) => { + calls.permissionChecks.push(params.username); + console.log(`\nπŸ” PERMISSION CHECK: @${params.username}`); + if (permissionNotFound) { + const err = new Error('Not Found'); + err.status = 404; + throw err; + } + if (permissionShouldFail) { + const err = new Error('Simulated permission check failure'); + err.status = 500; + throw err; + } + console.log(` β†’ role_name: ${roleName}`); + return { data: { role_name: roleName, permission: roleName } }; + }, + }, + issues: { + createComment: async (params) => { + calls.comments.push(params.body); + console.log('\nπŸ“ COMMENT POSTED:'); + console.log('─'.repeat(60)); + console.log(params.body); + console.log('─'.repeat(60)); + }, + update: async (params) => { + if (updateShouldFail) { + throw new Error('Simulated issue update failure'); + } + calls.issueUpdates.push({ title: params.title, body: params.body }); + console.log(`\n✏️ ISSUE UPDATED: title="${params.title}"`); + }, + addLabels: async (params) => { + if (addLabelShouldFail) { + throw new Error('Simulated add label failure'); + } + calls.labelsAdded.push(...params.labels); + console.log(`\n🏷️ LABEL ADDED: ${params.labels.join(', ')}`); + }, + removeLabel: async (params) => { + if (removeLabelShouldFail) { + throw new Error('Simulated remove label failure'); + } + calls.labelsRemoved.push(params.name); + console.log(`\n🏷️ LABEL REMOVED: ${params.name}`); + }, + }, + }, + }; +} + +// ============================================================================= +// HELPERS +// ============================================================================= + +function makeIssue(overrides = {}) { + return { + number: 42, + title: 'Fix something', + state: 'open', + body: '### πŸ‘Ύ Description of the Issue\n\nThis thing is broken.\n\n### βœ”οΈ Acceptance Criteria\n\n- [ ] Fixed', + labels: [ + { name: LABELS.AWAITING_TRIAGE }, + { name: LABELS.BEGINNER }, + { name: 'priority: medium' }, + { name: 'kind: maintenance' }, + ], + type: { name: 'Task' }, + assignees: [], + ...overrides, + }; +} + +function makeContext(issue, commentBody = '/finalize', commenter = 'maintainer') { + return { + eventName: 'issue_comment', + payload: { + issue, + comment: { + id: 9001, + body: commentBody, + user: { login: commenter, type: 'User' }, + }, + }, + repo: { owner: 'hiero-ledger', repo: 'hiero-sdk-cpp' }, + }; +} + +async function runScenario(scenario) { + console.log('\n' + '='.repeat(70)); + console.log(`TEST: ${scenario.name}`); + console.log(`DESC: ${scenario.description}`); + console.log('='.repeat(70)); + + const github = createMockGithub(scenario.githubOptions || {}); + const context = scenario.context; + + let threw = null; + try { + await script({ github, context }); + } catch (e) { + threw = e; + } + + // Run assertions + let passed = true; + const failures = []; + + for (const assertion of scenario.assertions || []) { + const result = assertion(github.calls, threw); + if (result !== true) { + passed = false; + failures.push(result); + } + } + + if (passed) { + console.log('\nβœ… PASSED\n'); + } else { + console.log('\n❌ FAILED'); + for (const f of failures) console.log(' -', f); + console.log(); + } + + return passed; +} + +// ============================================================================= +// ASSERTION HELPERS +// ============================================================================= + +const assert = { + commentContains: (text) => (calls) => { + const found = calls.comments.some((c) => c.includes(text)); + return found || `Expected a comment containing: "${text}"`; + }, + noComments: () => (calls) => calls.comments.length === 0 || `Expected no comments, got ${calls.comments.length}`, + labelAdded: (label) => (calls) => calls.labelsAdded.includes(label) || `Expected label added: "${label}"`, + labelRemoved: (label) => (calls) => calls.labelsRemoved.includes(label) || `Expected label removed: "${label}"`, + noLabelsAdded: () => (calls) => calls.labelsAdded.length === 0 || `Expected no labels added, got: ${calls.labelsAdded}`, + noLabelsRemoved: () => (calls) => calls.labelsRemoved.length === 0 || `Expected no labels removed, got: ${calls.labelsRemoved}`, + noIssueUpdate: () => (calls) => calls.issueUpdates.length === 0 || `Expected no issue update, got ${calls.issueUpdates.length}`, + issueUpdated: () => (calls) => calls.issueUpdates.length > 0 || 'Expected issue to be updated', + titleContains: (text) => (calls) => { + const found = calls.issueUpdates.some((u) => u.title && u.title.includes(text)); + return found || `Expected updated title to contain: "${text}"`; + }, + bodyContains: (text) => (calls) => { + const found = calls.issueUpdates.some((u) => u.body && u.body.includes(text)); + return found || `Expected updated body to contain: "${text}"`; + }, + reactionAdded: () => (calls) => calls.reactions.length > 0 || 'Expected thumbs-up reaction to be added', +}; + +// ============================================================================= +// SCENARIOS +// ============================================================================= + +const scenarios = [ + // --------------------------------------------------------------------------- + // AUTHORIZATION + // --------------------------------------------------------------------------- + + { + name: 'Unauthorized β€” read-only collaborator', + description: 'A collaborator with "read" role is rejected', + context: makeContext(makeIssue()), + githubOptions: { roleName: 'read' }, + assertions: [ + assert.reactionAdded(), + assert.commentContains('reserved for maintainers'), + assert.noIssueUpdate(), + assert.noLabelsAdded(), + ], + }, + + { + name: 'Unauthorized β€” non-collaborator (404)', + description: 'A user who is not a repo collaborator is rejected', + context: makeContext(makeIssue()), + githubOptions: { permissionNotFound: true }, + assertions: [ + assert.reactionAdded(), + assert.commentContains('reserved for maintainers'), + assert.noIssueUpdate(), + ], + }, + + { + name: 'Permission check API error', + description: 'When the permission API fails, posts an error comment and tags maintainers', + context: makeContext(makeIssue()), + githubOptions: { permissionShouldFail: true }, + assertions: [ + assert.reactionAdded(), + assert.commentContains('encountered an error while trying to verify your permissions'), + assert.noIssueUpdate(), + ], + }, + + // --------------------------------------------------------------------------- + // LABEL VALIDATION + // --------------------------------------------------------------------------- + + { + name: 'Validation β€” missing status: awaiting triage', + description: 'Issue has a different status label β€” should fail validation', + context: makeContext(makeIssue({ + labels: [ + { name: LABELS.READY_FOR_DEV }, + { name: LABELS.BEGINNER }, + { name: 'priority: medium' }, + { name: 'kind: maintenance' }, + ], + type: { name: 'Task' }, + })), + githubOptions: { roleName: 'triage' }, + assertions: [ + assert.commentContains('status: awaiting triage'), + assert.noIssueUpdate(), + assert.noLabelsAdded(), + ], + }, + + { + name: 'Validation β€” no skill label', + description: 'Issue is missing a skill: label', + context: makeContext(makeIssue({ + labels: [ + { name: LABELS.AWAITING_TRIAGE }, + { name: 'priority: medium' }, + { name: 'kind: maintenance' }, + ], + type: { name: 'Task' }, + })), + githubOptions: { roleName: 'triage' }, + assertions: [ + assert.commentContains('skill:'), + assert.noIssueUpdate(), + ], + }, + + { + name: 'Validation β€” multiple skill labels', + description: 'Issue has two skill: labels β€” exactly one is required', + context: makeContext(makeIssue({ + labels: [ + { name: LABELS.AWAITING_TRIAGE }, + { name: LABELS.BEGINNER }, + { name: LABELS.INTERMEDIATE }, + { name: 'priority: medium' }, + { name: 'kind: maintenance' }, + ], + type: { name: 'Task' }, + })), + githubOptions: { roleName: 'triage' }, + assertions: [ + assert.commentContains('skill:'), + assert.noIssueUpdate(), + ], + }, + + { + name: 'Validation β€” no priority label', + description: 'Issue is missing a priority: label', + context: makeContext(makeIssue({ + labels: [ + { name: LABELS.AWAITING_TRIAGE }, + { name: LABELS.BEGINNER }, + { name: 'kind: maintenance' }, + ], + type: { name: 'Task' }, + })), + githubOptions: { roleName: 'triage' }, + assertions: [ + assert.commentContains('priority:'), + assert.noIssueUpdate(), + ], + }, + + { + name: 'Validation β€” Task missing kind label', + description: 'Task issues require exactly one kind: label', + context: makeContext(makeIssue({ + labels: [ + { name: LABELS.AWAITING_TRIAGE }, + { name: LABELS.BEGINNER }, + { name: 'priority: medium' }, + ], + type: { name: 'Task' }, + })), + githubOptions: { roleName: 'triage' }, + assertions: [ + assert.commentContains('kind:'), + assert.noIssueUpdate(), + ], + }, + + { + name: 'Validation β€” Bug missing kind label', + description: 'Bug issues require exactly one kind: label', + context: makeContext(makeIssue({ + labels: [ + { name: LABELS.AWAITING_TRIAGE }, + { name: LABELS.BEGINNER }, + { name: 'priority: medium' }, + ], + type: { name: 'Bug' }, + })), + githubOptions: { roleName: 'triage' }, + assertions: [ + assert.commentContains('kind:'), + assert.noIssueUpdate(), + ], + }, + + { + name: 'Validation β€” Feature with kind label', + description: 'Feature issues must NOT have a kind: label', + context: makeContext(makeIssue({ + labels: [ + { name: LABELS.AWAITING_TRIAGE }, + { name: LABELS.BEGINNER }, + { name: 'priority: medium' }, + { name: 'kind: enhancement' }, + ], + type: { name: 'Feature' }, + })), + githubOptions: { roleName: 'triage' }, + assertions: [ + assert.commentContains('Feature issues should not have a `kind:`'), + assert.noIssueUpdate(), + ], + }, + + { + name: 'Validation β€” Multiple violations listed in one comment', + description: 'All violations (no skill, no priority, wrong kind for feature) reported together', + context: makeContext(makeIssue({ + labels: [ + { name: LABELS.AWAITING_TRIAGE }, + { name: 'kind: maintenance' }, + ], + type: { name: 'Feature' }, + })), + githubOptions: { roleName: 'admin' }, + assertions: [ + assert.commentContains('skill:'), + assert.commentContains('priority:'), + assert.commentContains('Feature issues should not'), + assert.noIssueUpdate(), + ], + }, + + { + name: 'Validation β€” Unknown issue type', + description: 'Issue created without a recognized type triggers a validation error', + context: makeContext(makeIssue({ type: null })), + githubOptions: { roleName: 'triage' }, + assertions: [ + assert.commentContains('issue type'), + assert.noIssueUpdate(), + ], + }, + + // --------------------------------------------------------------------------- + // HAPPY PATHS + // --------------------------------------------------------------------------- + + { + name: 'Happy Path β€” Good First Issue (Feature)', + description: 'Valid GFI feature issue is finalized successfully', + context: makeContext(makeIssue({ + title: 'Add batch query support', + labels: [ + { name: LABELS.AWAITING_TRIAGE }, + { name: LABELS.GOOD_FIRST_ISSUE }, + { name: 'priority: low' }, + ], + type: { name: 'Feature' }, + })), + githubOptions: { roleName: 'triage' }, + assertions: [ + assert.reactionAdded(), + assert.issueUpdated(), + assert.titleContains('[Good First Issue]:'), + assert.bodyContains('First-Time Friendly'), + assert.bodyContains('About Good First Issues'), + assert.bodyContains('Step-by-Step Contribution Guide'), + assert.labelAdded(LABELS.READY_FOR_DEV), + assert.labelRemoved(LABELS.AWAITING_TRIAGE), + assert.commentContains('finalized by @maintainer'), + ], + }, + + { + name: 'Happy Path β€” Beginner Task', + description: 'Valid beginner task is finalized; title prefix added, body reconstructed', + context: makeContext(makeIssue()), + githubOptions: { roleName: 'triage' }, + assertions: [ + assert.reactionAdded(), + assert.issueUpdated(), + assert.titleContains('[Beginner]: Fix something'), + assert.bodyContains('Beginner Friendly'), + assert.bodyContains('About Beginner Issues'), + assert.bodyContains('Step-by-Step Contribution Guide'), + assert.labelAdded(LABELS.READY_FOR_DEV), + assert.labelRemoved(LABELS.AWAITING_TRIAGE), + assert.commentContains('finalized by @maintainer'), + ], + }, + + { + name: 'Happy Path β€” Intermediate Bug', + description: 'Valid intermediate bug report is finalized', + context: makeContext(makeIssue({ + title: 'Client fee cap silently ignored', + labels: [ + { name: LABELS.AWAITING_TRIAGE }, + { name: LABELS.INTERMEDIATE }, + { name: 'priority: high' }, + { name: 'kind: security' }, + ], + type: { name: 'Bug' }, + })), + githubOptions: { roleName: 'write' }, + assertions: [ + assert.issueUpdated(), + assert.titleContains('[Intermediate]:'), + assert.bodyContains('Intermediate Friendly'), + assert.bodyContains('About Intermediate Issues'), + assert.bodyContains('Step-by-Step Contribution Guide'), + assert.labelAdded(LABELS.READY_FOR_DEV), + assert.commentContains('finalized by @maintainer'), + ], + }, + + { + name: 'Happy Path β€” Advanced Task', + description: 'Valid advanced task is finalized', + context: makeContext(makeIssue({ + title: 'Improve issue triage workflow', + labels: [ + { name: LABELS.AWAITING_TRIAGE }, + { name: LABELS.ADVANCED }, + { name: 'priority: medium' }, + { name: 'kind: maintenance' }, + { name: 'scope: ci' }, + ], + type: { name: 'Task' }, + })), + githubOptions: { roleName: 'admin' }, + assertions: [ + assert.issueUpdated(), + assert.titleContains('[Advanced]:'), + assert.bodyContains('🧠 Advanced'), + assert.bodyContains('About Advanced Issues'), + assert.bodyContains('Step-by-Step Contribution Guide'), + assert.labelAdded(LABELS.READY_FOR_DEV), + assert.commentContains('finalized by @maintainer'), + ], + }, + + { + name: 'Happy Path β€” existing prefix is replaced', + description: 'An issue that was already finalized once gets the correct prefix when re-finalized', + context: makeContext(makeIssue({ + title: '[Beginner]: Fix something', + labels: [ + { name: LABELS.AWAITING_TRIAGE }, + { name: LABELS.ADVANCED }, + { name: 'priority: medium' }, + { name: 'kind: maintenance' }, + ], + type: { name: 'Task' }, + })), + githubOptions: { roleName: 'maintain' }, + assertions: [ + assert.titleContains('[Advanced]: Fix something'), + assert.issueUpdated(), + ], + }, + + // --------------------------------------------------------------------------- + // API FAILURE PATHS + // --------------------------------------------------------------------------- + + { + name: 'API failure β€” issue update fails', + description: 'When issues.update throws, a failure comment is posted and labels are NOT swapped', + context: makeContext(makeIssue()), + githubOptions: { roleName: 'triage', updateShouldFail: true }, + assertions: [ + assert.commentContains('encountered an error while trying to update'), + assert.noLabelsAdded(), + assert.noLabelsRemoved(), + ], + }, + + { + name: 'API failure β€” label swap fails after successful update', + description: 'When removeLabel throws after a successful update, maintainers are tagged', + context: makeContext(makeIssue()), + githubOptions: { roleName: 'triage', removeLabelShouldFail: true }, + assertions: [ + assert.issueUpdated(), + assert.commentContains('encountered an error swapping the status labels'), + ], + }, +]; + +// ============================================================================= +// RUNNER +// ============================================================================= + +(async () => { + let passed = 0; + let failed = 0; + + for (const scenario of scenarios) { + const ok = await runScenario(scenario); + if (ok) passed++; + else failed++; + } + + console.log('\n' + '='.repeat(70)); + console.log(`RESULTS: ${passed} passed, ${failed} failed`); + console.log('='.repeat(70) + '\n'); + + if (failed > 0) process.exit(1); +})(); diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f446acb82..8f6d74cb8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,8 +7,7 @@ code contributions. **Jump To:** - [Code Contributions](#code-contributions) -- [Bug Reports](#bug-reports) -- [Feature Requests](#feature-requests) +- [Submitting an Issue](#submitting-an-issue) - [Release New Version](#release-new-version) - [Blog Posts](#blog-posts) @@ -26,7 +25,7 @@ Browse [open, unassigned issues](https://github.com/hiero-ledger/hiero-sdk-cpp/i | **Intermediate** | Multi-file changes with design decisions | 3 completed Beginner Issues | | **Advanced** | Architecture and design-heavy work | 3 completed Intermediate Issues | -Look for issues with the `status: ready for dev` label β€” these are ready to be worked on. +Look for issues with the `status: ready for dev` label β€” these have been triaged and are ready to be worked on. ### Getting Assigned @@ -56,51 +55,53 @@ Note: - The SDK is released under the [Apache 2.0 License][license]. Any code you submit will be released under this license. - Pull requests **cannot be merged** without signed commits. See the [Signing Guide](docs/training/signing.md). -## Feature Requests +## Submitting an Issue -**NOTE:** If you intend to implement a feature request, please submit the feature request _before_ working on any code -changes and ask to get assigned. +Not sure which issue type to use? See the [Issue Types Guide](docs/contributing/issue-types.md) for a full breakdown +with examples of what belongs in each category. -1. Visit [C++ SDK Issue](https://github.com/hiero-ledger/hiero-sdk-cpp/issues) -2. Verify the Feature Request is not already proposed. -2. Click 'New Issue' and click the Feature Request template. -**Ensure** the [New Feature][label-new-feature] or [Feature Enhancements][label-feature-enhancement] label is attached. +### Bug Reports -### Submitting a Feature Request +⚠️ **Ensure you are using the latest release of the SDK** β€” it's possible the bug is already fixed. -Open an [issue][issues] with the following: +1. Visit the [C++ SDK Issues page][issues] +2. ⚠️ **Check the bug is not already reported.** If it is, comment to confirm you're also experiencing it. +3. Click **New Issue** and choose the **Bug Report** template. -- A short, descriptive title. Other community members should be able to understand the nature of the issue by reading - this title. -- A detailed description of the proposed feature. Explain why you believe it should be added to the SDK. - Illustrative example code may also be provided to help explain how the feature should work. -- [Markdown][markdown] formatting as appropriate to make the request easier to read. -- If you plan to implement this feature yourself, please let us know that you'd like the issue to be assigned to you. +The template will guide you through providing a description, steps to reproduce, expected vs. actual behavior, and +environment details. The more reproducible the report, the faster a fix can land. -## Bug Reports +Security vulnerabilities should be disclosed responsibly via our [bug bounty program](https://hedera.com/bounty) +rather than as a public issue. -⚠️ **Ensure you are using the latest release of the SDK**. +### Feature Requests -It's possible the bug is already fixed. We will do our utmost to maintain backwards compatibility between patch version releases, so that you can be - confident that your application will continue to work as expected with the newer version. +**Note:** If you intend to implement a feature yourself, please submit the request _before_ writing any code and +ask to be assigned. Features are for user-facing SDK capabilities β€” new transactions, queries, API methods, etc. +Improvements to tooling, CI, or the contribution process are [Tasks](#tasks) instead. -1. Visit [C++ SDK Issue Page](https://github.com/hiero-ledger/hiero-sdk-cpp/issues) -2. ⚠️ **Check the Bug is not Already Reported**. If it is, comment to confirm you are also experiencing this bug. -3. Click 'New Issue' and choose the `Bug Report` template +1. Visit the [C++ SDK Issues page][issues] +2. Verify the feature has not already been proposed. +3. Click **New Issue** and choose the **Feature Request** template. -**Ensure** the [bug][label-bug] label is attached. +For large or protocol-level changes, consider opening a +[Hiero Improvement Proposal](https://github.com/hiero-ledger/hiero-improvement-proposals) or a +[GitHub Discussion](https://github.com/hiero-ledger/hiero-sdk-cpp/discussions) first. -Please ensure that your bug report contains the following: +### Tasks -- A short, descriptive title. Other community members should be able to understand the nature of the issue by reading - this title. -- A succinct, detailed description of the problem you're experiencing. This should include: - - Expected behavior of the SDK and the actual behavior exhibited. - - Any details of your application development environment that may be relevant. - - If applicable, the exception stack-trace. - - If you are able to create one, include a [Minimal Working Example][mwe] that reproduces the issue. -- [Markdown][markdown] formatting as appropriate to make the report easier to read; for example use code blocks when - pasting a code snippet or exception stack-trace. +Use a Task for maintenance, improvement, or operational work: refactoring, dependency updates, documentation +improvements, CI/CD changes, test coverage, or enhancements to existing features. + +1. Visit the [C++ SDK Issues page][issues] +2. Verify the work has not already been proposed. +3. Click **New Issue** and choose the **Task** template. + +### After Submitting + +All three templates automatically apply `status: awaiting triage`. A maintainer will review the issue, apply the +appropriate skill level and priority labels, and run `/finalize` to prepare it for contributors. Once finalized, +the issue will have `status: ready for dev` and can be claimed via `/assign`. ## Release New Version @@ -152,11 +153,8 @@ Here should be listed all of the Pull Requests (PRs) which are part of the relea > (pull-request-title) by (@author) in (#link) [issues]: https://github.com/hiero-ledger/hiero-sdk-cpp/issues -[label-bug]: https://github.com/hiero-ledger/hiero-sdk-cpp/labels/bug [mwe]: https://en.wikipedia.org/wiki/Minimal_Working_Example [markdown]: https://guides.github.com/features/mastering-markdown/ -[label-feature-enhancement]: https://github.com/hiero-ledger/hiero-sdk-cpp/labels/Feature%20Enhancement -[label-new-feature]: https://github.com/hiero-ledger/hiero-sdk-cpp/labels/New%20Feature [pull-requests]: https://github.com/hiero-ledger/hiero-sdk-cpp/pulls [license]: https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/LICENSE [discord]: https://hedera.com/discord diff --git a/docs/contributing/issue-types.md b/docs/contributing/issue-types.md new file mode 100644 index 000000000..62218614d --- /dev/null +++ b/docs/contributing/issue-types.md @@ -0,0 +1,139 @@ +# Issue Types β€” Hiero C++ SDK + +## Overview + +When submitting an issue to the Hiero C++ SDK, you'll be asked to choose one of three issue types: + +| Type | Use when... | +|---|---| +| **Bug** | Something isn't working correctly | +| **Feature** | You want a capability that doesn't exist yet | +| **Task** | Maintenance, improvement, or operational work | + +Each type has its own submission template to help you provide the right level of detail. + +> **Note:** You do not need to assess how difficult an issue is to implement β€” that's determined by maintainers during triage. Simply choose the type that best describes the work. + +--- + +## Bug + +A **bug** is an unintended behavior, crash, incorrect output, or regression in the SDK. + +Use the **Bug** template when the SDK does something it shouldn't do, or fails to do something it should. + +### Examples of bugs + +- A method returns an incorrect value or throws unexpectedly +- A transaction executes successfully when it should have been rejected +- A query returns stale or malformed data +- The SDK crashes or throws an unhandled exception +- A previously working feature stopped working after an update +- The build fails on a supported platform +- A test fails in CI without any relevant code change + +### Not a bug + +- A request for behavior the SDK has never supported β†’ that's a **Feature** +- A desire to improve performance without a regression β†’ that's a **Task** +- Something you'd like to work differently as a matter of preference β†’ that might be a **Feature** or **Task** + +### What makes a good bug report + +A useful bug report includes: +- A clear description of what went wrong +- Steps that reliably reproduce the issue +- The expected vs. actual behavior +- Environment details (SDK version, OS, compiler) + +The more reproducible the report, the faster a fix can land. + +--- + +## Feature + +A **feature** is a new capability for **consumers of the SDK** β€” developers building applications on Hiero β€” that does not currently exist. + +Use the **Feature** template when you want the SDK to do something it has never done before, and that something is user-facing: it would show up in the SDK's public API documentation or release notes as something an application developer can now do. + +A useful test: *Would this change appear in the SDK's public API documentation or release notes as a user-visible capability?* If yes, it's likely a Feature. If the change improves the SDK's development process, tooling, or contributor experience rather than what application developers can do with it, it's a **Task**. + +### Examples of features + +- A new transaction type not yet implemented (e.g. a new HIP) +- A new query type or query mode +- A new public API method or configuration option +- A new example program demonstrating an SDK capability +- Support for a new network or environment type + +### Not a feature + +- An improvement to an existing feature β†’ that's a **Task** (`kind: enhancement`) +- A fix for broken existing behavior β†’ that's a **Bug** +- A refactor that doesn't change the public API β†’ that's a **Task** (`kind: refactor`) +- A new bot command, CI workflow, issue template, or contributor tooling addition β†’ that's a **Task** (`kind: maintenance`), even if it's genuinely "new" β€” it improves the development process, not the SDK's public capabilities + +### What makes a good feature request + +A useful feature request focuses on the problem being solved, not just the solution. Ask yourself: + +- Why does this capability need to exist? +- Who benefits, and how? +- Is there a workaround today, and why is it insufficient? + +Large or protocol-level feature requests may benefit from a +[Hiero Improvement Proposal (HIP)](https://github.com/hiero-ledger/hiero-improvement-proposals) +or a [GitHub Discussion](https://github.com/hiero-ledger/hiero-sdk-cpp/discussions) before a formal issue. + +--- + +## Task + +A **task** is maintenance, improvement, or operational work that keeps the SDK healthy and easy to contribute to. + +Use the **Task** template when the work isn't a bug fix and doesn't add new public API surface β€” it improves the existing codebase. + +### Examples of tasks + +| What you want to do | `kind:` label to use | +|---|---| +| Improve or clarify documentation or code comments | `kind: documentation` | +| Refactor existing code without changing behavior | `kind: refactor` | +| Update a dependency or build tooling | `kind: maintenance` | +| Improve CI/CD pipelines or GitHub Actions workflows | `kind: maintenance` | +| Add or improve tests | `kind: testing` | +| Improve performance of existing functionality | `kind: enhancement` | +| Improve an existing feature without adding new API | `kind: enhancement` | +| Address a security concern that isn't a regression | `kind: security` | + +> **Note:** The `kind:` label is applied by maintainers during triage β€” you don't need to choose it when submitting. Just describe what needs to be done and why. + +### Not a task + +- Something that is broken β†’ that's a **Bug** +- A new capability that doesn't currently exist β†’ that's a **Feature** + +### What makes a good task report + +A useful task report includes: +- A clear description of the current state and why it should change +- A concrete proposed approach +- Step-by-step implementation guidance (if you know it) + +Even if you don't know the full implementation, describing the problem clearly is enough to get started. + +--- + +## After Submitting + +When you submit an issue using any of the three templates, it is automatically labeled `status: awaiting triage`. This signals to maintainers that the issue needs review. + +A maintainer will then: + +1. Review the issue for completeness +2. Apply the appropriate **skill level**, **priority**, **kind** (for bugs and tasks), and **scope** labels +3. Run `/finalize`, which updates the issue title and description to the standard format and marks it `status: ready for dev` + +Once an issue is `status: ready for dev`, any contributor meeting the skill prerequisites can comment `/assign` to pick it up. + +For more on how contribution works, see the [Workflow Guide](../training/workflow.md). diff --git a/docs/maintainers/guidelines-triage.md b/docs/maintainers/guidelines-triage.md new file mode 100644 index 000000000..0d94c802a --- /dev/null +++ b/docs/maintainers/guidelines-triage.md @@ -0,0 +1,150 @@ +# Issue Triage Guide β€” Hiero C++ SDK + +## Overview + +This guide explains how to triage newly submitted issues and use the `/finalize` command to prepare them for contributors. + +Issues submitted via the **Bug**, **Feature**, or **Task** templates are automatically labeled `status: awaiting triage`. The triage process turns a raw submission into a well-labeled, clearly structured issue that contributors can confidently pick up. + +--- + +## Finding Issues to Triage + +Filter the issue list by `status: awaiting triage` to find issues that need attention: + +[Browse issues awaiting triage](https://github.com/hiero-ledger/hiero-sdk-cpp/issues?q=is%3Aissue+is%3Aopen+label%3A%22status%3A+awaiting+triage%22) + +--- + +## The Triage Checklist + +Before running `/finalize`, apply all required labels and verify the issue is complete: + +### 1. Review the issue + +- Does the description make sense? +- Is there enough information to act on it? +- If it's a bug, is it reproducible? If not, apply `status: needs repro` or `status: needs info` and ask for more details. +- Is it a duplicate? If so, apply `resolution: duplicate`, link the original, and close it. + +### 2. Apply labels + +All of the following must be applied before `/finalize` will succeed: + +| Label group | Rule | Examples | +|---|---|---| +| `skill:` | **Exactly 1** β€” how hard is this to implement? | `skill: beginner`, `skill: advanced` | +| `priority:` | **Exactly 1** β€” how urgently should this be done? | `priority: medium`, `priority: high` | +| `kind:` | **Exactly 1** for Bug and Task; **0** for Feature | `kind: maintenance`, `kind: refactor` | +| `scope:` | **0 or more** β€” which part of the SDK is affected? | `scope: api`, `scope: grpc` | + +> The `status:` label is managed automatically by the bot and should not be changed manually unless something goes wrong. + +#### Choosing a skill level + +See the skill level guidelines for detailed guidance: +- [Good First Issue Guidelines](./guidelines-good-first-issues.md) +- [Beginner Issue Guidelines](./guidelines-beginner-issues.md) +- [Intermediate Issue Guidelines](./guidelines-intermediate-issues.md) +- [Advanced Issue Guidelines](./guidelines-advanced-issues.md) +- [Difficulty Overview](./guidelines-difficulty-overview.md) + +As a quick reference: + +| Level | Scope | Expected time | Who it's for | +|---|---|---|---| +| `skill: good first issue` | Single file, step-by-step | 1–4 hours | First-time contributors | +| `skill: beginner` | 1–3 related files | 4–8 hours | Contributors with 2 completed GFIs | +| `skill: intermediate` | Multiple modules | 1–3 days | Contributors familiar with the codebase | +| `skill: advanced` | Repository-wide | 3+ days | Experienced contributors | + +#### Choosing a kind label (for bugs and tasks) + +| Label | When to use | +|---|---| +| `kind: enhancement` | Improving existing functionality without adding new public API | +| `kind: documentation` | README, guides, API doc comments | +| `kind: refactor` | Code restructuring without behavior change | +| `kind: security` | Security-related improvements or hardening | +| `kind: testing` | Adding or improving tests | +| `kind: maintenance` | Dependencies, build system, CI/CD | + +### 3. Edit the issue body if needed + +Before running `/finalize`, ensure the issue body is complete and actionable. For complex issues, it's fine to add implementation guidance, point to relevant files, or clarify the scope directly in the issue. + +`/finalize` will preserve all the existing content and add skill-level context and a contribution guide around it. + +--- + +## Running `/finalize` + +Once all labels are applied and the issue looks good, comment `/finalize` on the issue. + +``` +/finalize +``` + +The bot will: + +1. Verify you have **triage or above** repository permissions +2. Check that all required labels are correctly applied β€” if not, it will post a comment listing every violation +3. Update the **issue title** to include the skill-level prefix (e.g. `[Beginner]: Fix thing`) +4. Rebuild the **issue body** in the standard skill-level template format: + - Prepends the skill-level intro block and `[!IMPORTANT]` callout + - Preserves all of the submitter's original content + - Appends a standard contribution guide and additional information section +5. Swap the status label: `status: awaiting triage` β†’ `status: ready for dev` +6. Post a success comment confirming the finalization + +The issue is now ready for contributors to pick up via `/assign`. + +--- + +## Handling Validation Errors + +If `/finalize` responds with a validation error comment, read the listed violations carefully. Each one explains exactly what label is missing, duplicated, or incorrectly applied. + +Common fixes: + +| Error | Fix | +|---|---| +| No `skill:` label | Apply one of the four skill labels | +| Multiple `skill:` labels | Remove all but one | +| No `priority:` label | Apply a priority label | +| Bug/Task missing `kind:` label | Apply the appropriate kind label | +| Feature has a `kind:` label | Remove the kind label | +| `status: awaiting triage` not present | Check that the issue hasn't already been finalized | + +After fixing the labels, comment `/finalize` again. + +--- + +## Edge Cases + +### Issue already finalized (re-finalized) + +Running `/finalize` on an already-finalized issue (one with `status: ready for dev`) will fail validation because `status: awaiting triage` is no longer present. To re-finalize, manually swap the status label back to `status: awaiting triage` first, then run `/finalize` again. + +If you only need to update the title prefix (e.g. because you changed the skill label), you can do that manually rather than going through the full re-finalize process. + +### Blank issues + +Issues created without a template won't have a recognized type (Bug, Feature, or Task). `/finalize` will report this as a validation error. Either ask the submitter to re-submit using a template, or manually set the GitHub issue type before running `/finalize`. + +### API errors + +If the bot reports an API error (permission check failure, issue update failure, or label swap failure), it will tag `@hiero-ledger/hiero-sdk-cpp-maintainers` in the comment with instructions for completing the finalization manually. + +--- + +## Why This Process Exists + +Previously, issue submitters chose a skill-level template themselves. This led to mislabeled issues: contributors had no way to assess implementation complexity, so difficulty labels were inconsistently applied and the triage step was unclear. + +The new process separates concerns: +- **Submitters** describe the problem (Bug, Feature, or Task) +- **Maintainers** assess complexity and apply the skill label, then run `/finalize` +- **Contributors** pick up well-structured, clearly labeled issues via `/assign` + +This makes the contribution experience more predictable for everyone. From f436e8e13618bed493dee7fa68494c0054113220 Mon Sep 17 00:00:00 2001 From: Rob Walworth Date: Wed, 25 Mar 2026 17:07:18 -0400 Subject: [PATCH 2/5] refactor: make optimizations and use snapshot testing Signed-off-by: Rob Walworth --- .github/scripts/commands/assign.js | 27 +- .github/scripts/commands/finalize-comments.js | 1 - .github/scripts/commands/finalize.js | 88 ++---- .github/scripts/commands/unassign.js | 22 +- .github/scripts/helpers/api.js | 41 +++ .github/scripts/helpers/logger.js | 14 + .github/scripts/tests/test-assign-bot.js | 2 +- .github/scripts/tests/test-finalize-bot.js | 274 +++++++++++++++--- 8 files changed, 325 insertions(+), 144 deletions(-) diff --git a/.github/scripts/commands/assign.js b/.github/scripts/commands/assign.js index beb62d9fc..a2971208d 100644 --- a/.github/scripts/commands/assign.js +++ b/.github/scripts/commands/assign.js @@ -9,12 +9,11 @@ const { LABELS, ISSUE_STATE, - getLogger, + createDelegatingLogger, isNonNegativeInteger, isSafeSearchToken, hasLabel, - addLabels, - removeLabel, + swapLabels, addAssignees, postComment, acknowledgeComment, @@ -37,10 +36,7 @@ const { // Delegate to the active logger set by the dispatcher (bot-on-comment.js). // This ensures the correct prefix is used after command parsing. -const logger = { - log: (...args) => getLogger().log(...args), - error: (...args) => getLogger().error(...args), -}; +const logger = createDelegatingLogger(); /** * Returns the skill-level label on an issue, checking in ascending order: @@ -203,20 +199,9 @@ async function checkPrerequisites(botContext, skillLevel, requesterUsername) { * @returns {Promise} */ async function updateLabels(botContext, requesterUsername) { - let labelUpdateFailed = false; - let labelUpdateError = ''; - const removeResult = await removeLabel(botContext, LABELS.READY_FOR_DEV); - if (!removeResult.success) { - labelUpdateFailed = true; - labelUpdateError = `Failed to remove "${LABELS.READY_FOR_DEV}" label: ${removeResult.error}`; - } - const addResult = await addLabels(botContext, [LABELS.IN_PROGRESS]); - if (!addResult.success) { - labelUpdateFailed = true; - labelUpdateError += (labelUpdateError ? '; ' : '') + `Failed to add "${LABELS.IN_PROGRESS}" label: ${addResult.error}`; - } - if (labelUpdateFailed) { - await postComment(botContext, buildLabelUpdateFailureComment(requesterUsername, labelUpdateError)); + const { success, errorDetails } = await swapLabels(botContext, LABELS.READY_FOR_DEV, LABELS.IN_PROGRESS); + if (!success) { + await postComment(botContext, buildLabelUpdateFailureComment(requesterUsername, errorDetails)); logger.log('Posted label update failure comment, tagged maintainers'); } } diff --git a/.github/scripts/commands/finalize-comments.js b/.github/scripts/commands/finalize-comments.js index e6aa97f27..ec420bcd5 100644 --- a/.github/scripts/commands/finalize-comments.js +++ b/.github/scripts/commands/finalize-comments.js @@ -431,7 +431,6 @@ function buildSuccessComment(username, skillLevel, priorityLabel) { module.exports = { SKILL_TITLE_PREFIXES, - SKILL_BOILERPLATE, CONTRIBUTION_GUIDE_LABEL, CONTRIBUTION_GUIDE_CONTENT, DEFAULT_ADDITIONAL_INFO_LABEL, diff --git a/.github/scripts/commands/finalize.js b/.github/scripts/commands/finalize.js index a71cb9f54..268b486b4 100644 --- a/.github/scripts/commands/finalize.js +++ b/.github/scripts/commands/finalize.js @@ -11,17 +11,16 @@ const { LABELS, - getLogger, + createDelegatingLogger, hasLabel, - addLabels, - removeLabel, + getLabelsByPrefix, + swapLabels, postComment, acknowledgeComment, } = require('../helpers'); const { SKILL_TITLE_PREFIXES, - SKILL_BOILERPLATE, reconstructBody, buildUnauthorizedComment, buildValidationErrorComment, @@ -32,10 +31,7 @@ const { } = require('./finalize-comments'); // Delegate to the active logger set by the dispatcher (bot-on-comment.js). -const logger = { - log: (...args) => getLogger().log(...args), - error: (...args) => getLogger().error(...args), -}; +const logger = createDelegatingLogger(); // Permission levels that are allowed to run /finalize (triage and above). const ALLOWED_ROLE_NAMES = new Set(['triage', 'write', 'maintain', 'admin']); @@ -65,16 +61,16 @@ async function checkPermission(botContext, username) { username, }); const roleName = data?.role_name; - logger.log(`[finalize] Permission check for @${username}: role_name="${roleName}"`); + logger.log(`Permission check for @${username}: role_name="${roleName}"`); if (ALLOWED_ROLE_NAMES.has(roleName)) return 'authorized'; return 'unauthorized'; } catch (error) { const status = error?.status ?? error?.response?.status; if (status === 404) { - logger.log(`[finalize] @${username} is not a collaborator (404) β€” unauthorized`); + logger.log(`@${username} is not a collaborator (404) β€” unauthorized`); return 'unauthorized'; } - logger.error(`[finalize] Permission check failed for @${username}:`, error.message); + logger.error(`Permission check failed for @${username}:`, error.message); return 'error'; } } @@ -96,19 +92,6 @@ function getIssueTypeName(issue) { return null; } -/** - * Returns all label names on the issue that start with the given prefix. - * - * @param {object} issue - The issue object. - * @param {string} prefix - Label group prefix (e.g. 'skill:'). - * @returns {string[]} - */ -function getLabelsByPrefix(issue, prefix) { - return (issue.labels || []) - .map((l) => (typeof l === 'string' ? l : l?.name || '')) - .filter((name) => name.toLowerCase().startsWith(prefix.toLowerCase())); -} - /** * Collects all label validation violations for /finalize. Returns an empty * array when everything is valid. @@ -210,41 +193,6 @@ function buildNewTitle(currentTitle, skillLevel) { return `${prefix}${strippedTitle}`; } -// ============================================================================= -// LABEL SWAP -// ============================================================================= - -/** - * Swaps `status: awaiting triage` β†’ `status: ready for dev` after a successful - * issue update. If either label operation fails, posts a comment tagging - * maintainers with manual instructions. - * - * @param {object} botContext - Bot context from buildBotContext. - * @param {string} finalizerUsername - Username who ran /finalize (for the failure comment). - * @returns {Promise} - */ -async function swapTriageLabels(botContext, finalizerUsername) { - let failed = false; - let errorDetails = ''; - - const removeResult = await removeLabel(botContext, LABELS.AWAITING_TRIAGE); - if (!removeResult.success) { - failed = true; - errorDetails += `Failed to remove \`${LABELS.AWAITING_TRIAGE}\`: ${removeResult.error}`; - } - - const addResult = await addLabels(botContext, [LABELS.READY_FOR_DEV]); - if (!addResult.success) { - failed = true; - errorDetails += (errorDetails ? '; ' : '') + `Failed to add \`${LABELS.READY_FOR_DEV}\`: ${addResult.error}`; - } - - if (failed) { - await postComment(botContext, buildLabelSwapFailureComment(finalizerUsername, errorDetails)); - logger.log('[finalize] Posted label swap failure comment, tagged maintainers'); - } -} - // ============================================================================= // MAIN HANDLER // ============================================================================= @@ -275,12 +223,12 @@ async function handleFinalize(botContext) { // STEP 2: Permission check const permResult = await checkPermission(botContext, finalizerUsername); if (permResult === 'error') { - logger.log('[finalize] Exit: permission check API error'); + logger.log('Exit: permission check API error'); await postComment(botContext, buildPermissionCheckErrorComment(finalizerUsername)); return; } if (permResult === 'unauthorized') { - logger.log(`[finalize] Exit: @${finalizerUsername} is not authorized`); + logger.log(`Exit: @${finalizerUsername} is not authorized`); await postComment(botContext, buildUnauthorizedComment(finalizerUsername)); return; } @@ -288,7 +236,7 @@ async function handleFinalize(botContext) { // STEP 3: Label validation β€” collect ALL violations before posting const violations = collectLabelViolations(botContext.issue); if (violations.length > 0) { - logger.log(`[finalize] Exit: ${violations.length} label violation(s) found`); + logger.log(`Exit: ${violations.length} label violation(s) found`); await postComment(botContext, buildValidationErrorComment(finalizerUsername, violations)); return; } @@ -300,7 +248,7 @@ async function handleFinalize(botContext) { const newTitle = buildNewTitle(botContext.issue.title, skillLevel); const newBody = reconstructBody(botContext.issue.body || '', skillLevel); - logger.log(`[finalize] Updating issue #${botContext.number}: title="${newTitle}", skillLevel="${skillLevel}"`); + logger.log(`Updating issue #${botContext.number}: title="${newTitle}", skillLevel="${skillLevel}"`); // STEP 5: Update issue title and body let updateError = null; @@ -312,10 +260,10 @@ async function handleFinalize(botContext) { title: newTitle, body: newBody, }); - logger.log('[finalize] Issue updated successfully'); + logger.log('Issue updated successfully'); } catch (error) { updateError = error instanceof Error ? error.message : String(error); - logger.error('[finalize] Issue update failed:', updateError); + logger.error('Issue update failed:', updateError); } if (updateError) { @@ -323,13 +271,17 @@ async function handleFinalize(botContext) { return; } - // STEP 6: Swap status labels - await swapTriageLabels(botContext, finalizerUsername); + // STEP 6: Swap status labels: awaiting triage β†’ ready for dev + const swapResult = await swapLabels(botContext, LABELS.AWAITING_TRIAGE, LABELS.READY_FOR_DEV); + if (!swapResult.success) { + await postComment(botContext, buildLabelSwapFailureComment(finalizerUsername, swapResult.errorDetails)); + logger.log('Posted label swap failure comment, tagged maintainers'); + } // STEP 7: Post success comment const priorityLabel = getLabelsByPrefix(botContext.issue, 'priority:')[0]; await postComment(botContext, buildSuccessComment(finalizerUsername, skillLevel, priorityLabel)); - logger.log('[finalize] Finalize flow completed successfully'); + logger.log('Finalize flow completed successfully'); } module.exports = { handleFinalize }; diff --git a/.github/scripts/commands/unassign.js b/.github/scripts/commands/unassign.js index e80f7a4ca..eac65fa44 100644 --- a/.github/scripts/commands/unassign.js +++ b/.github/scripts/commands/unassign.js @@ -9,9 +9,8 @@ const { LABELS, ISSUE_STATE, - getLogger, - addLabels, - removeLabel, + createDelegatingLogger, + swapLabels, removeAssignees, postComment, acknowledgeComment, @@ -26,10 +25,7 @@ const { } = require('./unassign-comments'); // Delegate to the active logger set by the dispatcher. -const logger = { - log: (...args) => getLogger().log(...args), - error: (...args) => getLogger().error(...args), -}; +const logger = createDelegatingLogger(); /** * Main handler for the /unassign command. Runs the following gates in order: @@ -89,15 +85,9 @@ async function handleUnassign(botContext) { // ACTION 2: Label Swapping (Mirroring assign.js style - no stale checks) logger.log(`Swapping labels: removing ${LABELS.IN_PROGRESS}, adding ${LABELS.READY_FOR_DEV}`); - - const removeLabelResult = await removeLabel(botContext, LABELS.IN_PROGRESS); - if (!removeLabelResult.success) { - logger.error(`Failed to remove ${LABELS.IN_PROGRESS}: ${removeLabelResult.error}`); - } - - const addLabelResult = await addLabels(botContext, [LABELS.READY_FOR_DEV]); - if (!addLabelResult.success) { - logger.error(`Failed to add ${LABELS.READY_FOR_DEV}: ${addLabelResult.error}`); + const { success: swapSuccess, errorDetails: swapError } = await swapLabels(botContext, LABELS.IN_PROGRESS, LABELS.READY_FOR_DEV); + if (!swapSuccess) { + logger.error(`Label swap failed: ${swapError}`); } // ACTION 3: Post success acknowledgment diff --git a/.github/scripts/helpers/api.js b/.github/scripts/helpers/api.js index 84b14adaf..b11e8079c 100644 --- a/.github/scripts/helpers/api.js +++ b/.github/scripts/helpers/api.js @@ -260,6 +260,45 @@ function hasLabel(issueOrPr, labelName) { }); } +/** + * Returns all label names on an issue or PR that start with the given prefix. + * The comparison is case-insensitive. + * + * @param {object} issueOrPr - The issue or PR object. + * @param {string} prefix - Label group prefix (e.g. 'skill:'). + * @returns {string[]} + */ +function getLabelsByPrefix(issueOrPr, prefix) { + return (issueOrPr.labels || []) + .map((l) => (typeof l === 'string' ? l : l?.name || '')) + .filter((name) => name.toLowerCase().startsWith(prefix.toLowerCase())); +} + +/** + * Removes `fromLabel` and adds `toLabel` on the issue/PR. Both operations are + * always attempted; errors are collected and returned rather than thrown. + * + * @param {object} botContext - Bot context (github, owner, repo, number). + * @param {string} fromLabel - Label to remove. + * @param {string} toLabel - Label to add. + * @returns {Promise<{ success: boolean, errorDetails: string }>} + */ +async function swapLabels(botContext, fromLabel, toLabel) { + const errors = []; + + const removeResult = await removeLabel(botContext, fromLabel); + if (!removeResult.success) { + errors.push(`Failed to remove '${fromLabel}': ${removeResult.error}`); + } + + const addResult = await addLabels(botContext, [toLabel]); + if (!addResult.success) { + errors.push(`Failed to add '${toLabel}': ${addResult.error}`); + } + + return { success: errors.length === 0, errorDetails: errors.join('; ') }; +} + /** * Posts a new comment or updates an existing one identified by an HTML marker. * Paginates through all comments to find a match. @@ -494,6 +533,8 @@ module.exports = { removeAssignees, postComment, hasLabel, + getLabelsByPrefix, + swapLabels, postOrUpdateComment, fetchPRCommits, fetchIssue, diff --git a/.github/scripts/helpers/logger.js b/.github/scripts/helpers/logger.js index 86ec01d3e..133c8ee26 100644 --- a/.github/scripts/helpers/logger.js +++ b/.github/scripts/helpers/logger.js @@ -34,7 +34,21 @@ function getLogger() { return _logger; } +/** + * Returns a logger proxy that always delegates to whichever logger is currently + * active (i.e., the one last set by createLogger). Use this in command modules + * so log calls automatically pick up the prefix set by the dispatcher. + * @returns {{ log: function, error: function }} + */ +function createDelegatingLogger() { + return { + log: (...args) => getLogger().log(...args), + error: (...args) => getLogger().error(...args), + }; +} + module.exports = { createLogger, getLogger, + createDelegatingLogger, }; diff --git a/.github/scripts/tests/test-assign-bot.js b/.github/scripts/tests/test-assign-bot.js index 4f21e5fd4..542cbc48f 100644 --- a/.github/scripts/tests/test-assign-bot.js +++ b/.github/scripts/tests/test-assign-bot.js @@ -947,7 +947,7 @@ Good luck, and welcome aboard! πŸš€`, - Remove the \`status: ready for dev\` label - Add the \`status: in progress\` label -Error details: Failed to remove "status: ready for dev" label: Simulated remove label failure; Failed to add "status: in progress" label: Simulated add label failure`, +Error details: Failed to remove 'status: ready for dev': Simulated remove label failure; Failed to add 'status: in progress': Simulated add label failure`, ], }, diff --git a/.github/scripts/tests/test-finalize-bot.js b/.github/scripts/tests/test-finalize-bot.js index 58b338aaa..d41f0faf4 100644 --- a/.github/scripts/tests/test-finalize-bot.js +++ b/.github/scripts/tests/test-finalize-bot.js @@ -10,6 +10,11 @@ const { LABELS } = require('../helpers'); const script = require('../bot-on-comment.js'); +const { runTestSuite, verifyComments } = require('./test-utils'); +const { + parseSections, + isMeaningfulContent, +} = require('../commands/finalize-comments'); // ============================================================================= // MOCK GITHUB API @@ -132,9 +137,9 @@ function makeContext(issue, commentBody = '/finalize', commenter = 'maintainer') }; } -async function runScenario(scenario) { +async function runScenario(scenario, index) { console.log('\n' + '='.repeat(70)); - console.log(`TEST: ${scenario.name}`); + console.log(`TEST ${index + 1}: ${scenario.name}`); console.log(`DESC: ${scenario.description}`); console.log('='.repeat(70)); @@ -148,10 +153,19 @@ async function runScenario(scenario) { threw = e; } - // Run assertions let passed = true; const failures = []; + // Snapshot verification for comment text + if (scenario.expectedComments !== undefined) { + const commentResult = verifyComments(scenario.expectedComments, github.calls.comments); + if (!commentResult.passed) { + passed = false; + failures.push(...commentResult.details.filter((d) => d.startsWith('❌'))); + } + } + + // Other behavioural assertions (labels, reactions, issue updates, body content) for (const assertion of scenario.assertions || []) { const result = assertion(github.calls, threw); if (result !== true) { @@ -195,6 +209,10 @@ const assert = { const found = calls.issueUpdates.some((u) => u.body && u.body.includes(text)); return found || `Expected updated body to contain: "${text}"`; }, + bodyNotContains: (text) => (calls) => { + const found = calls.issueUpdates.some((u) => u.body && u.body.includes(text)); + return !found || `Expected updated body NOT to contain: "${text}"`; + }, reactionAdded: () => (calls) => calls.reactions.length > 0 || 'Expected thumbs-up reaction to be added', }; @@ -202,6 +220,93 @@ const assert = { // SCENARIOS // ============================================================================= +// ============================================================================= +// EXPECTED COMMENT SNAPSHOTS +// ============================================================================= +// Defined as constants so the same text can be reused across scenarios that +// produce the same comment (e.g. both unauthorized scenarios). + +const COMMENT_UNAUTHORIZED = `πŸ‘‹ Hi @maintainer! The \`/finalize\` command is reserved for maintainers and contributors with **triage** (or higher) repository permissions. + +If you believe you should have access, please reach out to a maintainer.`; + +const COMMENT_PERMISSION_ERROR = `πŸ‘‹ Hi @maintainer! I encountered an error while trying to verify your permissions. + +@hiero-ledger/hiero-sdk-cpp-maintainers β€” could you please verify @maintainer's permissions and complete the finalization manually if appropriate? + +Sorry for the inconvenience!`; + +const COMMENT_UPDATE_FAILURE = `⚠️ Hi @maintainer! I encountered an error while trying to update the issue title or body. + +@hiero-ledger/hiero-sdk-cpp-maintainers β€” could you please complete the finalization manually? + +Error details: Simulated issue update failure`; + +const COMMENT_SWAP_FAILURE_REMOVE = `⚠️ The issue was updated successfully, but I encountered an error swapping the status labels. + +@hiero-ledger/hiero-sdk-cpp-maintainers β€” please manually: +- Remove the \`status: awaiting triage\` label +- Add the \`status: ready for dev\` label + +Error details: Failed to remove 'status: awaiting triage': Simulated remove label failure`; + +const COMMENT_SWAP_FAILURE_ADD = `⚠️ The issue was updated successfully, but I encountered an error swapping the status labels. + +@hiero-ledger/hiero-sdk-cpp-maintainers β€” please manually: +- Remove the \`status: awaiting triage\` label +- Add the \`status: ready for dev\` label + +Error details: Failed to add 'status: ready for dev': Simulated add label failure`; + +const COMMENT_SUCCESS_GFI_LOW = `βœ… Issue finalized by @maintainer! + +**Skill level:** \`Good First Issue\` +**Priority:** \`priority: low\` + +The issue body has been updated with the appropriate skill-level context and contribution guide. This issue is now ready for contributors to pick up via \`/assign\`.`; + +const COMMENT_SUCCESS_BEGINNER_MEDIUM = `βœ… Issue finalized by @maintainer! + +**Skill level:** \`Beginner\` +**Priority:** \`priority: medium\` + +The issue body has been updated with the appropriate skill-level context and contribution guide. This issue is now ready for contributors to pick up via \`/assign\`.`; + +const COMMENT_SUCCESS_INTERMEDIATE_HIGH = `βœ… Issue finalized by @maintainer! + +**Skill level:** \`Intermediate\` +**Priority:** \`priority: high\` + +The issue body has been updated with the appropriate skill-level context and contribution guide. This issue is now ready for contributors to pick up via \`/assign\`.`; + +const COMMENT_SUCCESS_ADVANCED_MEDIUM = `βœ… Issue finalized by @maintainer! + +**Skill level:** \`Advanced\` +**Priority:** \`priority: medium\` + +The issue body has been updated with the appropriate skill-level context and contribution guide. This issue is now ready for contributors to pick up via \`/assign\`.`; + +/** Builds the standard validation-error comment for one or more violations. */ +function validationComment(...errors) { + const errorList = errors.map((e) => `- ${e}`).join('\n'); + return `πŸ‘‹ Hi @maintainer! The issue isn't quite ready to finalize yet. Please fix the following labeling issue(s) and then comment \`/finalize\` again:\n\n${errorList}\n\nIf you have questions about which labels to apply, see the maintainer documentation or ask in the team channel.`; +} + +// Pre-built validation error strings matching the exact output of collectLabelViolations. +const ERR_MISSING_TRIAGE = `The \`status: awaiting triage\` label must be present to run \`/finalize\`. Current status label(s): \`status: ready for dev\`.`; +const ERR_NO_SKILL = `Exactly one \`skill:\` label is required (e.g. \`skill: beginner\`). None found. Choose from: \`skill: good first issue\`, \`skill: beginner\`, \`skill: intermediate\`, \`skill: advanced\`.`; +const ERR_MULTIPLE_SKILLS = `Exactly one \`skill:\` label is required. Found 2: \`skill: beginner\`, \`skill: intermediate\`. Please remove all but one.`; +const ERR_NO_PRIORITY = `Exactly one \`priority:\` label is required (e.g. \`priority: medium\`). None found.`; +const ERR_TASK_NO_KIND = `Task issues require exactly one \`kind:\` label (e.g. \`kind: maintenance\`). None found.`; +const ERR_BUG_NO_KIND = `Bug issues require exactly one \`kind:\` label (e.g. \`kind: maintenance\`). None found.`; +const ERR_FEATURE_WITH_KIND_ENHANCEMENT = `Feature issues should not have a \`kind:\` label. Found: \`kind: enhancement\`. Please remove it.`; +const ERR_FEATURE_WITH_KIND_MAINTENANCE = `Feature issues should not have a \`kind:\` label. Found: \`kind: maintenance\`. Please remove it.`; +const ERR_UNKNOWN_TYPE = `The issue type (Bug, Feature, or Task) could not be determined. Ensure the issue was submitted using one of the official issue templates.`; + +// ============================================================================= +// SCENARIOS +// ============================================================================= + const scenarios = [ // --------------------------------------------------------------------------- // AUTHORIZATION @@ -212,9 +317,9 @@ const scenarios = [ description: 'A collaborator with "read" role is rejected', context: makeContext(makeIssue()), githubOptions: { roleName: 'read' }, + expectedComments: [COMMENT_UNAUTHORIZED], assertions: [ assert.reactionAdded(), - assert.commentContains('reserved for maintainers'), assert.noIssueUpdate(), assert.noLabelsAdded(), ], @@ -225,9 +330,9 @@ const scenarios = [ description: 'A user who is not a repo collaborator is rejected', context: makeContext(makeIssue()), githubOptions: { permissionNotFound: true }, + expectedComments: [COMMENT_UNAUTHORIZED], assertions: [ assert.reactionAdded(), - assert.commentContains('reserved for maintainers'), assert.noIssueUpdate(), ], }, @@ -237,9 +342,9 @@ const scenarios = [ description: 'When the permission API fails, posts an error comment and tags maintainers', context: makeContext(makeIssue()), githubOptions: { permissionShouldFail: true }, + expectedComments: [COMMENT_PERMISSION_ERROR], assertions: [ assert.reactionAdded(), - assert.commentContains('encountered an error while trying to verify your permissions'), assert.noIssueUpdate(), ], }, @@ -261,8 +366,8 @@ const scenarios = [ type: { name: 'Task' }, })), githubOptions: { roleName: 'triage' }, + expectedComments: [validationComment(ERR_MISSING_TRIAGE)], assertions: [ - assert.commentContains('status: awaiting triage'), assert.noIssueUpdate(), assert.noLabelsAdded(), ], @@ -280,8 +385,8 @@ const scenarios = [ type: { name: 'Task' }, })), githubOptions: { roleName: 'triage' }, + expectedComments: [validationComment(ERR_NO_SKILL)], assertions: [ - assert.commentContains('skill:'), assert.noIssueUpdate(), ], }, @@ -300,8 +405,8 @@ const scenarios = [ type: { name: 'Task' }, })), githubOptions: { roleName: 'triage' }, + expectedComments: [validationComment(ERR_MULTIPLE_SKILLS)], assertions: [ - assert.commentContains('skill:'), assert.noIssueUpdate(), ], }, @@ -318,8 +423,8 @@ const scenarios = [ type: { name: 'Task' }, })), githubOptions: { roleName: 'triage' }, + expectedComments: [validationComment(ERR_NO_PRIORITY)], assertions: [ - assert.commentContains('priority:'), assert.noIssueUpdate(), ], }, @@ -336,8 +441,8 @@ const scenarios = [ type: { name: 'Task' }, })), githubOptions: { roleName: 'triage' }, + expectedComments: [validationComment(ERR_TASK_NO_KIND)], assertions: [ - assert.commentContains('kind:'), assert.noIssueUpdate(), ], }, @@ -354,8 +459,8 @@ const scenarios = [ type: { name: 'Bug' }, })), githubOptions: { roleName: 'triage' }, + expectedComments: [validationComment(ERR_BUG_NO_KIND)], assertions: [ - assert.commentContains('kind:'), assert.noIssueUpdate(), ], }, @@ -373,8 +478,8 @@ const scenarios = [ type: { name: 'Feature' }, })), githubOptions: { roleName: 'triage' }, + expectedComments: [validationComment(ERR_FEATURE_WITH_KIND_ENHANCEMENT)], assertions: [ - assert.commentContains('Feature issues should not have a `kind:`'), assert.noIssueUpdate(), ], }, @@ -390,10 +495,8 @@ const scenarios = [ type: { name: 'Feature' }, })), githubOptions: { roleName: 'admin' }, + expectedComments: [validationComment(ERR_NO_SKILL, ERR_NO_PRIORITY, ERR_FEATURE_WITH_KIND_MAINTENANCE)], assertions: [ - assert.commentContains('skill:'), - assert.commentContains('priority:'), - assert.commentContains('Feature issues should not'), assert.noIssueUpdate(), ], }, @@ -403,8 +506,8 @@ const scenarios = [ description: 'Issue created without a recognized type triggers a validation error', context: makeContext(makeIssue({ type: null })), githubOptions: { roleName: 'triage' }, + expectedComments: [validationComment(ERR_UNKNOWN_TYPE)], assertions: [ - assert.commentContains('issue type'), assert.noIssueUpdate(), ], }, @@ -426,6 +529,7 @@ const scenarios = [ type: { name: 'Feature' }, })), githubOptions: { roleName: 'triage' }, + expectedComments: [COMMENT_SUCCESS_GFI_LOW], assertions: [ assert.reactionAdded(), assert.issueUpdated(), @@ -435,7 +539,6 @@ const scenarios = [ assert.bodyContains('Step-by-Step Contribution Guide'), assert.labelAdded(LABELS.READY_FOR_DEV), assert.labelRemoved(LABELS.AWAITING_TRIAGE), - assert.commentContains('finalized by @maintainer'), ], }, @@ -444,6 +547,7 @@ const scenarios = [ description: 'Valid beginner task is finalized; title prefix added, body reconstructed', context: makeContext(makeIssue()), githubOptions: { roleName: 'triage' }, + expectedComments: [COMMENT_SUCCESS_BEGINNER_MEDIUM], assertions: [ assert.reactionAdded(), assert.issueUpdated(), @@ -453,7 +557,6 @@ const scenarios = [ assert.bodyContains('Step-by-Step Contribution Guide'), assert.labelAdded(LABELS.READY_FOR_DEV), assert.labelRemoved(LABELS.AWAITING_TRIAGE), - assert.commentContains('finalized by @maintainer'), ], }, @@ -471,6 +574,7 @@ const scenarios = [ type: { name: 'Bug' }, })), githubOptions: { roleName: 'write' }, + expectedComments: [COMMENT_SUCCESS_INTERMEDIATE_HIGH], assertions: [ assert.issueUpdated(), assert.titleContains('[Intermediate]:'), @@ -478,7 +582,6 @@ const scenarios = [ assert.bodyContains('About Intermediate Issues'), assert.bodyContains('Step-by-Step Contribution Guide'), assert.labelAdded(LABELS.READY_FOR_DEV), - assert.commentContains('finalized by @maintainer'), ], }, @@ -497,6 +600,7 @@ const scenarios = [ type: { name: 'Task' }, })), githubOptions: { roleName: 'admin' }, + expectedComments: [COMMENT_SUCCESS_ADVANCED_MEDIUM], assertions: [ assert.issueUpdated(), assert.titleContains('[Advanced]:'), @@ -504,7 +608,6 @@ const scenarios = [ assert.bodyContains('About Advanced Issues'), assert.bodyContains('Step-by-Step Contribution Guide'), assert.labelAdded(LABELS.READY_FOR_DEV), - assert.commentContains('finalized by @maintainer'), ], }, @@ -522,9 +625,10 @@ const scenarios = [ type: { name: 'Task' }, })), githubOptions: { roleName: 'maintain' }, + expectedComments: [COMMENT_SUCCESS_ADVANCED_MEDIUM], assertions: [ - assert.titleContains('[Advanced]: Fix something'), assert.issueUpdated(), + assert.titleContains('[Advanced]: Fix something'), ], }, @@ -537,42 +641,138 @@ const scenarios = [ description: 'When issues.update throws, a failure comment is posted and labels are NOT swapped', context: makeContext(makeIssue()), githubOptions: { roleName: 'triage', updateShouldFail: true }, + expectedComments: [COMMENT_UPDATE_FAILURE], assertions: [ - assert.commentContains('encountered an error while trying to update'), assert.noLabelsAdded(), assert.noLabelsRemoved(), ], }, { - name: 'API failure β€” label swap fails after successful update', - description: 'When removeLabel throws after a successful update, maintainers are tagged', + name: 'API failure β€” remove label fails after successful update', + description: 'When removeLabel throws, the swap failure comment and success comment are both posted', context: makeContext(makeIssue()), githubOptions: { roleName: 'triage', removeLabelShouldFail: true }, + expectedComments: [COMMENT_SWAP_FAILURE_REMOVE, COMMENT_SUCCESS_BEGINNER_MEDIUM], assertions: [ assert.issueUpdated(), - assert.commentContains('encountered an error swapping the status labels'), + assert.labelAdded(LABELS.READY_FOR_DEV), // add still runs and succeeds + assert.noLabelsRemoved(), // remove failed + ], + }, + + { + name: 'API failure β€” add label fails after successful update and remove', + description: 'When addLabels throws, the swap failure comment and success comment are both posted', + context: makeContext(makeIssue()), + githubOptions: { roleName: 'triage', addLabelShouldFail: true }, + expectedComments: [COMMENT_SWAP_FAILURE_ADD, COMMENT_SUCCESS_BEGINNER_MEDIUM], + assertions: [ + assert.issueUpdated(), + assert.labelRemoved(LABELS.AWAITING_TRIAGE), // remove succeeded + assert.noLabelsAdded(), // add failed + ], + }, + + { + name: 'Body reconstruction β€” user-provided Additional Information is preserved', + description: 'When the Additional Information section has real user content it should not be replaced with the default Discord link text', + context: makeContext(makeIssue({ + body: [ + '### πŸ‘Ύ Description of the Issue\n\nSome bug.\n\n', + '### βœ”οΈ Acceptance Criteria\n\n- [ ] Fixed\n\n', + '### πŸ€” Additional Information\n\nSee the internal bug tracker for full repro steps.', + ].join(''), + labels: [ + { name: LABELS.AWAITING_TRIAGE }, + { name: LABELS.BEGINNER }, + { name: 'priority: medium' }, + { name: 'kind: maintenance' }, + ], + type: { name: 'Task' }, + })), + githubOptions: { roleName: 'triage' }, + expectedComments: [COMMENT_SUCCESS_BEGINNER_MEDIUM], + assertions: [ + assert.issueUpdated(), + assert.bodyContains('See the internal bug tracker for full repro steps.'), + assert.bodyNotContains('If you have questions while working on this issue'), ], }, ]; // ============================================================================= -// RUNNER +// UNIT TESTS β€” pure functions from finalize-comments.js // ============================================================================= -(async () => { +async function runUnitTests() { + let total = 0; let passed = 0; let failed = 0; - for (const scenario of scenarios) { - const ok = await runScenario(scenario); - if (ok) passed++; - else failed++; + function check(name, actual, expected) { + total++; + const ok = JSON.stringify(actual) === JSON.stringify(expected); + if (ok) { + passed++; + console.log(` βœ… ${name}`); + } else { + failed++; + console.log(` ❌ ${name}`); + console.log(` expected: ${JSON.stringify(expected)}`); + console.log(` actual: ${JSON.stringify(actual)}`); + } } - console.log('\n' + '='.repeat(70)); - console.log(`RESULTS: ${passed} passed, ${failed} failed`); - console.log('='.repeat(70) + '\n'); + console.log('\nπŸ“ UNIT TESTS β€” parseSections / isMeaningfulContent'); + console.log('─'.repeat(60)); + + // parseSections + check('parseSections: null β†’ []', parseSections(null), []); + check('parseSections: empty string β†’ []', parseSections(''), []); + check('parseSections: no headers β†’ single null-header entry', + parseSections('just some text'), + [{ header: null, content: 'just some text' }] + ); + check('parseSections: single section', + parseSections('### My Header\n\nsome content'), + [{ header: 'My Header', content: 'some content' }] + ); + check('parseSections: two sections', + parseSections('### First\n\ncontent one\n\n### Second\n\ncontent two'), + [ + { header: 'First', content: 'content one' }, + { header: 'Second', content: 'content two' }, + ] + ); + check('parseSections: section with no content', + parseSections('### Header\n'), + [{ header: 'Header', content: '' }] + ); + check('parseSections: leading content then a header', + parseSections('preamble\n### Section\n\nbody'), + [ + { header: null, content: 'preamble' }, + { header: 'Section', content: 'body' }, + ] + ); + + // isMeaningfulContent + check('isMeaningfulContent: null β†’ false', isMeaningfulContent(null), false); + check('isMeaningfulContent: empty string β†’ false', isMeaningfulContent(''), false); + check('isMeaningfulContent: whitespace only β†’ false', isMeaningfulContent(' '), false); + check('isMeaningfulContent: "Optional." β†’ false', isMeaningfulContent('Optional.'), false); + check('isMeaningfulContent: "_No response_" β†’ false', isMeaningfulContent('_No response_'), false); + check('isMeaningfulContent: real content β†’ true', isMeaningfulContent('Some real text here.'), true); + check('isMeaningfulContent: whitespace-padded real content β†’ true', isMeaningfulContent(' Real content '), true); + + return { total, passed, failed }; +} + +// ============================================================================= +// RUNNER +// ============================================================================= - if (failed > 0) process.exit(1); -})(); +runTestSuite('FINALIZE COMMAND TEST SUITE', scenarios, runScenario, [ + { label: 'Unit Tests', run: runUnitTests }, +]); From 6ea3025c0b25a1567f8979f2b2da9832e54bd8e0 Mon Sep 17 00:00:00 2001 From: Rob Walworth Date: Wed, 25 Mar 2026 17:18:25 -0400 Subject: [PATCH 3/5] refactor: more optimizations Signed-off-by: Rob Walworth --- .github/scripts/commands/finalize-comments.js | 16 ++++----- .github/scripts/commands/finalize.js | 33 ++++++++++++++----- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/.github/scripts/commands/finalize-comments.js b/.github/scripts/commands/finalize-comments.js index ec420bcd5..8eabf2262 100644 --- a/.github/scripts/commands/finalize-comments.js +++ b/.github/scripts/commands/finalize-comments.js @@ -218,12 +218,16 @@ function parseSections(body) { let currentHeader = null; let currentLines = []; + function flush() { + if (currentHeader !== null || currentLines.some((l) => l.trim())) { + sections.push({ header: currentHeader, content: currentLines.join('\n').trim() }); + } + } + for (const line of lines) { const match = line.match(/^### (.+)$/); if (match) { - if (currentHeader !== null || currentLines.some((l) => l.trim())) { - sections.push({ header: currentHeader, content: currentLines.join('\n').trim() }); - } + flush(); currentHeader = match[1].trim(); currentLines = []; } else { @@ -231,11 +235,7 @@ function parseSections(body) { } } - // Flush last section - if (currentHeader !== null || currentLines.some((l) => l.trim())) { - sections.push({ header: currentHeader, content: currentLines.join('\n').trim() }); - } - + flush(); return sections; } diff --git a/.github/scripts/commands/finalize.js b/.github/scripts/commands/finalize.js index 268b486b4..6acd71b90 100644 --- a/.github/scripts/commands/finalize.js +++ b/.github/scripts/commands/finalize.js @@ -36,6 +36,16 @@ const logger = createDelegatingLogger(); // Permission levels that are allowed to run /finalize (triage and above). const ALLOWED_ROLE_NAMES = new Set(['triage', 'write', 'maintain', 'admin']); +// Regex that strips any existing skill-level title prefix (e.g. "[Beginner]: "). +// Built from SKILL_TITLE_PREFIXES so it stays in sync if new levels are added. +const EXISTING_PREFIX_RE = new RegExp( + `^\\[(${Object.values(SKILL_TITLE_PREFIXES) + .map((p) => p.match(/^\[(.+)\]:/)?.[1]) + .filter(Boolean) + .join('|')})\\]:\\s*`, + 'i' +); + // Recognized GitHub issue type names set by our three templates. const KNOWN_ISSUE_TYPES = new Set(['Bug', 'Feature', 'Task']); @@ -92,6 +102,15 @@ function getIssueTypeName(issue) { return null; } +/** + * Formats an array of label names as a comma-separated inline code list. + * @param {string[]} labels + * @returns {string} e.g. "`skill: beginner`, `skill: intermediate`" + */ +function formatLabelList(labels) { + return labels.map((l) => `\`${l}\``).join(', '); +} + /** * Collects all label validation violations for /finalize. Returns an empty * array when everything is valid. @@ -118,7 +137,7 @@ function collectLabelViolations(issue) { // 1. status: awaiting triage must be present if (!hasLabel(issue, LABELS.AWAITING_TRIAGE)) { const statusLabels = getLabelsByPrefix(issue, 'status:'); - const currentStatus = statusLabels.length > 0 ? statusLabels.map((l) => `\`${l}\``).join(', ') : 'none'; + const currentStatus = statusLabels.length > 0 ? formatLabelList(statusLabels) : 'none'; errors.push( `The \`${LABELS.AWAITING_TRIAGE}\` label must be present to run \`/finalize\`. Current status label(s): ${currentStatus}.` ); @@ -131,7 +150,7 @@ function collectLabelViolations(issue) { ); } else if (skillLabels.length > 1) { errors.push( - `Exactly one \`skill:\` label is required. Found ${skillLabels.length}: ${skillLabels.map((l) => `\`${l}\``).join(', ')}. Please remove all but one.` + `Exactly one \`skill:\` label is required. Found ${skillLabels.length}: ${formatLabelList(skillLabels)}. Please remove all but one.` ); } @@ -142,7 +161,7 @@ function collectLabelViolations(issue) { ); } else if (priorityLabels.length > 1) { errors.push( - `Exactly one \`priority:\` label is required. Found ${priorityLabels.length}: ${priorityLabels.map((l) => `\`${l}\``).join(', ')}. Please remove all but one.` + `Exactly one \`priority:\` label is required. Found ${priorityLabels.length}: ${formatLabelList(priorityLabels)}. Please remove all but one.` ); } @@ -154,7 +173,7 @@ function collectLabelViolations(issue) { } else if (issueTypeName === 'Feature') { if (kindLabels.length > 0) { errors.push( - `Feature issues should not have a \`kind:\` label. Found: ${kindLabels.map((l) => `\`${l}\``).join(', ')}. Please remove it.` + `Feature issues should not have a \`kind:\` label. Found: ${formatLabelList(kindLabels)}. Please remove it.` ); } } else { @@ -165,7 +184,7 @@ function collectLabelViolations(issue) { ); } else if (kindLabels.length > 1) { errors.push( - `${issueTypeName} issues require exactly one \`kind:\` label. Found ${kindLabels.length}: ${kindLabels.map((l) => `\`${l}\``).join(', ')}. Please remove all but one.` + `${issueTypeName} issues require exactly one \`kind:\` label. Found ${kindLabels.length}: ${formatLabelList(kindLabels)}. Please remove all but one.` ); } } @@ -186,9 +205,7 @@ function collectLabelViolations(issue) { * @returns {string} The updated title. */ function buildNewTitle(currentTitle, skillLevel) { - const strippedTitle = currentTitle - .replace(/^\[(Good First Issue|Beginner|Intermediate|Advanced)\]:\s*/i, '') - .trim(); + const strippedTitle = currentTitle.replace(EXISTING_PREFIX_RE, '').trim(); const prefix = SKILL_TITLE_PREFIXES[skillLevel] || ''; return `${prefix}${strippedTitle}`; } From 0ff248067e618b8b60fe1699226d5ff77f889751 Mon Sep 17 00:00:00 2001 From: Rob Walworth Date: Thu, 26 Mar 2026 10:26:46 -0400 Subject: [PATCH 4/5] docs: address PR comments Signed-off-by: Rob Walworth --- .github/ISSUE_TEMPLATE/bug.yml | 4 ++-- .github/ISSUE_TEMPLATE/feature.yml | 3 ++- .github/ISSUE_TEMPLATE/task.yml | 7 +++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index c5df02a5d..4193e4cf0 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -84,7 +84,7 @@ body: id: expected attributes: label: βœ… Expected Behavior - description: What should have happened? + description: What should have happened? Even a simple "I expected no error / no crash / the function to return a value" is helpful. value: | Describe the expected behavior here. validations: @@ -143,7 +143,7 @@ body: id: acceptance-criteria attributes: label: βœ”οΈ Acceptance Criteria - description: What needs to be true for this bug to be considered fixed? + description: These are the standard acceptance criteria for a bug fix. You can leave them as-is β€” a maintainer will refine them during triage if needed. value: | - [ ] The described behavior is no longer reproducible - [ ] A regression test is added that fails before the fix and passes after diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml index 8635e2784..6eea2c361 100644 --- a/.github/ISSUE_TEMPLATE/feature.yml +++ b/.github/ISSUE_TEMPLATE/feature.yml @@ -15,7 +15,7 @@ body: - Search [existing issues](https://github.com/hiero-ledger/hiero-sdk-cpp/issues) to see if this has been proposed before - For large or protocol-level changes, consider opening a [Hiero Improvement Proposal](https://github.com/hiero-ledger/hiero-improvement-proposals) or starting a [GitHub Discussion](https://github.com/hiero-ledger/hiero-sdk-cpp/discussions) first - A well-described feature request makes it much easier for maintainers to evaluate, plan, and implement. + A well-described feature request makes it much easier for maintainers and the project team to evaluate, plan, and implement. --- - type: textarea @@ -25,6 +25,7 @@ body: description: | What problem are you trying to solve? Describe the need or gap clearly. Focus on the problem itself β€” the proposed solution comes next. + If you already have a specific solution in mind, describe it under "Proposed Solution" β€” just include a brief summary of the need here. value: | Describe the problem here. validations: diff --git a/.github/ISSUE_TEMPLATE/task.yml b/.github/ISSUE_TEMPLATE/task.yml index 4e7e9f508..caace6e3b 100644 --- a/.github/ISSUE_TEMPLATE/task.yml +++ b/.github/ISSUE_TEMPLATE/task.yml @@ -19,6 +19,13 @@ body: - Clarifying documentation or code comments - Enhancements to existing features (without adding new public API) + **Not sure if this is a Task?** + - Adding new public API or behavior β†’ use the **Feature Request** template + - Reporting incorrect behavior β†’ use the **Bug Report** template + - Everything else (maintenance, tooling, quality, enhancements) β†’ you're in the right place! + + Not sure which to pick? See the [Issue Types Guide](https://github.com/hiero-ledger/hiero-sdk-cpp/blob/main/docs/contributing/issue-types.md). + Before submitting, search [existing issues](https://github.com/hiero-ledger/hiero-sdk-cpp/issues) to avoid duplicating work in progress. --- From cfb8bc552003419f48a98c65843632c94dc068a5 Mon Sep 17 00:00:00 2001 From: Rob Walworth Date: Fri, 27 Mar 2026 10:38:00 -0400 Subject: [PATCH 5/5] docs: address PR comments Signed-off-by: Rob Walworth --- docs/maintainers/guidelines-triage.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/maintainers/guidelines-triage.md b/docs/maintainers/guidelines-triage.md index 0d94c802a..cd3ed392f 100644 --- a/docs/maintainers/guidelines-triage.md +++ b/docs/maintainers/guidelines-triage.md @@ -51,16 +51,18 @@ See the skill level guidelines for detailed guidance: As a quick reference: -| Level | Scope | Expected time | Who it's for | -|---|---|---|---| -| `skill: good first issue` | Single file, step-by-step | 1–4 hours | First-time contributors | -| `skill: beginner` | 1–3 related files | 4–8 hours | Contributors with 2 completed GFIs | -| `skill: intermediate` | Multiple modules | 1–3 days | Contributors familiar with the codebase | -| `skill: advanced` | Repository-wide | 3+ days | Experienced contributors | +| Level | Expected time | Who it's for | +|---|---|---| +| `skill: good first issue` | 1–4 hours | First-time contributors | +| `skill: beginner` | 4–8 hours | Contributors with 2 completed GFIs | +| `skill: intermediate` | 1–3 days | Contributors familiar with the codebase | +| `skill: advanced` | 3+ days | Experienced contributors | + +> **Note:** Time estimates are rough orientation guides, not hard targets β€” a change's true complexity depends on the contributor's familiarity and the specifics of the issue. See the linked guidelines above for the full criteria used to assign each level. #### Choosing a kind label (for bugs and tasks) -| Label | When to use | +| Kind | When to use | |---|---| | `kind: enhancement` | Improving existing functionality without adding new public API | | `kind: documentation` | README, guides, API doc comments |