Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 171 additions & 20 deletions .github/actions/versioning/check-issues-for-version/action.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: 'Check Issues for Version'
description: 'Check if any open issues exist with a specific version label within a time period'
description: 'Check promotion eligibility: milestone status, release age, and open issues with version label (all stages)'
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

The action description claims it checks “milestone status”, but the implementation no longer checks milestones (and no milestone-related outputs are produced). Please update the description to match the current behavior or reintroduce milestone checking/outputs if they’re still required.

Copilot uses AI. Check for mistakes.
inputs:
version:
description: 'Version to check for issues (e.g., 1.4.2-alpha)'
Expand All @@ -23,6 +23,30 @@ outputs:
issues-list:
description: 'Comma-separated list of issue numbers'
value: ${{ steps.check-issues.outputs.issues_list }}
release-date:
description: 'Release published date (ISO 8601)'
value: ${{ steps.check-issues.outputs.release_date }}
release-age-days:
description: 'Number of days since release was published'
value: ${{ steps.check-issues.outputs.release_age_days }}
release-old-enough:
description: 'Whether release is at least 30 days old'
value: ${{ steps.check-issues.outputs.release_old_enough }}
last-closed-issue-date:
description: 'Date of last closed issue with this version label (ISO 8601)'
value: ${{ steps.check-issues.outputs.last_closed_issue_date }}
last-closed-age-days:
description: 'Number of days since last issue was closed'
value: ${{ steps.check-issues.outputs.last_closed_age_days }}
last-closed-old-enough:
description: 'Whether last closed issue is at least 30 days old'
value: ${{ steps.check-issues.outputs.last_closed_old_enough }}
can-promote:
description: 'Whether all conditions are met for promotion (milestone clear, release old enough, no recent closed issues)'
value: ${{ steps.check-issues.outputs.can_promote }}
blocking-reason:
description: 'Reason why promotion is blocked (if can-promote is false)'
value: ${{ steps.check-issues.outputs.blocking_reason }}

runs:
using: "composite"
Expand All @@ -38,53 +62,180 @@ runs:
$daysLookback = [int]"${{ inputs.days-lookback }}"
$repo = "${{ github.repository }}"

Write-Host "Checking for issues with label: $labelName"
Write-Host "Looking back $daysLookback days"
Write-Host "=== Promotion Eligibility Check for $version ==="
Write-Host ""

$headers = @{
"Authorization" = "Bearer $env:GITHUB_TOKEN"
"Accept" = "application/vnd.github.v3+json"
}

# Calculate cutoff date
$cutoffDate = (Get-Date).AddDays(-$daysLookback).ToString("yyyy-MM-dd")
$repoOwner = $repo.Split('/')[0]
$repoName = $repo.Split('/')[1]
$canPromote = $true
$blockingReasons = @()

# Use Search API with created date filter for accurate "created since" semantics
# This is more accurate than the REST API's 'since' parameter which filters by updated_at
$issues = @()
# ============================================================
# CHECK 1: Open issues with version label (any stage)
# ============================================================
Write-Host "CHECK 1: Looking for ALL open issues with version label (base + all stages)"

# Parse version to get base version (X.Y.Z without stage suffix)
if ($version -match '^(\d+\.\d+\.\d+)') {
$baseVersion = $matches[1]
Write-Host " Base version: $baseVersion"
} else {
Write-Host " ⚠️ Could not parse base version from '$version'"
$baseVersion = $version
}

# 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 API query: state:open label:"version: X.Y.Z" created:>=YYYY-MM-DD
$query = "state:open label:""$labelName"" created:>=$cutoffDate repo:$repo"
# 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"
Comment on lines +92 to 101
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

The open-issue search query uses label:"version: $baseVersion", but GitHub search label matching is exact—this will not match stage labels like version: 1.4.3-alpha / version: 1.4.3-beta as the comment claims. If you want “all stages”, you’ll need to explicitly query each expected label (e.g., base + alpha/beta/rc) or first discover matching labels via the Labels API and then query issues for each label.

