Add extension release workflow with AI-generated changelog#15766
Add extension release workflow with AI-generated changelog#15766
Conversation
…kflow - Remove invalid 'models: read' permission - Fix multi-line strings that broke YAML block scalars (column-0 content) - Build context and PR body via temp files instead of heredocs - Use printf instead of echo -e to avoid backslash mangling - Build fallback notes with $'\n' instead of literal \n - Handle branch collision for idempotent re-runs - Remove non-existent label from PR creation - Use --body-file instead of --body for PR creation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 15766Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 15766" |
…ion in run blocks
There was a problem hiding this comment.
Pull request overview
Adds a manually triggered GitHub Actions workflow to automate preparing VS Code extension releases, including version bumping, changelog generation, and opening a draft PR.
Changes:
- Introduces
.github/workflows/extension-release.ymlwithworkflow_dispatchinputs for version and commit range. - Generates AI-based release notes (with a commit-log fallback), then updates
extension/package.jsonand creates/prependsextension/CHANGELOG.md. - Creates a release branch, commits the changes, pushes the branch, and opens a draft PR.
…dempotent PR creation - Default empty to_sha to origin/main instead of HEAD - Update remote URL with app token before git push - Use git checkout -B and --force-with-lease for idempotent branch handling - Check for existing PR and use gh pr edit if found
|
🎬 CLI E2E Test Recordings — 53 recordings uploaded (commit View recordings
📹 Recordings uploaded automatically from CI run #23863067781 |
joperezr
left a comment
There was a problem hiding this comment.
Review feedback — mostly around hardening shell scripts, concurrency safety, and API error handling. Nothing blocking the overall design, which looks solid. See inline comments.
| env: | ||
| INPUT_FROM_SHA: ${{ inputs.from_sha }} | ||
| INPUT_TO_SHA: ${{ inputs.to_sha }} | ||
| run: | |
There was a problem hiding this comment.
All run: blocks in this workflow lack set -eo pipefail. Other workflows in this repo (e.g., update-github-models.yml) use set -e. Without it, intermediate command failures in multi-command blocks are silently ignored and the workflow continues with stale/incorrect data. Consider adding set -eo pipefail at the top of each major run: block.
| {"role": "system", "content": $system}, | ||
| {"role": "user", "content": $user} | ||
| ], | ||
| "model": "openai/gpt-4o-mini", |
There was a problem hiding this comment.
The curl call to the GitHub Models API has no timeout. If the API hangs or is unresponsive, this step blocks indefinitely, consuming a runner. Consider adding --max-time 60 --retry 2:
RESPONSE=$(curl -s --max-time 60 --retry 2 -X POST \| DIFF_CONTENT=$(git diff "${FROM_SHA}..${TO_SHA}" -- extension/src/ extension/package.json extension/package.nls.json 2>/dev/null | head -3000) | ||
|
|
||
| # Write to files for the AI step | ||
| echo "$COMMITS" > /tmp/commits.txt |
There was a problem hiding this comment.
The workflow writes intermediate files to fixed paths in /tmp (/tmp/commits.txt, /tmp/diffstat.txt, etc.). If two workflow runs execute concurrently (even for different versions), they'll overwrite each other's files. Use $RUNNER_TEMP (provided by GitHub Actions) with a run-unique subdirectory instead:
WORK_DIR="${RUNNER_TEMP}/extension-release"
mkdir -p "$WORK_DIR"
echo "$COMMITS" > "$WORK_DIR/commits.txt"(This applies to all /tmp references throughout the workflow.)
| echo "Calling GitHub Models API for release notes generation..." | ||
|
|
||
| RESPONSE=$(curl -s -X POST \ | ||
| "https://models.github.ai/inference/chat/completions" \ |
There was a problem hiding this comment.
The curl call doesn't capture the HTTP status code. If the GitHub Models API returns a 4xx/5xx or an HTML error page, jq will silently fail or return empty, triggering the fallback without a clear diagnostic. Consider capturing the HTTP code for better observability:
HTTP_CODE=$(curl -s -o "$WORK_DIR/ai_response.json" -w "%{http_code}" --max-time 60 --retry 2 -X POST \
"https://models.github.ai/inference/chat/completions" \
-H "Authorization: Bearer $GITHUB_TOKEN" \
-H "Content-Type: application/json" \
-d "$REQUEST_BODY")
if [ "$HTTP_CODE" != "200" ]; then
echo "⚠️ GitHub Models API returned HTTP $HTTP_CODE"
fi
AI_NOTES=$(jq -r '.choices[0].message.content // empty' "$WORK_DIR/ai_response.json")| contents: write | ||
| pull-requests: write | ||
|
|
||
| jobs: |
There was a problem hiding this comment.
Unlike release-github-tasks.yml which uses concurrency: { group: release-github-tasks }, this workflow has no concurrency control. Simultaneous manual dispatches could race on git operations and temp files. Consider adding:
concurrency:
group: extension-release
cancel-in-progress: false| DIFFSTAT=$(git diff --stat "${FROM_SHA}..${TO_SHA}" -- extension/ 2>/dev/null || echo "") | ||
|
|
||
| # Get the actual diff (truncated for AI context) | ||
| DIFF_CONTENT=$(git diff "${FROM_SHA}..${TO_SHA}" -- extension/src/ extension/package.json extension/package.nls.json 2>/dev/null | head -3000) |
There was a problem hiding this comment.
Truncating the diff at 3000 lines is arbitrary and could cut in the middle of a diff hunk, feeding incomplete context to the AI. A byte-based limit (e.g., head -c 200000) would be more predictable and less likely to break mid-change. Alternatively, it would be good to document why 3000 lines was chosen so future maintainers can adjust the threshold.
| env: | ||
| NEW_VERSION: ${{ inputs.release_version }} | ||
| run: | | ||
| CHANGELOG="extension/CHANGELOG.md" |
There was a problem hiding this comment.
The prepend logic assumes the first line of CHANGELOG.md is a title/header (# ...). If the file has been manually edited, is empty, or starts with a blank line, the head -1 / tail -n +2 split will produce malformed output. A defensive check (e.g., verifying the first line starts with #) would improve robustness:
HEADER=$(head -1 "$CHANGELOG")
if [[ "$HEADER" != \#* ]]; then
echo "⚠️ CHANGELOG.md doesn't start with a header. Prepending entry at top."
{ echo "$ENTRY"; echo ""; cat "$CHANGELOG"; } > "${CHANGELOG}.tmp"
else
{ echo "$HEADER"; echo ""; echo "$ENTRY"; echo ""; tail -n +2 "$CHANGELOG"; } > "${CHANGELOG}.tmp"
fi| -H "Content-Type: application/json" \ | ||
| -d "$REQUEST_BODY") | ||
|
|
||
| # Extract the generated text |
There was a problem hiding this comment.
When AI generation fails, the fallback uses raw commit messages under a generic ### Changes header, but neither the changelog entry nor the PR body indicate that AI notes were not used. A reviewer could assume the changelog is AI-curated when it's actually just a raw commit dump. Consider:
- Changing the fallback header to
### Changes (auto-generated from commits)so the CHANGELOG clearly reflects this. - Adding a note in the PR body when the fallback was triggered (e.g., via an output variable or marker file checked in the PR creation step).
| echo "DIFF STATISTICS:" | ||
| cat /tmp/diffstat.txt | ||
| echo "" | ||
| echo "CODE DIFF (truncated):" |
There was a problem hiding this comment.
nit: The two-step escaping pattern (jq -Rs . → --argjson) is harder to audit than the simpler --arg which does JSON-escaping automatically in one step:
REQUEST_BODY=$(jq -n \
--arg system "$PROMPT" \
--arg user "$(cat /tmp/context.txt)" \
'{ "messages": [ ... ] }')| prepare-release: | ||
| name: Prepare Extension Release | ||
| runs-on: ubuntu-latest | ||
| if: ${{ github.repository_owner == 'microsoft' }} |
There was a problem hiding this comment.
The if: github.repository_owner == 'microsoft' guard prevents fork execution, but workflow_dispatch can be triggered by anyone with write access to the repository — not just maintainers or admins. Since this workflow uses a GitHub App token to push branches/create PRs, consider restricting who can trigger it.
Following the same pattern as release-github-tasks.yml, add an early authorization step that checks the triggering user's permission level via the GitHub API:
- name: Check if user is authorized
env:
GH_TOKEN: ${{ github.token }}
run: |
set -eo pipefail
echo "Checking if ${{ github.actor }} is authorized to run releases..."
PERMISSION=$(gh api repos/${{ github.repository }}/collaborators/${{ github.actor }}/permission --jq '.permission')
echo "User permission level: $PERMISSION"
if [[ "$PERMISSION" != "admin" && "$PERMISSION" != "maintain" ]]; then
echo "❌ ERROR: User ${{ github.actor }} does not have sufficient permissions."
echo "Required: 'admin' or 'maintain' permission level."
echo "Current: '$PERMISSION'"
exit 1
fi
echo "✓ User ${{ github.actor }} is authorized (permission: $PERMISSION)"This should be added as an early step (before checkout or any other work), matching the existing convention.
Description
Add a new GitHub Actions workflow (
extension-release.yml) that automates VS Code extension release preparation.What it does
When triggered manually via
workflow_dispatch, the workflow:extension/package.jsonextension/between the specified SHA rangepackage.json: Bumps the version fieldCHANGELOG.md: Prepends a new versioned entry toextension/CHANGELOG.mdWorkflow inputs
release_version1.0.8)from_shato_shaChecklist