# Milestones

Milestones group issues and pull requests into trackable goals with optional due dates. They provide progress tracking through open/closed issue counts.

## Endpoints

### List milestones

```
GET /repos/{owner}/{repo}/milestones
```

**Query parameters:**
| Param | Type | Default | Description |
|-------|------|---------|-------------|
| `state` | string | `"open"` | Filter by state: `open`, `closed`, or `all` |
| `name` | string | — | Filter by milestone name (partial match) |
| `page` | integer | 1 | Page number (1-based) |
| `limit` | integer | — | Items per page |

```bash
# List open milestones
curl -s \
  -H "Authorization: token ${FORGEJO_PAT}" \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/milestones?state=open"

# List all milestones (open and closed)
curl -s \
  -H "Authorization: token ${FORGEJO_PAT}" \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/milestones?state=all&limit=50"

# Search milestones by name
curl -s \
  -H "Authorization: token ${FORGEJO_PAT}" \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/milestones?name=v2.0"
```

### Create a milestone

```
POST /repos/{owner}/{repo}/milestones
```

**Body: CreateMilestoneOption**

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `title` | string | **yes** | Milestone title |
| `description` | string | no | Milestone description (supports markdown) |
| `due_on` | string | no | Due date in ISO 8601 format (e.g., `"2025-06-30T00:00:00Z"`) |
| `state` | string | no | Initial state: `"open"` (default) or `"closed"` |

```bash
# Create a milestone with a due date
curl -s -X POST \
  -H "Authorization: token ${FORGEJO_PAT}" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "v2.0 Release",
    "description": "All features and fixes for the v2.0 release",
    "due_on": "2025-06-30T00:00:00Z"
  }' \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/milestones"

# Create a milestone without a due date
curl -s -X POST \
  -H "Authorization: token ${FORGEJO_PAT}" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Backlog",
    "description": "Low-priority items to address eventually"
  }' \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/milestones"
```

### Get a milestone

```
GET /repos/{owner}/{repo}/milestones/{id}
```

**Path parameters:**
| Param | Type | Description |
|-------|------|-------------|
| `id` | integer | Milestone ID |

```bash
curl -s \
  -H "Authorization: token ${FORGEJO_PAT}" \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/milestones/{milestone_id}"
```

### Edit a milestone

```
PATCH /repos/{owner}/{repo}/milestones/{id}
```

**Body: EditMilestoneOption** — All fields are optional.

| Field | Type | Description |
|-------|------|-------------|
| `title` | string | New title |
| `description` | string | New description |
| `due_on` | string | New due date (ISO 8601) |
| `state` | string | New state: `"open"` or `"closed"` |

```bash
# Update the due date
curl -s -X PATCH \
  -H "Authorization: token ${FORGEJO_PAT}" \
  -H "Content-Type: application/json" \
  -d '{
    "due_on": "2025-07-15T00:00:00Z"
  }' \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/milestones/{milestone_id}"

# Close a milestone
curl -s -X PATCH \
  -H "Authorization: token ${FORGEJO_PAT}" \
  -H "Content-Type: application/json" \
  -d '{
    "state": "closed"
  }' \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/milestones/{milestone_id}"

# Reopen a closed milestone
curl -s -X PATCH \
  -H "Authorization: token ${FORGEJO_PAT}" \
  -H "Content-Type: application/json" \
  -d '{
    "state": "open"
  }' \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/milestones/{milestone_id}"
```

### Delete a milestone

```
DELETE /repos/{owner}/{repo}/milestones/{id}
```

Deleting a milestone does **not** delete the issues assigned to it. Those issues simply become unassigned from any milestone.

```bash
curl -s -X DELETE \
  -H "Authorization: token ${FORGEJO_PAT}" \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/milestones/{milestone_id}"
```

## Milestone Response Object

```json
{
  "id": 3,
  "title": "v2.0 Release",
  "description": "All features and fixes for the v2.0 release",
  "state": "open",
  "open_issues": 12,
  "closed_issues": 34,
  "due_on": "2025-06-30T00:00:00Z",
  "created_at": "2025-01-15T10:30:00Z",
  "updated_at": "2025-04-10T14:22:00Z",
  "closed_at": null
}
```

