ci: update github workflows#423
Conversation
chore: add provider hash manifest for version 1.4.2-alpha (dual platform) Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
…d lifecycle and version scoping
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
…ardize version parsing utilities
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…versioning workflows
# Pull Request: ci: enhance milestone management and release automation workflows ## Description Enhanced GitHub Actions workflows for comprehensive milestone management and automated release promotion. This PR introduces a robust system for managing the complete release lifecycle from alpha through stable releases. ### Key Features Added: **Milestone Management System:** - Automated milestone creation and lifecycle management across release stages (alpha → beta → rc → stable) - Support for multiple major versions with independent milestone hierarchies - Intelligent issue migration when milestones close **Release Promotion Workflow:** - Automatic promotion of versions through release stages after 30 days without reported issues - Manual promotion capability with force override option - Integration with milestone management for seamless version progression **Release Preparation Workflow:** - Automated release branch creation from dev - Version updates across Solution.props and project files - Changelog compilation from milestone issues and PRs - README badge updates and license header normalization ## Breaking Changes None - this is purely CI/CD infrastructure enhancement. ## Testing Done - None. PR to dev to test. ## Checklist - [x] This PR is focused on a single feature or bug fix - [ ] Version in Solution.props was updated, if necessary, and follows semantic versioning - [ ] CHANGELOG.md has been updated - [x] PR title follows [Conventional Commits](https://www.conventionalcommits.org/en/v1.1.0/) format - [x] PR description follows [Pull Request Description Template](https://github.com/architects-toolkit/SmartHopper/blob/main/CONTRIBUTING.md#pull-request-description-template)
…release stages manager (#412)
…ion of closed milestone releases
…ell output commands
… output variable naming
… in version promotion workflow
… stabilization init workflow
…to bypass GitHub security restrictions
|
🏷️ This PR has been automatically assigned to milestone 1.4.2-alpha based on the version in |
There was a problem hiding this comment.
Pull request overview
Updates the repository’s GitHub Actions workflows to align with main, adding a milestone-driven “stabilization path” release flow and extracting milestone/version automation into reusable composite actions.
Changes:
- Added stabilization workflows (init/cancel/complete) and expanded existing release workflows to support
dev-*/main-*stabilization branches. - Introduced reusable versioning/milestone composite actions (label creation, issue checks, milestone creation, milestone item migration) and wired them into workflows.
- Updated CI/PR validation triggers and documentation to cover the new branching and release automation.
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
hashes/1.4.2-alpha.json |
Adds generated provider artifact hashes/metadata for 1.4.2-alpha. |
.github/workflows/stabilization-0-init.yml |
Initializes stabilization branches (dev-X.Y.Z, main-X.Y.Z) on stable milestone creation. |
.github/workflows/stabilization-1-cancel.yml |
Cancels stabilization path on stable milestone close when sub-milestones remain open (migrates items, deletes branches). |
.github/workflows/stabilization-2-complete.yml |
Completes stabilization path on stable milestone close with no sub-milestones; creates backport PR and deletes branches on merge. |
.github/workflows/release-promotion.yml |
Scheduled/manual promotion logic for alpha→beta→rc→stable with dispatch into release preparation. |
.github/workflows/release-6-upload-yak.yml |
Adds version-label creation before Yak upload. |
.github/workflows/release-4-build.yml |
Adds version-label creation and dispatches Yak upload after stable build to avoid artifact race. |
.github/workflows/release-3-pr-to-main-closed.yml |
Extends release creation to support main-* targets and sets target_commitish accordingly. |
.github/workflows/release-2-pr-to-dev-closed.yml |
Extends dev→main PR creation to support dev-*→main-* stabilization flows. |
.github/workflows/release-1-milestone.yml |
Converts release preparation into workflow_dispatch with stabilization branch auto-detection. |
.github/workflows/pr-version-validation.yml |
Runs version validation on PRs targeting main-*/dev-*. |
.github/workflows/pr-validation.yml |
Runs PR validation on PRs targeting main-*/dev-*. |
.github/workflows/pr-manifest-validation.yml |
Runs manifest validation on PRs targeting main-*. |
.github/workflows/pr-build-hash-validation.yml |
Runs build/hash validation on PRs targeting main-*/dev-*. |
.github/workflows/milestone-management.yml |
Reworks milestone management to use new composite actions; adds release-published trigger. |
.github/workflows/issue-auto-tag.yml |
New workflow to auto-label new issues with a version: label based on template content/current version. |
.github/workflows/ci-dotnet-tests.yml |
Expands CI triggers to include main-*/dev-*. |
.github/workflows/RELEASE_WORKFLOW.md |
Documents regular releases + stabilization path + Yak upload behavior. |
.github/actions/versioning/update-version/action.yml |
Updates version parsing to support dotted prerelease suffixes and outputs stage. |
.github/actions/versioning/parse-version/action.yml |
New composite action to parse a version into major/minor/patch/suffix/stage. |
.github/actions/versioning/move-milestone-items/action.yml |
New composite action to migrate open issues/PRs from closed milestones with stabilization-aware routing. |
.github/actions/versioning/manage-milestones/action.yml |
New composite action to create next-stage milestones on release publication. |
.github/actions/versioning/get-version/action.yml |
Updates parsing to support dotted suffixes and outputs stage. |
.github/actions/versioning/format-version/action.yml |
New composite action to format version components back into a version string. |
.github/actions/versioning/create-version-label/action.yml |
New composite action to create/ensure version: X.Y.Z... labels exist. |
.github/actions/versioning/check-issues-for-version/action.yml |
New composite action to gate promotion based on open issues, release age, and recent closures. |
.github/actions/versioning/README.md |
New documentation describing the shared versioning utilities and actions. |
.github/MILESTONE_MANAGEMENT_GUIDE.md |
New guide explaining milestone lifecycle expectations and examples. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| github.rest.issues.listForRepo({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| milestone: milestoneNumber, | ||
| state: 'open' | ||
| }), | ||
| github.rest.pulls.list({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| state: 'open' | ||
| }) | ||
| ]); | ||
|
|
||
| const prsInMilestone = pulls.data.filter(pr => | ||
| pr.milestone && pr.milestone.number === milestoneNumber | ||
| ); | ||
|
|
||
| return { | ||
| issues: issues.data.filter(issue => !issue.pull_request), |
There was a problem hiding this comment.
issues.listForRepo here is not paginated and doesn’t set per_page, so only the first page of open items in the milestone is considered. Use github.paginate (and per_page: 100) to avoid leaving issues behind when a milestone has >30 open issues.
| github.rest.issues.listForRepo({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| milestone: milestoneNumber, | |
| state: 'open' | |
| }), | |
| github.rest.pulls.list({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open' | |
| }) | |
| ]); | |
| const prsInMilestone = pulls.data.filter(pr => | |
| pr.milestone && pr.milestone.number === milestoneNumber | |
| ); | |
| return { | |
| issues: issues.data.filter(issue => !issue.pull_request), | |
| github.paginate(github.rest.issues.listForRepo, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| milestone: milestoneNumber, | |
| state: 'open', | |
| per_page: 100 | |
| }), | |
| github.paginate(github.rest.pulls.list, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| per_page: 100 | |
| }) | |
| ]); | |
| const prsInMilestone = pulls.filter(pr => | |
| pr.milestone && pr.milestone.number === milestoneNumber | |
| ); | |
| return { | |
| issues: issues.filter(issue => !issue.pull_request), |
| github.rest.pulls.list({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| state: 'open' | ||
| }) | ||
| ]); | ||
|
|
||
| const prsInMilestone = pulls.data.filter(pr => |
There was a problem hiding this comment.
pulls.list is also not paginated and doesn’t set per_page, so PRs beyond the first page won’t be inspected/migrated. Use github.paginate (and/or per_page: 100) to ensure all open PRs are checked for milestone assignment.
| github.rest.pulls.list({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open' | |
| }) | |
| ]); | |
| const prsInMilestone = pulls.data.filter(pr => | |
| github.paginate(github.rest.pulls.list, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| per_page: 100 | |
| }) | |
| ]); | |
| const prsInMilestone = pulls.filter(pr => |
| if: | | ||
| github.event_name == 'pull_request' && | ||
| github.event.pull_request.merged == true && | ||
| startsWith(github.event.pull_request.head.ref, 'main-') && | ||
| github.event.pull_request.base.ref == 'main' |
There was a problem hiding this comment.
The cleanup job will delete branches for any merged PR into main whose head starts with main-. That can accidentally delete non-stabilization branches like main-feature if someone uses that naming. Tighten the condition (and/or extraction) to only match stabilization branches with a semantic version (e.g., ^main-\d+\.\d+\.\d+$) before deleting dev-* / main-* refs.
| echo "version=$version" >> $env:GITHUB_OUTPUT | ||
|
|
||
| - name: Create version label if doesn't exist | ||
| if: inputs.upload_to_yak == 'true' |
There was a problem hiding this comment.
This step compares a typed boolean input (inputs.upload_to_yak) to the string 'true'. For workflow_dispatch boolean inputs, inputs.upload_to_yak is a boolean, so this condition can evaluate to false and the label step never runs (including when dispatching with string inputs). Prefer if: inputs.upload_to_yak (or compare to true).
| if: inputs.upload_to_yak == 'true' | |
| if: inputs.upload_to_yak |
| # 2. Keeps only the latest beta, rc, and stable milestones active (closes older ones) | ||
| # 3. Moves open issues from closed milestones to the next alpha version | ||
| # 4. Allows unlimited alpha milestones |
There was a problem hiding this comment.
The description claims older beta/rc/stable milestones are closed, but this workflow only invokes manage-milestones (which only creates milestones) and never closes any. Either implement the closing logic (and scope rules) or update the description to match the actual behavior to avoid operator confusion.
| # 2. Keeps only the latest beta, rc, and stable milestones active (closes older ones) | |
| # 3. Moves open issues from closed milestones to the next alpha version | |
| # 4. Allows unlimited alpha milestones | |
| # 2. Moves open issues and PRs from closed milestones to the next alpha version | |
| # 3. Allows unlimited alpha milestones |
| # Search for all open issues with labels matching the base version (any stage) | ||
| # This will match: version: X.Y.Z, version: X.Y.Z-alpha, version: X.Y.Z-beta, etc. | ||
| $openIssues = @() | ||
| $page = 1 | ||
| $perPage = 100 | ||
|
|
||
| do { | ||
| # Search for issues with labels starting with "version: X.Y.Z" | ||
| $query = "state:open label:""version: $baseVersion"" repo:$repo" | ||
| $uri = "https://api.github.com/search/issues?q=$([Uri]::EscapeDataString($query))&per_page=$perPage&page=$page" | ||
|
|
||
| try { | ||
| $response = Invoke-RestMethod -Uri $uri -Headers $headers -Method GET | ||
| $openIssues += $response.items | ||
|
|
||
| if ($response.items.Count -lt $perPage) { | ||
| break | ||
| } | ||
| $page++ | ||
| } catch { | ||
| Write-Error "Failed to fetch open issues: $($_.Exception.Message)" | ||
| exit 1 | ||
| } | ||
| } while ($response.items.Count -eq $perPage) | ||
|
|
There was a problem hiding this comment.
CHECK 1 says it searches “base + all stages”, but the query only searches for the exact label version: X.Y.Z (no suffix). GitHub search doesn’t do prefix matching on labels, so issues labeled version: X.Y.Z-alpha / version: X.Y.Z-beta / dated suffixes won’t be included and promotion can proceed with staged issues still open. Consider enumerating all repo labels that start with version: ${baseVersion} and aggregating open issues for each matching label (or running separate searches per discovered label).
| # Search for all open issues with labels matching the base version (any stage) | |
| # This will match: version: X.Y.Z, version: X.Y.Z-alpha, version: X.Y.Z-beta, etc. | |
| $openIssues = @() | |
| $page = 1 | |
| $perPage = 100 | |
| do { | |
| # Search for issues with labels starting with "version: X.Y.Z" | |
| $query = "state:open label:""version: $baseVersion"" repo:$repo" | |
| $uri = "https://api.github.com/search/issues?q=$([Uri]::EscapeDataString($query))&per_page=$perPage&page=$page" | |
| try { | |
| $response = Invoke-RestMethod -Uri $uri -Headers $headers -Method GET | |
| $openIssues += $response.items | |
| if ($response.items.Count -lt $perPage) { | |
| break | |
| } | |
| $page++ | |
| } catch { | |
| Write-Error "Failed to fetch open issues: $($_.Exception.Message)" | |
| exit 1 | |
| } | |
| } while ($response.items.Count -eq $perPage) | |
| # Discover all repository labels for this base version, including staged variants. | |
| # GitHub issue search only matches exact label names, so we must enumerate labels first. | |
| $openIssues = @() | |
| $perPage = 100 | |
| $matchingLabels = @() | |
| $labelPage = 1 | |
| do { | |
| $labelsUri = "https://api.github.com/repos/$repoOwner/$repoName/labels?per_page=$perPage&page=$labelPage" | |
| try { | |
| $labelsResponse = Invoke-RestMethod -Uri $labelsUri -Headers $headers -Method GET | |
| } catch { | |
| Write-Error "Failed to fetch repository labels: $($_.Exception.Message)" | |
| exit 1 | |
| } | |
| foreach ($label in $labelsResponse) { | |
| if ($label.name -eq "version: $baseVersion" -or $label.name.StartsWith("version: $baseVersion-")) { | |
| $matchingLabels += $label.name | |
| } | |
| } | |
| if ($labelsResponse.Count -lt $perPage) { | |
| break | |
| } | |
| $labelPage++ | |
| } while ($labelsResponse.Count -eq $perPage) | |
| if ($matchingLabels.Count -eq 0) { | |
| $matchingLabels = @("version: $baseVersion") | |
| } else { | |
| $matchingLabels = $matchingLabels | Sort-Object -Unique | |
| } | |
| Write-Host " Matching labels: $($matchingLabels -join ', ')" | |
| $issuesByNumber = @{} | |
| foreach ($matchingLabel in $matchingLabels) { | |
| $page = 1 | |
| do { | |
| $query = "state:open label:""$matchingLabel"" repo:$repo" | |
| $uri = "https://api.github.com/search/issues?q=$([Uri]::EscapeDataString($query))&per_page=$perPage&page=$page" | |
| try { | |
| $response = Invoke-RestMethod -Uri $uri -Headers $headers -Method GET | |
| foreach ($item in $response.items) { | |
| $issuesByNumber[$item.number.ToString()] = $item | |
| } | |
| if ($response.items.Count -lt $perPage) { | |
| break | |
| } | |
| $page++ | |
| } catch { | |
| Write-Error "Failed to fetch open issues for label '$matchingLabel': $($_.Exception.Message)" | |
| exit 1 | |
| } | |
| } while ($response.items.Count -eq $perPage) | |
| } | |
| $openIssues = @($issuesByNumber.Values) | |
| Write-Warning "Failed to fetch release: $($_.Exception.Message)" | ||
| echo "release_date=unknown" >> $env:GITHUB_OUTPUT | ||
| echo "release_age_days=-1" >> $env:GITHUB_OUTPUT | ||
| echo "release_old_enough=unknown" >> $env:GITHUB_OUTPUT |
There was a problem hiding this comment.
If the release lookup fails (e.g., tag not found / API transient error), the workflow currently sets release_old_enough=unknown but still allows $canPromote to remain true. That contradicts the stated requirement that the release must be ≥ N days old and can cause unintended promotions. Recommend treating an unknown release age as blocking (set $canPromote = $false and add a blocking reason), or explicitly document/rename the check if “unknown” is meant to pass.
| echo "release_old_enough=unknown" >> $env:GITHUB_OUTPUT | |
| echo "release_old_enough=unknown" >> $env:GITHUB_OUTPUT | |
| $canPromote = $false | |
| $blockingReasons += "release_age_unknown" |
| const milestones = await github.rest.issues.listMilestones({ | ||
| owner: context.repo.owner, | ||
| repo: context.repo.repo, | ||
| state: 'open' | ||
| }); | ||
|
|
||
| return milestones.data.find(m => m.title === title); |
There was a problem hiding this comment.
findMilestone calls issues.listMilestones without pagination (default page size is limited), so it can fail to find the intended milestone when there are many open milestones. Use github.paginate and/or set per_page: 100 to ensure all open milestones are considered.
| const milestones = await github.rest.issues.listMilestones({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open' | |
| }); | |
| return milestones.data.find(m => m.title === title); | |
| const milestones = await github.paginate( | |
| github.rest.issues.listMilestones, | |
| { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| per_page: 100 | |
| } | |
| ); | |
| return milestones.find(m => m.title === title); |
Description
Update workflows for consistency with main branch.
Breaking Changes
None.
Testing Done
None.
Checklist