Skip to content

Commit f524d5a

Browse files
authored
Merge pull request #223 from exploreriii/test-labels
test: labels
2 parents 1bbc92a + 957cc56 commit f524d5a

35 files changed

Lines changed: 231 additions & 1684 deletions

.coderabbit.yaml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ reviews:
277277
- Payment-safe
278278
- Execution-consistent
279279
- Strictly aligned with Hedera query semantics
280-
280+
281281
# TRANSACTION REVIEW INSTRUCTIONS - CORE FOUNDATION (MOST CRITICAL MODULE)
282282
transaction_review_instructions: &transaction_review_instructions |
283283
You are acting as a senior maintainer and security architect reviewing the **core transaction module** in the hiero-sdk-python project.
@@ -599,7 +599,7 @@ reviews:
599599
- Changes encoding/decoding results for same inputs
600600
- Modifies ContractId EVM alias ↔ numeric mapping
601601
- Alters payable amount (amount vs initial_balance) semantics
602-
602+
603603
If breaking: require deprecation warnings (e.g. warnings.warn(..., DeprecationWarning)), migration docs, dual-behavior tests.
604604
----------------------------------------------------------
605605
REVIEW FOCUS 2 — ABI ENCODING/DECODING SAFETY (CRITICAL)
@@ -670,7 +670,7 @@ reviews:
670670
- Protobuf-aligned with Hedera/Hiero
671671
- Gas/value safe
672672
- Deterministic and user-protective
673-
673+
674674
# NODES REVIEW INSTRUCTIONS — PRIVILEGED NODE LIFECYCLE OPERATIONS
675675
node_review_instructions: &node_review_instructions |
676676
You are acting as a senior maintainer and security architect reviewing the nodes module
@@ -795,7 +795,7 @@ reviews:
795795
- Certificate and endpoint handling edge cases are tested
796796

797797
Missing coverage should be flagged clearly.
798-
798+
799799
----------------------------------------------------------
800800
EXPLICIT NON-GOALS
801801
----------------------------------------------------------
@@ -819,7 +819,7 @@ reviews:
819819
file_review_instructions: &file_review_instructions |
820820
You are acting as a senior maintainer reviewing file service-related code
821821
in the hiero-sdk-python project.
822-
822+
823823
This includes:
824824
- File transactions (FileCreateTransaction, FileUpdateTransaction, FileAppendTransaction, FileDeleteTransaction)
825825
- File query (FileContentsQuery, FileInfoQuery)
@@ -839,19 +839,19 @@ reviews:
839839
(HIGH SENSITIVITY)
840840
----------------------------------------------------------
841841
Public API contracts for FileId, FileInfo, and file transactions are user-facing.
842-
842+
843843
Verify that:
844844
- Setter method signatures (e.g., set_contents, set_file_memo) stay backward compatible.
845845
- Method chaining (returning self) is preserved.
846846
- FileId constructors and string representations don't break existing use cases.
847-
847+
848848
If breaking changes are necessary, they must be explicit and deprecation warnings should be added.
849849