| Field | Type | Description |
|-------|------|-------------|
| `id` | integer | Milestone ID |
| `title` | string | Title |
| `description` | string | Description (markdown) |
| `state` | string | `"open"` or `"closed"` |
| `open_issues` | integer | Number of open issues in this milestone |
| `closed_issues` | integer | Number of closed issues in this milestone |
| `due_on` | string or null | Due date (ISO 8601) or null if no due date |
| `created_at` | string | Creation timestamp |
| `updated_at` | string | Last update timestamp |
| `closed_at` | string or null | When the milestone was closed, or null |

## Calculating Completion Percentage

The API does not return a percentage directly. Compute it from the issue counts:

```
total = open_issues + closed_issues
percentage = (closed_issues / total) * 100    if total > 0
percentage = 0                                 if total == 0
```

### Example: Fetch milestone with completion percentage

```bash
# Get milestone and compute progress
curl -s \
  -H "Authorization: token ${FORGEJO_PAT}" \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/milestones/{milestone_id}" | \
  jq '{
    title: .title,
    state: .state,
    open_issues: .open_issues,
    closed_issues: .closed_issues,
    total_issues: (.open_issues + .closed_issues),
    completion_pct: (if (.open_issues + .closed_issues) > 0
      then ((.closed_issues / (.open_issues + .closed_issues)) * 100 | round)
      else 0 end),
    due_on: .due_on
  }'
```

**Example output:**
```json
{
  "title": "v2.0 Release",
  "state": "open",
  "open_issues": 12,
  "closed_issues": 34,
  "total_issues": 46,
  "completion_pct": 74,
  "due_on": "2025-06-30T00:00:00Z"
}
```

### Example: List all milestones with progress

```bash
curl -s \
  -H "Authorization: token ${FORGEJO_PAT}" \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/milestones?state=all&limit=50" | \
  jq '[.[] | {
    id: .id,
    title: .title,
    state: .state,
    progress: (if (.open_issues + .closed_issues) > 0
      then "\(.closed_issues)/\(.open_issues + .closed_issues) (\((.closed_issues / (.open_issues + .closed_issues)) * 100 | round)%)"
      else "0/0 (0%)" end),
    due_on: .due_on
  }]'
```

## Assigning Issues to a Milestone

To assign an issue to a milestone, use the issue edit endpoint with the milestone ID:

```bash
# Assign issue #42 to milestone 3
curl -s -X PATCH \
  -H "Authorization: token ${FORGEJO_PAT}" \
  -H "Content-Type: application/json" \
  -d '{
    "milestone": 3
  }' \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}"

# Remove issue from milestone (set to 0)
curl -s -X PATCH \
  -H "Authorization: token ${FORGEJO_PAT}" \
  -H "Content-Type: application/json" \
  -d '{
    "milestone": 0
  }' \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues/{index}"
```

## Filtering Issues by Milestone

```bash
# List open issues in milestone 3
curl -s \
  -H "Authorization: token ${FORGEJO_PAT}" \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/issues?milestones={milestone_name}&state=open"
```

The `milestones` query parameter on the issues list endpoint accepts the milestone **name** (not ID). Multiple milestones can be comma-separated.

## Common Workflows

### Closing a milestone when all issues are done

```bash
MILESTONE_ID="{milestone_id}"

# Check if any issues are still open
OPEN=$(curl -s \
  -H "Authorization: token ${FORGEJO_PAT}" \
  "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/milestones/${MILESTONE_ID}" | \
  jq '.open_issues')

if [ "$OPEN" -eq 0 ]; then
  curl -s -X PATCH \
    -H "Authorization: token ${FORGEJO_PAT}" \
    -H "Content-Type: application/json" \
    -d '{"state": "closed"}' \
    "${FORGEJO_URL}/api/v1/repos/{owner}/{repo}/milestones/${MILESTONE_ID}"
  echo "Milestone closed."
else
  echo "Cannot close: ${OPEN} issues still open."
fi
```
