Sync Winget #216
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: Sync Winget | |
| on: | |
| push: | |
| paths: | |
| - 'bucket/*.json' | |
| workflow_dispatch: | |
| inputs: | |
| manifests: | |
| description: 'Comma or whitespace separated manifest names to process (without bucket/ and .json)' | |
| required: false | |
| type: string | |
| jobs: | |
| detect: | |
| name: Detect manifest version changes | |
| runs-on: ubuntu-latest | |
| if: github.event_name != 'push' || github.actor != 'github-actions[bot]' | |
| outputs: | |
| has_updates: ${{ steps.collect.outputs.has_updates }} | |
| packages: ${{ steps.collect.outputs.packages }} | |
| steps: | |
| - name: Checkout bucket | |
| uses: actions/checkout@main | |
| with: | |
| fetch-depth: 0 | |
| - name: Collect Winget updates | |
| id: collect | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| before="${{ github.event.before }}" | |
| head_sha="${{ github.sha }}" | |
| map_file="winget/package-map.json" | |
| event_name="${{ github.event_name }}" | |
| dispatch_manifests="${{ github.event.inputs.manifests || '' }}" | |
| emit_empty() { | |
| echo "has_updates=false" >> "$GITHUB_OUTPUT" | |
| echo "packages=[]" >> "$GITHUB_OUTPUT" | |
| } | |
| if [ "$event_name" != "workflow_dispatch" ] && { [ -z "$before" ] || [ "$before" = "0000000000000000000000000000000000000000" ]; }; then | |
| before="$(git rev-list --max-count=1 HEAD^ 2>/dev/null || true)" | |
| fi | |
| if [ ! -f "$map_file" ]; then | |
| emit_empty | |
| exit 0 | |
| fi | |
| if [ "$event_name" = "workflow_dispatch" ]; then | |
| if [ -n "$dispatch_manifests" ]; then | |
| mapfile -t changed_files < <( | |
| printf '%s\n' "$dispatch_manifests" \ | |
| | tr ',\t ' '\n\n\n' \ | |
| | sed -E 's/\r$//' \ | |
| | sed -E 's#^bucket/##; s#\.json$##' \ | |
| | sed '/^$/d' \ | |
| | sort -u \ | |
| | while IFS= read -r manifest_name; do | |
| manifest_path="bucket/${manifest_name}.json" | |
| [ -f "$manifest_path" ] && printf '%s\n' "$manifest_path" | |
| done | |
| ) | |
| else | |
| mapfile -t changed_files < <(find bucket -maxdepth 1 -type f -name '*.json' | sort) | |
| fi | |
| else | |
| mapfile -t changed_files < <( | |
| [ -n "$before" ] && git diff --name-only "$before" "$head_sha" -- 'bucket/*.json' | sort -u || true | |
| ) | |
| fi | |
| if [ "${#changed_files[@]}" -eq 0 ]; then | |
| emit_empty | |
| exit 0 | |
| fi | |
| updates_json='[]' | |
| for manifest_path in "${changed_files[@]}"; do | |
| manifest_name="$(basename "$manifest_path" .json)" | |
| winget_id=$(jq -r --arg name "$manifest_name" '.[$name] // empty' "$map_file") | |
| if [ -z "$winget_id" ]; then | |
| continue | |
| fi | |
| new_version=$(jq -r '.version // empty' "$manifest_path") | |
| if [ -z "$new_version" ]; then | |
| if [ "$event_name" = "workflow_dispatch" ]; then | |
| echo "::error::Manifest '$manifest_path' is missing a version field" | |
| exit 1 | |
| fi | |
| continue | |
| fi | |
| old_version="" | |
| if [ "$event_name" != "workflow_dispatch" ] && git cat-file -e "$before:$manifest_path" 2>/dev/null; then | |
| old_version=$(git show "$before:$manifest_path" | jq -r '.version // empty' || true) | |
| fi | |
| if [ "$event_name" != "workflow_dispatch" ]; then | |
| if [ -z "$old_version" ] || [ "$old_version" = "$new_version" ] || [ "$(printf '%s\n%s\n' "$old_version" "$new_version" | sort -V | tail -n1)" != "$new_version" ]; then | |
| echo "Skipping $manifest_path: version did not increase ($old_version -> $new_version)" | |
| continue | |
| fi | |
| fi | |
| winget_first_char="$(printf '%s' "$winget_id" | cut -c1 | tr '[:upper:]' '[:lower:]')" | |
| winget_manifest_base="manifests/${winget_first_char}/${winget_id//./\/}" | |
| versions_json=$(curl -fsSL "https://api.github.com/repos/microsoft/winget-pkgs/contents/${winget_manifest_base}") || { | |
| echo "Skipping $manifest_path: cannot read Winget package path for $winget_id" | |
| continue | |
| } | |
| winget_version=$(jq -r '[.[] | select(.type == "dir" and (.name | test("^[0-9]"))) | .name] | sort_by(split(".") | map(tonumber? // .)) | last // empty' <<< "$versions_json") | |
| if [ -z "$winget_version" ]; then | |
| echo "Skipping $manifest_path: no Winget versions found for $winget_id" | |
| continue | |
| fi | |
| if [ "$winget_version" = "$new_version" ] || [ "$(printf '%s\n%s\n' "$winget_version" "$new_version" | sort -V | tail -n1)" != "$new_version" ]; then | |
| echo "Skipping $manifest_path: Scoop version is not newer than Winget ($new_version <= $winget_version)" | |
| continue | |
| fi | |
| installer_manifest_path="${winget_manifest_base}/${winget_version}/${winget_id}.installer.yaml" | |
| installer_manifest=$(curl -fsSL "https://raw.githubusercontent.com/microsoft/winget-pkgs/master/${installer_manifest_path}") || { | |
| echo "Skipping $manifest_path: cannot read $installer_manifest_path" | |
| continue | |
| } | |
| urls_json=$(printf '%s\n' "$installer_manifest" \ | |
| | sed -nE 's/^[[:space:]]*(-[[:space:]]*)?InstallerUrl:[[:space:]]*(.+)$/\2/p' \ | |
| | sed -E 's/[[:space:]]+$//' \ | |
| | jq -R -s 'split("\n") | map(select(length > 0))') | |
| if [ "$(jq length <<< "$urls_json")" -eq 0 ]; then | |
| echo "Skipping $manifest_path: no InstallerUrl entries found in Winget manifest" | |
| continue | |
| fi | |
| urls_json=$(jq -c --arg old "$winget_version" --arg new "$new_version" ' | |
| map(if contains($old) then split($old) | join($new) else . end) | |
| ' <<< "$urls_json") | |
| updates_json=$(jq -c --arg id "$winget_id" --arg version "$new_version" --argjson urls "$urls_json" \ | |
| '. + [{identifier: $id, version: $version, urls: $urls}]' <<< "$updates_json") | |
| done | |
| if [ "$(jq length <<< "$updates_json")" -eq 0 ]; then | |
| emit_empty | |
| exit 0 | |
| fi | |
| echo "has_updates=true" >> "$GITHUB_OUTPUT" | |
| { | |
| echo "packages<<EOF" | |
| echo "$updates_json" | |
| echo "EOF" | |
| } >> "$GITHUB_OUTPUT" | |
| submit: | |
| name: Submit Winget updates | |
| runs-on: ubuntu-latest | |
| needs: detect | |
| if: needs.detect.outputs.has_updates == 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| package: ${{ fromJson(needs.detect.outputs.packages) }} | |
| steps: | |
| - name: Resolve Komac token | |
| id: token | |
| env: | |
| WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| token="${WINGET_TOKEN:-}" | |
| if [ -z "$token" ]; then | |
| echo "has_token=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| echo "::add-mask::$token" | |
| echo "has_token=true" >> "$GITHUB_OUTPUT" | |
| echo "token=$token" >> "$GITHUB_OUTPUT" | |
| - name: Skip Komac update when token is missing | |
| if: steps.token.outputs.has_token != 'true' | |
| run: echo "::warning::Skipping Komac submission because WINGET_TOKEN is not set" | |
| - name: Validate Komac token permissions | |
| if: steps.token.outputs.has_token == 'true' | |
| id: validate | |
| env: | |
| GH_TOKEN: ${{ steps.token.outputs.token }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| api() { | |
| curl -fsSL \ | |
| -H "Authorization: Bearer $GH_TOKEN" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| "$1" | |
| } | |
| viewer=$(api "https://api.github.com/user") | |
| login=$(jq -r '.login // empty' <<< "$viewer") | |
| if [ -z "$login" ]; then | |
| echo "::error::WINGET_TOKEN can authenticate, but GitHub did not return an account login." | |
| exit 1 | |
| fi | |
| echo "login=$login" >> "$GITHUB_OUTPUT" | |
| fork_repo=$(api "https://api.github.com/repos/${login}/winget-pkgs" 2>/dev/null || true) | |
| if [ -z "$fork_repo" ]; then | |
| echo "::error::WINGET_TOKEN belongs to '${login}', but https://github.com/${login}/winget-pkgs is missing or not accessible." | |
| echo "::error::Create a fork of microsoft/winget-pkgs under '${login}' and grant the token repository contents write access." | |
| exit 1 | |
| fi | |
| default_branch=$(jq -r '.default_branch // empty' <<< "$fork_repo") | |
| if [ -z "$default_branch" ]; then | |
| echo "::error::GitHub did not report a default branch for ${login}/winget-pkgs." | |
| exit 1 | |
| fi | |
| echo "default_branch=$default_branch" >> "$GITHUB_OUTPUT" | |
| if [ "$(jq -r '.fork // false' <<< "$fork_repo")" != "true" ]; then | |
| echo "::error::${login}/winget-pkgs exists, but GitHub does not report it as a fork." | |
| echo "::error::Komac submits through a fork of microsoft/winget-pkgs; recreate the fork before rerunning." | |
| exit 1 | |
| fi | |
| if [ "$(jq -r '.permissions.push // false' <<< "$fork_repo")" != "true" ]; then | |
| echo "::error::WINGET_TOKEN for '${login}' can read ${login}/winget-pkgs but cannot push branches to it." | |
| echo "::error::Grant repository contents write access to the token, or replace it with a token that can create refs in the fork." | |
| exit 1 | |
| fi | |
| - name: Sync winget-pkgs fork | |
| if: steps.token.outputs.has_token == 'true' | |
| env: | |
| GH_TOKEN: ${{ steps.token.outputs.token }} | |
| FORK_OWNER: ${{ steps.validate.outputs.login }} | |
| FORK_BRANCH: ${{ steps.validate.outputs.default_branch }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| response_file="$(mktemp)" | |
| status_code=$(curl -sSL -o "$response_file" -w '%{http_code}' -X POST \ | |
| -H "Authorization: Bearer $GH_TOKEN" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| "https://api.github.com/repos/${FORK_OWNER}/winget-pkgs/merge-upstream" \ | |
| -d "{\"branch\":\"${FORK_BRANCH}\"}") | |
| response="$(cat "$response_file")" | |
| if [ "$status_code" != "200" ]; then | |
| message=$(jq -r '.message // empty' <<< "$response") | |
| echo "::error::Failed to sync ${FORK_OWNER}/winget-pkgs:${FORK_BRANCH} before running Komac (HTTP ${status_code}${message:+: ${message}})." | |
| exit 1 | |
| fi | |
| message=$(jq -r '.message // empty' <<< "$response") | |
| if [ -n "$message" ]; then | |
| echo "::notice::$message" | |
| fi | |
| - name: Run Komac update | |
| if: steps.token.outputs.has_token == 'true' | |
| uses: michidk/run-komac@v2 | |
| with: | |
| args: >- | |
| update ${{ matrix.package.identifier }} | |
| --version ${{ matrix.package.version }} | |
| --urls ${{ join(matrix.package.urls, ' ') }} | |
| --submit | |
| --token ${{ steps.token.outputs.token }} |