ci(winget): space out the issue and the manifest PR
Filing the Package Request issue and opening the PR that resolves it back to back reads as automated, so sleep a randomized interval after the issue step before wingetcreate runs. Document the WINGET_ISSUE_TOKEN / WINGET_PR_TOKEN split and the read:org and read:discussion scopes WINGET_PR_TOKEN needs for gh pr edit, in AGENTS.md and the release skill. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Co-Authored-By: siGit Code v1.6.8-acp <noreply@sigit.si>
Seto Elkahfi committed
Oct 12, 2026 at 20:01 UTC
33f3304821410cadbdbf299e72e6ba2655840ac1
3 files changed
+21
-8
.agents/AGENTS.md
+7
-2
@@ -541,8 +541,13 @@ Three of these need credentials or a one-time manual step before they work:
541
542
- Scoop needs a `getsigit/scoop-bucket` repo and a `SCOOP_BUCKET_TOKEN` secret, mirroring the
543
Homebrew tap setup.
544
-- winget needs a `WINGET_TOKEN` (PAT with `public_repo` scope) so `wingetcreate` can fork
545
- `microsoft/winget-pkgs` and open the manifest PR. It is passed as
544
+- winget files a Package Request issue first and then opens the manifest PR that resolves it,
545
+ authored by two separate classic PATs (replacing the old single `WINGET_TOKEN`):
546
+ `WINGET_ISSUE_TOKEN` (`public_repo`) for the issue and `WINGET_PR_TOKEN` for the PR.
547
+ `WINGET_PR_TOKEN` needs `public_repo` plus `read:org` and `read:discussion`: `public_repo`
548
+ lets `wingetcreate` fork `microsoft/winget-pkgs` and open the PR, and the later `gh pr edit`
549
+ that sets the PR body runs a GraphQL query resolving org/team and discussion fields that
550
+ fails with a scope error on a `public_repo`-only token. The PR token is passed as
551
`WINGET_CREATE_GITHUB_TOKEN` rather than `--token`, which wingetcreate warns can leak the
552
token into logs. `wingetcreate update` only works on a package that already exists in
553
`microsoft/winget-pkgs`, so the workflow checks the `manifests/g/getSigit/siGitCode` path
.agents/skills/sigit-code-release/SKILL.md
+2
-2
@@ -38,7 +38,7 @@ Use this skill when preparing a release for this repository.
38
- The GitHub release body is generated, not hand-written: `release-github.yml`'s release job checks out the tag, awk-extracts the `## <version>` section from `CHANGELOG.md` (a missing section is a hard failure), appends a full-changelog link and a `sigit.si/code` footnote, and publishes it via `body_path: release_notes.md`. The only file a release-prep change needs to touch is `CHANGELOG.md` — never edit release notes in the GitHub UI, because the next tag push would overwrite them.
39
- npm and NuGet publish via OIDC trusted publishing — neither has a stored token in the repo. On npm this is configured per package (each `@getsigit/*` name has a trusted publisher pointing at `release-npm.yml`), so a new package needs the same setup before its first publish will work. OIDC requires npm >= 11.5.1 and Node >= 22.14.0.
40
- The NuGet package (`SiGit.Code`, `dotnet tool install --global SiGit.Code`) bundles all six platform binaries under `native/<os>-<arch>/` behind a managed shim, rather than shipping one package per platform like npm/PyPI. Watch the nuget.org 250 MB package-size limit if it ever grows.
41
-- The winget channel (`release-winget.yml`) needs a `WINGET_TOKEN` secret that is a **classic PAT** with `public_repo` scope — not a fine-grained PAT. wingetcreate forks `microsoft/winget-pkgs` on first use and submits a PR from that fork; a fine-grained PAT scoped to `getsigit/sigit` cannot fork a repo outside its scope and every release fails with a 403/404 on the fork step. The token is passed as `WINGET_CREATE_GITHUB_TOKEN` (not `--token`) so it doesn't leak into logs. `wingetcreate update` only works on a package that already exists in `microsoft/winget-pkgs`, so the workflow checks `manifests/g/getSigit/siGitCode` first and falls back to `wingetcreate submit` with `packaging/winget/*.yaml.in` for the first release; every later release takes the `update` path. If a winget release fails with a fork/permission error, check the `WINGET_TOKEN` secret type before re-dispatching.
41
+- The winget channel (`release-winget.yml`) files a Package Request issue first and then opens the manifest PR that resolves it, authored by **two separate classic PATs** (not fine-grained) — `WINGET_ISSUE_TOKEN` for the issue and `WINGET_PR_TOKEN` for the PR, replacing the old single `WINGET_TOKEN`. `WINGET_ISSUE_TOKEN` needs `public_repo`. `WINGET_PR_TOKEN` needs `public_repo` **plus `read:org` and `read:discussion`**: wingetcreate forks `microsoft/winget-pkgs` and opens the PR (fork needs `public_repo`; a fine-grained PAT scoped to `getsigit/sigit` can't fork an outside repo and 403/404s), and the "Set the winget PR body" step then runs `gh pr edit`, whose GraphQL query resolves org/team and discussion fields and fails with a scope error on a `public_repo`-only token (CI run 38262301933). The PR token is passed as `WINGET_CREATE_GITHUB_TOKEN` (not `--token`) so it doesn't leak into logs. `wingetcreate update` only works on a package that already exists in `microsoft/winget-pkgs`, so the workflow checks `manifests/g/getSigit/siGitCode` first and falls back to `wingetcreate submit` with `packaging/winget/*.yaml.in` for the first release; every later release takes the `update` path. If a winget release fails on the fork step, check the token type; if it fails on the `gh pr edit` step with a scope error, check `WINGET_PR_TOKEN`'s scopes before re-dispatching.
42
- The baked-in official MCP server is separately listed in the public MCP Registry as `si.sigit/sigit` (`release-mcp-registry.yml`, driven by `server.json` at the repo root). It's a remote Streamable-HTTP listing, so it's verified by a DNS TXT record on the `sigit.si` domain rather than the GitHub-OIDC scheme used for package listings.
43
44
## Git release flow
@@ -121,4 +121,4 @@ flows start from different base commits and are not interchangeable.
121
- Package names, install commands, and branding stay consistent across npm, PyPI, crates.io, Homebrew, NuGet, Scoop, winget, AUR, and the `.deb`/`.rpm` packages.
122
- Every release asset (including the new `.deb`/`.rpm`/raw binaries consumed by Scoop, winget, and AUR) has a matching `.sha256` sidecar.
123
- Any known release limitations are called out explicitly, including a dispatch failure in `release-scoop`/`release-winget`/`release-aur` (non-fatal — logged as a warning by `release-github.yml`) versus a hard failure in a directly tag-triggered workflow like `release-nuget` or `release-mcp-registry`.
124
-- The `WINGET_TOKEN` secret is a classic PAT with `public_repo` scope (not fine-grained), so wingetcreate can fork `microsoft/winget-pkgs`. If a winget release fails on the fork step, verify the token type before re-dispatching.
124
+- The winget channel uses two classic PATs (not fine-grained): `WINGET_ISSUE_TOKEN` (`public_repo`) for the Package Request issue and `WINGET_PR_TOKEN` (`public_repo` + `read:org` + `read:discussion`) for the manifest PR, so wingetcreate can fork `microsoft/winget-pkgs` and `gh pr edit` can set the PR body. If a winget release fails on the fork step, verify the token type; if it fails on the `gh pr edit` step, verify `WINGET_PR_TOKEN`'s scopes before re-dispatching.
.github/workflows/release-winget.yml
+12
-4
@@ -132,8 +132,11 @@ jobs:
132
# The issue and the PR are authored by two separate PATs, configured as
133
# the repo secrets WINGET_ISSUE_TOKEN (the Package Request issue) and
134
# WINGET_PR_TOKEN (the manifest PR). Both need public_repo scope on
135
- # microsoft/winget-pkgs (fork + open issues/PRs). WINGET_PR_TOKEN
136
- # replaces the old single WINGET_TOKEN.
135
+ # microsoft/winget-pkgs (fork + open issues/PRs). WINGET_PR_TOKEN also
136
+ # needs read:org and read:discussion: the "Set the winget PR body" step
137
+ # below calls `gh pr edit`, whose GraphQL query resolves org/team and
138
+ # discussion fields and fails with a scope error without them.
139
+ # WINGET_PR_TOKEN replaces the old single WINGET_TOKEN.
140
- name: File the winget Package Request issue
141
id: issue
142
shell: bash
@@ -182,6 +185,11 @@ jobs:
185
echo "issue_url=${ISSUE_URL}" >> "$GITHUB_OUTPUT"
186
echo "issue_number=${ISSUE_URL##*/}" >> "$GITHUB_OUTPUT"
187
188
+ - name: Sebat dulu ...
189
+ if: steps.issue.outputs.issue_number != ''
190
+ shell: bash
191
+ run: sleep $(( RANDOM % 61 + 150 ))
192
+
193
- name: Submit new package
194
id: submit_new
195
if: steps.exists.outputs.found == 'false'
@@ -194,7 +202,7 @@ jobs:
202
WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_PR_TOKEN }}
203
run: |
204
if (-not $env:WINGET_CREATE_GITHUB_TOKEN) {
197
- Write-Error "WINGET_PR_TOKEN is not set. It needs a PAT with public_repo scope so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR."
205
+ Write-Error "WINGET_PR_TOKEN is not set. It needs a PAT with public_repo, read:org, and read:discussion scopes so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR, and gh can edit the PR body."
206
}
207
208
$version = "${{ steps.release.outputs.version }}"
@@ -228,7 +236,7 @@ jobs:
236
WINGET_CREATE_GITHUB_TOKEN: ${{ secrets.WINGET_PR_TOKEN }}
237
run: |
238
if (-not $env:WINGET_CREATE_GITHUB_TOKEN) {
231
- Write-Error "WINGET_PR_TOKEN is not set. It needs a PAT with public_repo scope so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR."
239
+ Write-Error "WINGET_PR_TOKEN is not set. It needs a PAT with public_repo, read:org, and read:discussion scopes so wingetcreate can fork microsoft/winget-pkgs and open the manifest PR, and gh can edit the PR body."
240
}
241
242
$tag = "${{ steps.release.outputs.tag }}"