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
212 changes: 196 additions & 16 deletions .github/workflows/maintenance-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,49 +71,229 @@ jobs:
name: audit-report
path: specs/audit-report.md

- name: Build issue body
- name: Classify severity and build issue
id: classify
if: steps.fetch.outputs.changes == 'true'
run: |
SEVERITY="low"
DATE=$(date -u +%Y-%m-%d)

# --- Parse diff report for high/medium signals ---
DIFF_FILE="specs/diff-report.md"
NEW_ENDPOINTS=0
REMOVED_ENDPOINTS=0
CHANGED_ENDPOINTS=0
SCHEMA_CHANGES=0

if [ -f "$DIFF_FILE" ]; then
# Count sections that have content (not just "No ... endpoints/changes")
if ! grep -q "No new endpoints" "$DIFF_FILE"; then
NEW_ENDPOINTS=$(grep -c '^\- \*\*' "$DIFF_FILE" 2>/dev/null || echo 0)

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NEW_ENDPOINTS counter is computed by grepping for - ** across the entire diff report, which will also count bullets in other sections (Removed Endpoints, Schema Changes, Deprecations). This can inflate the “new endpoints” count and summary; consider scoping the count to just the “## New Endpoints” section (e.g., by extracting that section first).

Suggested change
NEW_ENDPOINTS=$(grep -c '^\- \*\*' "$DIFF_FILE" 2>/dev/null || echo 0)
NEW_ENDPOINTS=$(sed -n '/^## New Endpoints$/,/^## /p' "$DIFF_FILE" | grep -c '^\- \*\*' 2>/dev/null || echo 0)

Copilot uses AI. Check for mistakes.
fi
if ! grep -q "No removed endpoints" "$DIFF_FILE"; then
REMOVED_ENDPOINTS=1
fi
if ! grep -q "No changed endpoints" "$DIFF_FILE"; then
CHANGED_ENDPOINTS=1
fi
if ! grep -q "No schema changes" "$DIFF_FILE"; then
SCHEMA_CHANGES=1
fi
fi

# --- Parse audit report for drift signals ---
AUDIT_FILE="specs/audit-report.md"
MISSING_ENDPOINTS=0
BODY_DRIFT=0
PARAM_DRIFT=0
CODE_ISSUES=0

if [ -f "$AUDIT_FILE" ]; then
MISSING_ENDPOINTS=$(grep -oP 'Missing from SDK: \K[0-9]+' "$AUDIT_FILE" | head -1 || echo 0)
[ -z "$MISSING_ENDPOINTS" ] && MISSING_ENDPOINTS=0

if ! grep -q "No request body drift" "$AUDIT_FILE"; then
BODY_DRIFT=1
fi
if ! grep -q "No query/path parameter drift" "$AUDIT_FILE"; then
PARAM_DRIFT=1
fi
CODE_ISSUES=$(grep -oP 'Code issues found: \K[0-9]+' "$AUDIT_FILE" | head -1 || echo 0)
[ -z "$CODE_ISSUES" ] && CODE_ISSUES=0
fi

# --- Determine severity ---
# High: removed endpoints, missing endpoints, breaking schema changes, body drift
if [ "$REMOVED_ENDPOINTS" -gt 0 ] || [ "$MISSING_ENDPOINTS" -gt 0 ] || [ "$BODY_DRIFT" -gt 0 ]; then
SEVERITY="high"
# Medium: new endpoints, changed endpoints, param drift, schema changes, real code issues
elif [ "$NEW_ENDPOINTS" -gt 0 ] || [ "$CHANGED_ENDPOINTS" -gt 0 ] || [ "$PARAM_DRIFT" -gt 0 ] || [ "$SCHEMA_CHANGES" -gt 0 ]; then

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The severity logic comment says medium includes “real code issues”, but CODE_ISSUES is never used when computing SEVERITY. If code issues should bump severity, include a CODE_ISSUES > 0 check (or update the comment to reflect the intended behavior).

