diff --git a/.github/workflows/squad-standalone-release.yml b/.github/workflows/squad-standalone-release.yml index 1a4582e4d..b8e631e4e 100644 --- a/.github/workflows/squad-standalone-release.yml +++ b/.github/workflows/squad-standalone-release.yml @@ -429,19 +429,41 @@ 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 path: packaging - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 #v7 - with: - repository: bradygaster/homebrew-squad - ref: main - path: homebrew-tap - fetch-depth: 1 - token: ${{ secrets.HOMEBREW_TAP_TOKEN }} - persist-credentials: true + - 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; never passed to the read-only clone) + 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 +593,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 +648,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 @@ -702,29 +740,40 @@ 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 - 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 + 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 }} + 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' diff --git a/test/standalone-release-workflow.test.ts b/test/standalone-release-workflow.test.ts index 2d378fa0b..5ef612717 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'); @@ -666,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}');