microsoft/typespec
Publicmirrored from https://github.com/microsoft/typespecAvailable
.github/workflows/python-check-spec-tests.yml
175lines · modecode
| 1 | name: "[Python] Check Spec Test Cases" |
| 2 | |
| 3 | on: |
| 4 | # schedule: |
| 5 | # - cron: "0 0 * * *" # Daily at midnight UTC |
| 6 | workflow_dispatch: |
| 7 | |
| 8 | permissions: |
| 9 | issues: write |
| 10 | |
| 11 | concurrency: |
| 12 | group: python-check-spec-tests |
| 13 | cancel-in-progress: true |
| 14 | |
| 15 | jobs: |
| 16 | check-and-create-issues: |
| 17 | runs-on: ubuntu-latest |
| 18 | steps: |
| 19 | - uses: actions/setup-node@v4 |
| 20 | with: |
| 21 | node-version: "20" |
| 22 | |
| 23 | - name: Check for new spec changes and create issues |
| 24 | env: |
| 25 | GH_TOKEN: ${{ secrets.ISSUE_CREATE_GITHUB_TOKEN }} |
| 26 | run: | |
| 27 | set -euo pipefail |
| 28 | |
| 29 | PACKAGE_JSON_URL="https://raw.githubusercontent.com/microsoft/typespec/main/packages/http-client-python/package.json" |
| 30 | SKILL_URL="https://github.com/microsoft/typespec/blob/main/.github/skills/python-sdk-spector-mock-api-tests/SKILL.md" |
| 31 | ISSUE_REPO="microsoft/typespec" |
| 32 | |
| 33 | # Fetch package.json from main branch once |
| 34 | PACKAGE_JSON_CONTENT=$(curl -fsSL "$PACKAGE_JSON_URL") |
| 35 | if [ -z "$PACKAGE_JSON_CONTENT" ]; then |
| 36 | echo "ERROR: Could not fetch package.json from main branch" |
| 37 | exit 1 |
| 38 | fi |
| 39 | |
| 40 | check_package() { |
| 41 | local pkg_name="$1" |
| 42 | local repo="$2" |
| 43 | local specs_path="$3" |
| 44 | |
| 45 | echo "==============================" |
| 46 | echo "Checking package: $pkg_name" |
| 47 | echo "==============================" |
| 48 | |
| 49 | # Step 1: Extract version A from package.json devDependencies (main branch) |
| 50 | local version_a |
| 51 | version_a=$(echo "$PACKAGE_JSON_CONTENT" | jq -r ".devDependencies[\"$pkg_name\"]") |
| 52 | if [ -z "$version_a" ] || [ "$version_a" = "null" ]; then |
| 53 | echo "WARNING: Could not find $pkg_name in devDependencies, skipping" |
| 54 | return |
| 55 | fi |
| 56 | echo "Version A (from main branch package.json): $version_a" |
| 57 | |
| 58 | # Step 2: Get publish dates and find the most recently published version (including dev/prerelease) |
| 59 | local time_data |
| 60 | time_data=$(npm view "$pkg_name" time --json 2>/dev/null || true) |
| 61 | if [ -z "$time_data" ]; then |
| 62 | echo "WARNING: Could not get time data for $pkg_name from npm, skipping" |
| 63 | return |
| 64 | fi |
| 65 | |
| 66 | # Find the most recently published version (excluding "created" and "modified" metadata keys) |
| 67 | local version_b |
| 68 | version_b=$(echo "$time_data" | jq -r 'to_entries | map(select(.key != "created" and .key != "modified")) | sort_by(.value) | last | .key') |
| 69 | if [ -z "$version_b" ] || [ "$version_b" = "null" ]; then |
| 70 | echo "WARNING: Could not determine latest version from npm time data, skipping" |
| 71 | return |
| 72 | fi |
| 73 | echo "Version B (most recently published): $version_b" |
| 74 | |
| 75 | local date_a |
| 76 | date_a=$(echo "$time_data" | jq -r ".[\"$version_a\"]") |
| 77 | local date_b |
| 78 | date_b=$(echo "$time_data" | jq -r ".[\"$version_b\"]") |
| 79 | |
| 80 | if [ "$date_a" = "null" ] || [ -z "$date_a" ]; then |
| 81 | echo "WARNING: Could not find publish date for version $version_a, skipping" |
| 82 | return |
| 83 | fi |
| 84 | if [ "$date_b" = "null" ] || [ -z "$date_b" ]; then |
| 85 | echo "WARNING: Could not find publish date for version $version_b, skipping" |
| 86 | return |
| 87 | fi |
| 88 | |
| 89 | echo "Date A ($version_a): $date_a" |
| 90 | echo "Date B ($version_b): $date_b" |
| 91 | |
| 92 | # Compare dates - if B (latest) is newer than A (pinned), there are new specs to check |
| 93 | if [[ "$date_b" > "$date_a" ]]; then |
| 94 | echo "Latest version is newer than pinned, proceeding to check for PRs..." |
| 95 | else |
| 96 | echo "Pinned version is up-to-date, skipping $pkg_name" |
| 97 | return |
| 98 | fi |
| 99 | |
| 100 | # Step 3: Adjust since date using git commit time instead of npm publish time. |
| 101 | # npm publish dates lag behind actual commit times due to CI/CD pipeline delays, |
| 102 | # which can cause recently-merged commits to be missed when the GitHub Commits API |
| 103 | # filters by author date. We find the last commit on the specs path before version A |
| 104 | # was published, and use that commit's author date as the since boundary. |
| 105 | local last_commit_date |
| 106 | last_commit_date=$(gh api "repos/${repo}/commits" \ |
| 107 | -f path="${specs_path}" \ |
| 108 | -f until="${date_a}" \ |
| 109 | -f per_page=1 \ |
| 110 | --jq '.[0].commit.author.date' 2>/dev/null || true) |
| 111 | if [ -n "$last_commit_date" ] && [ "$last_commit_date" != "null" ]; then |
| 112 | echo "Adjusted since date from npm publish ($date_a) to last spec commit ($last_commit_date)" |
| 113 | date_a="$last_commit_date" |
| 114 | fi |
| 115 | |
| 116 | # Step 4: Use GitHub commits API (supports path filtering, unlike gh search prs) |
| 117 | echo "Searching for commits between $date_a and $date_b in $repo affecting $specs_path" |
| 118 | |
| 119 | local pr_numbers |
| 120 | pr_numbers=$(gh api "repos/${repo}/commits" \ |
| 121 | --paginate \ |
| 122 | -f path="${specs_path}" \ |
| 123 | -f since="${date_a}" \ |
| 124 | -f until="${date_b}" \ |
| 125 | --jq '.[].commit.message' 2>/dev/null \ |
| 126 | | grep -oP '\(#\K[0-9]+(?=\))' \ |
| 127 | | sort -un || true) |
| 128 | |
| 129 | if [ -z "$pr_numbers" ]; then |
| 130 | echo "No PRs found affecting specs path in date range, skipping" |
| 131 | return |
| 132 | fi |
| 133 | |
| 134 | local pr_count |
| 135 | pr_count=$(echo "$pr_numbers" | wc -l | tr -d ' ') |
| 136 | echo "Found $pr_count PRs affecting specs in date range" |
| 137 | |
| 138 | # Step 5: Fetch existing issues once (across all states) to avoid duplicate creation |
| 139 | local existing_issues |
| 140 | existing_issues=$(gh issue list --repo "$ISSUE_REPO" --state all --search "in:title [python] add test case" --json title --limit 200 2>/dev/null || echo "[]") |
| 141 | |
| 142 | while IFS= read -r pr_number; do |
| 143 | [ -z "$pr_number" ] && continue |
| 144 | local pr_url="https://github.com/${repo}/pull/${pr_number}" |
| 145 | |
| 146 | echo "PR #$pr_number affects specs: $pr_url" |
| 147 | |
| 148 | local title="[python] add test case for ${pr_url}" |
| 149 | local body="follow skill ${SKILL_URL} to write test case for ${pr_url}" |
| 150 | |
| 151 | # Check if an issue with the same title already exists (open or closed) |
| 152 | local has_match |
| 153 | has_match=$(echo "$existing_issues" | jq --arg t "$title" '[.[] | select(.title == $t)] | length') |
| 154 | |
| 155 | if [ "$has_match" -gt 0 ]; then |
| 156 | echo "Issue already exists for PR $pr_url, skipping" |
| 157 | continue |
| 158 | fi |
| 159 | |
| 160 | # Create the issue |
| 161 | gh issue create --repo "$ISSUE_REPO" \ |
| 162 | --title "$title" \ |
| 163 | --body "$body" \ |
| 164 | --label "emitter:client:python" \ |
| 165 | --assignee "copilot" |
| 166 | |
| 167 | echo "Created issue for PR $pr_url" |
| 168 | done <<< "$pr_numbers" |
| 169 | } |
| 170 | |
| 171 | # Check both packages |
| 172 | check_package "@typespec/http-specs" "microsoft/typespec" "packages/http-specs/specs" |
| 173 | check_package "@azure-tools/azure-http-specs" "Azure/typespec-azure" "packages/azure-http-specs/specs" |
| 174 | |
| 175 | echo "Done!" |
| 176 | |