|
20 | 20 | permissions: |
21 | 21 | contents: write |
22 | 22 | issues: write |
| 23 | + pull-requests: write |
23 | 24 |
|
24 | 25 | # Note: with cancel-in-progress, a newer run can cancel an older one after it |
25 | 26 | # has force-pushed the branch but before it finishes updating the tracking |
@@ -157,45 +158,230 @@ jobs: |
157 | 158 | ! -path "$TARGET/template/*" \ |
158 | 159 | -print -exec cp -f "$TEMPLATE" {} \; |
159 | 160 |
|
160 | | - - name: Commit and push to dedicated branch |
| 161 | + - name: Create or update tracking issue with PR link |
| 162 | + env: |
| 163 | + GH_TOKEN: ${{ github.token }} |
161 | 164 | run: | |
162 | 165 | set -euo pipefail |
163 | | - BRANCH="typespec-python-generated-tests" |
| 166 | + TARGET_BRANCH="typespec-python-generated-tests" |
164 | 167 | git config user.name "github-actions[bot]" |
165 | 168 | git config user.email "github-actions[bot]@users.noreply.github.com" |
166 | 169 |
|
| 170 | + GENERATED_DIR="eng/tools/azure-sdk-tools/emitter/generated" |
| 171 | +
|
167 | 172 | # Quick check: skip if regeneration produced no changes vs HEAD. |
168 | | - if [ -z "$(git status --porcelain -- eng/tools/azure-sdk-tools/emitter/generated/)" ]; then |
| 173 | + if [ -z "$(git status --porcelain -- "$GENERATED_DIR")" ]; then |
169 | 174 | echo "No changes to commit" |
170 | 175 | exit 0 |
171 | 176 | fi |
172 | 177 |
|
173 | | - # Push regenerated files directly to the dedicated branch. |
174 | | - # This branch is machine-managed and may be force-pushed. |
175 | 178 | PR_NUMBER="${{ steps.typespec-info.outputs.typespec_pr_number }}" |
| 179 | + # Use a distinct source branch per origin so a main-based run and a |
| 180 | + # PR-based run never force-push over each other. |
176 | 181 | if [ -n "$PR_NUMBER" ]; then |
177 | 182 | SOURCE_LABEL="microsoft/typespec PR #${PR_NUMBER}" |
| 183 | + SOURCE_BRANCH="regen/typespec-python-pr-${PR_NUMBER}" |
178 | 184 | else |
179 | 185 | SOURCE_LABEL="microsoft/typespec@main" |
| 186 | + SOURCE_BRANCH="regen/typespec-python-main" |
180 | 187 | fi |
181 | 188 |
|
182 | | - # Base on origin/main so the dedicated branch never inherits |
183 | | - # unrelated content from whatever ref the workflow checked out. |
| 189 | + # Save regenerated files to a temp dir before switching branches, |
| 190 | + # since they are untracked and would be lost on checkout. |
| 191 | + TMPDIR=$(mktemp -d) |
| 192 | + cp -r "$GENERATED_DIR"/. "$TMPDIR" |
| 193 | +
|
| 194 | + # Ensure the target branch exists; create from origin/main if not. |
184 | 195 | git fetch --no-tags --depth=1 origin main |
185 | | - git checkout -B "$BRANCH" origin/main |
| 196 | + if ! git fetch --no-tags --depth=1 origin "$TARGET_BRANCH" 2>/dev/null; then |
| 197 | + git push origin "origin/main:refs/heads/$TARGET_BRANCH" |
| 198 | + git fetch --no-tags --depth=1 origin "$TARGET_BRANCH" |
| 199 | + fi |
| 200 | +
|
| 201 | + # Clean up untracked generated files so checkout doesn't conflict. |
| 202 | + rm -rf "$GENERATED_DIR" |
| 203 | +
|
| 204 | + # Create source branch based on the target branch. |
| 205 | + git checkout -B "$SOURCE_BRANCH" "origin/$TARGET_BRANCH" |
186 | 206 |
|
187 | | - # Re-apply just the regenerated tree on top of origin/main. |
188 | | - git checkout HEAD@{1} -- eng/tools/azure-sdk-tools/emitter/generated |
189 | | - git add -f eng/tools/azure-sdk-tools/emitter/generated/ |
| 207 | + # Restore regenerated files from the temp dir. |
| 208 | + mkdir -p "$GENERATED_DIR" |
| 209 | + rm -rf "$GENERATED_DIR"/* |
| 210 | + cp -r "$TMPDIR"/. "$GENERATED_DIR" |
| 211 | + rm -rf "$TMPDIR" |
| 212 | + git add -f "$GENERATED_DIR"/ |
190 | 213 |
|
191 | 214 | if git diff --cached --quiet; then |
192 | | - echo "No changes vs origin/main" |
| 215 | + echo "No changes vs $TARGET_BRANCH" |
193 | 216 | exit 0 |
194 | 217 | fi |
195 | 218 |
|
196 | | - git commit -m "[typespec-python] Regenerate tests from ${SOURCE_LABEL}" |
197 | | - git push origin "$BRANCH" --force-with-lease |
198 | | - echo "::notice::Pushed regenerated tests to $BRANCH" |
| 219 | + COMMIT_MSG="[typespec-python] Regenerate tests from ${SOURCE_LABEL}" |
| 220 | + git commit -m "$COMMIT_MSG" |
| 221 | + git push origin "$SOURCE_BRANCH" --force-with-lease |
| 222 | +
|
| 223 | + # GitHub Actions' default GITHUB_TOKEN is not permitted to create pull |
| 224 | + # requests in this repository, so instead of opening a PR directly we |
| 225 | + # create/update a tracking issue containing a pre-filled "compare" link |
| 226 | + # that a maintainer can click to open the PR manually. |
| 227 | + REPO="${{ github.repository }}" |
| 228 | + SERVER="${{ github.server_url }}" |
| 229 | + RUN_URL="${SERVER}/${REPO}/actions/runs/${{ github.run_id }}" |
| 230 | + TS_REF_URL="${{ steps.typespec-info.outputs.typespec_ref_url }}" |
| 231 | +
|
| 232 | + # Determine assignees. For manual (workflow_dispatch) triggers, assign |
| 233 | + # to the user who triggered the run. For automatic triggers (push, |
| 234 | + # schedule), fall back to the default maintainers. |
| 235 | + EVENT_NAME="${{ github.event_name }}" |
| 236 | + ACTOR="${{ github.actor }}" |
| 237 | + if [ "$EVENT_NAME" = "workflow_dispatch" ] && [ -n "$ACTOR" ]; then |
| 238 | + ASSIGNEES="$ACTOR" |
| 239 | + CC_LINE="cc @${ACTOR}" |
| 240 | + else |
| 241 | + ASSIGNEES="iscai-msft,msyyc" |
| 242 | + CC_LINE="cc @iscai-msft @msyyc" |
| 243 | + fi |
| 244 | +
|
| 245 | + TITLE="$COMMIT_MSG" |
| 246 | +
|
| 247 | + # Reuse an existing open tracking issue if one exists (matched by exact |
| 248 | + # title). We list by label and match the title with jq because GitHub's |
| 249 | + # search tokenizer strips characters like [ ] @ # and /, making a title |
| 250 | + # search ambiguous. |
| 251 | + ISSUES_JSON=$(gh issue list --state open --label typespec-python \ |
| 252 | + --limit 100 --json number,title) |
| 253 | + EXISTING_ISSUE=$(jq -r --arg title "$TITLE" \ |
| 254 | + 'first(.[] | select(.title == $title) | .number) // ""' <<< "$ISSUES_JSON") |
| 255 | +
|
| 256 | + if [ -n "$EXISTING_ISSUE" ]; then |
| 257 | + ISSUE_NUMBER="$EXISTING_ISSUE" |
| 258 | + echo "Reusing existing tracking issue #$ISSUE_NUMBER" |
| 259 | + else |
| 260 | + echo "Creating new tracking issue" |
| 261 | + # `gh issue create` prints the new issue's URL to stdout; parse the |
| 262 | + # trailing number out of it. The body is filled in below once we have |
| 263 | + # the compare URL. |
| 264 | + # Assignees are applied best-effort in the final `gh issue edit` |
| 265 | + # below, so a non-assignable actor never fails issue creation. |
| 266 | + ISSUE_URL=$(gh issue create --title "$TITLE" \ |
| 267 | + --body "Tracking issue for TypeSpec Python regeneration. Details will be filled in shortly." \ |
| 268 | + --label "typespec-python") |
| 269 | + ISSUE_NUMBER="${ISSUE_URL##*/}" |
| 270 | + echo "Created issue #$ISSUE_NUMBER ($ISSUE_URL)" |
| 271 | + fi |
| 272 | +
|
| 273 | + # If an open PR already exists from the source branch to the target |
| 274 | + # branch, point the tracking issue at it instead of asking for a new one. |
| 275 | + EXISTING_PR_JSON=$(gh pr list --state open --head "$SOURCE_BRANCH" --base "$TARGET_BRANCH" \ |
| 276 | + --json number,url --limit 1) |
| 277 | + EXISTING_PR_URL=$(echo "$EXISTING_PR_JSON" | jq -r '.[0].url // empty') |
| 278 | + EXISTING_PR_NUMBER=$(echo "$EXISTING_PR_JSON" | jq -r '.[0].number // empty') |
| 279 | +
|
| 280 | + if [ -n "$EXISTING_PR_URL" ]; then |
| 281 | + ISSUE_BODY="A pull request already exists for this regeneration. |
| 282 | +
|
| 283 | + 👉 [View pull request #${EXISTING_PR_NUMBER}](${EXISTING_PR_URL}) |
| 284 | +
|
| 285 | + The branch \`${SOURCE_BRANCH}\` was just updated with the latest regenerated tests; the existing PR will reflect those changes automatically. |
| 286 | +
|
| 287 | + Details: |
| 288 | + - Source: [${SOURCE_LABEL}](${TS_REF_URL}) |
| 289 | + - Branch: [\`${SOURCE_BRANCH}\`](${SERVER}/${REPO}/tree/${SOURCE_BRANCH}) |
| 290 | + - Latest workflow run: ${RUN_URL} |
| 291 | +
|
| 292 | + > Note: the PR targets the \`${TARGET_BRANCH}\` branch, so merging it will **not** auto-close this issue. Please close this issue manually after the PR is merged. |
| 293 | +
|
| 294 | + ${CC_LINE}" |
| 295 | + else |
| 296 | + # Build a "compare" URL that opens the PR creation page pre-filled. |
| 297 | + # GitHub Actions cannot create PRs directly (org policy), so the |
| 298 | + # reviewer just needs to click the link to open the PR. The PR is |
| 299 | + # based on the dedicated target branch, not main. |
| 300 | + ISSUE_LINK="${SERVER}/${REPO}/issues/${ISSUE_NUMBER}" |
| 301 | + # For PR-sourced runs, mark the prefilled PR as do-not-merge: it only |
| 302 | + # exists to surface the code diff of the upstream typespec PR. |
| 303 | + PR_TITLE="$TITLE" |
| 304 | + NOTE_PREFIX="" |
| 305 | + DRAFT_PARAM="" |
| 306 | + if [ -n "$PR_NUMBER" ]; then |
| 307 | + PR_TITLE="$TITLE (DO NOT MERGE)" |
| 308 | + NOTE_PREFIX=$'NOTE: This PR exists only to display the code diff. Please do not merge it.\n\n' |
| 309 | + # PR-sourced regenerations open the prefilled PR as a draft |
| 310 | + # (draft=1 is undocumented but honored by the quick_pull form). |
| 311 | + DRAFT_PARAM="&draft=1" |
| 312 | + fi |
| 313 | + PR_TITLE_ENC=$(jq -rn --arg t "$PR_TITLE" '$t|@uri') |
| 314 | + PR_BODY_RAW="${NOTE_PREFIX}Fixes ${ISSUE_LINK} |
| 315 | +
|
| 316 | + Source: ${TS_REF_URL} |
| 317 | +
|
| 318 | + Automated regeneration of TypeSpec Python generated tests from ${SOURCE_LABEL}. |
| 319 | +
|
| 320 | + - Workflow run: ${RUN_URL} |
| 321 | +
|
| 322 | + This PR was auto-generated. Re-run the workflow to update it after new commits are pushed to the upstream TypeSpec PR" |
| 323 | + PR_BODY_ENC=$(jq -rn --arg b "$PR_BODY_RAW" '$b|@uri') |
| 324 | + COMPARE_URL="${SERVER}/${REPO}/compare/${TARGET_BRANCH}...${SOURCE_BRANCH}?quick_pull=1${DRAFT_PARAM}&title=${PR_TITLE_ENC}&body=${PR_BODY_ENC}" |
| 325 | +
|
| 326 | + ISSUE_BODY="GitHub Actions is not permitted to create pull requests in this repository, so this issue tracks the regeneration instead. |
| 327 | +
|
| 328 | + **Click the link below to open a pre-filled PR:** |
| 329 | +
|
| 330 | + 👉 [Create pull request from \`${SOURCE_BRANCH}\`](${COMPARE_URL}) |
| 331 | +
|
| 332 | + Details: |
| 333 | + - Source: [${SOURCE_LABEL}](${TS_REF_URL}) |
| 334 | + - Branch: [\`${SOURCE_BRANCH}\`](${SERVER}/${REPO}/tree/${SOURCE_BRANCH}) |
| 335 | + - Latest workflow run: ${RUN_URL} |
| 336 | +
|
| 337 | + > Note: the PR targets the \`${TARGET_BRANCH}\` branch, so merging it will **not** auto-close this issue. Please close this issue manually after the PR is merged. |
| 338 | +
|
| 339 | + ${CC_LINE}" |
| 340 | + fi |
| 341 | +
|
| 342 | + # Write the final body onto the tracking issue (whether reused or just |
| 343 | + # created) and re-apply the expected label. Assignment is best-effort: |
| 344 | + # a non-assignable actor must not fail the workflow. |
| 345 | + gh issue edit "$ISSUE_NUMBER" --body "$ISSUE_BODY" --add-label "typespec-python" |
| 346 | + gh issue edit "$ISSUE_NUMBER" --add-assignee "$ASSIGNEES" \ |
| 347 | + || echo "::warning::Could not assign issue #${ISSUE_NUMBER} to ${ASSIGNEES}" |
| 348 | + echo "::notice::Tracking issue #${ISSUE_NUMBER} updated with PR compare link" |
| 349 | +
|
| 350 | + - name: Clean up stale regen branches |
| 351 | + if: always() |
| 352 | + env: |
| 353 | + GH_TOKEN: ${{ github.token }} |
| 354 | + run: | |
| 355 | + set -euo pipefail |
| 356 | + TARGET_BRANCH="typespec-python-generated-tests" |
| 357 | + PR_NUMBER="${{ steps.typespec-info.outputs.typespec_pr_number }}" |
| 358 | + if [ -n "$PR_NUMBER" ]; then |
| 359 | + CURRENT_BRANCH="regen/typespec-python-pr-${PR_NUMBER}" |
| 360 | + else |
| 361 | + CURRENT_BRANCH="regen/typespec-python-main" |
| 362 | + fi |
| 363 | +
|
| 364 | + # Prune any regen/typespec-python-* branch whose PR (to the target |
| 365 | + # branch) is no longer open but did exist at some point (merged or |
| 366 | + # closed). Branches that never had a PR opened are left alone, since a |
| 367 | + # maintainer may still click the pre-filled compare link to open one. |
| 368 | + BRANCHES=$(git ls-remote --heads origin \ |
| 369 | + | sed 's#.*refs/heads/##' \ |
| 370 | + | grep '^regen/typespec-python-' || true) |
| 371 | + for b in $BRANCHES; do |
| 372 | + if [ "$b" = "$CURRENT_BRANCH" ]; then |
| 373 | + continue |
| 374 | + fi |
| 375 | + OPEN_COUNT=$(gh pr list --state open --head "$b" --base "$TARGET_BRANCH" \ |
| 376 | + --json number --jq 'length') |
| 377 | + ALL_COUNT=$(gh pr list --state all --head "$b" --base "$TARGET_BRANCH" \ |
| 378 | + --json number --jq 'length') |
| 379 | + if [ "$OPEN_COUNT" = "0" ] && [ "$ALL_COUNT" != "0" ]; then |
| 380 | + echo "Deleting stale branch $b (PR merged/closed, none open)" |
| 381 | + git push origin --delete "$b" \ |
| 382 | + || echo "::warning::Failed to delete branch $b" |
| 383 | + fi |
| 384 | + done |
199 | 385 |
|
200 | 386 | notify-on-failure: |
201 | 387 | name: "Notify on failure" |
|
0 commit comments