Suggested change
elif [ "$NEW_ENDPOINTS" -gt 0 ] || [ "$CHANGED_ENDPOINTS" -gt 0 ] || [ "$PARAM_DRIFT" -gt 0 ] || [ "$SCHEMA_CHANGES" -gt 0 ]; then
elif [ "$NEW_ENDPOINTS" -gt 0 ] || [ "$CHANGED_ENDPOINTS" -gt 0 ] || [ "$PARAM_DRIFT" -gt 0 ] || [ "$SCHEMA_CHANGES" -gt 0 ] || [ "$CODE_ISSUES" -gt 0 ]; then

Copilot uses AI. Check for mistakes.
SEVERITY="medium"
# Low: cosmetic changes only (descriptions, formatting)
else
SEVERITY="low"
fi

echo "severity=$SEVERITY" >> $GITHUB_OUTPUT
echo "date=$DATE" >> $GITHUB_OUTPUT

# --- Build summary line ---
SUMMARY=""
[ "$REMOVED_ENDPOINTS" -gt 0 ] && SUMMARY="${SUMMARY}removed endpoints, "
[ "$MISSING_ENDPOINTS" -gt 0 ] && SUMMARY="${SUMMARY}${MISSING_ENDPOINTS} missing SDK methods, "
[ "$BODY_DRIFT" -gt 0 ] && SUMMARY="${SUMMARY}request body drift, "
[ "$PARAM_DRIFT" -gt 0 ] && SUMMARY="${SUMMARY}parameter drift, "
[ "$NEW_ENDPOINTS" -gt 0 ] && SUMMARY="${SUMMARY}${NEW_ENDPOINTS} new endpoints, "
[ "$CHANGED_ENDPOINTS" -gt 0 ] && SUMMARY="${SUMMARY}changed endpoints, "
[ "$SCHEMA_CHANGES" -gt 0 ] && SUMMARY="${SUMMARY}schema changes, "
[ "$CODE_ISSUES" -gt 0 ] && SUMMARY="${SUMMARY}${CODE_ISSUES} code issues, "

if [ -z "$SUMMARY" ]; then
SUMMARY="cosmetic spec changes only"
else
SUMMARY="${SUMMARY%, }"
fi

