Skip to content

Commit 9a41de1

Browse files
authored
Merge branch 'main' into cancelAirdrop
2 parents 04ae9b6 + 47fe801 commit 9a41de1

30 files changed

Lines changed: 974 additions & 398 deletions
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
// __tests__/jest/post-comment.test.js
2+
//
3+
// Run from .github/scripts:
4+
// npm run test:js -- post-comment.test.js
5+
6+
const { postComment } = require('../../shared/helpers/pr-helpers');
7+
8+
function createMockGithub({ shouldFail = false, errorMessage = 'API error' } = {}) {
9+
return {
10+
rest: {
11+
issues: {
12+
createComment: jest.fn(async () => {
13+
if (shouldFail) {
14+
throw new Error(errorMessage);
15+
}
16+
return { data: { id: 101 } };
17+
}),
18+
},
19+
},
20+
};
21+
}
22+
23+
function createMockCore() {
24+
return {
25+
info: jest.fn(),
26+
error: jest.fn(),
27+
warning: jest.fn(),
28+
setFailed: jest.fn(),
29+
};
30+
}
31+
32+
describe('postComment helper', () => {
33+
test('happy path: returns true and logs info when createComment resolves', async () => {
34+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
35+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
36+
37+
try {
38+
const github = createMockGithub();
39+
const core = createMockCore();
40+
const owner = 'hiero-ledger';
41+
const repo = 'hiero-sdk-python';
42+
const prNumber = 123;
43+
const body = 'Great work on this PR!';
44+
45+
const result = await postComment(github, owner, repo, prNumber, body, core);
46+
47+
expect(result).toBe(true);
48+
expect(github.rest.issues.createComment).toHaveBeenCalledTimes(1);
49+
expect(github.rest.issues.createComment).toHaveBeenCalledWith({
50+
owner,
51+
repo,
52+
issue_number: prNumber,
53+
body,
54+
});
55+
expect(core.info).toHaveBeenCalledTimes(1);
56+
expect(core.info).toHaveBeenCalledWith(`Posted recommendation comment to PR #${prNumber}`);
57+
expect(core.error).not.toHaveBeenCalled();
58+
expect(consoleLogSpy).not.toHaveBeenCalled();
59+
expect(consoleErrorSpy).not.toHaveBeenCalled();
60+
} finally {
61+
consoleLogSpy.mockRestore();
62+
consoleErrorSpy.mockRestore();
63+
}
64+
});
65+
66+
test('failure path: returns false, logs error, and does not throw when createComment rejects', async () => {
67+
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
68+
const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(() => {});
69+
70+
try {
71+
const errorMessage = 'Network error: 500 Internal Server Error';
72+
const github = createMockGithub({ shouldFail: true, errorMessage });
73+
const core = createMockCore();
74+
const owner = 'hiero-ledger';
75+
const repo = 'hiero-sdk-python';
76+
const prNumber = 456;
77+
const body = 'Recommendation comment';
78+
79+
let result;
80+
let thrownError = null;
81+
82+
try {
83+
result = await postComment(github, owner, repo, prNumber, body, core);
84+
} catch (err) {
85+
thrownError = err;
86+
}
87+
88+
expect(thrownError).toBeNull();
89+
expect(result).toBe(false);
90+
expect(github.rest.issues.createComment).toHaveBeenCalledTimes(1);
91+
expect(github.rest.issues.createComment).toHaveBeenCalledWith({
92+
owner,
93+
repo,
94+
issue_number: prNumber,
95+
body,
96+
});
97+
expect(core.error).toHaveBeenCalledTimes(1);
98+
expect(core.error).toHaveBeenCalledWith(`Failed to post comment: ${errorMessage}`);
99+
expect(core.info).not.toHaveBeenCalled();
100+
expect(consoleLogSpy).not.toHaveBeenCalled();
101+
expect(consoleErrorSpy).not.toHaveBeenCalled();
102+
} finally {
103+
consoleLogSpy.mockRestore();
104+
consoleErrorSpy.mockRestore();
105+
}
106+
});
107+
});

.github/scripts/shared/api/github-api.js