850850
----------------------------------------------------------
851851
REVIEW FOCUS 2 — CHUNKING SEMANTICS (HIGH VERIFICATION)
852852
----------------------------------------------------------
853853
Specific to FileAppendTransaction, which natively handles chunking.
854-
854+
855855
Verify that:
856856
- freeze_with() correctly generates sequential TransactionIds for each chunk.
857857
- valid_start timestamps for each chunk are spaced out correctly (e.g., incremented by at least 1 nanosecond).
@@ -864,7 +864,7 @@ reviews:
864864
REVIEW FOCUS 3 — MEMO HANDLING
865865
----------------------------------------------------------
866866
Nuanced memo distinction across the file module:
867-
867+
868868
Verify that:
869869
- The distinction between file_memo (the metadata attribute of the file) and transaction_memo (the note on the transaction itself) is respected.
870870
- FileCreateTransaction handles file_memo as a native string.
@@ -876,7 +876,7 @@ reviews:
876876
REVIEW FOCUS 4 — TRANSACTION BASE CLASS CONTRACT
877877
----------------------------------------------------------
878878
All file transaction classes MUST inherit from Transaction.
879-
879+
880880
Required implementations:
881881
- _build_proto_body()
882882
- build_transaction_body()
@@ -891,7 +891,7 @@ reviews:
891891
REVIEW FOCUS 5 — PROTOBUF ALIGNMENT
892892
----------------------------------------------------------
893893
Serialization and deserialization MUST map directly to Hedera protobufs.
894-
894+
895895
Verify that:
896896
- fileCreate, fileUpdate, fileAppend, and fileDelete fields map exactly to their respective protobuf bodies.
897897
- Null-safe conversions are handling optional properties safely.
Lines changed: 208 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,208 @@
1+
// Checks if the given login belongs to a bot account (ends with [bot] or contains "dependabot").
2+
function isBotLogin(login = "") {
3+
return /\[bot\]$/i.test(login) || /dependabot/i.test(login);
4+
}
5+
6+
// Extracts issue numbers from PR body closing keywords (Fixes #123, Closes #456, Resolves #789, etc.).
7+
function extractLinkedIssueNumbers(prBody = "") {
8+
const closingReferenceRegex =
9+
/\b(?:fix(?:es|ed)?|close(?:s|d)?|resolve(?:s|d)?)\s*:?\s*((?:#\d+)(?:\s*(?:,|and)\s*#\d+)*)/gi;
10+
const numbers = new Set();
11+
let referenceMatch;
12+
13+
while ((referenceMatch = closingReferenceRegex.exec(prBody)) !== null) {
14+
const referencesText = referenceMatch[1] || "";
15+
const issueMatches = referencesText.matchAll(/#(\d+)/g);
16+
17+
for (const issueMatch of issueMatches) {
18+
numbers.add(Number(issueMatch[1]));
19+
}
20+
}
21+
22+
return [...numbers];
23+
}
24+
25+
// Normalizes label objects/strings to an array of trimmed label names.
26+
function normalizeLabelNames(labels = []) {
27+
const names = [];
28+
for (const label of labels) {
29+
if (typeof label === "string" && label.trim()) {
30+
names.push(label.trim());
31+
continue;
32+
}
33+
34+
if (label && typeof label.name === "string" && label.name.trim()) {
35+
names.push(label.name.trim());
36+
}
37+
}
38+
return names;
39+
}
40+
41+
// Fetches PR data from the payload or GitHub API if not present in payload.
42+
async function getPullRequestData({ github, context, prNumber }) {
43+
if (context.payload.pull_request) {
44+
return context.payload.pull_request;
45+
}
46+
47+
const response = await github.rest.pulls.get({
48+
owner: context.repo.owner,
49+
repo: context.repo.repo,
50+
pull_number: prNumber,
51+
});
52+
53+
return response.data;
54+
}
55+
56+
// Resolves PR number from environment or context, and determines if dry-run mode is enabled.
57+
function resolveExecutionContext(context) {
58+
const isDryRun = /^true$/i.test(process.env.DRY_RUN || "");
59+
const prNumber = Number(process.env.PR_NUMBER) || context.payload.pull_request?.number;
60+
return { prNumber, isDryRun, owner: context.repo.owner, repo: context.repo.repo };
61+
}
62+
63+
// Determines if the PR should be skipped (e.g., bot-authored PRs, no linked issues).
64+
function shouldSkipPR(prData, linkedIssueNumbers) {
65+
const prAuthor = prData?.user?.login || "";
66+
67+
if (isBotLogin(prAuthor)) {
68+
return { skip: true, reason: `Skipping bot-authored PR from ${prAuthor}.` };
69+
}
70+
71+
if (!linkedIssueNumbers.length) {
72+
return { skip: true, reason: "No linked issue references found in PR body." };
73+
}
74+
75+
return { skip: false };
76+
}
77+
78+
// Collects labels from all linked issues, handling 404s and PR references.
79+
async function collectLabelsFromLinkedIssues({ github, owner, repo, linkedIssueNumbers }) {
80+
const labelsFromIssues = new Set();
81+
82+
for (const issueNumber of linkedIssueNumbers) {
83+
try {
84+
const issueResponse = await github.rest.issues.get({
85+
owner,
86+
repo,
87+
issue_number: issueNumber,
88+
});
89+
90+
if (issueResponse?.data?.pull_request) {
91+
console.log(`[sync-issue-labels] Skipping #${issueNumber}: reference points to a pull request.`);
92+
continue;
93+
}
94+
95+
const issueLabelNames = normalizeLabelNames(issueResponse?.data?.labels || []);
96+
console.log(
97+
`[sync-issue-labels] Issue #${issueNumber} labels: ${
98+
issueLabelNames.length ? issueLabelNames.join(", ") : "(none)"
99+
}`
100+
);
101+
102+
for (const label of issueLabelNames) {
103+
labelsFromIssues.add(label);
104+
}
105+
} catch (error) {
106+
if (error?.status === 404) {
107+
console.log(`[sync-issue-labels] Linked issue #${issueNumber} not found. Skipping.`);
108+
continue;
109+
}
110+
111+
throw error;
112+
}
113+
}
114+
115+
return labelsFromIssues;
116+
}
117+
118+
// Computes which labels should be added to the PR (missing from PR but present in issues).
119+
function computeLabelsToAdd(prData, labelsFromIssues) {
120+
const prLabelNames = new Set(normalizeLabelNames(prData?.labels || []));
121+
return [...labelsFromIssues].filter((label) => !prLabelNames.has(label));
122+
}
123+
124+
// Adds labels to the pull request via GitHub API.
125+
async function addLabelsToPullRequest({ github, owner, repo, prNumber, labelsToAdd }) {
126+
await github.rest.issues.addLabels({
127+
owner,
128+
repo,
129+
issue_number: prNumber,
130+
labels: labelsToAdd,
131+
});
132+
133+
console.log(`[sync-issue-labels] Added labels to PR #${prNumber}: ${labelsToAdd.join(", ")}`);
134+
}
135+
136+
// Main entry point: syncs labels from linked issues to the PR.
137+
module.exports = async ({ github, context }) => {
138+
const { prNumber, isDryRun, owner, repo } = resolveExecutionContext(context);
139+
140+
if (!prNumber) {
141+
throw new Error("PR number could not be determined.");
142+
}
143+
144+
console.log(
145+
`[sync-issue-labels] Processing PR #${prNumber} in ${owner}/${repo} (dry_run=${isDryRun}).`
146+
);
147+
148+
let prData;
149+
try {
150+
prData = await getPullRequestData({ github, context, prNumber });
151+
} catch (error) {
152+
throw new Error(`[sync-issue-labels] Failed to fetch PR #${prNumber}: ${error?.message || error}`);
153+
}
154+
155+
const linkedIssueNumbers = extractLinkedIssueNumbers(prData?.body || "");
156+
const skipResult = shouldSkipPR(prData, linkedIssueNumbers);
157+
158+
if (skipResult.skip) {
159+
console.log(`[sync-issue-labels] ${skipResult.reason}`);
160+
return;
161+
}
162+
163+
console.log(
164+
`[sync-issue-labels] Linked issues detected: ${linkedIssueNumbers.map((n) => `#${n}`).join(", ")}`
165+
);
166+
167+
const labelsFromIssues = await collectLabelsFromLinkedIssues({
168+
github,
169+
owner,
170+
repo,
171+
linkedIssueNumbers,
172+
});
173+
174+
if (!labelsFromIssues.size) {
175+
console.log("[sync-issue-labels] No labels found on linked issues. Nothing to sync.");
176+
return;
177+
}
178+
179+
const labelsToAdd = computeLabelsToAdd(prData, labelsFromIssues);
180+
const prLabelNames = new Set(normalizeLabelNames(prData?.labels || []));
181+
182+
console.log(
183+
`[sync-issue-labels] Existing PR labels: ${
184+
prLabelNames.size ? [...prLabelNames].join(", ") : "(none)"
185+
}`
186+
);
187+
console.log(
188+
`[sync-issue-labels] Labels to add: ${labelsToAdd.length ? labelsToAdd.join(", ") : "(none)"}`
189+
);
190+
191+
if (!labelsToAdd.length) {
192+
console.log("[sync-issue-labels] PR already contains all labels from linked issues.");
193+
return;
194+
}
195+
196+
if (isDryRun) {
197+
console.log(`[sync-issue-labels] DRY_RUN enabled; would add labels: ${labelsToAdd.join(", ")}`);
198+
return;
199+
}
200+
201+
try {
202+
await addLabelsToPullRequest({ github, owner, repo, prNumber, labelsToAdd });
203+
} catch (error) {
204+
throw new Error(
205+
`[sync-issue-labels] Failed to add labels to PR #${prNumber}: ${error?.message || error}`
206+
);
207+
}
208+
};

.github/workflows/bot-advanced-check.yml

Lines changed: 0 additions & 66 deletions
This file was deleted.

0 commit comments

Comments
 (0)