Skip to content

Add extension release workflow with AI-generated changelog#15766

Draft
adamint wants to merge 5 commits intomainfrom
adamratzman/extension-release-workflow
Draft

Add extension release workflow with AI-generated changelog#15766
adamint wants to merge 5 commits intomainfrom
adamratzman/extension-release-workflow

Conversation

@adamint
Copy link
Copy Markdown
Member

@adamint adamint commented Apr 1, 2026

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:

  1. Validates inputs: Checks the release version format and ensures it's greater than the current version in extension/package.json
  2. Generates a diff: Collects commit log, diff stats, and code diff for extension/ between the specified SHA range
  3. AI-generated release notes: Calls the GitHub Models API (gpt-4o-mini) to generate user-facing, categorized release notes from the diff. Falls back to a commit-log-based changelog if AI generation fails.
  4. Updates package.json: Bumps the version field
  5. Creates/updates CHANGELOG.md: Prepends a new versioned entry to extension/CHANGELOG.md
  6. Opens a draft PR: Pushes a branch and creates a draft PR with the AI-generated release notes in the body

Workflow inputs

Input Required Description
release_version Yes New semver version (e.g., 1.0.8)
from_sha Yes Start commit SHA (changes after this) — short or full
to_sha No End commit SHA (defaults to HEAD) — short or full

Checklist

  • Is this feature complete?
    • Yes. Ready to ship.
    • No. Follow-up changes expected.
  • Are you including unit tests for the changes and scenario tests if relevant?
    • Yes
    • No
  • Did you add public API?
    • Yes
    • No
  • Does the change make any security assumptions or guarantees?
    • Yes
    • No
  • Does the change require an update in our Aspire docs?
    • Yes
    • No

@adamint adamint added the area-engineering-systems infrastructure helix infra engineering repo stuff label Apr 1, 2026
@adamint adamint requested review from joperezr and radical April 1, 2026 17:48
@adamint adamint marked this pull request as ready for review April 1, 2026 17:48
Copilot AI review requested due to automatic review settings April 1, 2026 17:48
…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
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

🚀 Dogfood this PR with:

⚠️ WARNING: Do not do this without first carefully reviewing the code of this PR to satisfy yourself it is safe.

curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 15766

Or

  • Run remotely in PowerShell:
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 15766"

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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.yml with workflow_dispatch inputs for version and commit range.
  • Generates AI-based release notes (with a commit-log fallback), then updates extension/package.json and creates/prepends extension/CHANGELOG.md.
  • Creates a release branch, commits the changes, pushes the branch, and opens a draft PR.

Comment thread .github/workflows/extension-release.yml
Comment thread .github/workflows/extension-release.yml Outdated
Comment thread .github/workflows/extension-release.yml
Comment thread .github/workflows/extension-release.yml
Comment thread .github/workflows/extension-release.yml
Comment thread .github/workflows/extension-release.yml
…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
@adamint adamint marked this pull request as draft April 1, 2026 18:19
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 1, 2026

🎬 CLI E2E Test Recordings — 53 recordings uploaded (commit 63f6b44)

