Skip to content

Sync Winget

Sync Winget #216

Workflow file for this run

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 }}