Copilot uses AI. Check for mistakes.

try {
$response = Invoke-RestMethod -Uri $uri -Headers $headers -Method GET
$issues += $response.items
$openIssues += $response.items

if ($response.items.Count -lt $perPage) {
break
}
$page++
} catch {
Write-Error "Failed to fetch issues: $($_.Exception.Message)"
Write-Error "Failed to fetch open issues: $($_.Exception.Message)"
exit 1
}
} while ($response.items.Count -eq $perPage)

$issueCount = $issues.Count
$issueNumbers = ($issues | ForEach-Object { $_.number }) -join ","
$openIssueCount = $openIssues.Count
$openIssueNumbers = ($openIssues | ForEach-Object { $_.number }) -join ","

Write-Host "Found $issueCount open issues with label '$labelName'"

if ($issueCount -gt 0) {
Write-Host "Issues: $issueNumbers"
if ($openIssueCount -gt 0) {
Write-Host " ❌ Found $openIssueCount open issue(s): $openIssueNumbers"
Write-Host " (Issues discovered in version $baseVersion line, any stage)"
echo "has_issues=true" >> $env:GITHUB_OUTPUT
$canPromote = $false
$blockingReasons += "version_label_has_open_issues"
} else {
Write-Host " ✅ No open issues with version label (checked all stages)"
echo "has_issues=false" >> $env:GITHUB_OUTPUT
}

echo "issue_count=$issueCount" >> $env:GITHUB_OUTPUT
echo "issues_list=$issueNumbers" >> $env:GITHUB_OUTPUT
echo "issue_count=$openIssueCount" >> $env:GITHUB_OUTPUT
echo "issues_list=$openIssueNumbers" >> $env:GITHUB_OUTPUT
Write-Host ""

# (CHECK 2 removed: target milestone check was redundant with CHECK 1.
# Bugs from alpha labeled 'version: 1.4.3-alpha' are caught by CHECK 1.
# Checking the beta milestone also incorrectly blocks on planned features.)

# ============================================================
# CHECK 3: Release age (must be >= 30 days old)
# ============================================================
Write-Host "CHECK 3: Checking release age"

try {
# Get release by tag
$releaseUri = "https://api.github.com/repos/$repo/releases/tags/$version"
$release = Invoke-RestMethod -Uri $releaseUri -Headers $headers -Method GET

$publishedAt = [DateTime]::Parse($release.published_at)
$now = Get-Date
$ageDays = ($now - $publishedAt).Days
$releaseOldEnough = $ageDays -ge $daysLookback

Write-Host " Release published: $($publishedAt.ToString('yyyy-MM-dd'))"
Write-Host " Age: $ageDays days"

if ($releaseOldEnough) {
Write-Host " ✅ Release is old enough (>= $daysLookback days)"
echo "release_old_enough=true" >> $env:GITHUB_OUTPUT
} else {
Write-Host " ❌ Release is too recent (< $daysLookback days)"
echo "release_old_enough=false" >> $env:GITHUB_OUTPUT
$canPromote = $false
$blockingReasons += "release_too_recent"
}

echo "release_date=$($publishedAt.ToString('yyyy-MM-ddTHH:mm:ssZ'))" >> $env:GITHUB_OUTPUT
echo "release_age_days=$ageDays" >> $env:GITHUB_OUTPUT
} catch {
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
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

On release-fetch failure, the code sets release_old_enough=unknown but does not set $canPromote = $false or add a blocking reason. This can incorrectly allow promotion when the release tag doesn’t exist or the API call fails. Consider treating this as a blocking condition (e.g., release_check_failed) and setting $canPromote = $false in the catch.

Suggested change
echo "release_old_enough=unknown" >> $env:GITHUB_OUTPUT
echo "release_old_enough=unknown" >> $env:GITHUB_OUTPUT
$canPromote = $false
$blockingReasons += "release_check_failed"

Copilot uses AI. Check for mistakes.
}
Write-Host ""

# ============================================================
# CHECK 4: Last closed issue age (must be >= 30 days old)
# ============================================================
Write-Host "CHECK 4: Checking last closed issue with label '$labelName'"

try {
# Search for closed issues with version label, sorted by closed date (most recent first)
$query = "state:closed label:""$labelName"" repo:$repo"
$closedIssuesUri = "https://api.github.com/search/issues?q=$([Uri]::EscapeDataString($query))&sort=updated&order=desc&per_page=1"
$closedResponse = Invoke-RestMethod -Uri $closedIssuesUri -Headers $headers -Method GET

if ($closedResponse.total_count -gt 0 -and $closedResponse.items.Count -gt 0) {
$lastClosedIssue = $closedResponse.items[0]
Comment on lines +183 to +189
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

The comment says the search is “sorted by closed date”, but the query uses sort=updated. An older closed issue that was edited/commented recently can appear first, causing an incorrect last_closed_age_days. To make this accurate, fetch a larger set of closed issues and compute the max closed_at yourself (or use a different endpoint/strategy that avoids relying on updated).

Suggested change
# Search for closed issues with version label, sorted by closed date (most recent first)
$query = "state:closed label:""$labelName"" repo:$repo"
$closedIssuesUri = "https://api.github.com/search/issues?q=$([Uri]::EscapeDataString($query))&sort=updated&order=desc&per_page=1"
$closedResponse = Invoke-RestMethod -Uri $closedIssuesUri -Headers $headers -Method GET
if ($closedResponse.total_count -gt 0 -and $closedResponse.items.Count -gt 0) {
$lastClosedIssue = $closedResponse.items[0]
# Fetch recent closed issues with version label and compute most recent closed date
$query = "state:closed label:""$labelName"" repo:$repo"
$closedIssuesUri = "https://api.github.com/search/issues?q=$([Uri]::EscapeDataString($query))&sort=updated&order=desc&per_page=100"
$closedResponse = Invoke-RestMethod -Uri $closedIssuesUri -Headers $headers -Method GET
if ($closedResponse.total_count -gt 0 -and $closedResponse.items.Count -gt 0) {
$lastClosedIssue = $closedResponse.items | Sort-Object {[DateTime]::Parse($_.closed_at)} -Descending | Select-Object -First 1

Copilot uses AI. Check for mistakes.
$closedAt = [DateTime]::Parse($lastClosedIssue.closed_at)
$now = Get-Date
$closedAgeDays = ($now - $closedAt).Days
$closedOldEnough = $closedAgeDays -ge $daysLookback

Write-Host " Last closed issue: #$($lastClosedIssue.number)"
Write-Host " Closed: $($closedAt.ToString('yyyy-MM-dd'))"
Write-Host " Age: $closedAgeDays days"

if ($closedOldEnough) {
Write-Host " ✅ Last closed issue is old enough (>= $daysLookback days)"
echo "last_closed_old_enough=true" >> $env:GITHUB_OUTPUT
} else {
Write-Host " ❌ Last closed issue is too recent (< $daysLookback days)"
echo "last_closed_old_enough=false" >> $env:GITHUB_OUTPUT
$canPromote = $false
$blockingReasons += "last_closed_too_recent"
}

echo "last_closed_issue_date=$($closedAt.ToString('yyyy-MM-ddTHH:mm:ssZ'))" >> $env:GITHUB_OUTPUT
echo "last_closed_age_days=$closedAgeDays" >> $env:GITHUB_OUTPUT
} else {
Write-Host " ✅ No closed issues found with version label"
echo "last_closed_issue_date=none" >> $env:GITHUB_OUTPUT
echo "last_closed_age_days=-1" >> $env:GITHUB_OUTPUT
echo "last_closed_old_enough=true" >> $env:GITHUB_OUTPUT
}
} catch {
Write-Warning "Failed to check closed issues: $($_.Exception.Message)"
echo "last_closed_issue_date=unknown" >> $env:GITHUB_OUTPUT
echo "last_closed_age_days=-1" >> $env:GITHUB_OUTPUT
echo "last_closed_old_enough=unknown" >> $env:GITHUB_OUTPUT
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

Similarly, if the closed-issues check fails, the action sets last_closed_old_enough=unknown but doesn’t block promotion. That can allow promotion even when eligibility couldn’t be validated. Recommend setting $canPromote = $false and adding a blocking reason (e.g., closed_issue_check_failed) in the catch.

Suggested change
echo "last_closed_old_enough=unknown" >> $env:GITHUB_OUTPUT
echo "last_closed_old_enough=unknown" >> $env:GITHUB_OUTPUT
# Block promotion if closed-issue eligibility could not be validated
if (-not $blockingReasons) {
$blockingReasons = @()
}
$reason = "closed_issue_check_failed"
$blockingReasons += $reason
$canPromote = $false

Copilot uses AI. Check for mistakes.
}
Write-Host ""

# ============================================================
# FINAL DECISION
# ============================================================
Write-Host "=== PROMOTION DECISION ==="

if ($canPromote) {
Write-Host "✅ ALL CONDITIONS MET - Can promote $version"
echo "can_promote=true" >> $env:GITHUB_OUTPUT
echo "blocking_reason=none" >> $env:GITHUB_OUTPUT
} else {
$reasonsText = $blockingReasons -join ", "
Write-Host "❌ BLOCKED - Cannot promote $version"
Write-Host " Reasons: $reasonsText"
echo "can_promote=false" >> $env:GITHUB_OUTPUT
echo "blocking_reason=$reasonsText" >> $env:GITHUB_OUTPUT
}
Write-Host ""
25 changes: 20 additions & 5 deletions .github/workflows/RELEASE_WORKFLOW.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ Triggered manually via `release-1-milestone.yml` when a milestone is ready to re

## Promotion Release Flow

Automatic stage progression after 30 days with no issues:
Automatic stage progression when ALL conditions are met:

1. Daily cron job checks all prerelease versions (alpha/beta/rc)
2. If no open issues for 30 days → promotes to next stage
3. Creates promotion PR (e.g., `1.4.3-alpha` → `1.4.3-beta`)
2. **Promotion requires ALL three conditions**:
- ✅ **No open issues with version label** (any stage of base version, e.g., `version: 1.4.3-alpha`)
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

The doc states promotion blocks on “any open issue labeled version: 1.4.3 or version: 1.4.3-alpha (any stage)”, but the current action implementation only searches for the exact base label version: X.Y.Z (it won’t match version: X.Y.Z-alpha/beta/rc). Either adjust the doc to the actual behavior or update the action to truly check all stages.

Suggested change
-**No open issues with version label** (any stage of base version, e.g., `version: 1.4.3-alpha`)
-**No open issues with base version label** (e.g., `version: 1.4.3`; stage-specific labels like `version: 1.4.3-alpha/beta/rc` are not checked)

Copilot uses AI. Check for mistakes.
- ✅ **Original release published at least 30 days ago**
- ✅ **Last closed issue with original version label at least 30 days ago**
3. If all conditions met → creates promotion PR (e.g., `1.4.3-alpha` → `1.4.3-beta`)
4. On merge, release-1-milestone can be triggered for the new version

## When to Use Regular Releases
Expand Down Expand Up @@ -197,11 +200,17 @@ All PRs (release → dev, dev → main) run:

### Promotion Release Example

**Scenario**: Version `1.4.3-alpha` has been stable for 30 days with no issues.
**Scenario**: Version `1.4.3-alpha` meets all promotion criteria.

**Validation Checks (checking `1.4.3-alpha` for promotion to `1.4.3-beta`):**

1. ✅ **Version label issues**: No open issues labeled `version: 1.4.3` or `version: 1.4.3-alpha`
2. ✅ **Release age**: `1.4.3-alpha` published 35 days ago (≥30 days required)
3. ✅ **Last closed issue**: Last issue labeled `version: 1.4.3-alpha` closed 32 days ago (≥30 days required)

**Process:**

1. **Daily cron** checks `1.4.3-alpha` milestone - no open issues
1. **Daily cron** validates `1.4.3-alpha` against all three conditions
2. **release-promotion.yml** creates PR: `release/1.4.3-beta` → `dev`
3. Review and merge PR to `dev`
4. **Workflow 2** creates PR from `dev` to `main`
Expand All @@ -215,6 +224,12 @@ All PRs (release → dev, dev → main) run:
- Closes older `1.x.x-beta` milestones
10. Run **Workflow 5** to upload to Yak

**Blocking Scenarios** (promotion will NOT happen):

- ❌ Any open issue labeled `version: 1.4.3` or `version: 1.4.3-alpha` (bugs still unresolved)
- ❌ `1.4.3-alpha` release published < 30 days ago
- ❌ Last issue with `version: 1.4.3-alpha` label closed < 30 days ago

### Regular Release Example

**Goal:** Release version `1.2.0` with new AI features.
Expand Down
73 changes: 48 additions & 25 deletions .github/workflows/release-promotion.yml
Original file line number Diff line number Diff line change
Expand Up @@ -152,31 +152,36 @@ jobs:
id: should-promote
shell: pwsh
run: |
$hasIssues = "${{ steps.check-issues.outputs.has-issues }}" -eq "true"
$canPromote = "${{ steps.check-issues.outputs.can-promote }}" -eq "true"
$forcePromote = "${{ github.event.inputs.force-promote }}" -eq "true"
$issueCount = "${{ steps.check-issues.outputs.issue-count }}"
$blockingReason = "${{ steps.check-issues.outputs.blocking-reason }}"
$version = "${{ matrix.version }}"

Write-Host "Checking promotion for version: $version"
Write-Host "Has issues: $hasIssues"
Write-Host "Issue count: $issueCount"
Write-Host "Force promote: $forcePromote"

# Check if version is old enough (at least 30 days since release)
# This is a basic check - in a full implementation you'd query the release date
Write-Host "=== Promotion Decision for $version ==="
Write-Host ""
Write-Host "Validation Results:"
Write-Host " • Milestone open issues: ${{ steps.check-issues.outputs.milestone-open-count }}"
Copy link

Copilot AI Mar 24, 2026

Choose a reason for hiding this comment

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

This step prints steps.check-issues.outputs.milestone-open-count, but check-issues-for-version doesn’t define or set a milestone-open-count output anymore. This will always be blank and is misleading in logs; either remove this line or add the corresponding output back to the action.

Suggested change
Write-Host " • Milestone open issues: ${{ steps.check-issues.outputs.milestone-open-count }}"

Copilot uses AI. Check for mistakes.
Write-Host " • Release age: ${{ steps.check-issues.outputs.release-age-days }} days"
Write-Host " • Last closed issue age: ${{ steps.check-issues.outputs.last-closed-age-days }} days"
Write-Host " • Can promote: $canPromote"
if (-not $canPromote) {
Write-Host " • Blocking reason: $blockingReason"
}
Write-Host " • Force promote: $forcePromote"
Write-Host ""

if ($forcePromote) {
Write-Host "⚠️ Force promotion enabled - promoting despite $issueCount issues"
Write-Host "⚠️ FORCE PROMOTION ENABLED - Bypassing all validation checks"
echo "should_promote=true" >> $env:GITHUB_OUTPUT
echo "reason=force_promote" >> $env:GITHUB_OUTPUT
} elseif (-not $hasIssues) {
Write-Host "✅ No issues reported for 30 days - ready for promotion"
} elseif ($canPromote) {
Write-Host "✅ ALL CONDITIONS MET - Promoting $version"
echo "should_promote=true" >> $env:GITHUB_OUTPUT
echo "reason=no_issues" >> $env:GITHUB_OUTPUT
echo "reason=all_conditions_met" >> $env:GITHUB_OUTPUT
} else {
Write-Host "⏸️ $issueCount issues still open - deferring promotion"
Write-Host "❌ PROMOTION BLOCKED - $blockingReason"
echo "should_promote=false" >> $env:GITHUB_OUTPUT
echo "reason=issues_exist" >> $env:GITHUB_OUTPUT
echo "reason=$blockingReason" >> $env:GITHUB_OUTPUT
}

- name: Promote version
Expand All @@ -198,17 +203,35 @@ jobs:
- name: Summary
if: always()
run: |
echo "## Promotion Check for ${{ matrix.version }}"
echo "## 🔄 Promotion Check for ${{ matrix.version }}"
echo ""
echo "### Validation Checks"
echo ""
echo "- Has Issues: ${{ steps.check-issues.outputs.has-issues }}"
echo "- Issue Count: ${{ steps.check-issues.outputs.issue-count }}"
echo "- Should Promote: ${{ steps.should-promote.outputs.should-promote }}"
echo "- Reason: ${{ steps.should-promote.outputs.reason }}"
echo "| Check | Status | Details |"
echo "|-------|--------|---------|"
echo "| **Version Label Issues** | ${{ steps.check-issues.outputs.has-issues == 'false' && '✅ Pass' || '❌ Fail' }} | ${{ steps.check-issues.outputs.issue-count }} open issue(s) with label |"
echo "| **Release Age** | ${{ steps.check-issues.outputs.release-old-enough == 'true' && '✅ Pass' || '❌ Fail' }} | ${{ steps.check-issues.outputs.release-age-days }} days (min: 30) |"
echo "| **Last Closed Issue** | ${{ steps.check-issues.outputs.last-closed-old-enough == 'true' && '✅ Pass' || '❌ Fail' }} | ${{ steps.check-issues.outputs.last-closed-age-days }} days since last close |"
echo ""
echo "### Decision"
echo ""
echo "- **Can Promote:** ${{ steps.check-issues.outputs.can-promote }}"
echo "- **Should Promote:** ${{ steps.should-promote.outputs.should-promote }}"
echo "- **Reason:** ${{ steps.should-promote.outputs.reason }}"
if [ "${{ steps.check-issues.outputs.can-promote }}" != "true" ]; then
echo "- **Blocking Reason:** ${{ steps.check-issues.outputs.blocking-reason }}"
fi
echo ""
if [ "${{ steps.should-promote.outputs.should-promote }}" == "true" ]; then
echo "- ✅ Promoted: ${{ matrix.version }} → ${{ steps.promote.outputs.new-version }}"
echo "- Previous Stage: ${{ steps.promote.outputs.previous-stage }}"
echo "- New Stage: ${{ steps.promote.outputs.new-stage }}"
echo "- PR Created: ${{ steps.promote.outputs.pr-created }}"
echo "- PR Number: ${{ steps.promote.outputs.pr-number }}"
echo "### ✅ Promotion Successful"
echo ""
echo "- **Promoted:** ${{ matrix.version }} → ${{ steps.promote.outputs.new-version }}"
echo "- **Previous Stage:** ${{ steps.promote.outputs.previous-stage }}"
echo "- **New Stage:** ${{ steps.promote.outputs.new-stage }}"
echo "- **PR Created:** ${{ steps.promote.outputs.pr-created }}"
echo "- **PR Number:** #${{ steps.promote.outputs.pr-number }}"
else
echo "### ❌ Promotion Blocked"
echo ""
echo "Version ${{ matrix.version }} cannot be promoted at this time."
fi
Loading
Loading