Auto Release opencode Package #22807
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Auto Release opencode Package | |
| on: | |
| schedule: | |
| - cron: '*/15 * * * *' | |
| workflow_dispatch: | |
| inputs: | |
| force: | |
| description: 'Force release even if version matches' | |
| required: false | |
| type: boolean | |
| default: false | |
| dry_run: | |
| description: 'Dry run mode - validate but do not publish' | |
| required: false | |
| type: boolean | |
| default: false | |
| skip_published_check: | |
| description: 'Skip the "already published" check (use if CDN is desynced)' | |
| required: false | |
| type: boolean | |
| default: false | |
| concurrency: | |
| group: opencode-auto-release | |
| cancel-in-progress: false | |
| permissions: | |
| contents: write | |
| actions: write | |
| jobs: | |
| auto-release: | |
| name: Check, Build & Release | |
| runs-on: windows-latest | |
| timeout-minutes: 25 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Cache Chocolatey Dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| C:\ProgramData\chocolatey\bin | |
| C:\ProgramData\chocolatey\lib\ripgrep | |
| C:\ProgramData\chocolatey\lib\fzf | |
| C:\ProgramData\chocolatey\lib\unzip | |
| ${{ runner.temp }}/opencode-cache | |
| key: ${{ runner.os }}-choco-deps-v3 | |
| restore-keys: | | |
| ${{ runner.os }}-choco-deps- | |
| - name: Check for new release | |
| id: check | |
| shell: pwsh | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| # Get current version from nuspec | |
| [xml]$nuspec = Get-Content 'opencode.nuspec' | |
| $currentVersion = $nuspec.package.metadata.version | |
| # Get latest release from GitHub | |
| $headers = @{ | |
| 'User-Agent' = 'Chocolatey-Updater' | |
| 'Accept' = 'application/vnd.github.v3+json' | |
| 'Authorization' = "Bearer $env:GITHUB_TOKEN" | |
| } | |
| $maxRetries = 3 | |
| $release = $null | |
| for ($i = 1; $i -le $maxRetries; $i++) { | |
| try { | |
| $release = Invoke-RestMethod -Uri 'https://api.github.com/repos/anomalyco/opencode/releases/latest' -Headers $headers -TimeoutSec 30 | |
| break | |
| } catch { | |
| if ($_.Exception.Response.StatusCode -eq 403) { | |
| Write-Host "::warning::GitHub API rate limit hit, backing off..." | |
| Start-Sleep -Seconds (30 * $i) | |
| } | |
| if ($i -eq $maxRetries) { | |
| Write-Host "::error::Failed to fetch latest release after $maxRetries attempts: $_" | |
| exit 1 | |
| } | |
| Write-Host "::warning::API call attempt $i failed, retrying..." | |
| Start-Sleep -Seconds (5 * $i) | |
| } | |
| } | |
| $latestVersion = $release.tag_name.TrimStart('v') | |
| # Check for Windows x64 asset | |
| $asset = $release.assets | Where-Object { $_.name -eq 'opencode-windows-x64.zip' } | |
| if (-not $asset) { | |
| Write-Host "::notice::No Windows x64 asset available for $latestVersion" | |
| "needs_release=false" >> $env:GITHUB_OUTPUT | |
| exit 0 | |
| } | |
| # Determine if release is needed | |
| $forceRelease = '${{ inputs.force }}' -eq 'true' | |
| $needsRelease = ($currentVersion -ne $latestVersion) -or $forceRelease | |
| "needs_release=$($needsRelease.ToString().ToLower())" >> $env:GITHUB_OUTPUT | |
| "current_version=$currentVersion" >> $env:GITHUB_OUTPUT | |
| "latest_version=$latestVersion" >> $env:GITHUB_OUTPUT | |
| "download_url=$($asset.browser_download_url)" >> $env:GITHUB_OUTPUT | |
| "dry_run=${{ inputs.dry_run }}" >> $env:GITHUB_OUTPUT | |
| Write-Host "Current: $currentVersion, Latest: $latestVersion, Needs Release: $needsRelease" | |
| if ('${{ inputs.dry_run }}' -eq 'true') { | |
| Write-Host "::notice::Running in DRY RUN mode" | |
| } | |
| - name: Cache opencode download | |
| if: steps.check.outputs.needs_release == 'true' | |
| uses: actions/cache@v4 | |
| id: cache-opencode | |
| with: | |
| path: ${{ runner.temp }}/opencode-cache/opencode-${{ steps.check.outputs.latest_version }}.zip | |
| key: opencode-download-${{ steps.check.outputs.latest_version }} | |
| - name: Download and update package | |
| if: steps.check.outputs.needs_release == 'true' | |
| shell: pwsh | |
| run: | | |
| $version = "${{ steps.check.outputs.latest_version }}" | |
| $downloadUrl = "${{ steps.check.outputs.download_url }}" | |
| $cacheDir = "${{ runner.temp }}/opencode-cache" | |
| $cachedFile = "$cacheDir/opencode-$version.zip" | |
| $tempFile = "$env:TEMP\opencode-$version.zip" | |
| # Create cache directory | |
| if (-not (Test-Path $cacheDir)) { | |
| New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null | |
| } | |
| # Use cached version or download fresh | |
| if ('${{ steps.cache-opencode.outputs.cache-hit }}' -eq 'true' -and (Test-Path $cachedFile)) { | |
| Write-Host "✓ Using cached opencode v$version" | |
| Copy-Item $cachedFile $tempFile | |
| } else { | |
| Write-Host "Downloading opencode v$version..." | |
| $maxRetries = 3 | |
| for ($i = 1; $i -le $maxRetries; $i++) { | |
| try { | |
| Invoke-WebRequest -Uri $downloadUrl -OutFile $tempFile -UserAgent 'Chocolatey-Updater' -TimeoutSec 300 | |
| break | |
| } catch { | |
| if ($i -eq $maxRetries) { | |
| throw "Failed to download after $maxRetries attempts: $_" | |
| } | |
| Write-Host "::warning::Download attempt $i failed, retrying..." | |
| Start-Sleep -Seconds 5 | |
| } | |
| } | |
| Copy-Item $tempFile $cachedFile | |
| } | |
| # Validate downloaded file | |
| if (-not (Test-Path $tempFile) -or (Get-Item $tempFile).Length -eq 0) { | |
| throw "Downloaded file is missing or empty" | |
| } | |
| # Validate zip integrity | |
| try { | |
| Add-Type -AssemblyName System.IO.Compression.FileSystem | |
| $zip = [System.IO.Compression.ZipFile]::OpenRead($tempFile) | |
| $zip.Dispose() | |
| } catch { | |
| throw "Downloaded file is not a valid zip: $_" | |
| } | |
| # Calculate checksum | |
| Write-Host "Calculating checksum..." | |
| $checksum = (Get-FileHash -Path $tempFile -Algorithm SHA256).Hash | |
| Remove-Item $tempFile -Force | |
| # Update nuspec | |
| [xml]$nuspec = Get-Content 'opencode.nuspec' | |
| $nuspec.package.metadata.version = $version | |
| $nuspec.package.metadata.releaseNotes = "https://github.com/anomalyco/opencode/releases/tag/v$version" | |
| $nuspec.Save('opencode.nuspec') | |
| # Update install script checksum | |
| $installScript = Get-Content 'tools\chocolateyinstall.ps1' -Raw | |
| $installScript = $installScript -replace "checksum64\s*=\s*'([A-F0-9]{64}|PLACEHOLDER_CHECKSUM)'", "checksum64 = '$checksum'" | |
| Set-Content -Path 'tools\chocolateyinstall.ps1' -Value $installScript -NoNewline | |
| Write-Host "✓ Package updated to v$version (checksum: $($checksum.Substring(0,16))...)" | |
| - name: Setup Chocolatey | |
| if: steps.check.outputs.needs_release == 'true' | |
| shell: pwsh | |
| run: | | |
| if (Get-Command choco -ErrorAction SilentlyContinue) { | |
| Write-Host "✓ Chocolatey already available" | |
| } else { | |
| Write-Host "Installing Chocolatey..." | |
| Set-ExecutionPolicy Bypass -Scope Process -Force | |
| [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072 | |
| iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) | |
| Write-Host "✓ Chocolatey installed" | |
| } | |
| choco feature enable -n allowGlobalConfirmation | |
| if ($LASTEXITCODE -ne 0) { throw "Failed to enable allowGlobalConfirmation" } | |
| - name: Install dependencies | |
| if: steps.check.outputs.needs_release == 'true' | |
| shell: pwsh | |
| run: | | |
| @('ripgrep', 'fzf', 'unzip') | ForEach-Object { | |
| $libPath = "C:\ProgramData\chocolatey\lib\$_" | |
| if (Test-Path $libPath) { | |
| Write-Host "✓ $_ available (cached)" | |
| } else { | |
| Write-Host "Installing $_..." | |
| choco install $_ -y --limit-output | |
| if ($LASTEXITCODE -ne 0) { throw "Failed to install $_" } | |
| Write-Host "✓ $_ installed" | |
| } | |
| } | |
| - name: Build package | |
| if: steps.check.outputs.needs_release == 'true' | |
| shell: pwsh | |
| run: | | |
| Write-Host "Building package..." | |
| $output = choco pack 2>&1 | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-Host "::error::Package build failed:" | |
| Write-Host $output | |
| throw "choco pack failed with exit code $LASTEXITCODE" | |
| } | |
| $packageFile = "opencode.${{ steps.check.outputs.latest_version }}.nupkg" | |
| if (-not (Test-Path $packageFile)) { | |
| throw "Expected package file not found: $packageFile" | |
| } | |
| Write-Host "✓ Package built: $packageFile" | |
| - name: Test package installation | |
| if: steps.check.outputs.needs_release == 'true' | |
| shell: pwsh | |
| run: | | |
| Write-Host "Testing package installation..." | |
| $output = choco install opencode -y -s . --force 2>&1 | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-Host "::error::Package installation failed:" | |
| Write-Host $output | |
| throw "choco install failed with exit code $LASTEXITCODE" | |
| } | |
| # Verify installed version | |
| $installedVersion = opencode --version 2>&1 | |
| if ($installedVersion -match '(\d+\.\d+\.\d+)') { | |
| $extractedVersion = $matches[1] | |
| $expectedVersion = '${{ steps.check.outputs.latest_version }}' | |
| if ($extractedVersion -ne $expectedVersion) { | |
| throw "Version mismatch: expected $expectedVersion, got $extractedVersion" | |
| } | |
| Write-Host "✓ Package test passed (v$extractedVersion)" | |
| } else { | |
| throw "Could not extract version from: $installedVersion" | |
| } | |
| - name: Check if version already published | |
| if: steps.check.outputs.needs_release == 'true' && inputs.skip_published_check != true | |
| id: check_published | |
| shell: pwsh | |
| run: | | |
| $version = "${{ steps.check.outputs.latest_version }}" | |
| # Actually try to download the package, not just check metadata | |
| # The OData API can show a version exists before the CDN has the file | |
| Write-Host "Verifying if v$version is actually downloadable from Chocolatey CDN..." | |
| try { | |
| # This URL returns the actual nupkg file - if it 404s, the package isn't really there | |
| $testUrl = "https://community.chocolatey.org/api/v2/package/opencode/$version" | |
| $response = Invoke-WebRequest -Uri $testUrl -Method GET -TimeoutSec 30 -MaximumRedirection 5 | |
| # If we get here without error, the package is actually downloadable | |
| if ($response.StatusCode -eq 200 -and $response.Content.Length -gt 1000) { | |
| Write-Host "✓ Version $version is confirmed downloadable (size: $($response.Content.Length) bytes)" | |
| "already_published=true" >> $env:GITHUB_OUTPUT | |
| "publish_reason=Version $version already exists and is downloadable" >> $env:GITHUB_OUTPUT | |
| } else { | |
| Write-Host "::warning::Got response but content seems invalid (status: $($response.StatusCode), size: $($response.Content.Length))" | |
| "already_published=false" >> $env:GITHUB_OUTPUT | |
| } | |
| } catch { | |
| $statusCode = $_.Exception.Response.StatusCode.value__ | |
| if ($statusCode -eq 404) { | |
| Write-Host "Package v$version not found on CDN (404) - needs publishing" | |
| } else { | |
| Write-Host "::warning::CDN check failed (status: $statusCode): $($_.Exception.Message)" | |
| Write-Host "::warning::Will attempt to publish anyway" | |
| } | |
| "already_published=false" >> $env:GITHUB_OUTPUT | |
| } | |
| - name: Commit and tag | |
| if: steps.check.outputs.needs_release == 'true' && inputs.dry_run != true | |
| shell: pwsh | |
| run: | | |
| $version = "${{ steps.check.outputs.latest_version }}" | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| # Check if files changed | |
| git diff --quiet HEAD -- opencode.nuspec tools/chocolateyinstall.ps1 | |
| $hasChanges = $LASTEXITCODE -ne 0 | |
| if ($hasChanges) { | |
| Write-Host "Committing changes..." | |
| git add opencode.nuspec tools/chocolateyinstall.ps1 | |
| git commit -m "chore: update opencode to v$version" | |
| # Handle existing tag | |
| $tagExists = git tag -l "v$version" | |
| if ($tagExists) { | |
| Write-Host "::warning::Tag v$version already exists, recreating..." | |
| git tag -d "v$version" | |
| git push origin ":refs/tags/v$version" 2>&1 | Out-Null | |
| } | |
| git tag -a "v$version" -m "Release v$version" | |
| # Push with retry | |
| $maxRetries = 3 | |
| for ($i = 1; $i -le $maxRetries; $i++) { | |
| $pushOutput = git push origin main 2>&1 | |
| if ($LASTEXITCODE -eq 0) { | |
| git push origin "v$version" | |
| Write-Host "✓ Changes pushed to repository" | |
| break | |
| } | |
| if ($i -eq $maxRetries) { | |
| Write-Host "::error::Failed to push after $maxRetries attempts: $pushOutput" | |
| exit 1 | |
| } | |
| Write-Host "::warning::Push failed, rebasing and retrying..." | |
| git pull --rebase origin main | |
| Start-Sleep -Seconds 2 | |
| } | |
| } else { | |
| Write-Host "::notice::No changes detected in package files" | |
| } | |
| - name: Create GitHub Release | |
| if: steps.check.outputs.needs_release == 'true' && inputs.dry_run != true | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: v${{ steps.check.outputs.latest_version }} | |
| name: v${{ steps.check.outputs.latest_version }} | |
| body: | | |
| **Full Changelog**: https://github.com/itsrainingmani/opencode-chocolatey/compare/v${{ steps.check.outputs.current_version }}...v${{ steps.check.outputs.latest_version }} | |
| files: opencode.${{ steps.check.outputs.latest_version }}.nupkg | |
| draft: false | |
| prerelease: false | |
| - name: Publish to Chocolatey | |
| if: | | |
| steps.check.outputs.needs_release == 'true' && | |
| inputs.dry_run != true && | |
| (steps.check_published.outputs.already_published != 'true' || inputs.skip_published_check == true) | |
| id: publish | |
| shell: pwsh | |
| env: | |
| CHOCO_API_KEY: ${{ secrets.CHOCO_API_KEY }} | |
| run: | | |
| if (-not $env:CHOCO_API_KEY) { | |
| Write-Host "::error::CHOCO_API_KEY secret not configured" | |
| exit 1 | |
| } | |
| $version = "${{ steps.check.outputs.latest_version }}" | |
| $packageFile = "opencode.$version.nupkg" | |
| if (-not (Test-Path $packageFile)) { | |
| throw "Package file not found: $packageFile" | |
| } | |
| # Set API key | |
| Write-Host "Setting Chocolatey API key..." | |
| $output = choco apikey -k $env:CHOCO_API_KEY -s https://push.chocolatey.org/ 2>&1 | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-Host "::error::Failed to set API key: $output" | |
| throw "choco apikey failed" | |
| } | |
| # Push with retry | |
| Write-Host "Publishing $packageFile to Chocolatey..." | |
| $maxRetries = 3 | |
| $lastError = $null | |
| for ($i = 1; $i -le $maxRetries; $i++) { | |
| Write-Host "Push attempt $i of $maxRetries..." | |
| $output = choco push $packageFile -s https://push.chocolatey.org/ --timeout=600 2>&1 | |
| if ($LASTEXITCODE -eq 0) { | |
| Write-Host "✓ Successfully published opencode v$version to Chocolatey" | |
| "published=true" >> $env:GITHUB_OUTPUT | |
| exit 0 | |
| } | |
| $lastError = $output | |
| $outputStr = $output | Out-String | |
| # Check for "already exists" which is actually success | |
| if ($outputStr -match "already exists|409|conflict") { | |
| Write-Host "::notice::Package v$version already exists on Chocolatey (this is OK)" | |
| "published=true" >> $env:GITHUB_OUTPUT | |
| "already_existed=true" >> $env:GITHUB_OUTPUT | |
| exit 0 | |
| } | |
| Write-Host "::warning::Push attempt $i failed (exit code: $LASTEXITCODE):" | |
| Write-Host $output | |
| if ($i -lt $maxRetries) { | |
| Write-Host "Retrying in 10 seconds..." | |
| Start-Sleep -Seconds 10 | |
| } | |
| } | |
| Write-Host "::error::Failed to publish after $maxRetries attempts" | |
| Write-Host "Last error: $lastError" | |
| throw "choco push failed after $maxRetries attempts" | |
| - name: Verify publication | |
| if: steps.publish.outputs.published == 'true' && steps.publish.outputs.already_existed != 'true' | |
| shell: pwsh | |
| run: | | |
| $version = "${{ steps.check.outputs.latest_version }}" | |
| Write-Host "Waiting for CDN propagation (60 seconds)..." | |
| Start-Sleep -Seconds 60 | |
| # Verify the package is actually downloadable | |
| $maxRetries = 3 | |
| for ($i = 1; $i -le $maxRetries; $i++) { | |
| try { | |
| $testUrl = "https://community.chocolatey.org/api/v2/package/opencode/$version" | |
| $response = Invoke-WebRequest -Uri $testUrl -Method GET -TimeoutSec 30 | |
| if ($response.StatusCode -eq 200 -and $response.Content.Length -gt 1000) { | |
| Write-Host "✓ Verified: v$version is now downloadable from Chocolatey CDN" | |
| exit 0 | |
| } | |
| } catch { | |
| Write-Host "::warning::Verification attempt $i failed: $_" | |
| } | |
| if ($i -lt $maxRetries) { | |
| Write-Host "Waiting 30 more seconds for CDN propagation..." | |
| Start-Sleep -Seconds 30 | |
| } | |
| } | |
| Write-Host "::warning::Could not verify package availability - CDN may still be propagating" | |
| Write-Host "::warning::Package should be available within 30 minutes" | |
| - name: Cleanup on failure | |
| if: failure() | |
| shell: pwsh | |
| run: | | |
| Get-ChildItem -Path . -Filter "*.nupkg" | Remove-Item -Force -ErrorAction SilentlyContinue | |
| Write-Host "Cleaned up build artifacts" | |
| - name: Discord Notification | |
| if: steps.check.outputs.needs_release == 'true' && inputs.dry_run != true && success() | |
| shell: pwsh | |
| env: | |
| DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }} | |
| run: | | |
| if (-not $env:DISCORD_WEBHOOK_URL) { | |
| Write-Host "::notice::Discord webhook not configured, skipping notification" | |
| exit 0 | |
| } | |
| $version = "${{ steps.check.outputs.latest_version }}" | |
| $payload = @{ | |
| content = "🎉 **OpenCode Chocolatey Package Released!**" | |
| embeds = @( | |
| @{ | |
| title = "New Release: v$version" | |
| description = "Successfully published OpenCode v$version to Chocolatey" | |
| color = 65280 | |
| fields = @( | |
| @{ name = "Version"; value = "v$version"; inline = $true } | |
| @{ name = "Package"; value = "[View on Chocolatey](https://community.chocolatey.org/packages/opencode/$version)"; inline = $true } | |
| @{ name = "Changelog"; value = "[View Release](https://github.com/anomalyco/opencode/releases/tag/v$version)"; inline = $true } | |
| ) | |
| timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ") | |
| } | |
| ) | |
| } | ConvertTo-Json -Depth 10 | |
| try { | |
| Invoke-RestMethod -Uri $env:DISCORD_WEBHOOK_URL -Method Post -ContentType "application/json" -Body $payload -TimeoutSec 30 | Out-Null | |
| Write-Host "✓ Discord notification sent" | |
| } catch { | |
| Write-Host "::warning::Failed to send Discord notification: $_" | |
| } | |
| - name: Summary | |
| if: always() | |
| shell: pwsh | |
| run: | | |
| $needsRelease = "${{ steps.check.outputs.needs_release }}" | |
| $currentVersion = "${{ steps.check.outputs.current_version }}" | |
| $latestVersion = "${{ steps.check.outputs.latest_version }}" | |
| $dryRun = "${{ inputs.dry_run }}" | |
| $skipCheck = "${{ inputs.skip_published_check }}" | |
| $alreadyPublished = "${{ steps.check_published.outputs.already_published }}" | |
| $published = "${{ steps.publish.outputs.published }}" | |
| $summary = "## Auto-Release Summary`n`n" | |
| $summary += "| Property | Value |`n" | |
| $summary += "|----------|-------|`n" | |
| $summary += "| Current Version | $currentVersion |`n" | |
| $summary += "| Latest Version | $latestVersion |`n" | |
| $summary += "| Needs Release | $needsRelease |`n" | |
| $summary += "| Dry Run | $dryRun |`n" | |
| $summary += "| Skip Published Check | $skipCheck |`n" | |
| $summary += "| Already Published | $alreadyPublished |`n" | |
| $summary += "| Published This Run | $published |`n" | |
| $summary += "| Status | ${{ job.status }} |" | |
| if ($needsRelease -eq 'true') { | |
| if ($dryRun -eq 'true') { | |
| $summary += "`n`n✅ **DRY RUN**: Would have released opencode v$latestVersion" | |
| } elseif ('${{ job.status }}' -eq 'success') { | |
| if ($published -eq 'true') { | |
| $summary += "`n`n✅ **Successfully published** opencode v$latestVersion to Chocolatey" | |
| } elseif ($alreadyPublished -eq 'true') { | |
| $summary += "`n`nℹ️ **Skipped publish**: v$latestVersion already available on Chocolatey" | |
| } | |
| } else { | |
| $summary += "`n`n❌ **Failed** to release opencode v$latestVersion" | |
| } | |
| } else { | |
| $summary += "`n`nℹ️ **No release needed** - already at latest version" | |
| } | |
| $summary | Out-File -FilePath $env:GITHUB_STEP_SUMMARY -Append | |
| # Console output | |
| if ($needsRelease -eq 'true' -and '${{ job.status }}' -eq 'success' -and $dryRun -ne 'true') { | |
| Write-Host "::notice::✅ Auto-released opencode v$latestVersion" | |
| } elseif ($needsRelease -eq 'true' -and '${{ job.status }}' -ne 'success') { | |
| Write-Host "::error::❌ Failed to auto-release opencode v$latestVersion" | |
| } |