View recordings
Test Recording
AddPackageInteractiveWhileAppHostRunningDetached ▶️ View Recording
AddPackageWhileAppHostRunningDetached ▶️ View Recording
AgentCommands_AllHelpOutputs_AreCorrect ▶️ View Recording
AgentInitCommand_DefaultSelection_InstallsSkillOnly ▶️ View Recording
AgentInitCommand_MigratesDeprecatedConfig ▶️ View Recording
AspireAddPackageVersionToDirectoryPackagesProps ▶️ View Recording
AspireUpdateRemovesAppHostPackageVersionFromDirectoryPackagesProps ▶️ View Recording
Banner_DisplayedOnFirstRun ▶️ View Recording
Banner_DisplayedWithExplicitFlag ▶️ View Recording
Banner_NotDisplayedWithNoLogoFlag ▶️ View Recording
CertificatesClean_RemovesCertificates ▶️ View Recording
CertificatesTrust_WithNoCert_CreatesAndTrustsCertificate ▶️ View Recording
CertificatesTrust_WithUntrustedCert_TrustsCertificate ▶️ View Recording
ConfigSetGet_CreatesNestedJsonFormat ▶️ View Recording
CreateAndRunAspireStarterProject ▶️ View Recording
CreateAndRunAspireStarterProjectWithBundle ▶️ View Recording
CreateAndRunEmptyAppHostProject ▶️ View Recording
CreateAndRunJavaEmptyAppHostProject ▶️ View Recording
CreateAndRunJsReactProject ▶️ View Recording
CreateAndRunPythonReactProject ▶️ View Recording
CreateAndRunTypeScriptEmptyAppHostProject ▶️ View Recording
CreateAndRunTypeScriptStarterProject ▶️ View Recording
CreateJavaAppHostWithViteApp ▶️ View Recording
CreateStartAndStopAspireProject ▶️ View Recording
CreateTypeScriptAppHostWithViteApp ▶️ View Recording
DescribeCommandResolvesReplicaNames ▶️ View Recording
DescribeCommandShowsRunningResources ▶️ View Recording
DetachFormatJsonProducesValidJson ▶️ View Recording
DoctorCommand_DetectsDeprecatedAgentConfig ▶️ View Recording
DoctorCommand_WithSslCertDir_ShowsTrusted ▶️ View Recording
DoctorCommand_WithoutSslCertDir_ShowsPartiallyTrusted ▶️ View Recording
GlobalMigration_HandlesCommentsAndTrailingCommas ▶️ View Recording
GlobalMigration_HandlesMalformedLegacyJson ▶️ View Recording
GlobalMigration_PreservesAllValueTypes ▶️ View Recording
GlobalMigration_SkipsWhenNewConfigExists ▶️ View Recording
GlobalSettings_MigratedFromLegacyFormat ▶️ View Recording
InvalidAppHostPathWithComments_IsHealedOnRun ▶️ View Recording
LogsCommandShowsResourceLogs ▶️ View Recording
PsCommandListsRunningAppHost ▶️ View Recording
PsFormatJsonOutputsOnlyJsonToStdout ▶️ View Recording
PublishWithDockerComposeServiceCallbackSucceeds ▶️ View Recording
RestoreGeneratesSdkFiles ▶️ View Recording
RestoreSupportsConfigOnlyHelperPackageAndCrossPackageTypes ▶️ View Recording
RunWithMissingAwaitShowsHelpfulError ▶️ View Recording
SecretCrudOnDotNetAppHost ▶️ View Recording
SecretCrudOnTypeScriptAppHost ▶️ View Recording
StagingChannel_ConfigureAndVerifySettings_ThenSwitchChannels ▶️ View Recording
StopAllAppHostsFromAppHostDirectory ▶️ View Recording
StopAllAppHostsFromUnrelatedDirectory ▶️ View Recording
StopNonInteractiveMultipleAppHostsShowsError ▶️ View Recording
StopNonInteractiveSingleAppHost ▶️ View Recording
StopWithNoRunningAppHostExitsSuccessfully ▶️ View Recording
TypeScriptAppHostWithProjectReferenceIntegration ▶️ View Recording

📹 Recordings uploaded automatically from CI run #23863067781

Copy link
Copy Markdown
Member

@joperezr joperezr left a comment

Choose a reason for hiding this comment

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

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: |
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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" \
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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:

  1. Changing the fallback header to ### Changes (auto-generated from commits) so the CHANGELOG clearly reflects this.
  2. 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):"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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' }}
Copy link
Copy Markdown
Member

@joperezr joperezr Apr 3, 2026

Choose a reason for hiding this comment

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

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-engineering-systems infrastructure helix infra engineering repo stuff

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants