-
Notifications
You must be signed in to change notification settings - Fork 0
Add GitHub Issue Milestones functionality #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
This commit implements comprehensive support for GitHub Issue Milestones, enabling project planning and tracking capabilities. Features added: - Milestone Data class with full GitHub API field support - ManagesMilestones trait with CRUD operations for milestones - ManagesMilestonesInterface contract defining the milestone operations API - Methods to assign/remove issues from milestones - Method to list issues within a milestone - Full test coverage for Milestone data transformations Breaking changes: - Issue::$milestone changed from ?string to ?Milestone object Resolves #9
WalkthroughAdds comprehensive milestone management support to the GitHub Issues library through a new Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes
Possibly related issues
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/Data/Issue.php (1)
31-51: Avoid undefined-index notices whenmilestoneis missing from payload.
$data['milestone'] ? ... : nullwill emit a notice if the key is absent (common with partial responses/mocks).- milestone: $data['milestone'] ? Milestone::fromArray($data['milestone']) : null, + milestone: (isset($data['milestone']) && is_array($data['milestone'])) + ? Milestone::fromArray($data['milestone']) + : null,
🧹 Nitpick comments (2)
tests/Unit/Data/MilestoneTest.php (1)
77-109: Consider asserting exact date formatting (if output format stability matters).
Right nowdue_onis only asserted as “string”, so a formatting change could slip through unnoticed.src/Data/Milestone.php (1)
46-63:toArray()shape is good; note it won’t necessarily round-trip the exact GitHub timestamp string.
format('c')yields+00:00instead ofZfor UTC inputs (still valid ISO-8601).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (8)
src/Contracts/IssuesServiceInterface.php(1 hunks)src/Contracts/ManagesMilestonesInterface.php(1 hunks)src/Data/Issue.php(3 hunks)src/Data/Milestone.php(1 hunks)src/Services/IssuesService.php(1 hunks)src/Traits/ManagesMilestones.php(1 hunks)tests/Unit/Data/IssueTest.php(6 hunks)tests/Unit/Data/MilestoneTest.php(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/Data/Milestone.php (1)
src/Data/User.php (1)
User(7-38)
tests/Unit/Data/MilestoneTest.php (2)
src/Data/Milestone.php (5)
Milestone(9-74)fromArray(27-44)toArray(46-63)isOpen(65-68)isClosed(70-73)src/Data/User.php (1)
User(7-38)
src/Services/IssuesService.php (1)
src/Data/Issue.php (1)
Issue(9-86)
src/Data/Issue.php (1)
src/Data/Milestone.php (1)
Milestone(9-74)
tests/Unit/Data/IssueTest.php (3)
src/Data/Issue.php (1)
Issue(9-86)src/Data/Milestone.php (1)
Milestone(9-74)src/Data/User.php (1)
User(7-38)
🔇 Additional comments (9)
src/Data/Issue.php (2)
54-75:toArray()now emits a milestone object shape; ensure callers don’t expect a string.This is consistent with the new model, but any serialization consumers (JSON snapshots, caching, custom mappers) may need updates.
11-29: The?Milestonetype change is properly implemented with no downstream breaking changes. All usages correctly handle the nullableMilestoneinstance: deserialization viaMilestone::fromArray()with null coalescing, serialization via optional chaining (?->toArray()), and tests expectMilestoneinstances.src/Contracts/IssuesServiceInterface.php (1)
7-7: Interface composition looks consistent with new milestone capability.src/Services/IssuesService.php (1)
7-23: Trait wiring is clean; just watch for future trait method name collisions.tests/Unit/Data/MilestoneTest.php (1)
8-45: fromArray coverage (including nested creator) looks solid.tests/Unit/Data/IssueTest.php (2)
35-96: Test updates correctly validateIssue::fromArray()produces aMilestoneobject.
98-150:Issue::toArray()milestone assertions match the new nested shape.src/Traits/ManagesMilestones.php (1)
60-78: Confirm GitHub API semantics for milestone assignment and issue filtering.
update issuepayload: whethermilestoneexpects the milestone number (as implemented) across all API versions you target.list issuesfiltering: whethermilestonemust be a string and whether casting is necessary/desired.src/Contracts/ManagesMilestonesInterface.php (1)
11-34: Contract matches the trait API and keeps return types explicit.
| public static function fromArray(array $data): self | ||
| { | ||
| return new self( | ||
| id: $data['id'], | ||
| number: $data['number'], | ||
| title: $data['title'], | ||
| description: $data['description'] ?? null, | ||
| state: $data['state'], | ||
| openIssues: $data['open_issues'], | ||
| closedIssues: $data['closed_issues'], | ||
| createdAt: new DateTime($data['created_at']), | ||
| updatedAt: new DateTime($data['updated_at']), | ||
| closedAt: $data['closed_at'] ? new DateTime($data['closed_at']) : null, | ||
| dueOn: $data['due_on'] ? new DateTime($data['due_on']) : null, | ||
| htmlUrl: $data['html_url'], | ||
| creator: User::fromArray($data['creator']), | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Harden fromArray() against missing closed_at / due_on keys.
- closedAt: $data['closed_at'] ? new DateTime($data['closed_at']) : null,
- dueOn: $data['due_on'] ? new DateTime($data['due_on']) : null,
+ closedAt: (isset($data['closed_at']) && $data['closed_at'] !== null) ? new DateTime($data['closed_at']) : null,
+ dueOn: (isset($data['due_on']) && $data['due_on'] !== null) ? new DateTime($data['due_on']) : null,📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| public static function fromArray(array $data): self | |
| { | |
| return new self( | |
| id: $data['id'], | |
| number: $data['number'], | |
| title: $data['title'], | |
| description: $data['description'] ?? null, | |
| state: $data['state'], | |
| openIssues: $data['open_issues'], | |
| closedIssues: $data['closed_issues'], | |
| createdAt: new DateTime($data['created_at']), | |
| updatedAt: new DateTime($data['updated_at']), | |
| closedAt: $data['closed_at'] ? new DateTime($data['closed_at']) : null, | |
| dueOn: $data['due_on'] ? new DateTime($data['due_on']) : null, | |
| htmlUrl: $data['html_url'], | |
| creator: User::fromArray($data['creator']), | |
| ); | |
| } | |
| public static function fromArray(array $data): self | |
| { | |
| return new self( | |
| id: $data['id'], | |
| number: $data['number'], | |
| title: $data['title'], | |
| description: $data['description'] ?? null, | |
| state: $data['state'], | |
| openIssues: $data['open_issues'], | |
| closedIssues: $data['closed_issues'], | |
| createdAt: new DateTime($data['created_at']), | |
| updatedAt: new DateTime($data['updated_at']), | |
| closedAt: (isset($data['closed_at']) && $data['closed_at'] !== null) ? new DateTime($data['closed_at']) : null, | |
| dueOn: (isset($data['due_on']) && $data['due_on'] !== null) ? new DateTime($data['due_on']) : null, | |
| htmlUrl: $data['html_url'], | |
| creator: User::fromArray($data['creator']), | |
| ); | |
| } |
🤖 Prompt for AI Agents
In src/Data/Milestone.php around lines 27 to 44, the fromArray() method assumes
'closed_at' and 'due_on' keys always exist; update it to first check those keys
(use array_key_exists or isset) and that their values are non-empty before
constructing DateTime, otherwise pass null. Replace the current ternary
expressions with checks like: if key exists and not null/empty then new
DateTime(value) else null; keep other fields unchanged.
| public function listMilestones(string $owner, string $repo, array $filters = []): Collection | ||
| { | ||
| $response = $this->connector->send( | ||
| $this->connector->get("/repos/{$owner}/{$repo}/milestones", $filters) | ||
| ); | ||
|
|
||
| return collect($response->json()) | ||
| ->map(fn (array $data) => Milestone::fromArray($data)); | ||
| } | ||
|
|
||
| public function getMilestone(string $owner, string $repo, int $milestoneNumber): Milestone | ||
| { | ||
| $response = $this->connector->send( | ||
| $this->connector->get("/repos/{$owner}/{$repo}/milestones/{$milestoneNumber}") | ||
| ); | ||
|
|
||
| return Milestone::fromArray($response->json()); | ||
| } | ||
|
|
||
| public function createMilestone(string $owner, string $repo, array $data): Milestone | ||
| { | ||
| $response = $this->connector->send( | ||
| $this->connector->post("/repos/{$owner}/{$repo}/milestones", $data) | ||
| ); | ||
|
|
||
| return Milestone::fromArray($response->json()); | ||
| } | ||
|
|
||
| public function updateMilestone(string $owner, string $repo, int $milestoneNumber, array $data): Milestone | ||
| { | ||
| $response = $this->connector->send( | ||
| $this->connector->patch("/repos/{$owner}/{$repo}/milestones/{$milestoneNumber}", $data) | ||
| ); | ||
|
|
||
| return Milestone::fromArray($response->json()); | ||
| } | ||
|
|
||
| public function deleteMilestone(string $owner, string $repo, int $milestoneNumber): void | ||
| { | ||
| $this->connector->send( | ||
| $this->connector->delete("/repos/{$owner}/{$repo}/milestones/{$milestoneNumber}") | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
find . -type f -name "*.php" | grep -i connector | head -20Repository: conduit-ui/issue
Length of output: 42
🏁 Script executed:
find . -type f -name "*.php" -path "*/src/*" | head -30Repository: conduit-ui/issue
Length of output: 621
🏁 Script executed:
git ls-files "*.php" | grep -E "(Connector|connector)" | head -20Repository: conduit-ui/issue
Length of output: 42
🏁 Script executed:
cat -n src/Services/IssuesService.php | head -100Repository: conduit-ui/issue
Length of output: 834
🏁 Script executed:
cat -n src/Traits/ManagesIssues.php | head -150Repository: conduit-ui/issue
Length of output: 2340
🏁 Script executed:
find . -type f -name "composer.json" -o -name "composer.lock" | head -5Repository: conduit-ui/issue
Length of output: 91
🏁 Script executed:
cat composer.json | grep -A 5 -B 5 "GitHubConnector" 2>/dev/null || cat composer.json | head -100Repository: conduit-ui/issue
Length of output: 2747
🏁 Script executed:
find . -path "*/vendor/*" -name "Connector.php" 2>/dev/null | grep -i github | head -5Repository: conduit-ui/issue
Length of output: 42
🏁 Script executed:
find . -path "*/vendor/conduit-ui/*" -type f -name "*.php" 2>/dev/null | head -20Repository: conduit-ui/issue
Length of output: 42
🏁 Script executed:
rg "class Connector" -A 20 --type phpRepository: conduit-ui/issue
Length of output: 42
🏁 Script executed:
find ./tests -type f -name "*.php" 2>/dev/null | head -10Repository: conduit-ui/issue
Length of output: 228
🏁 Script executed:
rg "listMilestones|getMilestone" -A 10 --type phpRepository: conduit-ui/issue
Length of output: 2820
🌐 Web query:
conduit-ui/connector GitHub repository
💡 Result:
I couldn't find a GitHub repository named exactly "conduit-ui/connector". Related projects you might mean:
- conduit-ui/conduit (Conduit CLI platform) — package info and source reference. [1]
- conduit-ui/github-connector (a GitHub API connector package from conduit-ui on Packagist/Libraries.io). [1][2]
- ConduitPlatform/conduit-ui (a separate Conduit web UI repo / project surfaced by mirrors/analysis tools). [3]
If you want, tell me which one you meant (or paste a link) and I’ll open it and summarize the repo contents.
Sources: [1] Packagist conduit-ui listings; [2] Libraries.io entry for conduit-ui/github-connector; [3] Conduit-UI code listing (mirror/analysis).
🌐 Web query:
Saloon PHP HTTP client URL path parameter encoding
💡 Result:
Short answer: Saloon does not automatically percent‑encode URL path segments for you — you should encode path parameters yourself before returning the endpoint in resolveEndpoint. Use rawurlencode (RFC‑3986) per segment (or a URI library like League\Uri\Encoder::encodePath if you need finer control) and then build the endpoint string. [1][2][3]
Example (concept):
$segment = rawurlencode($value);
return "/items/{$segment}/details";
Sources:
[1] Saloon — Requests / resolveEndpoint (docs). (docs.saloon.dev)
[2] PHP rawurlencode() manual (RFC‑3986 behavior). (php.net)
[3] League\Uri Encoder — encodePath utility. (uri.thephpleague.com)
Citations:
- 1: https://docs.saloon.dev/2/the-basics/requests?utm_source=openai
- 2: https://www.php.net/manual/es/function.rawurlencode.php?utm_source=openai
- 3: https://uri.thephpleague.com/interfaces/7.0/encoder/?utm_source=openai
🌐 Web query:
site:packagist.org conduit-ui/connector
💡 Result:
I couldn't find a Packagist package named "conduit-ui/connector". The closest match is "conduit-ui/conduit" on Packagist. [1]
Sources
[1] conduit-ui/conduit — Packagist.
🏁 Script executed:
cat -n tests/TestCase.phpRepository: conduit-ui/issue
Length of output: 924
🏁 Script executed:
cat -n src/Traits/ManagesMilestones.phpRepository: conduit-ui/issue
Length of output: 3235
Encode $owner and $repo path parameters with rawurlencode().
Saloon (the underlying HTTP client) does not automatically percent-encode URL path segments. If these parameters contain special characters (including /), the endpoint will be malformed. Add rawurlencode() to all methods that interpolate these values:
public function listMilestones(string $owner, string $repo, array $filters = []): Collection
{
+ $owner = rawurlencode($owner);
+ $repo = rawurlencode($repo);
$response = $this->connector->send(
$this->connector->get("/repos/{$owner}/{$repo}/milestones", $filters)
);
@@
public function getMilestone(string $owner, string $repo, int $milestoneNumber): Milestone
{
+ $owner = rawurlencode($owner);
+ $repo = rawurlencode($repo);
$response = $this->connector->send(
$this->connector->get("/repos/{$owner}/{$repo}/milestones/{$milestoneNumber}")
);
@@
public function createMilestone(string $owner, string $repo, array $data): Milestone
{
+ $owner = rawurlencode($owner);
+ $repo = rawurlencode($repo);
$response = $this->connector->send(
$this->connector->post("/repos/{$owner}/{$repo}/milestones", $data)
);
@@
public function updateMilestone(string $owner, string $repo, int $milestoneNumber, array $data): Milestone
{
+ $owner = rawurlencode($owner);
+ $repo = rawurlencode($repo);
$response = $this->connector->send(
$this->connector->patch("/repos/{$owner}/{$repo}/milestones/{$milestoneNumber}", $data)
);
@@
public function deleteMilestone(string $owner, string $repo, int $milestoneNumber): void
{
+ $owner = rawurlencode($owner);
+ $repo = rawurlencode($repo);
$this->connector->send(
$this->connector->delete("/repos/{$owner}/{$repo}/milestones/{$milestoneNumber}")
);
}Note: This same pattern appears in ManagesIssues and other traits and should be fixed consistently across the codebase.
🤖 Prompt for AI Agents
In src/Traits/ManagesMilestones.php around lines 16 to 58, path parameters
$owner and $repo are interpolated directly into URLs and must be
percent-encoded; update every method that builds a repo path (listMilestones,
getMilestone, createMilestone, updateMilestone, deleteMilestone) to wrap $owner
and $repo with rawurlencode() when constructing the endpoint strings so special
characters (including "/") are encoded, and apply the same change to other
traits like ManagesIssues that use the same pattern to ensure consistent, safe
URL construction across the codebase.
|
Features already shipped in #14 (bundled with gate workflow) |
Summary
This PR implements comprehensive support for GitHub Issue Milestones, enabling project planning and tracking capabilities as requested in #9.
Features Implemented
Milestone Operations:
listMilestones)getMilestone)createMilestone)updateMilestone)deleteMilestone)Milestone Assignment:
assignIssueToMilestone)removeIssueFromMilestone)listMilestoneIssues)Implementation Details
New Files:
src/Data/Milestone.php- Milestone data class with full GitHub API field supportsrc/Contracts/ManagesMilestonesInterface.php- Interface defining milestone operationssrc/Traits/ManagesMilestones.php- Trait implementing milestone CRUD and issue assignmenttests/Unit/Data/MilestoneTest.php- Comprehensive test coverage for Milestone data transformationsModified Files:
src/Services/IssuesService.php- Added ManagesMilestones traitsrc/Contracts/IssuesServiceInterface.php- Extended ManagesMilestonesInterfacesrc/Data/Issue.php- Changed milestone property from ?string to ?Milestone objecttests/Unit/Data/IssueTest.php- Updated tests for new Milestone object typeBreaking Changes
Issue::$milestoneproperty changed from?stringto?Milestoneobject$issue->milestonereturned a string like "v1.0"$issue->milestone->titlereturns "v1.0", with access to full milestone detailsGitHub API Endpoints Used
GET /repos/{owner}/{repo}/milestones- List milestonesGET /repos/{owner}/{repo}/milestones/{milestone_number}- Get milestonePOST /repos/{owner}/{repo}/milestones- Create milestonePATCH /repos/{owner}/{repo}/milestones/{milestone_number}- Update milestoneDELETE /repos/{owner}/{repo}/milestones/{milestone_number}- Delete milestoneTesting
All tests pass successfully:
Closes #9
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.