From 8131ac645bbe6c2929622efd73af480b2d4a9b6e Mon Sep 17 00:00:00 2001 From: David Refoua Date: Sat, 20 Jun 2026 03:13:34 +0330 Subject: [PATCH 1/2] Add package manager release prep --- docs/package-managers.md | 83 ++++++++ scripts/package-managers.ps1 | 375 +++++++++++++++++++++++++++++++++++ 2 files changed, 458 insertions(+) create mode 100644 docs/package-managers.md create mode 100644 scripts/package-managers.ps1 diff --git a/docs/package-managers.md b/docs/package-managers.md new file mode 100644 index 000000000..daf712261 --- /dev/null +++ b/docs/package-managers.md @@ -0,0 +1,83 @@ +# Package Manager Release Prep + +This document describes the Cmder-side preparation for Windows package-manager releases. It does not publish anything by itself; maintainers still need to review and submit generated files to the external registries. + +Canonical tracking: + +- Master issue: +- Wiki status: + +## Generate Release Files + +After a Cmder release has assets and `hashes.txt`, run: + +```powershell +.\scripts\package-managers.ps1 ` + -Version 1.3.25 ` + -ReleaseTag v1.3.25 ` + -ReleaseDate 2024-05-31 ` + -Clean +``` + +By default the script reads `build\hashes.txt` if present. If it is not present, it downloads `hashes.txt` from: + +```text +https://github.com/cmderdev/cmder/releases/download//hashes.txt +``` + +Generated files are written to `build\package-managers`. + +## WinGet + +The generated WinGet files are under: + +```text +build\package-managers\winget\manifests\c\Cmder\Cmder\ +build\package-managers\winget\manifests\c\Cmder\CmderMini\ +``` + +Validate them locally: + +```powershell +winget validate --manifest .\build\package-managers\winget\manifests\c\Cmder\Cmder\1.3.25 +winget validate --manifest .\build\package-managers\winget\manifests\c\Cmder\CmderMini\1.3.25 +``` + +To submit, copy the generated manifest folders into a fork of [`microsoft/winget-pkgs`](https://github.com/microsoft/winget-pkgs) and open a PR. Proposed package IDs are: + +- `Cmder.Cmder` for the full archive, using `cmder.zip` and `Architecture: x64` +- `Cmder.CmderMini` for the mini archive, using `cmder_mini.zip` and `Architecture: neutral` + +The full package is marked `x64` because the full Cmder archive vendors 64-bit Git for Windows. + +## Chocolatey + +The generated Chocolatey package sources are under: + +```text +build\package-managers\chocolatey\Cmder +build\package-managers\chocolatey\cmdermini +``` + +Build them locally: + +```powershell +choco pack .\build\package-managers\chocolatey\Cmder\Cmder.nuspec +choco pack .\build\package-managers\chocolatey\cmdermini\cmdermini.nuspec +``` + +Current Chocolatey packages already exist, but they are community-maintained rather than owned by `cmderdev`: + +- +- + +Before publishing official packages, contact the current maintainers and ask them to add Cmder maintainers or coordinate ownership. Chocolatey's vendor guidance asks software vendors/authors to contact current maintainers first, then contact Chocolatey site admins with the contact history if there is no response after 7 days. + +## Scoop + +Scoop manifests already exist in `ScoopInstaller/Main`: + +- +- + +They already use official Cmder release assets and `hashes.txt`. If the manifests lag after a release, submit an update PR to `ScoopInstaller/Main` unless the core team decides to create a separate official bucket. diff --git a/scripts/package-managers.ps1 b/scripts/package-managers.ps1 new file mode 100644 index 000000000..506738996 --- /dev/null +++ b/scripts/package-managers.ps1 @@ -0,0 +1,375 @@ +<# +.Synopsis + Generate package-manager release files for Cmder. +.DESCRIPTION + Renders WinGet manifests and Chocolatey package sources from an official + Cmder release tag and hashes.txt file. This script does not publish or push + anything to external registries. +.EXAMPLE + .\package-managers.ps1 -Version 1.3.25 -ReleaseTag v1.3.25 -ReleaseDate 2024-05-31 + + Generates files under ..\build\package-managers using hashes.txt from the + official GitHub release. +.EXAMPLE + .\package-managers.ps1 -Version 1.3.25 -ReleaseTag v1.3.25 -HashesPath ..\build\hashes.txt + + Generates files from a local release build hashes.txt file. +#> + +[CmdletBinding()] +Param( + [string]$Version = "", + + [string]$ReleaseTag = "", + + [string]$ReleaseDate = (Get-Date -Format "yyyy-MM-dd"), + + [string]$Repository = "cmderdev/cmder", + + [string]$AssetBaseUrl = "", + + [string]$HashesPath = "$PSScriptRoot\..\build\hashes.txt", + + [string]$OutputPath = "$PSScriptRoot\..\build\package-managers", + + [switch]$Clean +) + +$ErrorActionPreference = "Stop" + +function Get-CmderVersion { + if ($Version) { + return ($Version -replace "^v", "") + } + + $versionString = "" + if (Get-Command "git.exe" -ErrorAction SilentlyContinue) { + $gitPresent = git rev-parse --is-inside-work-tree 2>$null + if ($gitPresent -eq "true") { + $versionString = git describe --abbrev=0 --tags 2>$null + } + } + + if (-not $versionString) { + $changelogPath = Resolve-Path "$PSScriptRoot\..\CHANGELOG.md" + $match = Select-String -Path $changelogPath -Pattern "^## \[(?[\w\-\.]+)\]\([^\n()]+\)\s+\([^\n()]+\)$" | Select-Object -First 1 + if ($match) { + $versionString = $match.Matches[0].Groups["version"].Value + } + } + + if (-not $versionString) { + throw "Could not determine Cmder version. Pass -Version explicitly." + } + + return ($versionString -replace "^v+", "") +} + +function New-Utf8NoBomEncoding { + return New-Object System.Text.UTF8Encoding($false) +} + +function Write-TextFile { + param( + [Parameter(Mandatory = $true)][string]$Path, + [AllowEmptyString()][string[]]$Lines + ) + + $parent = Split-Path -Parent $Path + if (-not (Test-Path -PathType Container $parent)) { + New-Item -ItemType Directory -Path $parent | Out-Null + } + + $content = ($Lines -join [Environment]::NewLine) + [Environment]::NewLine + [System.IO.File]::WriteAllText($Path, $content, (New-Utf8NoBomEncoding)) +} + +function Read-ReleaseHashes { + param( + [Parameter(Mandatory = $true)][string]$Path, + [Parameter(Mandatory = $true)][string]$HashesUrl + ) + + $resolvedPath = $Path + if (-not (Test-Path -PathType Leaf $resolvedPath)) { + $tempPath = Join-Path ([System.IO.Path]::GetTempPath()) ("cmder-hashes-{0}.txt" -f ([System.Guid]::NewGuid())) + Write-Verbose "Downloading release hashes from $HashesUrl" + Invoke-WebRequest -UseBasicParsing -Uri $HashesUrl -OutFile $tempPath + $resolvedPath = $tempPath + } + + $hashes = @{} + foreach ($line in Get-Content -Path $resolvedPath) { + if ($line -match "^\s*(\S+)\s+([A-Fa-f0-9]{64})\s*$") { + $hashes[$Matches[1]] = $Matches[2].ToUpperInvariant() + } + } + + foreach ($asset in @("cmder.zip", "cmder.7z", "cmder_mini.zip")) { + if (-not $hashes.ContainsKey($asset)) { + throw "Could not find SHA256 hash for '$asset' in '$resolvedPath'." + } + } + + return $hashes +} + +function New-WinGetManifest { + param( + [Parameter(Mandatory = $true)][string]$PackageIdentifier, + [Parameter(Mandatory = $true)][string]$PackageName, + [Parameter(Mandatory = $true)][string]$Description, + [Parameter(Mandatory = $true)][string]$AssetName, + [Parameter(Mandatory = $true)][string]$AssetSha256, + [Parameter(Mandatory = $true)][string]$Architecture, + [Parameter(Mandatory = $true)][string]$PackageVersion, + [Parameter(Mandatory = $true)][string]$Tag, + [Parameter(Mandatory = $true)][string]$BaseUrl, + [Parameter(Mandatory = $true)][string]$Date, + [Parameter(Mandatory = $true)][string]$Destination + ) + + $packageLeaf = ($PackageIdentifier -split "\.")[-1] + $manifestPath = Join-Path $Destination ("winget\manifests\c\Cmder\{0}\{1}" -f $packageLeaf, $PackageVersion) + $schemaVersion = "1.10.0" + + Write-TextFile -Path (Join-Path $manifestPath "$PackageIdentifier.yaml") -Lines @( + "# yaml-language-server: `$schema=https://aka.ms/winget-manifest.version.$schemaVersion.schema.json", + "", + "PackageIdentifier: $PackageIdentifier", + "PackageVersion: $PackageVersion", + "DefaultLocale: en-US", + "ManifestType: version", + "ManifestVersion: $schemaVersion" + ) + + Write-TextFile -Path (Join-Path $manifestPath "$PackageIdentifier.locale.en-US.yaml") -Lines @( + "# yaml-language-server: `$schema=https://aka.ms/winget-manifest.defaultLocale.$schemaVersion.schema.json", + "", + "PackageIdentifier: $PackageIdentifier", + "PackageVersion: $PackageVersion", + "PackageLocale: en-US", + "Publisher: Cmder", + "PublisherUrl: https://github.com/cmderdev", + "PublisherSupportUrl: https://github.com/cmderdev/cmder/issues", + "Author: Samuel Vasko", + "PackageName: $PackageName", + "PackageUrl: https://cmder.app", + "License: MIT", + "LicenseUrl: https://github.com/cmderdev/cmder/blob/master/LICENSE", + "Copyright: Copyright (c) 2016 Samuel Vasko", + "ShortDescription: Lovely console emulator package for Windows.", + "Description: >-", + " $Description", + "Moniker: cmder", + "Tags:", + "- cmder", + "- conemu", + "- console", + "- terminal", + "ReleaseNotesUrl: https://github.com/cmderdev/cmder/releases/tag/$Tag", + "ManifestType: defaultLocale", + "ManifestVersion: $schemaVersion" + ) + + Write-TextFile -Path (Join-Path $manifestPath "$PackageIdentifier.installer.yaml") -Lines @( + "# yaml-language-server: `$schema=https://aka.ms/winget-manifest.installer.$schemaVersion.schema.json", + "", + "PackageIdentifier: $PackageIdentifier", + "PackageVersion: $PackageVersion", + "InstallerType: zip", + "NestedInstallerType: portable", + "NestedInstallerFiles:", + "- RelativeFilePath: Cmder.exe", + " PortableCommandAlias: cmder", + "ArchiveBinariesDependOnPath: true", + "UpgradeBehavior: install", + "Commands:", + "- cmder", + "ReleaseDate: $Date", + "Installers:", + "- Architecture: $Architecture", + " InstallerUrl: $BaseUrl/$AssetName", + " InstallerSha256: $AssetSha256", + "ManifestType: installer", + "ManifestVersion: $schemaVersion" + ) +} + +function New-ChocolateyPackage { + param( + [Parameter(Mandatory = $true)][string]$PackageId, + [Parameter(Mandatory = $true)][string]$Title, + [Parameter(Mandatory = $true)][string]$Description, + [Parameter(Mandatory = $true)][string]$Notes, + [Parameter(Mandatory = $true)][string]$AssetName, + [Parameter(Mandatory = $true)][string]$AssetSha256, + [Parameter(Mandatory = $true)][string]$PackageVersion, + [Parameter(Mandatory = $true)][string]$Tag, + [Parameter(Mandatory = $true)][string]$BaseUrl, + [Parameter(Mandatory = $true)][string]$Destination + ) + + $packageRoot = Join-Path $Destination "chocolatey\$PackageId" + $toolsPath = Join-Path $packageRoot "tools" + $legalPath = Join-Path $packageRoot "legal" + + New-Item -ItemType Directory -Path $toolsPath -Force | Out-Null + New-Item -ItemType Directory -Path $legalPath -Force | Out-Null + + Write-TextFile -Path (Join-Path $packageRoot "$PackageId.nuspec") -Lines @( + "", + "", + " ", + " $PackageId", + " $PackageVersion", + " https://github.com/cmderdev/cmder", + " cmderdev", + " $Title", + " Samuel Vasko", + " https://cmder.app/", + " https://github.com/cmderdev/cmder/blob/master/LICENSE", + " false", + " https://github.com/cmderdev/cmder/", + " https://github.com/cmderdev/cmder/wiki", + " https://github.com/cmderdev/cmder/issues", + " cmder console terminal cli foss", + " Lovely console emulator package for Windows", + " ", + " https://github.com/cmderdev/cmder/releases/tag/$Tag", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + " ", + "" + ) + + Write-TextFile -Path (Join-Path $toolsPath "chocolateyInstall.ps1") -Lines @( + "`$ErrorActionPreference = 'Stop'", + "", + "`$installPath = Join-Path (Get-ToolsLocation) `$env:ChocolateyPackageName", + "", + "`$packageArgs = @{", + " packageName = `$env:ChocolateyPackageName", + " unzipLocation = `$installPath", + " url = '$BaseUrl/$AssetName'", + " checksum = '$AssetSha256'", + " checksumType = 'sha256'", + "}", + "", + "Install-ChocolateyZipPackage @packageArgs", + "Install-ChocolateyPath `$installPath 'User'" + ) + + Write-TextFile -Path (Join-Path $toolsPath "chocolateyUninstall.ps1") -Lines @( + "`$ErrorActionPreference = 'Stop'", + "", + "`$toolsPath = Split-Path -Parent `$MyInvocation.MyCommand.Definition", + "`$unScriptPath = Join-Path `$toolsPath 'Uninstall-ChocolateyPath.psm1'", + "`$installPath = Join-Path (Get-ToolsLocation) `$env:ChocolateyPackageName", + "", + "Import-Module `$unScriptPath", + "Uninstall-ChocolateyPath `$installPath 'User'", + "", + "if (Test-Path `$installPath) {", + " Remove-Item -Path `$installPath -Recurse -Force", + "}" + ) + + Write-TextFile -Path (Join-Path $legalPath "VERIFICATION.txt") -Lines @( + "VERIFICATION", + "", + "This package downloads Cmder from the official GitHub release.", + "", + "Package: $PackageId", + "Version: $PackageVersion", + "Release: https://github.com/cmderdev/cmder/releases/tag/$Tag", + "File: $BaseUrl/$AssetName", + "Checksum type: sha256", + "Checksum: $AssetSha256" + ) + + $licenseSource = Resolve-Path "$PSScriptRoot\..\LICENSE" + Copy-Item -Path $licenseSource -Destination (Join-Path $legalPath "LICENSE.txt") -Force +} + +$packageVersion = Get-CmderVersion +if (-not $ReleaseTag) { + $ReleaseTag = "v$packageVersion" +} + +if (-not $AssetBaseUrl) { + $AssetBaseUrl = "https://github.com/$Repository/releases/download/$ReleaseTag" +} + +$OutputPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($OutputPath) + +if ($Clean -and (Test-Path $OutputPath)) { + Remove-Item -Path $OutputPath -Recurse -Force +} + +New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null + +$hashes = Read-ReleaseHashes -Path $HashesPath -HashesUrl "$AssetBaseUrl/hashes.txt" + +$cmderDescription = "Cmder is a portable console emulator package for Windows. It combines ConEmu, Clink, a custom prompt layout, and Cmder defaults for a productive Windows command-line experience." +$cmderMiniDescription = "Cmder Mini is the smaller Cmder package for Windows. It includes Cmder's console experience without the vendored Git for Windows tools included in the full package." + +New-WinGetManifest ` + -PackageIdentifier "Cmder.Cmder" ` + -PackageName "Cmder" ` + -Description $cmderDescription ` + -AssetName "cmder.zip" ` + -AssetSha256 $hashes["cmder.zip"] ` + -Architecture "x64" ` + -PackageVersion $packageVersion ` + -Tag $ReleaseTag ` + -BaseUrl $AssetBaseUrl ` + -Date $ReleaseDate ` + -Destination $OutputPath + +New-WinGetManifest ` + -PackageIdentifier "Cmder.CmderMini" ` + -PackageName "Cmder Mini" ` + -Description $cmderMiniDescription ` + -AssetName "cmder_mini.zip" ` + -AssetSha256 $hashes["cmder_mini.zip"] ` + -Architecture "neutral" ` + -PackageVersion $packageVersion ` + -Tag $ReleaseTag ` + -BaseUrl $AssetBaseUrl ` + -Date $ReleaseDate ` + -Destination $OutputPath + +New-ChocolateyPackage ` + -PackageId "Cmder" ` + -Title "Cmder" ` + -Description $cmderDescription ` + -Notes "This package installs the full Cmder archive, including vendored Git for Windows. See cmdermini for the smaller package without vendored Git." ` + -AssetName "cmder.zip" ` + -AssetSha256 $hashes["cmder.zip"] ` + -PackageVersion $packageVersion ` + -Tag $ReleaseTag ` + -BaseUrl $AssetBaseUrl ` + -Destination $OutputPath + +New-ChocolateyPackage ` + -PackageId "cmdermini" ` + -Title "Cmder Mini" ` + -Description $cmderMiniDescription ` + -Notes "This package installs the mini Cmder archive without vendored Git for Windows. See Cmder for the full package." ` + -AssetName "cmder_mini.zip" ` + -AssetSha256 $hashes["cmder_mini.zip"] ` + -PackageVersion $packageVersion ` + -Tag $ReleaseTag ` + -BaseUrl $AssetBaseUrl ` + -Destination $OutputPath + +Write-Host "Generated package-manager files in $OutputPath" From d09ad40f14c3ebbbdd4f1cc09c3d0b55367ae65f Mon Sep 17 00:00:00 2001 From: David Refoua Date: Wed, 1 Jul 2026 02:20:14 +0330 Subject: [PATCH 2/2] Add package manager publishing workflow --- .github/workflows/package-managers.yml | 304 +++++++++++++++++++++++++ docs/package-managers.md | 39 +++- scripts/package-managers.ps1 | 75 ++++++ 3 files changed, 415 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/package-managers.yml diff --git a/.github/workflows/package-managers.yml b/.github/workflows/package-managers.yml new file mode 100644 index 000000000..7c414a2fd --- /dev/null +++ b/.github/workflows/package-managers.yml @@ -0,0 +1,304 @@ +name: Package Manager Publishing + +on: + workflow_dispatch: + inputs: + version: + description: Cmder version without the leading v + required: true + type: string + release_tag: + description: GitHub release tag + required: true + type: string + release_date: + description: Release date for WinGet manifests (yyyy-MM-dd) + required: true + type: string + dry_run: + description: Generate, validate, and upload package files without publishing + required: true + default: true + type: boolean + publish_winget: + description: Submit WinGet manifests with WingetCreate + required: true + default: false + type: boolean + publish_chocolatey: + description: Push Chocolatey packages + required: true + default: false + type: boolean + publish_scoop: + description: Open or update a Scoop bucket pull request + required: true + default: false + type: boolean + release: + types: [published] + +defaults: + run: + shell: pwsh + +permissions: + contents: read + +concurrency: + group: package-managers-${{ github.event.release.tag_name || inputs.release_tag || github.ref_name }} + cancel-in-progress: false + +jobs: + configure: + name: Configure package-manager release + runs-on: ubuntu-latest + outputs: + version: ${{ steps.config.outputs.version }} + release_tag: ${{ steps.config.outputs.release_tag }} + release_date: ${{ steps.config.outputs.release_date }} + dry_run: ${{ steps.config.outputs.dry_run }} + publish_winget: ${{ steps.config.outputs.publish_winget }} + publish_chocolatey: ${{ steps.config.outputs.publish_chocolatey }} + publish_scoop: ${{ steps.config.outputs.publish_scoop }} + steps: + - id: config + name: Resolve release inputs + run: | + function Convert-ToBoolString { + param( + [AllowEmptyString()][string]$Value, + [bool]$Default = $false + ) + + if ([string]::IsNullOrWhiteSpace($Value)) { + return $Default.ToString().ToLowerInvariant() + } + + return ($Value.Trim().ToLowerInvariant() -in @("true", "1", "yes", "on")).ToString().ToLowerInvariant() + } + + $eventName = "${{ github.event_name }}" + $version = "${{ inputs.version }}" + $releaseTag = "${{ inputs.release_tag }}" + $releaseDate = "${{ inputs.release_date }}" + $dryRun = Convert-ToBoolString "${{ inputs.dry_run }}" $true + $publishWinget = Convert-ToBoolString "${{ inputs.publish_winget }}" $false + $publishChocolatey = Convert-ToBoolString "${{ inputs.publish_chocolatey }}" $false + $publishScoop = Convert-ToBoolString "${{ inputs.publish_scoop }}" $false + + if ($eventName -eq "release") { + $releaseTag = "${{ github.event.release.tag_name }}" + $version = $releaseTag -replace "^v+", "" + $publishedAt = "${{ github.event.release.published_at }}" + $releaseDate = if ($publishedAt) { ([datetime]$publishedAt).ToUniversalTime().ToString("yyyy-MM-dd") } else { (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd") } + $dryRun = (-not ("${{ vars.PACKAGE_MANAGERS_AUTO_PUBLISH }}".Trim().ToLowerInvariant() -in @("true", "1", "yes", "on"))).ToString().ToLowerInvariant() + $publishWinget = Convert-ToBoolString "${{ vars.PACKAGE_MANAGERS_PUBLISH_WINGET }}" $false + $publishChocolatey = Convert-ToBoolString "${{ vars.PACKAGE_MANAGERS_PUBLISH_CHOCOLATEY }}" $false + $publishScoop = Convert-ToBoolString "${{ vars.PACKAGE_MANAGERS_PUBLISH_SCOOP }}" $false + } + + if (-not $version) { + throw "Version is required." + } + if (-not $releaseTag) { + $releaseTag = "v$version" + } + if (-not $releaseDate) { + $releaseDate = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd") + } + + "version=$version" >> $env:GITHUB_OUTPUT + "release_tag=$releaseTag" >> $env:GITHUB_OUTPUT + "release_date=$releaseDate" >> $env:GITHUB_OUTPUT + "dry_run=$dryRun" >> $env:GITHUB_OUTPUT + "publish_winget=$publishWinget" >> $env:GITHUB_OUTPUT + "publish_chocolatey=$publishChocolatey" >> $env:GITHUB_OUTPUT + "publish_scoop=$publishScoop" >> $env:GITHUB_OUTPUT + + $summary = @" + ## 📦 Package Manager Publishing + + | Setting | Value | + | --- | --- | + | Version | ``$version`` | + | Release tag | ``$releaseTag`` | + | Release date | ``$releaseDate`` | + | Dry run | ``$dryRun`` | + | WinGet publish requested | ``$publishWinget`` | + | Chocolatey publish requested | ``$publishChocolatey`` | + | Scoop publish requested | ``$publishScoop`` | + + "@ + + $summary | Add-Content -Path $env:GITHUB_STEP_SUMMARY -Encoding utf8 + + generate: + name: Generate and validate package files + needs: configure + runs-on: windows-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Generate package-manager files + run: | + .\scripts\package-managers.ps1 ` + -Version '${{ needs.configure.outputs.version }}' ` + -ReleaseTag '${{ needs.configure.outputs.release_tag }}' ` + -ReleaseDate '${{ needs.configure.outputs.release_date }}' ` + -Clean + + - name: Validate WinGet manifests when winget is available + run: | + if (-not (Get-Command winget.exe -ErrorAction SilentlyContinue)) { + Write-Warning "winget.exe is not available on this runner; skipping WinGet CLI validation." + "### ⚠️ WinGet validation skipped`n`n``winget.exe`` was not available on the runner." | Add-Content -Path $env:GITHUB_STEP_SUMMARY -Encoding utf8 + return + } + + winget validate --manifest .\build\package-managers\winget\manifests\c\Cmder\Cmder\${{ needs.configure.outputs.version }} + winget validate --manifest .\build\package-managers\winget\manifests\c\Cmder\CmderMini\${{ needs.configure.outputs.version }} + "### ✅ WinGet manifests validated" | Add-Content -Path $env:GITHUB_STEP_SUMMARY -Encoding utf8 + + - name: Pack Chocolatey packages + run: | + choco pack .\build\package-managers\chocolatey\Cmder\Cmder.nuspec --out .\build\package-managers\chocolatey + choco pack .\build\package-managers\chocolatey\cmdermini\cmdermini.nuspec --out .\build\package-managers\chocolatey + "### ✅ Chocolatey packages packed" | Add-Content -Path $env:GITHUB_STEP_SUMMARY -Encoding utf8 + + - name: Validate Scoop manifests as JSON + run: | + Get-ChildItem .\build\package-managers\scoop\bucket\*.json | ForEach-Object { + Get-Content -Raw $_.FullName | ConvertFrom-Json | Out-Null + } + "### ✅ Scoop manifests parsed as JSON" | Add-Content -Path $env:GITHUB_STEP_SUMMARY -Encoding utf8 + + - name: Upload package-manager files + uses: actions/upload-artifact@v7 + with: + name: package-manager-files + path: build/package-managers + if-no-files-found: error + + publish-winget: + name: Submit WinGet manifests + needs: [configure, generate] + if: needs.configure.outputs.publish_winget == 'true' && needs.configure.outputs.dry_run != 'true' + runs-on: windows-latest + steps: + - uses: actions/download-artifact@v7 + with: + name: package-manager-files + path: package-manager-files + + - name: Download WingetCreate + run: Invoke-WebRequest https://aka.ms/wingetcreate/latest -OutFile wingetcreate.exe + + - name: Submit WinGet pull requests + env: + WINGETCREATE_GITHUB_TOKEN: ${{ secrets.WINGETCREATE_GITHUB_TOKEN }} + run: | + if (-not $env:WINGETCREATE_GITHUB_TOKEN) { + throw "Missing WINGETCREATE_GITHUB_TOKEN secret." + } + + $version = '${{ needs.configure.outputs.version }}' + $fullManifest = Resolve-Path "package-manager-files\winget\manifests\c\Cmder\Cmder\$version" + $miniManifest = Resolve-Path "package-manager-files\winget\manifests\c\Cmder\CmderMini\$version" + + .\wingetcreate.exe submit --prtitle "Add Cmder.Cmder $version" --token $env:WINGETCREATE_GITHUB_TOKEN --no-open $fullManifest + .\wingetcreate.exe submit --prtitle "Add Cmder.CmderMini $version" --token $env:WINGETCREATE_GITHUB_TOKEN --no-open $miniManifest + + publish-chocolatey: + name: Push Chocolatey packages + needs: [configure, generate] + if: needs.configure.outputs.publish_chocolatey == 'true' && needs.configure.outputs.dry_run != 'true' + runs-on: windows-latest + env: + CHOCOLATEY_PUSH_SOURCE: ${{ vars.CHOCOLATEY_PUSH_SOURCE || 'https://push.chocolatey.org/' }} + steps: + - uses: actions/download-artifact@v7 + with: + name: package-manager-files + path: package-manager-files + + - name: Push packages + env: + CHOCOLATEY_API_KEY: ${{ secrets.CHOCOLATEY_API_KEY }} + run: | + if (-not $env:CHOCOLATEY_API_KEY) { + throw "Missing CHOCOLATEY_API_KEY secret." + } + + Get-ChildItem package-manager-files\chocolatey\*.nupkg | ForEach-Object { + choco push $_.FullName --source $env:CHOCOLATEY_PUSH_SOURCE --key $env:CHOCOLATEY_API_KEY + } + + publish-scoop: + name: Open Scoop bucket PR + needs: [configure, generate] + if: needs.configure.outputs.publish_scoop == 'true' && needs.configure.outputs.dry_run != 'true' + runs-on: ubuntu-latest + env: + SCOOP_BUCKET_REPOSITORY: ${{ vars.SCOOP_BUCKET_REPOSITORY || 'ScoopInstaller/Main' }} + SCOOP_BUCKET_PUSH_REPOSITORY: ${{ vars.SCOOP_BUCKET_PUSH_REPOSITORY }} + SCOOP_BUCKET_BASE_BRANCH: ${{ vars.SCOOP_BUCKET_BASE_BRANCH || 'master' }} + SCOOP_BUCKET_MANIFEST_DIR: ${{ vars.SCOOP_BUCKET_MANIFEST_DIR || 'bucket' }} + GH_TOKEN: ${{ secrets.SCOOP_GITHUB_TOKEN }} + steps: + - uses: actions/download-artifact@v7 + with: + name: package-manager-files + path: package-manager-files + + - name: Create or update Scoop bucket pull request + run: | + if (-not $env:GH_TOKEN) { + throw "Missing SCOOP_GITHUB_TOKEN secret." + } + + $targetRepo = $env:SCOOP_BUCKET_REPOSITORY + $pushRepo = if ($env:SCOOP_BUCKET_PUSH_REPOSITORY) { $env:SCOOP_BUCKET_PUSH_REPOSITORY } else { $targetRepo } + $baseBranch = $env:SCOOP_BUCKET_BASE_BRANCH + $manifestDir = $env:SCOOP_BUCKET_MANIFEST_DIR + $version = '${{ needs.configure.outputs.version }}' + $branch = "cmder-$version" + + gh repo clone $pushRepo scoop-bucket -- --depth 1 + Set-Location scoop-bucket + + if ($pushRepo -ne $targetRepo) { + git remote add upstream "https://github.com/$targetRepo.git" + git fetch upstream $baseBranch --depth 1 + git checkout -B $branch "upstream/$baseBranch" + } else { + git fetch origin $baseBranch --depth 1 + git checkout -B $branch "origin/$baseBranch" + } + + New-Item -ItemType Directory -Force -Path $manifestDir | Out-Null + Copy-Item ../package-manager-files/scoop/bucket/cmder.json "$manifestDir/cmder.json" -Force + Copy-Item ../package-manager-files/scoop/bucket/cmder-full.json "$manifestDir/cmder-full.json" -Force + + if (-not (git status --porcelain)) { + "No Scoop manifest changes to publish." | Add-Content -Path $env:GITHUB_STEP_SUMMARY -Encoding utf8 + return + } + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add "$manifestDir/cmder.json" "$manifestDir/cmder-full.json" + git commit -m "Update Cmder manifests to $version" + git push origin $branch --force + + $pushOwner = ($pushRepo -split "/")[0] + $head = if ($pushRepo -ne $targetRepo) { "$pushOwner`:$branch" } else { $branch } + $body = @" + Updates the Cmder Scoop manifests from the official Cmder release assets. + + Source release: https://github.com/cmderdev/cmder/releases/tag/${{ needs.configure.outputs.release_tag }} + "@ + + gh pr create --repo $targetRepo --base $baseBranch --head $head --title "Update Cmder to $version" --body $body diff --git a/docs/package-managers.md b/docs/package-managers.md index daf712261..bad5ae082 100644 --- a/docs/package-managers.md +++ b/docs/package-managers.md @@ -1,6 +1,6 @@ # Package Manager Release Prep -This document describes the Cmder-side preparation for Windows package-manager releases. It does not publish anything by itself; maintainers still need to review and submit generated files to the external registries. +This document describes the Cmder-side preparation for Windows package-manager releases. Generated files can be reviewed locally, uploaded as workflow artifacts, or published through the package-manager workflow once maintainers configure the required external credentials. Canonical tracking: @@ -27,6 +27,8 @@ https://github.com/cmderdev/cmder/releases/download//hashes.txt Generated files are written to `build\package-managers`. +The same generation step is available in GitHub Actions through **Package Manager Publishing**. The workflow defaults to a dry run so maintainers can inspect generated files before enabling any external publishing. + ## WinGet The generated WinGet files are under: @@ -43,7 +45,7 @@ winget validate --manifest .\build\package-managers\winget\manifests\c\Cmder\Cmd winget validate --manifest .\build\package-managers\winget\manifests\c\Cmder\CmderMini\1.3.25 ``` -To submit, copy the generated manifest folders into a fork of [`microsoft/winget-pkgs`](https://github.com/microsoft/winget-pkgs) and open a PR. Proposed package IDs are: +To submit manually, copy the generated manifest folders into a fork of [`microsoft/winget-pkgs`](https://github.com/microsoft/winget-pkgs) and open a PR. To submit through GitHub Actions, configure `WINGETCREATE_GITHUB_TOKEN` and run the package-manager workflow with WinGet publishing enabled. - `Cmder.Cmder` for the full archive, using `cmder.zip` and `Architecture: x64` - `Cmder.CmderMini` for the mini archive, using `cmder_mini.zip` and `Architecture: neutral` @@ -73,11 +75,42 @@ Current Chocolatey packages already exist, but they are community-maintained rat Before publishing official packages, contact the current maintainers and ask them to add Cmder maintainers or coordinate ownership. Chocolatey's vendor guidance asks software vendors/authors to contact current maintainers first, then contact Chocolatey site admins with the contact history if there is no response after 7 days. +To publish through GitHub Actions, configure `CHOCOLATEY_API_KEY` and run the package-manager workflow with Chocolatey publishing enabled. The workflow uses `https://push.chocolatey.org/` by default, or `CHOCOLATEY_PUSH_SOURCE` if the repository variable is set. + ## Scoop -Scoop manifests already exist in `ScoopInstaller/Main`: +The generated Scoop manifests are under: + +```text +build\package-managers\scoop\bucket\cmder.json +build\package-managers\scoop\bucket\cmder-full.json +``` + +Scoop manifests also exist in `ScoopInstaller/Main`: - - They already use official Cmder release assets and `hashes.txt`. If the manifests lag after a release, submit an update PR to `ScoopInstaller/Main` unless the core team decides to create a separate official bucket. + +To publish through GitHub Actions, configure `SCOOP_GITHUB_TOKEN` and set the Scoop repository variables described in the private maintainer setup note. The workflow copies the generated manifests into the configured bucket and opens a pull request. + +## GitHub Actions Publishing + +Workflow: `.github/workflows/package-managers.yml` + +The workflow can be triggered manually with release details, or by a published GitHub release. Published releases remain dry-run only unless `PACKAGE_MANAGERS_AUTO_PUBLISH` is enabled through repository variables. + +Dry-run behavior: + +- generates WinGet, Chocolatey, and Scoop files; +- validates WinGet manifests when `winget` is available on the runner; +- packs Chocolatey `.nupkg` files; +- parses Scoop manifests as JSON; +- uploads all generated files as the `package-manager-files` artifact. + +Publishing behavior: + +- WinGet: uses WingetCreate to submit manifest PRs to `microsoft/winget-pkgs`; +- Chocolatey: pushes generated `.nupkg` files to the configured Chocolatey push source; +- Scoop: opens or updates a pull request against the configured Scoop bucket repository. diff --git a/scripts/package-managers.ps1 b/scripts/package-managers.ps1 index 506738996..ea832e2e5 100644 --- a/scripts/package-managers.ps1 +++ b/scripts/package-managers.ps1 @@ -84,6 +84,16 @@ function Write-TextFile { [System.IO.File]::WriteAllText($Path, $content, (New-Utf8NoBomEncoding)) } +function Write-JsonFile { + param( + [Parameter(Mandatory = $true)][string]$Path, + [Parameter(Mandatory = $true)]$Value + ) + + $json = $Value | ConvertTo-Json -Depth 10 + Write-TextFile -Path $Path -Lines ($json -split "`r?`n") +} + function Read-ReleaseHashes { param( [Parameter(Mandatory = $true)][string]$Path, @@ -300,6 +310,52 @@ function New-ChocolateyPackage { Copy-Item -Path $licenseSource -Destination (Join-Path $legalPath "LICENSE.txt") -Force } +function New-ScoopManifest { + param( + [Parameter(Mandatory = $true)][string]$PackageId, + [Parameter(Mandatory = $true)][string]$Description, + [Parameter(Mandatory = $true)][string]$AssetName, + [Parameter(Mandatory = $true)][string]$AssetSha256, + [Parameter(Mandatory = $true)][string]$PackageVersion, + [Parameter(Mandatory = $true)][string]$BaseUrl, + [Parameter(Mandatory = $true)][string]$Destination, + [string[]]$Persist = @("bin", "config", "vendor\conemu-maximus5\ConEmu.xml") + ) + + $manifest = [ordered]@{ + version = $PackageVersion + description = $Description + homepage = "https://cmder.app" + license = "MIT" + url = "$BaseUrl/$AssetName" + hash = $AssetSha256.ToLowerInvariant() + pre_install = @( + 'if (!(Test-Path "$persist_dir\vendor\conemu-maximus5\ConEmu.xml")) {', + ' Copy-Item "$dir\vendor\ConEmu.xml.default" "$dir\vendor\conemu-maximus5\ConEmu.xml"', + '}' + ) + bin = "Cmder.exe" + shortcuts = @(, @("Cmder.exe", "Cmder", '/start "%USERPROFILE%"')) + persist = $Persist + env_set = [ordered]@{ + CMDER_ROOT = '$dir' + ConEmuDir = '$dir\vendor\conemu-maximus5' + } + checkver = [ordered]@{ + github = "https://github.com/cmderdev/cmder" + } + autoupdate = [ordered]@{ + url = "https://github.com/cmderdev/cmder/releases/download/v`$version/$AssetName" + hash = [ordered]@{ + url = '$baseurl/hashes.txt' + regex = '$basename\s+$sha256' + } + } + } + + Write-JsonFile -Path (Join-Path $Destination "scoop\bucket\$PackageId.json") -Value $manifest +} + $packageVersion = Get-CmderVersion if (-not $ReleaseTag) { $ReleaseTag = "v$packageVersion" @@ -372,4 +428,23 @@ New-ChocolateyPackage ` -BaseUrl $AssetBaseUrl ` -Destination $OutputPath +New-ScoopManifest ` + -PackageId "cmder-full" ` + -Description "Portable console emulator for Windows. (Full version)" ` + -AssetName "cmder.7z" ` + -AssetSha256 $hashes["cmder.7z"] ` + -PackageVersion $packageVersion ` + -BaseUrl $AssetBaseUrl ` + -Destination $OutputPath + +New-ScoopManifest ` + -PackageId "cmder" ` + -Description "Portable console emulator for Windows. (Mini version)" ` + -AssetName "cmder_mini.zip" ` + -AssetSha256 $hashes["cmder_mini.zip"] ` + -PackageVersion $packageVersion ` + -BaseUrl $AssetBaseUrl ` + -Destination $OutputPath ` + -Persist @("bin", "config", "opt", "vendor\conemu-maximus5\ConEmu.xml") + Write-Host "Generated package-manager files in $OutputPath"