From 35f845259ad50cd22e9fe77118d924369a121867 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 12 Sep 2026 13:41:25 +0000 Subject: [PATCH] Harden CI: bind desktop-artifacts' version into env, not into the script The four packaging steps in desktop-artifacts.yml each read the resolved version by expanding `${{ steps.ver.outputs.version }}` directly inside a `run:` block. That substitution is textual: the runner splices the value into the script body before pwsh or bash ever parses it, so the value is evaluated as code rather than read as data. Every such site is one zizmor `template-injection` finding, and this workflow held nine of them -- the largest single concentration in the repository. Each step now receives the value as a step-level `env:` var and reads it back as a variable ($Env:APP_VERSION in pwsh, "$APP_VERSION" in bash), which is the same env-binding fix already applied to auto-deploy-cloud (#5673), verify-published-wheel (#5744) and the smoke-test matrix (#5363). The interpolation disappears from the script text; the packaging logic, the artifact names and the fixed-name stable-URL copies are unchanged. In the NSIS step the value is bound once into `$version` and the three later uses read that, replacing five separate expansions. PowerShell member-access ambiguity is avoided by writing `${version}` where a literal `.` follows the reference. Scope note: `permissions:` is untouched. This workflow is one of the six that need write scopes -- its `release` job elevates to `contents: write` to attach installers to the tag's Release -- and that elevation, plus the least-privilege `contents: read` top level, is exactly as it was. Verified: every workflow file still parses, and the repository's workflow guards pass (tests/test_workflow_yaml_valid.py + tests/test_ci_workflow_invocations_are_real.py: 594 passed, 369 skipped). No `${{ }}` expansion remains inside any `run:` block in this file. No-PRD: CI-only change under .github/, no product code touched. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01WGkmcn95GPNqMaK6dAnVNw --- .github/workflows/desktop-artifacts.yml | 32 +++++++++++++++++++------ 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/.github/workflows/desktop-artifacts.yml b/.github/workflows/desktop-artifacts.yml index 738c581473..5520235fd6 100644 --- a/.github/workflows/desktop-artifacts.yml +++ b/.github/workflows/desktop-artifacts.yml @@ -455,8 +455,14 @@ jobs: - name: Zip distribution folder working-directory: dist shell: pwsh + # The resolved version reaches the script through the environment + # rather than being expanded into it: a `${{ }}` substitution is + # textual, so the runner splices the value into the script body + # before pwsh ever parses it. Reading $Env: keeps it a value. + env: + APP_VERSION: ${{ steps.ver.outputs.version }} run: | - $version = "${{ steps.ver.outputs.version }}" + $version = $Env:APP_VERSION Compress-Archive -Path ClawMetry -DestinationPath "ClawMetry-$version-windows.zip" # Fixed-name copy so clawmetry.com/download/windows-zip can link a # stable URL (GitHub's /releases/latest/download/) instead @@ -478,6 +484,9 @@ jobs: # wrapper reads CLAWMETRY_SIGN_PW from this step's environment. env: CLAWMETRY_SIGN_PW: ${{ secrets.WINDOWS_CERT_PASSWORD }} + # Same reasoning as the zip step above: bound as a value, not + # spliced into the script text. + APP_VERSION: ${{ steps.ver.outputs.version }} run: | $makensis = (where.exe makensis 2>$null | Select-Object -First 1) if (-not $makensis) { @@ -487,15 +496,16 @@ jobs: # VIVERSION must be numeric x.y.z.w for VIProductVersion; releases # are x.y.z so append .0 (non-numeric dev versions fall back to # the script's 0.0.0.0 default by passing nothing). - $nsisArgs = @("/DVERSION=${{ steps.ver.outputs.version }}") - if ("${{ steps.ver.outputs.version }}" -match '^\d+\.\d+\.\d+$') { $nsisArgs += "/DVIVERSION=${{ steps.ver.outputs.version }}.0" } + $version = $Env:APP_VERSION + $nsisArgs = @("/DVERSION=$version") + if ($version -match '^\d+\.\d+\.\d+$') { $nsisArgs += "/DVIVERSION=${version}.0" } if ($Env:SIGN_ENABLED -eq 'true') { $nsisArgs += "/DSIGN_CMD=$Env:SIGN_CMD" } $nsisArgs += @("/DSRC_DIR=$PWD\dist\ClawMetry", "desktop\installer\windows.nsi") & $makensis @nsisArgs if ($LASTEXITCODE -ne 0) { throw "makensis failed with exit code $LASTEXITCODE" } - Move-Item "desktop\installer\ClawMetry-Setup-${{ steps.ver.outputs.version }}.exe" "dist\ClawMetry-Setup-${{ steps.ver.outputs.version }}.exe" + Move-Item "desktop\installer\ClawMetry-Setup-${version}.exe" "dist\ClawMetry-Setup-${version}.exe" # Fixed-name copy โ€” same stable-URL convention as the zip above. - Copy-Item "dist\ClawMetry-Setup-${{ steps.ver.outputs.version }}.exe" "dist\ClawMetry-windows-setup.exe" -Force + Copy-Item "dist\ClawMetry-Setup-${version}.exe" "dist\ClawMetry-windows-setup.exe" -Force # Verify the artifact, not the build log (FLYWHEEL ยง0b.5): a chained # Authenticode signature must actually be on the setup exe and the @@ -632,8 +642,12 @@ jobs: - name: Tarball distribution folder working-directory: dist + # Bound through the environment rather than expanded into the script + # body โ€” see the Windows zip step for the reasoning. + env: + APP_VERSION: ${{ steps.ver.outputs.version }} run: | - VERSION="${{ steps.ver.outputs.version }}" + VERSION="$APP_VERSION" tar czf "clawmetry-${VERSION}-linux-x86_64.tar.gz" clawmetry # Fixed-name copy so clawmetry.com/download/linux-tarball can link # a stable URL (GitHub's /releases/latest/download/) instead @@ -649,9 +663,13 @@ jobs: # containers). - name: Build AppImage working-directory: dist + # Bound through the environment rather than expanded into the script + # body โ€” see the Windows zip step for the reasoning. + env: + APP_VERSION: ${{ steps.ver.outputs.version }} run: | set -e - VERSION="${{ steps.ver.outputs.version }}" + VERSION="$APP_VERSION" APPDIR="ClawMetry.AppDir" mkdir -p "$APPDIR/usr/bin" cp -r clawmetry/. "$APPDIR/usr/bin/"