From fa4652ba9b91a943ee051281a65d1292d9c7ede6 Mon Sep 17 00:00:00 2001 From: Tamir Dresher Date: Sat, 3 Oct 2026 11:18:51 +0300 Subject: [PATCH 1/3] fix(release): repair Homebrew/WinGet publish auth for external-repo checkouts (#2161) * fix(release): replace broken external-repo checkout auth for Homebrew/WinGet publish The publish-homebrew and publish-winget jobs used actions/checkout with a custom classic PAT against external repositories (bradygaster/homebrew-squad, microsoft/winget-pkgs, tamirdresher/winget-pkgs). This failed deterministically in the v1.0.0 release run (reproduced on rerun) with: fatal: could not read Username for 'https://github.com': terminal prompts disabled Root cause not fully isolated (token validity could not be checked from outside Actions), so this change removes the dependency on actions/checkout's token/includeIf mechanism entirely rather than guessing around it: - All three target repos are public, so the initial read/clone never actually needs authentication - switched to plain anonymous 'git clone'. - Push credentials (needed only for the later 'git push'/gh pr create steps) are now set via a local, non-global 'git config --local http..extraheader' scoped to just that one clone directory - never embedded in a remote URL, never printed, and cleaned up automatically when the job's workspace is discarded. - Added an explicit preflight step to each job that validates the relevant token via 'gh api user' and checks '.permissions.push' on the target repo before attempting any clone/push, so an actually-invalid/expired PAT fails fast with a clear, actionable error instead of surfacing as a confusing low-level git auth error. No changes to job graph, secrets, or permissions scopes. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * fix(release): keep actions/checkout for contract-tested steps, disable credential persistence The contract test (test/standalone-release-workflow.test.ts) asserts the Homebrew tap checkout and the 'Check out existing WinGet version branch' step remain real actions/checkout steps with specific with: fields (repository/ref/path/token) - replacing them with raw git clone broke that contract and failed the test job on PR #2161. Revert those two specific steps to actions/checkout, but add persist-credentials: false so the action does not attempt to write its own git credential config (the suspected source of the original 'could not read Username... terminal prompts disabled' failure). Credential configuration for the later git push is now done explicitly via a scoped, local-only http.extraheader in a separate step - never persisted globally, never logged, never embedded in a remote URL. The two clones that are not covered by the contract test (the read-only winget-base base clone and the new-branch clone from microsoft/winget-pkgs master) remain anonymous git clone, since no credential is needed to read these public repos. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot --- .../workflows/squad-standalone-release.yml | 98 ++++++++++++++----- 1 file changed, 75 insertions(+), 23 deletions(-) diff --git a/.github/workflows/squad-standalone-release.yml b/.github/workflows/squad-standalone-release.yml index 1a4582e4d..5befaf35d 100644 --- a/.github/workflows/squad-standalone-release.yml +++ b/.github/workflows/squad-standalone-release.yml @@ -429,6 +429,23 @@ jobs: exit 1 fi + - name: Preflight - verify Homebrew tap token + env: + GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + run: | + set -euo pipefail + login="$(gh api user --jq '.login')" || { + echo "::error::HOMEBREW_TAP_TOKEN failed to authenticate against the GitHub API. The token is likely invalid, expired, or revoked and must be rotated by a maintainer with write access to bradygaster/homebrew-squad." + exit 1 + } + echo "Authenticated as: ${login}" + push_access="$(gh api repos/bradygaster/homebrew-squad --jq '.permissions.push // false')" + echo "Push access to bradygaster/homebrew-squad: ${push_access}" + if [ "${push_access}" != "true" ]; then + echo "::error::HOMEBREW_TAP_TOKEN authenticates as '${login}', but that account/token does not have push access to bradygaster/homebrew-squad." + exit 1 + fi + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8 with: name: packaging-manifests @@ -439,9 +456,17 @@ jobs: repository: bradygaster/homebrew-squad ref: main path: homebrew-tap - fetch-depth: 1 token: ${{ secrets.HOMEBREW_TAP_TOKEN }} - persist-credentials: true + persist-credentials: false + + - name: Configure Homebrew tap push credential (scoped locally; checkout does not persist it) + env: + TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} + working-directory: homebrew-tap + run: | + set -euo pipefail + auth_header="Authorization: basic $(printf 'x-access-token:%s' "${TOKEN}" | base64 -w0)" + git config --local http.https://github.com/.extraheader "${auth_header}" - name: Publish cask to the tap env: @@ -571,6 +596,23 @@ jobs: exit 1 fi + - name: Preflight - verify WinGet publish token + env: + GH_TOKEN: ${{ secrets.WINGET_CREATE_GITHUB_TOKEN }} + run: | + set -euo pipefail + login="$(gh api user --jq '.login')" || { + echo "::error::WINGET_CREATE_GITHUB_TOKEN failed to authenticate against the GitHub API. The token is likely invalid, expired, or revoked and must be rotated by a maintainer with push access to tamirdresher/winget-pkgs." + exit 1 + } + echo "Authenticated as: ${login}" + push_access="$(gh api repos/tamirdresher/winget-pkgs --jq '.permissions.push // false')" + echo "Push access to tamirdresher/winget-pkgs: ${push_access}" + if [ "${push_access}" != "true" ]; then + echo "::error::WINGET_CREATE_GITHUB_TOKEN authenticates as '${login}', but that account/token does not have push access to tamirdresher/winget-pkgs." + exit 1 + fi + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c #v8 with: name: packaging-manifests @@ -609,15 +651,14 @@ jobs: echo "package_identifier=${PACKAGE_IDENTIFIER}" >> "${GITHUB_OUTPUT}" echo "package_root=${package_root}" >> "${GITHUB_OUTPUT}" - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7 - with: - repository: microsoft/winget-pkgs - ref: master - path: winget-base - fetch-depth: 1 - sparse-checkout: ${{ steps.release.outputs.package_root }} - token: ${{ secrets.WINGET_CREATE_GITHUB_TOKEN }} - persist-credentials: false + - name: Clone WinGet manifests directory (read-only, public repo; no credential needed) + env: + PACKAGE_ROOT: ${{ steps.release.outputs.package_root }} + run: | + set -euo pipefail + git clone --no-checkout --depth 1 --branch master --filter=blob:none https://github.com/microsoft/winget-pkgs.git winget-base + git -C winget-base sparse-checkout set "${PACKAGE_ROOT}" + git -C winget-base checkout master - name: Check WinGet publication state id: guard @@ -709,22 +750,33 @@ jobs: repository: tamirdresher/winget-pkgs ref: ${{ steps.release.outputs.branch }} path: winget-pkgs + token: ${{ secrets.WINGET_CREATE_GITHUB_TOKEN }} + persist-credentials: false fetch-depth: 1 sparse-checkout: ${{ steps.release.outputs.package_root }} - token: ${{ secrets.WINGET_CREATE_GITHUB_TOKEN }} - persist-credentials: true - - name: Check out WinGet master for a new version branch + - name: Configure existing-branch push credential (scoped locally; checkout does not persist it) + if: steps.guard.outputs.skip == 'false' && steps.guard.outputs.branch_exists == 'true' + env: + TOKEN: ${{ secrets.WINGET_CREATE_GITHUB_TOKEN }} + working-directory: winget-pkgs + run: | + set -euo pipefail + auth_header="Authorization: basic $(printf 'x-access-token:%s' "${TOKEN}" | base64 -w0)" + git config --local http.https://github.com/.extraheader "${auth_header}" + + - name: Clone WinGet master for a new version branch (credential scoped locally for the later push) if: steps.guard.outputs.skip == 'false' && steps.guard.outputs.branch_exists == 'false' - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7 - with: - repository: microsoft/winget-pkgs - ref: master - path: winget-pkgs - fetch-depth: 1 - sparse-checkout: ${{ steps.release.outputs.package_root }} - token: ${{ secrets.WINGET_CREATE_GITHUB_TOKEN }} - persist-credentials: true + env: + TOKEN: ${{ secrets.WINGET_CREATE_GITHUB_TOKEN }} + PACKAGE_ROOT: ${{ steps.release.outputs.package_root }} + run: | + set -euo pipefail + git clone --no-checkout --depth 1 --branch master --filter=blob:none https://github.com/microsoft/winget-pkgs.git winget-pkgs + git -C winget-pkgs sparse-checkout set "${PACKAGE_ROOT}" + git -C winget-pkgs checkout master + auth_header="Authorization: basic $(printf 'x-access-token:%s' "${TOKEN}" | base64 -w0)" + git -C winget-pkgs config --local http.https://github.com/.extraheader "${auth_header}" - name: Push manifests and open upstream PR if: steps.guard.outputs.skip == 'false' From a785b2d60776c21bdbf4254a99b54e144bab4a62 Mon Sep 17 00:00:00 2001 From: Copilot Date: Sat, 3 Oct 2026 12:19:17 +0300 Subject: [PATCH 2/3] fix(release): avoid token-authenticated checkout for Homebrew tap read The Homebrew tap checkout previously used actions/checkout with 'token: secrets.HOMEBREW_TAP_TOKEN' and 'persist-credentials: false'. persist-credentials only controls whether the credential is scrubbed AFTER the step completes - it does not change how the step's own initial fetch authenticates. The real production failure (run 37104124444, job 111157909375) showed this exact checkout failing with 'fatal: could not read Username ... terminal prompts disabled' despite a valid, non-empty token (preflight passed) - meaning the prior persist-credentials:false change alone did not address the failing mechanism. bradygaster/homebrew-squad is a public repository, so the read-only clone does not need any credential at all. Replace the token-bearing actions/checkout with a plain anonymous 'git clone', and continue to configure the push credential as a locally-scoped git config entry (set only after the clone, only for the later push step) - this removes the token from the path that was actually failing while preserving scoped, non-persisted auth for the write operation. Root cause of the exact actions/checkout auth-setup failure itself remains unconfirmed (no definitive matching upstream report found); this change eliminates the observed failure path rather than claiming a confirmed upstream bug. Updates the standalone-release-workflow contract test to assert the new anonymous-clone + scoped-push-credential shape instead of the token-bearing checkout shape. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../workflows/squad-standalone-release.yml | 13 +++---- test/standalone-release-workflow.test.ts | 36 +++++++++++++------ 2 files changed, 31 insertions(+), 18 deletions(-) diff --git a/.github/workflows/squad-standalone-release.yml b/.github/workflows/squad-standalone-release.yml index 5befaf35d..ad7cab3bb 100644 --- a/.github/workflows/squad-standalone-release.yml +++ b/.github/workflows/squad-standalone-release.yml @@ -451,15 +451,12 @@ jobs: name: packaging-manifests path: packaging - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7 - with: - repository: bradygaster/homebrew-squad - ref: main - path: homebrew-tap - token: ${{ secrets.HOMEBREW_TAP_TOKEN }} - persist-credentials: false + - name: Clone Homebrew tap (anonymous read; bradygaster/homebrew-squad is public) + run: | + set -euo pipefail + git clone --depth 1 --branch main https://github.com/bradygaster/homebrew-squad.git homebrew-tap - - name: Configure Homebrew tap push credential (scoped locally; checkout does not persist it) + - name: Configure Homebrew tap push credential (scoped locally; never passed to the read-only clone) env: TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} working-directory: homebrew-tap diff --git a/test/standalone-release-workflow.test.ts b/test/standalone-release-workflow.test.ts index 2d378fa0b..7214120df 100644 --- a/test/standalone-release-workflow.test.ts +++ b/test/standalone-release-workflow.test.ts @@ -613,16 +613,32 @@ describe('automated package publication', () => { expect(wingetGate.run).toMatch(/classic PAT.*public_repo/); }); - it('updates only the tap cask and skips an identical manifest', () => { - const checkout = homebrew.steps?.find( - (step) => step.with?.repository === 'bradygaster/homebrew-squad', - ); - expect(checkout?.uses).toMatch(/^actions\/checkout@[0-9a-f]{40}$/); - expect(checkout?.with).toMatchObject({ - ref: 'main', - path: 'homebrew-tap', - token: '${{ secrets.HOMEBREW_TAP_TOKEN }}', - }); + it('clones the tap anonymously and scopes the push credential locally', () => { + const clone = stepNamed( + homebrew, + 'Clone Homebrew tap (anonymous read; bradygaster/homebrew-squad is public)', + ); + expect(clone.run).toContain( + 'git clone --depth 1 --branch main https://github.com/bradygaster/homebrew-squad.git homebrew-tap', + ); + // The read-only clone must never receive the publish token: if it did, the + // same auth-setup path that failed in production (actions/checkout + // configuring credentials for the initial fetch of an external repo) + // would be reintroduced. + expect(clone.run).not.toContain('secrets.HOMEBREW_TAP_TOKEN'); + expect(clone.env ?? {}).not.toHaveProperty('HOMEBREW_TAP_TOKEN'); + expect(homebrew.steps?.some((step) => step.with?.repository === 'bradygaster/homebrew-squad')).toBe(false); + + const credential = stepNamed( + homebrew, + 'Configure Homebrew tap push credential (scoped locally; never passed to the read-only clone)', + ); + expect(credential['working-directory']).toBe('homebrew-tap'); + expect(credential.env?.TOKEN).toBe('${{ secrets.HOMEBREW_TAP_TOKEN }}'); + expect(credential.run).toContain('git config --local http.https://github.com/.extraheader'); + expect(credential.run).not.toContain('--global'); + // The token must not be echoed/logged anywhere in the credential step. + expect(credential.run).not.toMatch(/echo.*TOKEN/); const publish = stepNamed(homebrew, 'Publish cask to the tap').run ?? ''; expect(publish).toContain('../packaging/homebrew/${CASK}.rb'); From 9ae555a84890d8044a255bbda4f8b4fd9837d054 Mon Sep 17 00:00:00 2001 From: Copilot Date: Sat, 3 Oct 2026 12:41:02 +0300 Subject: [PATCH 3/3] fix(release): apply same anonymous-clone pattern to WinGet branch reuse The 'Check out existing WinGet version branch' step (exercised only when re-publishing to an already-open version branch) used the same token-bearing actions/checkout + persist-credentials:false shape that was just shown to be unreliable for the Homebrew tap checkout. This exact path has never been exercised by a real production run (the only successful standalone-release run on record predates this branch-reuse code and always took the branch_exists=false path), so it carries the same latent risk without any positive evidence it works. tamirdresher/winget-pkgs is a public fork, so apply the identical fix: anonymous git clone for the read, scoped local push credential configured afterward only for the subsequent push. Update the matching contract test. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../workflows/squad-standalone-release.yml | 24 +++++++++---------- test/standalone-release-workflow.test.ts | 15 ++++++++++-- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/.github/workflows/squad-standalone-release.yml b/.github/workflows/squad-standalone-release.yml index ad7cab3bb..b8e631e4e 100644 --- a/.github/workflows/squad-standalone-release.yml +++ b/.github/workflows/squad-standalone-release.yml @@ -740,19 +740,19 @@ jobs: echo "skip=false" >> "${GITHUB_OUTPUT}" echo "branch_exists=${branch_exists}" >> "${GITHUB_OUTPUT}" - - name: Check out existing WinGet version branch + - name: Check out existing WinGet version branch (anonymous read; tamirdresher/winget-pkgs is public) if: steps.guard.outputs.skip == 'false' && steps.guard.outputs.branch_exists == 'true' - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7 - with: - repository: tamirdresher/winget-pkgs - ref: ${{ steps.release.outputs.branch }} - path: winget-pkgs - token: ${{ secrets.WINGET_CREATE_GITHUB_TOKEN }} - persist-credentials: false - fetch-depth: 1 - sparse-checkout: ${{ steps.release.outputs.package_root }} - - - name: Configure existing-branch push credential (scoped locally; checkout does not persist it) + env: + BRANCH: ${{ steps.release.outputs.branch }} + PACKAGE_ROOT: ${{ steps.release.outputs.package_root }} + run: | + set -euo pipefail + git clone --no-checkout --depth 1 --branch "${BRANCH}" --filter=blob:none \ + https://github.com/tamirdresher/winget-pkgs.git winget-pkgs + git -C winget-pkgs sparse-checkout set "${PACKAGE_ROOT}" + git -C winget-pkgs checkout "${BRANCH}" + + - name: Configure existing-branch push credential (scoped locally; never passed to the read-only clone) if: steps.guard.outputs.skip == 'false' && steps.guard.outputs.branch_exists == 'true' env: TOKEN: ${{ secrets.WINGET_CREATE_GITHUB_TOKEN }} diff --git a/test/standalone-release-workflow.test.ts b/test/standalone-release-workflow.test.ts index 7214120df..5ef612717 100644 --- a/test/standalone-release-workflow.test.ts +++ b/test/standalone-release-workflow.test.ts @@ -682,10 +682,21 @@ describe('automated package publication', () => { const existingCheckout = stepNamed( winget, - 'Check out existing WinGet version branch', + 'Check out existing WinGet version branch (anonymous read; tamirdresher/winget-pkgs is public)', ); - expect(existingCheckout.with?.repository).toBe('tamirdresher/winget-pkgs'); expect(existingCheckout.if).toContain("branch_exists == 'true'"); + expect(existingCheckout.run).toContain( + 'git clone --no-checkout --depth 1 --branch "${BRANCH}" --filter=blob:none \\\n https://github.com/tamirdresher/winget-pkgs.git winget-pkgs', + ); + expect(existingCheckout.run).not.toContain('secrets.WINGET_CREATE_GITHUB_TOKEN'); + expect(winget.steps?.some((step) => step.with?.repository === 'tamirdresher/winget-pkgs')).toBe(false); + + const existingCredential = stepNamed( + winget, + 'Configure existing-branch push credential (scoped locally; never passed to the read-only clone)', + ); + expect(existingCredential.env?.TOKEN).toBe('${{ secrets.WINGET_CREATE_GITHUB_TOKEN }}'); + expect(existingCredential.run).toContain('git config --local http.https://github.com/.extraheader'); const publish = stepNamed(winget, 'Push manifests and open upstream PR').run ?? ''; expect(publish).toContain('../packaging/winget/${manifest_name}');