Lines changed: 70 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,18 @@ async function fetchIssuesBatch(github, repoConfig) {
6565
}
6666
}
6767

68+
/**
69+
* Returns the number of open issues currently assigned to `username` in the
70+
* given repository. Pull requests are excluded because they do not consume
71+
* assignment capacity. Returns null on API failure so callers can fail open.
72+
*
73+
* @param {object} params
74+
* @param {import('@actions/github').GitHub} params.github
75+
* @param {string} params.owner - Repo owner.
76+
* @param {string} params.repo - Repo name.
77+
* @param {string} params.username - GitHub login of the contributor.
78+
* @returns {Promise<number|null>} Count of open issue assignments, or null on failure.
79+
*/
6880
async function getOpenAssignments({ github, owner, repo, username }) {
6981
try {
7082
const issues = await github.paginate(github.rest.issues.listForRepo, {
@@ -94,6 +106,14 @@ async function getOpenAssignments({ github, owner, repo, username }) {
94106
* Counts closed issues carrying `label` (in the given repo) assigned to
95107
* `username`. Returns null (rather than throwing) on unsafe input or API
96108
* error so callers can choose to fail open.
109+
*
110+
* @param {object} params
111+
* @param {import('@actions/github').GitHub} params.github
112+
* @param {string} params.owner - Repo owner.
113+
* @param {string} params.repo - Repo name.
114+
* @param {string} params.username - GitHub login of the contributor.
115+
* @param {string} params.label - Label string to filter by.
116+
* @returns {Promise<number|null>} Issue count, or null on invalid input or API failure.
97117
*/
98118
async function countCompletedIssuesWithLabel({ github, owner, repo, username, label }) {
99119
if (!isValidSearchToken(owner) || !isValidSearchToken(repo) || !isValidSearchToken(username)) {
@@ -133,12 +153,18 @@ async function countCompletedIssuesWithLabel({ github, owner, repo, username, la
133153
}
134154

135155
/**
136-
137156
* Determines whether a user has repository collaborator access.
138157
*
139158
* Repository owners are always considered collaborators.
140159
* GitHub returns 204 when the user is a collaborator and 404 otherwise.
141160
* Unexpected API failures are treated as non-collaborator access.
161+
*
162+
* @param {object} params
163+
* @param {import('@actions/github').GitHub} params.github
164+
* @param {string} params.owner - Repo owner.
165+
* @param {string} params.repo - Repo name.
166+
* @param {string} params.username - GitHub login to check.
167+
* @returns {Promise<boolean>} True if the user is a collaborator, false otherwise.
142168
*/
143169
async function isRepoCollaborator({ github, owner, repo, username }) {
144170
if (username === owner) {
@@ -176,16 +202,46 @@ async function isRepoCollaborator({ github, owner, repo, username }) {
176202
}
177203
}
178204

205+
/**
206+
* Posts a comment on an issue or pull request via the GitHub REST API.
207+
* When `logLabel` is provided, logs the outcome to the console; when omitted,
208+
* stays silent so the caller owns all logging. Re-throws any API error either
209+
* way so callers can handle or propagate failures themselves.
210+
*
211+
* @param {object} params
212+
* @param {import('@actions/github').GitHub} params.github
213+
* @param {string} params.owner - Repo owner.
214+
* @param {string} params.repo - Repo name.
215+
* @param {number} params.issueNumber - Issue or PR number to comment on.
216+
* @param {string} params.body - Markdown body of the comment.
217+
* @param {string} [logLabel] - Optional human-readable label used in console output.
218+
* @returns {Promise<void>}
219+
* @throws {Error} Re-throws the Octokit error on API failure.
220+
*/
179221
async function postIssueComment({ github, owner, repo, issueNumber, body }, logLabel) {
180222
try {
181223
await github.rest.issues.createComment({ owner, repo, issue_number: issueNumber, body });
182-
console.log(`[github-api] Posted comment: ${logLabel}`);
224+
if (logLabel) {
225+
console.log(`[github-api] Posted comment: ${logLabel}`);
226+
}
183227
} catch (error) {
184-
console.error(`[github-api] Failed to post comment (${logLabel}):`, { message: error.message });
228+
if (logLabel) {
229+
console.error(`[github-api] Failed to post comment (${logLabel}):`, { message: error.message });
230+
}
185231
throw error;
186232
}
187233
}
188234

235+
/**
236+
* Fetches all comments on an issue or pull request, paginating automatically.
237+
*
238+
* @param {object} params
239+
* @param {import('@actions/github').GitHub} params.github
240+
* @param {string} params.owner - Repo owner.
241+
* @param {string} params.repo - Repo name.
242+
* @param {number} params.issueNumber - Issue or PR number.
243+
* @returns {Promise<Array<object>>} Array of comment objects from the GitHub API.
244+
*/
189245
async function fetchAllComments({ github, owner, repo, issueNumber }) {
190246
return github.paginate(github.rest.issues.listComments, {
191247
owner,
@@ -195,6 +251,17 @@ async function fetchAllComments({ github, owner, repo, issueNumber }) {
195251
});
196252
}
197253

254+
/**
255+
* Assigns a contributor to an issue.
256+
*
257+
* @param {object} params
258+
* @param {import('@actions/github').GitHub} params.github
259+
* @param {string} params.owner - Repo owner.
260+
* @param {string} params.repo - Repo name.
261+
* @param {number} params.issueNumber - Issue number to assign.
262+
* @param {string} params.username - GitHub login of the contributor to assign.
263+
* @returns {Promise<void>}
264+
*/
198265
async function assignIssue({
199266
github,
200267
owner,

.github/scripts/shared/config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ const CONFIG = {
5858

5959
// Maximum simultaneous assignments .
6060
assignmentLimits: {
61-
[LEVEL_KEYS.GFI]: 2,
61+
[LEVEL_KEYS.GFI]: 1,
6262
[LEVEL_KEYS.BEGINNER]: 2,
6363
[LEVEL_KEYS.INTERMEDIATE]: 2,
6464
[LEVEL_KEYS.ADVANCED]: 2,

.github/scripts/shared/helpers/pr-helpers.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// ---------------------------------------------------------------------------
44

55
const { CONFIG } = require('../config');
6+
const { postIssueComment } = require('../api/github-api');
67

78
/**
89
* Extracts the first linked issue number from a PR body using closing keywords.
@@ -71,7 +72,9 @@ async function alreadyCommented(github, owner, repo, prNumber) {
7172
*/
7273
async function postComment(github, owner, repo, prNumber, body, core) {
7374
try {
74-
await github.rest.issues.createComment({ owner, repo, issue_number: prNumber, body });
75+
// No logLabel: this wrapper owns logging (via core), matching the
76+
// pre-consolidation log output exactly.
77+
await postIssueComment({ github, owner, repo, issueNumber: prNumber, body });
7578
core.info(`Posted recommendation comment to PR #${prNumber}`);
7679
return true;
7780
} catch (error) {

.github/workflows/on-review.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ jobs:
4545
- name: Checkout repository
4646
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
4747
with:
48-
ref: ${{ github.event.pull_request.base.sha || github.sha }}
48+
ref: ${{ github.event.repository.default_branch }}
4949
persist-credentials: false
5050

5151
- name: Run Add Reviewers as Assignees

.github/workflows/pr-check-primary-codeql.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ jobs:
4242
egress-policy: audit
4343

4444
- name: Initialize CodeQL
45-
uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
45+
uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
4646
with:
4747
languages: ${{ matrix.language }}
4848
build-mode: ${{ matrix.build-mode }}
@@ -66,6 +66,6 @@ jobs:
6666
run: uv sync --frozen --all-groups --all-packages --all-extras
6767

6868
- name: Perform CodeQL Analysis
69-
uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
69+
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
7070
with:
7171
category: "/language:${{matrix.language}}"

.github/workflows/pr-review-status-evaluator.yml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,11 @@ jobs:
5555
with:
5656
egress-policy: audit
5757

58-
- name: Checkout trusted evaluator scripts (PR base ref, not head)
58+
- name: Checkout trusted evaluator scripts (default branch))
5959
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
6060
with:
61-
# For PR events, the trusted base branch; for workflow_dispatch,
62-
# the (trusted) ref the workflow was dispatched from.
63-
ref: ${{ github.event.pull_request.base.sha || github.sha }}
61+
# Always check out trusted evaluator scripts from the repository default branch.
62+
ref: ${{ github.event.repository.default_branch }}
6463
persist-credentials: false
6564
fetch-depth: 1
6665

examples/account/account_create_transaction_create_with_alias.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
Client,
2424
Hbar,
2525
PrivateKey,
26+
ResponseCode,
2627
)
2728

2829

@@ -104,6 +105,9 @@ def create_account_with_ecdsa_alias(
104105

105106
response = transaction.execute(client)
106107

108+
if response.status != ResponseCode.SUCCESS:
109+
raise RuntimeError(f"Transaction failed with status: {ResponseCode(response.status).name}")
110+
107111
# Safe retrieval of account ID
108112
new_account_id = response.account_id
109113
if new_account_id is None:

examples/account/account_create_transaction_with_fallback_alias.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
Client,
2424
Hbar,
2525
PrivateKey,
26+
ResponseCode,
2627
)
2728

2829

@@ -68,6 +69,10 @@ def create_account_with_fallback_alias(client: Client, account_private_key: Priv
6869
transaction = transaction.freeze_with(client).sign(account_private_key)
6970

7071
response = transaction.execute(client)
72+
73+
if response.status != ResponseCode.SUCCESS:
74+
raise RuntimeError(f"Transaction failed with status: {ResponseCode(response.status).name}")
75+
7176
new_account_id = response.account_id
7277

7378
if new_account_id is None:

examples/transaction/transfer_transaction_fungible.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
CryptoGetAccountBalanceQuery,
1919
Hbar,
2020
PrivateKey,
21+
ResponseCode,
2122
TokenAssociateTransaction,
2223
TokenCreateTransaction,
2324
TransferTransaction,
@@ -58,6 +59,9 @@ def create_account(client, operator_key):
5859
.set_initial_balance(Hbar.from_tinybars(100_000_000))
5960
)
6061
receipt = tx.freeze_with(client).sign(operator_key).execute(client)
62+
if receipt.status != ResponseCode.SUCCESS:
63+
raise RuntimeError(f"Transaction failed with status: {ResponseCode(receipt.status).name}")
64+
6165
recipient_id = receipt.account_id
6266
print(f"✅ Success! Created a new recipient account with ID: {recipient_id}")
6367
return recipient_id, recipient_key
@@ -83,6 +87,9 @@ def create_token(client, operator_id, operator_key):
8387
.sign(operator_key)
8488
)
8589
token_receipt = token_tx.execute(client)
90+
if token_receipt.status != ResponseCode.SUCCESS:
91+
raise RuntimeError(f"Transaction failed with status: {ResponseCode(token_receipt.status).name}")
92+
8693
token_id = token_receipt.token_id
8794

8895
print(f"✅ Success! Created a token with Token ID: {token_id}")
@@ -103,7 +110,9 @@ def associate_token(client, recipient_id, recipient_key, token_id):
103110
.freeze_with(client)
104111
.sign(recipient_key)
105112
)
106-
association_tx.execute(client)
113+
receipt = association_tx.execute(client)
114+
if receipt.status != ResponseCode.SUCCESS:
115+
raise RuntimeError(f"Transaction failed with status: {ResponseCode(receipt.status).name}")
107116

108117
print("✅ Success! Token association complete.")
109118
except Exception as e:
@@ -139,7 +148,10 @@ def transfer_transaction(client, operator_id, operator_key, recipient_id, token_
139148
.sign(operator_key)
140149
)
141150

142-
tx.execute(client)
151+
receipt = tx.execute(client)
152+
if receipt.status != ResponseCode.SUCCESS:
153+
raise RuntimeError(f"Transaction failed with status: {ResponseCode(receipt.status).name}")
154+
143155
print("✅ Success! Token transfer complete.\n")
144156

145157
except Exception as e:

0 commit comments

Comments
 (0)