# --- Build issue body ---
{
echo "## Etsy API Drift Detected"
echo ""
echo "The weekly maintenance check found changes in the Etsy OpenAPI spec."
echo "## Etsy API Drift — \`${SEVERITY}\` severity"
echo ""
echo "### Diff Report"
echo "**Detected:** ${DATE} | **Severity:** \`${SEVERITY}\` | **Summary:** ${SUMMARY}"
echo ""
cat specs/diff-report.md

echo "### What Changed (Diff Report)"
echo ""
echo "### SDK Coverage"
if [ -f "$DIFF_FILE" ]; then
cat "$DIFF_FILE"
else
echo "No diff report generated."
fi
echo ""
sed -n '/## Coverage Summary/,/^## [^C]/p' specs/audit-report.md | head -20

echo "### SDK Impact (Audit Report)"
echo ""
if [ -f "$AUDIT_FILE" ]; then
# Coverage summary
sed -n '/## Coverage Summary/,/^## Missing Endpoints/p' "$AUDIT_FILE" | head -15
echo ""

# Missing endpoints (if any)
MISSING_SECTION=$(sed -n '/## Missing Endpoints/,/^## Not Implemented/p' "$AUDIT_FILE")
if ! echo "$MISSING_SECTION" | grep -q "All OAS operations are covered"; then
echo "#### Missing Endpoints"
echo "$MISSING_SECTION" | tail -n +2 | head -20
echo ""
fi

# Body drift (if any)
if ! grep -q "No request body drift" "$AUDIT_FILE"; then
echo "#### Request Body Drift"
sed -n '/## Request Body Drift/,/^## /p' "$AUDIT_FILE" | head -30
echo ""
fi

# Param drift (if any)
if ! grep -q "No query/path parameter drift" "$AUDIT_FILE"; then
echo "#### Parameter Drift"
sed -n '/## Query\/Path Parameter Drift/,/^## /p' "$AUDIT_FILE" | head -30
echo ""
fi
Comment on lines +188 to +200

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These sed -n '/## Request Body Drift/,/^## /p' (and similar) ranges will stop immediately because the end pattern ^## matches the same header line as the start pattern. As a result the issue body will include only the section header, not the section content. Use an end pattern that matches the next specific section header (or a range that excludes the first ^## match).

Copilot uses AI. Check for mistakes.

# Enum staleness (if any — only show real drift, not the known holiday IDs)
ENUM_SECTION=$(sed -n '/## Enum Staleness/,/^## /p' "$AUDIT_FILE")
if [ -n "$ENUM_SECTION" ] && ! echo "$ENUM_SECTION" | grep -q "^$"; then
ENUM_LINE_COUNT=$(echo "$ENUM_SECTION" | wc -l)
if [ "$ENUM_LINE_COUNT" -gt 3 ]; then
echo "#### Enum Staleness"
echo "$ENUM_SECTION" | head -30
echo ""
fi
fi

# Code issues (if any)
if [ "$CODE_ISSUES" -gt 0 ]; then
echo "#### Code Issues (${CODE_ISSUES})"
sed -n '/## Code Issues/,/^## /p' "$AUDIT_FILE" | head -30
echo ""
fi

# Extra SDK methods
EXTRA_SECTION=$(sed -n '/## Extra SDK Methods/,/^## /p' "$AUDIT_FILE")
if ! echo "$EXTRA_SECTION" | grep -q "no matching OAS operation" || echo "$EXTRA_SECTION" | grep -q "\*\*"; then
echo "<details><summary>Extra SDK Methods (no OAS match)</summary>"
echo ""
echo "$EXTRA_SECTION" | head -20
echo ""
echo "</details>"
echo ""
fi
Comment on lines +220 to +229

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The EXTRA_SECTION extraction uses sed -n '/## Extra SDK Methods/,/^## /p', which will also stop on the start header and likely capture only a single line. This prevents the <details> block from containing the actual list; extract until the next known section header (e.g., “## Missing Exports”) instead.

Copilot uses AI. Check for mistakes.
fi

echo "---"
echo ""
echo "**Next steps:**"
echo "1. Review the changes above"
echo '2. Run `/maintain-audit` to audit SDK coverage and implement changes'
echo "3. Update baseline: \`cp specs/latest.json specs/baseline.json\`"
echo "### Action Required"
echo ""
if [ "$SEVERITY" = "high" ]; then
echo "**Immediate action needed.** Breaking changes or missing SDK coverage detected."
echo ""
echo "1. Run \`/maintain-audit\` to review and implement fixes"
echo "2. Prioritize request body drift and missing endpoints"
elif [ "$SEVERITY" = "medium" ]; then
echo "**Review recommended.** New or changed endpoints that may need SDK updates."
echo ""
echo "1. Run \`/maintain-audit\` to review changes"
echo "2. Check if new endpoints need SDK methods"
else
echo "**Low priority.** Cosmetic spec changes only — no SDK code impact."
echo ""
echo "1. Review to confirm no hidden semantic changes"
echo "2. Update baseline if confirmed: \`cp specs/latest.json specs/baseline.json\`"
fi
echo ""
echo "*Auto-generated by [Maintenance Check](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})*"
} > issue-body.md

- name: Ensure severity labels exist
if: steps.fetch.outputs.changes == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh label create "severity: high" --color "B60205" --description "High severity — breaking changes or missing coverage" --force 2>/dev/null || true
gh label create "severity: medium" --color "D93F0B" --description "Medium severity — new/changed endpoints need review" --force 2>/dev/null || true
gh label create "severity: low" --color "0E8A16" --description "Low severity — cosmetic or informational only" --force 2>/dev/null || true

- name: Create or update issue on API drift
if: steps.fetch.outputs.changes == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SEVERITY: ${{ steps.classify.outputs.severity }}
DATE: ${{ steps.classify.outputs.date }}
run: |
TITLE="audit: Spec Drift [${DATE}] — ${SEVERITY}"
EXISTING=$(gh issue list --label "api-drift" --state open --json number --jq '.[0].number // empty')

if [ -n "$EXISTING" ]; then
echo "Updating existing issue #${EXISTING}"
gh issue edit "$EXISTING" --body-file issue-body.md
gh issue comment "$EXISTING" --body "Updated $(date -u +%Y-%m-%dT%H:%M:%SZ): New spec changes detected. Issue body updated with latest diff."
gh issue edit "$EXISTING" \
--title "$TITLE" \
--body-file issue-body.md \
--add-label "severity: ${SEVERITY}" \
--add-assignee amitray007
# Remove stale severity labels
for LBL in high medium low; do
if [ "$LBL" != "$SEVERITY" ]; then
gh issue edit "$EXISTING" --remove-label "severity: $LBL" 2>/dev/null || true
fi
done
gh issue comment "$EXISTING" --body "Updated ${DATE}: Spec changes re-evaluated. Severity: \`${SEVERITY}\`."
else
echo "Creating new api-drift issue"
gh issue create \
--title "Etsy API drift detected" \
--title "$TITLE" \
--body-file issue-body.md \
--label "api-drift"
--label "api-drift" \
--label "severity: ${SEVERITY}" \
--assignee amitray007
fi

- name: Close drift issue if no changes
Expand Down
6 changes: 3 additions & 3 deletions etsy_python/v3/models/Listing.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ def __init__(
personalization_is_required: Optional[bool] = None,
personalization_char_count_max: Optional[int] = None,
personalization_instructions: Optional[str] = None,
production_partner_ids: Optional[int] = None,
production_partner_ids: Optional[List[int]] = None,
image_ids: Optional[List[int]] = None,
is_supply: Optional[bool] = None,
is_customizable: Optional[bool] = None,
Expand Down Expand Up @@ -153,7 +153,7 @@ class UpdateListingRequest(Request):

def __init__(
self,
image_ids: Optional[List[str]] = None,
image_ids: Optional[List[int]] = None,
title: Optional[str] = None,
description: Optional[str] = None,
materials: Optional[List[str]] = None,
Expand Down Expand Up @@ -323,7 +323,7 @@ def __init__(

class UpdateVariationImagesRequest(Request):
nullable: List[str] = []
mandatory: List[str] = []
mandatory: List[str] = ["variation_images"]

def __init__(self, variation_images: List[Dict[str, Any]]) -> None:
self.variation_images = variation_images
Expand Down
6 changes: 4 additions & 2 deletions etsy_python/v3/resources/HolidayPreferences.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from dataclasses import dataclass
from enum import Enum
from typing import Optional, Union

Copilot AI Mar 23, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional is no longer used in this module after removing Optional[...] from the holiday_id type hint. Please drop the unused import to avoid lint/type-check noise.

Suggested change
from typing import Optional, Union
from typing import Union

Copilot uses AI. Check for mistakes.

from etsy_python.v3.enums.HolidayPreferences import CA_HOLIDAYS, HOLIDAYS, US_HOLIDAYS
Expand All @@ -20,10 +21,11 @@ def get_holiday_preferences(self, shop_id: int) -> Union[Response, RequestExcept
def update_holiday_preferences(
self,
shop_id: int,
holiday_id: Optional[Union[HOLIDAYS, US_HOLIDAYS, CA_HOLIDAYS, int]],
holiday_id: Union[HOLIDAYS, US_HOLIDAYS, CA_HOLIDAYS, int],
holiday_preference: UpdateHolidayPreferencesRequest,
) -> Union[Response, RequestException]:
endpoint = f"/shops/{shop_id}/holiday-preferences/{holiday_id}"
hid = holiday_id.value if isinstance(holiday_id, Enum) else holiday_id
endpoint = f"/shops/{shop_id}/holiday-preferences/{hid}"
return self.session.make_request(
endpoint, method=Method.PUT, payload=holiday_preference
)
2 changes: 1 addition & 1 deletion specs/baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -2388,7 +2388,7 @@
},
"put": {
"operationId": "updateListingInventory",
"description": "<div class=\"wt-display-flex-xs wt-align-items-center wt-mt-xs-2 wt-mb-xs-3\"><span class=\"wt-badge wt-badge--notificationPrimary wt-bg-slime-tint wt-mr-xs-2\">General Release</span><a class=\"wt-text-link\" href=\"https://github.com/etsy/open-api/discussions\" target=\"_blank\" rel=\"noopener noreferrer\">Report bug</a></div><div class=\"wt-display-flex-xs wt-align-items-center wt-mt-xs-2 wt-mb-xs-3\"><p class=\"wt-text-body-01 banner-text\">This endpoint is ready for production use.</p></div>\n\nUpdates the inventory for a listing identified by a listing ID. The update fails if the supplied values for product sku, offering quantity, and/or price are incompatible with values in `*_on_property_*` fields. When setting a price, assign a float equal to amount divided by divisor as specified in the Money resource.",
"description": "<div class=\"wt-display-flex-xs wt-align-items-center wt-mt-xs-2 wt-mb-xs-3\"><span class=\"wt-badge wt-badge--notificationPrimary wt-bg-slime-tint wt-mr-xs-2\">General Release</span><a class=\"wt-text-link\" href=\"https://github.com/etsy/open-api/discussions\" target=\"_blank\" rel=\"noopener noreferrer\">Report bug</a></div><div class=\"wt-display-flex-xs wt-align-items-center wt-mt-xs-2 wt-mb-xs-3\"><p class=\"wt-text-body-01 banner-text\">This endpoint is ready for production use.</p></div>\n\nUpdates the inventory for a listing identified by a listing ID. The update fails if the supplied values for product sku, offering quantity, and/or price are incompatible with values in `*_on_property` fields. When setting a price, assign a float equal to amount divided by divisor as specified in the Money resource.",
"tags": [
"ShopListing Inventory"
],
Expand Down
2 changes: 1 addition & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
MOCK_PRODUCT_ID = 70707
MOCK_OFFERING_ID = 80808
MOCK_USER_ADDRESS_ID = 90909
MOCK_HOLIDAY_ID = "thanksgiving"
MOCK_HOLIDAY_ID = 10


@pytest.fixture
Expand Down
4 changes: 2 additions & 2 deletions tests/fixtures/responses.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,8 +589,8 @@ def make_user_address(**overrides):

def make_holiday_preference(**overrides):
data = {
"holiday_id": "thanksgiving",
"holiday_name": "Thanksgiving",
"holiday_id": 10,
"holiday_name": "Thanksgiving Day",
"is_working": False,
}
data.update(overrides)
Expand Down
4 changes: 4 additions & 0 deletions tests/test_listing_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ def test_valid_request(self):
result = req.get_dict()
assert len(result["variation_images"]) == 1

def test_missing_variation_images_raises(self):
with pytest.raises(ValueError):
UpdateVariationImagesRequest(variation_images=None)


class TestUploadListingImageRequest:
def test_sets_file_and_data(self):
Expand Down
16 changes: 16 additions & 0 deletions tests/test_remaining_resources.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
CreateShopReturnPolicyRequest,
UpdateShopReturnPolicyRequest,
)
from etsy_python.v3.enums.HolidayPreferences import US_HOLIDAYS
from etsy_python.v3.models.HolidayPreferences import UpdateHolidayPreferencesRequest
from etsy_python.v3.resources.HolidayPreferences import HolidayPreferencesResource
from etsy_python.v3.resources.ListingFile import ListingFileResource
Expand Down Expand Up @@ -645,6 +646,21 @@ def test_update_holiday_preferences(self, mock_session):
payload=payload,
)

def test_update_holiday_preferences_with_enum(self, mock_session):
mock_session.make_request.return_value = Response(
200, make_holiday_preference()
)
resource = HolidayPreferencesResource(session=mock_session)
payload = MagicMock(spec=UpdateHolidayPreferencesRequest)
resource.update_holiday_preferences(
MOCK_SHOP_ID, US_HOLIDAYS.THANKSGIVING_DAY, payload
)
mock_session.make_request.assert_called_once_with(
f"/shops/{MOCK_SHOP_ID}/holiday-preferences/{US_HOLIDAYS.THANKSGIVING_DAY.value}",
method=Method.PUT,
payload=payload,
)


# --- ListingOffering ---
class TestListingOfferingResource:
Expand Down
Loading