Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ddev/changelog.d/24316.fixed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Retype async GitHub client response-model enum fields (PR state, workflow-job and check-run status/conclusion) as StrEnums.
22 changes: 19 additions & 3 deletions ddev/src/ddev/utils/github_async/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,12 +25,28 @@ is real and must be checked per field:
a `StrEnum` must list all six, not a guessed subset.

```python
from enum import StrEnum
from enum import StrEnum, auto


class PullRequestState(StrEnum):
OPEN = "open"
CLOSED = "closed"
OPEN = auto()
CLOSED = auto()
```

Prefer `auto()` for the values: on a `StrEnum` it generates each value as the
**lowercased member name**, with underscores preserved (`IN_PROGRESS` becomes
`"in_progress"`). Use it only when every declared enum value is exactly
`member_name.lower()`. When the API's value differs — a different case, hyphens,
or any form that isn't the lowercased name — spell the value out explicitly
instead of forcing `auto()`. For example `pull-request.author_association`
declares UPPERCASE values, so `auto()` would wrongly lowercase them and each must
be written out:

```python
class AuthorAssociation(StrEnum):
OWNER = "OWNER"
COLLABORATOR = "COLLABORATOR"
# ...
```

A plain `str` field for a declared enum hides the contract and lets invalid
Expand Down
3 changes: 2 additions & 1 deletion ddev/src/ddev/utils/github_async/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from .models import (
ArtifactsList,
CheckRun,
CheckRunConclusion,
IssueComment,
Label,
PullRequest,
Expand Down Expand Up @@ -548,7 +549,7 @@ async def update_check_run(
repo: str,
check_run_id: int,
status: Literal["queued", "in_progress", "completed"] | None = None,
conclusion: str | None = None,
conclusion: CheckRunConclusion | None = None,
details_url: str | None = None,
output: dict[str, Any] | None = None,
timeout: float | None = None,
Expand Down
12 changes: 12 additions & 0 deletions ddev/src/ddev/utils/github_async/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,24 @@
# break the lazy-loading guarantee. The `X as X` aliases mark these as
# explicit re-exports for linters.
from .check_run import CheckRun as CheckRun
from .check_run import CheckRunConclusion as CheckRunConclusion
from .check_run import CheckRunStatus as CheckRunStatus
from .comment import IssueComment as IssueComment
from .comment import PullRequestReviewComment as PullRequestReviewComment
from .label import Label as Label
from .pull_request import PullRequest as PullRequest
from .pull_request import PullRequestRef as PullRequestRef
from .pull_request import PullRequestState as PullRequestState
from .user import GitHubUser as GitHubUser
from .workflow import Artifact as Artifact
from .workflow import ArtifactsList as ArtifactsList
from .workflow import JobStep as JobStep
from .workflow import JobStepStatus as JobStepStatus
from .workflow import WorkflowDispatchResult as WorkflowDispatchResult
from .workflow import WorkflowJob as WorkflowJob
from .workflow import WorkflowJobConclusion as WorkflowJobConclusion
from .workflow import WorkflowJobsList as WorkflowJobsList
from .workflow import WorkflowJobStatus as WorkflowJobStatus
from .workflow import WorkflowRun as WorkflowRun

# Map of exported attribute name -> submodule (relative to this package) that
Expand All @@ -46,15 +52,21 @@
'Artifact': 'workflow',
'ArtifactsList': 'workflow',
'CheckRun': 'check_run',
'CheckRunConclusion': 'check_run',
'CheckRunStatus': 'check_run',
'GitHubUser': 'user',
'IssueComment': 'comment',
'JobStep': 'workflow',
'JobStepStatus': 'workflow',
'Label': 'label',
'PullRequest': 'pull_request',
'PullRequestRef': 'pull_request',
'PullRequestReviewComment': 'comment',
'PullRequestState': 'pull_request',
'WorkflowDispatchResult': 'workflow',
'WorkflowJob': 'workflow',
'WorkflowJobConclusion': 'workflow',
'WorkflowJobStatus': 'workflow',
'WorkflowJobsList': 'workflow',
'WorkflowRun': 'workflow',
}
Expand Down
52 changes: 49 additions & 3 deletions ddev/src/ddev/utils/github_async/models/check_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,63 @@

from __future__ import annotations

from enum import StrEnum, auto

from pydantic import BaseModel, ConfigDict


class CheckRunStatus(StrEnum):
"""The status of a check run.

The `check-run` schema declares `status` as
`enum: [queued, in_progress, completed, waiting, requested, pending]`.
Reference:
https://docs.github.com/en/rest/checks/runs#get-a-check-run
"""

QUEUED = auto()
IN_PROGRESS = auto()
COMPLETED = auto()
WAITING = auto()
REQUESTED = auto()
PENDING = auto()


class CheckRunConclusion(StrEnum):
"""The conclusion of a check run.

The `check-run` response schema declares `conclusion` as a nullable
`enum: [success, failure, neutral, cancelled, skipped, timed_out, action_required]`.
`stale` is added on top of those: it is a valid conclusion that only GitHub can
set (present in the update-a-check-run request enum and the docs, though the
response schema omits it), so a real response can carry it and the model must
accept it.
Reference:
https://docs.github.com/en/rest/checks/runs#update-a-check-run
"""

SUCCESS = auto()
FAILURE = auto()
NEUTRAL = auto()
CANCELLED = auto()
SKIPPED = auto()
TIMED_OUT = auto()
ACTION_REQUIRED = auto()
STALE = auto()


class CheckRun(BaseModel):
"""A GitHub Checks API check run."""
"""A GitHub Checks API check run.

Field reference:
https://docs.github.com/en/rest/checks/runs#get-a-check-run
"""

model_config = ConfigDict(extra="ignore")

id: int
name: str
status: str
conclusion: str | None
status: CheckRunStatus
conclusion: CheckRunConclusion | None
html_url: str | None
head_sha: str
16 changes: 15 additions & 1 deletion ddev/src/ddev/utils/github_async/models/pull_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,26 @@

from __future__ import annotations

from enum import StrEnum, auto

from pydantic import BaseModel, ConfigDict, Field

from .label import Label
from .user import GitHubUser


class PullRequestState(StrEnum):
"""The state of a pull request.

The `pull-request` schema declares `state` as `enum: [open, closed]`.
Reference:
https://docs.github.com/en/rest/pulls/pulls#get-a-pull-request
"""

OPEN = auto()
CLOSED = auto()


class PullRequestRef(BaseModel):
"""A head or base branch reference on a pull request.

Expand Down Expand Up @@ -50,7 +64,7 @@ class PullRequest(BaseModel):
patch_url: str | None = None

# State
state: str | None = None # 'open' or 'closed'
state: PullRequestState | None = None
draft: bool = False
merged: bool | None = None
locked: bool = False
Expand Down
85 changes: 79 additions & 6 deletions ddev/src/ddev/utils/github_async/models/workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,71 @@

from __future__ import annotations

from enum import StrEnum, auto

from pydantic import BaseModel, ConfigDict, Field


class WorkflowJobStatus(StrEnum):
"""The status of a workflow job.

The `job` schema declares `status` as
`enum: [queued, in_progress, completed, waiting, requested, pending]`.
Reference:
https://docs.github.com/en/rest/actions/workflow-jobs#get-a-job-for-a-workflow-run
"""

QUEUED = auto()
IN_PROGRESS = auto()
COMPLETED = auto()
WAITING = auto()
REQUESTED = auto()
PENDING = auto()


class WorkflowJobConclusion(StrEnum):
"""The conclusion of a workflow job.

The `job` schema declares `conclusion` as a nullable
`enum: [success, failure, neutral, cancelled, skipped, timed_out, action_required]`.
Reference:
https://docs.github.com/en/rest/actions/workflow-jobs#get-a-job-for-a-workflow-run
"""

SUCCESS = auto()
FAILURE = auto()
NEUTRAL = auto()
CANCELLED = auto()
SKIPPED = auto()
TIMED_OUT = auto()
ACTION_REQUIRED = auto()


class JobStepStatus(StrEnum):
"""The status of a single step within a workflow job.

The `job` schema's `steps` items declare `status` as
`enum: [queued, in_progress, completed]` (a narrower set than the job's own
status). Their `conclusion` is a nullable string with no `enum`, so it stays
a plain `str`.
Reference:
https://docs.github.com/en/rest/actions/workflow-jobs#get-a-job-for-a-workflow-run
"""

QUEUED = auto()
IN_PROGRESS = auto()
COMPLETED = auto()


class WorkflowRun(BaseModel):
"""A GitHub Actions workflow run."""
"""A GitHub Actions workflow run.

The `workflow-run` schema declares `status` and `conclusion` as plain
nullable strings with no `enum`, so they are intentionally kept as free-form
strings rather than modeled as a StrEnum.
Reference:
https://docs.github.com/en/rest/actions/workflow-runs#get-a-workflow-run
"""

model_config = ConfigDict(extra="ignore")

Expand All @@ -21,6 +81,11 @@ class WorkflowRun(BaseModel):
created_at: str | None = None
updated_at: str | None = None

@property
def is_completed(self) -> bool:
"""Whether the run has finished (``status == "completed"``)."""
return self.status == "completed"


class WorkflowDispatchResult(BaseModel):
"""Run metadata returned by `POST /actions/workflows/{id}/dispatches` when `return_run_details=True`."""
Expand Down Expand Up @@ -55,26 +120,34 @@ class ArtifactsList(BaseModel):


class JobStep(BaseModel):
"""A single step within a GitHub Actions job."""
"""A single step within a GitHub Actions job.

Field reference:
https://docs.github.com/en/rest/actions/workflow-jobs#get-a-job-for-a-workflow-run
"""

model_config = ConfigDict(extra="ignore")

name: str
status: str
status: JobStepStatus
conclusion: str | None = None
number: int | None = None


class WorkflowJob(BaseModel):
"""A single job within a GitHub Actions workflow run."""
"""A single job within a GitHub Actions workflow run.

Field reference:
https://docs.github.com/en/rest/actions/workflow-jobs#get-a-job-for-a-workflow-run
"""

model_config = ConfigDict(extra="ignore")

id: int
run_id: int
name: str
status: str
conclusion: str | None = None
status: WorkflowJobStatus
conclusion: WorkflowJobConclusion | None = None
html_url: str | None = None
steps: list[JobStep] = Field(default_factory=list)

Expand Down
Loading
Loading