Files
Ruslan KonviserandClaude Opus 5 a71e220509 fix(ci): keep the credential scrub INLINE — a local action cannot load if checkout fails
cubic raised this eight times on #9997 and it is right; worse, my PR body
explicitly dismissed it with "there's no workspace to scrub anyway", which is
false for the runners this step exists to protect.

A local composite action is resolved FROM the workspace. If `actions/checkout`
fails, `uses: ./.github/actions/scrub-registry-credentials` cannot be located, so
the `if: always()` step ERRORS instead of running. The previously inlined `run:`
had no such dependency and would still have scrubbed.

That matters because 30 of these 66 jobs check out with `clean: false`:

  jobs with the scrub: 66  of which checkout clean:false = 30

On those, a failed checkout leaves the PREVIOUS run's workspace in place — and any
live `//packages.ever.co/:_authToken=` in its .npmrc — readable by the next job on
that runner. Exactly the leak the step was added to close.

So the split is now drawn on the actual dependency:

  Configure Registry  -> composite action. It edits .npmrc/.yarnrc/yarn.lock in the
                         repo, so it genuinely cannot run without a checkout.
  Scrub credentials   -> stays INLINE, with a comment saying why, so nobody
                         "finishes the refactor" later and silently reopens this.

`.github/actions/scrub-registry-credentials/` is deleted rather than left unused —
an unused action that looks like the obvious next step is a trap.

Verified the scrub is unchanged, not merely restored:
  develop distinct scrub bodies: 1   now: 1
  scrub body identical to develop: true

Net effect vs develop: 19 files changed, 1192 insertions(+), 7110 deletions(-) —
about 5,900 lines of duplication still removed, without trading a credential leak
for it.

Also from this round (cubic P3, 2 comments): the `ever-k8s` retention rationale is
back beside the predicate at all 66 call sites. I had dropped it when moving the
predicate, which is the same comment-drift the extraction was meant to stop.

Not addressed, and pre-existing on develop rather than introduced here: the scrub's
symlink and staged-index edge cases (cubic D1-D3) and `always-auth=` without an
`_authToken=` line (D7). They apply equally to the merged develop implementation
and are worth their own change rather than widening a refactor.

Guards: 67 files parse; empty-${{ }} grep clean; cspell clean (reworded my own
coinage rather than adding it to the dictionary).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 15:06:31 +02:00

363 lines
18 KiB
YAML

name: Desktop App Build Demo
# The packaged desktop & server demo apps are built ONLY when 'develop' is promoted to the
# 'local-apps' branch. The build version is resolved at build time
# (.scripts/bump-version-electron.js) from the release tag of the promoted commit (on HEAD,
# or on the merge parent for PR-merge promotions), which 'Release Demo' creates on every
# merge to 'develop'.
on:
push:
branches:
- local-apps
workflow_dispatch:
concurrency:
group: ${{ github.ref }}-${{ github.workflow }}
cancel-in-progress: true
# Least-privilege scope for the automatic GITHUB_TOKEN.
# This workflow publishes its release assets with the separate `secrets.GH_TOKEN` PAT,
# which this block does not affect, so the automatic token only needs to read the repo.
permissions:
contents: read
jobs:
check-release-tag:
# Only build when the promoted commit carries a release tag (the version stamped into the
# packages - see header comment). 'local-apps' is promoted from 'develop' either by
# fast-forwarding to the tagged 'develop' commit (tag on HEAD) or by merging the
# 'develop' -> 'local-apps' promotion PR (the tag then points at the merged
# 'develop' tip, HEAD^2). Retries absorb the short delay until 'Release Demo' tags
# the 'develop' commit.
runs-on: ${{ vars.RUNNER_LINUX_X64_4 || 'ubuntu-latest' }}
timeout-minutes: 30
permissions:
contents: read
outputs:
# The resolved vX.Y.Z release tag; the build jobs stamp exactly this version.
tag: ${{ steps.resolve.outputs.tag }}
steps:
- name: Check out Git repository
uses: actions/checkout@v5
with:
persist-credentials: false
# Depth 2 so HEAD^2 resolves on merge-commit promotions
fetch-depth: 2
- name: Verify a release tag points at the promoted commit
id: resolve
shell: bash
run: |
if [ "$GITHUB_REF_NAME" != "local-apps" ]; then
echo "::error::This workflow only releases from the 'local-apps' branch (got '$GITHUB_REF_NAME')."
exit 1
fi
HEAD_SHA=$(git rev-parse HEAD)
# Present only when the promotion PR was merged as a merge commit; the release tag
# then points at the merged 'develop' tip, not at the merge commit itself.
PARENT2_SHA=$(git rev-parse --verify --quiet 'HEAD^2' || true)
resolve_tag() {
# Highest vX.Y.Z tag pointing at $1 in the remote listing (peeled '^{}' entries
# carry the commit sha of annotated tags); empty when none match.
printf '%s\n' "$REMOTE_REFS" | awk -v sha="$1" '
$1 == sha && $2 ~ /^refs\/tags\/v[0-9]+\.[0-9]+\.[0-9]+(\^\{\})?$/ {
t = $2
sub(/^refs\/tags\//, "", t)
sub(/\^\{\}$/, "", t)
print t
}' | sort -V | tail -n 1
}
for i in $(seq 1 20); do
if REMOTE_REFS=$(git ls-remote origin refs/heads/develop 'refs/tags/*'); then
SRC_TIP=$(printf '%s\n' "$REMOTE_REFS" | awk -v ref="refs/heads/develop" '$2 == ref { print $1 }')
TAG=$(resolve_tag "$HEAD_SHA")
# Accept the merge-parent tag only when HEAD^2 is the current 'develop' tip -
# i.e. this is the promotion merge of 'develop', not an arbitrary tagged branch
# merged in, nor a fast-forward racing 'Release Demo' (whose tag lands on HEAD).
if [ -z "$TAG" ] && [ -n "$PARENT2_SHA" ] && [ "$PARENT2_SHA" = "$SRC_TIP" ]; then
TAG=$(resolve_tag "$PARENT2_SHA")
fi
if [ -n "$TAG" ]; then
echo "Release tag on the promoted commit: $TAG"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
exit 0
fi
else
echo "git ls-remote failed (attempt $i); will retry"
fi
echo "No release tag points at this commit or its merge parent yet (attempt $i/20); retrying in 30s..."
sleep 30
done
echo "::error::No release tag points at this commit, and its merge parent does not match the tagged 'develop' tip. Promote by merging the 'develop' -> 'local-apps' PR (or fast-forwarding: git push origin origin/develop:local-apps) after 'Release Demo' has created the tag; if 'develop' has moved since the promotion PR was opened, re-promote."
exit 1
release-windows:
needs: check-release-tag
runs-on: ${{ matrix.os }}
timeout-minutes: 300
strategy:
matrix:
os: [[self-hosted, Windows, X64]]
steps:
- name: Check out Git repository
uses: actions/checkout@v5
with:
clean: false
- name: Selective cleanup (preserve .nx/cache)
shell: powershell
run: |
$ErrorActionPreference = 'SilentlyContinue'
# Stop NX daemon first to release file locks before cleanup (guard for fresh runners without Node)
if (Get-Command npx -ErrorAction SilentlyContinue) { npx nx daemon --stop 2>&1 | Out-Null }
# Remove build artifacts but keep NX cache for faster rebuilds
if (Test-Path "dist") { Remove-Item -Recurse -Force "dist" }
if (Test-Path "node_modules") { Remove-Item -Recurse -Force "node_modules" }
exit 0
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v6
with:
node-version: 24.17.0
- name: Install Visual Studio 2022 Build Tools (VCTools)
shell: powershell
run: |
choco install -y visualstudio2022buildtools --execution-timeout=21600 --package-parameters "--add Microsoft.VisualStudio.Workload.VCTools --includeRecommended --includeOptional --passive --norestart"
- name: Configure node-gyp to use VS 2022
shell: powershell
run: |
"GYP_MSVS_VERSION=2022" | Out-File -FilePath $env:GITHUB_ENV -Append
"npm_config_msvs_version=2022" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Fix node-gyp and Python
run: python3 -m pip install packaging setuptools
- name: Setup MSVC (VS 2022 dev env)
uses: ilammy/msvc-dev-cmd@v1
with:
arch: x64
- name: Install latest version of NPM
run: 'npm install -g npm@11.6.2'
- name: Install globally node-gyp, ts-node and nx packages
run: 'npm install --quiet -g node-gyp@10.2.0 ts-node@10.9.2 nx@^22.5.2'
- name: Configure npm python for node-gyp
shell: powershell
run: |
$py = (Get-Command python.exe).Source
Write-Host "python is: $py"
"npm_config_python=$py" | Out-File -FilePath $env:GITHUB_ENV -Append
"PYTHON=$py" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Configure Registry
uses: ./.github/actions/configure-registry
with:
verdaccio-registry: ${{ vars.VERDACCIO_REGISTRY }}
verdaccio-token: ${{ secrets.VERDACCIO_TOKEN }}
force-public: ${{ vars.VERDACCIO_FORCE_PUBLIC }}
# in-network runners: the self-hosted Windows boxes, plus any ever-k8s-* ARC pool a runner
# variable may select (release-linux takes its os from vars.RUNNER_LINUX_APPS_X64). Keep the
# ever-k8s disjunct even where a matrix cannot currently emit that label - it costs nothing,
# keeps all 66 call sites identical, and means a future ARC matrix entry inherits the VIP
# retry and the in-network warning instead of silently losing them.
expect-vip: ${{ contains(matrix.os, 'self-hosted') || contains(matrix.os, 'ever-k8s') }}
- name: Install Yarn dependencies
run: 'yarn install --network-timeout 1000000 --frozen-lockfile --ignore-scripts'
- name: Run Postinstall Manually
run: 'yarn postinstall.manual'
- name: Bump version desktop app
uses: actions/github-script@v8
with:
script: |
const script = require('./.scripts/bump-version-electron.js')
console.log(script.desktop(false))
env:
GAUZY_RELEASE_TAG: ${{ needs.check-release-tag.outputs.tag }}
PROJECT_REPO: 'https://github.com/ever-co/ever-gauzy.git'
DESKTOP_APP_NAME: 'gauzy-desktop'
# Demo electron releases go to the same repo
DESKTOP_APP_REPO_NAME: 'ever-gauzy'
DESKTOP_APP_REPO_OWNER: 'ever-co'
COMPANY_SITE_LINK: 'https://gauzy.co'
DESKTOP_APP_DESCRIPTION: 'Gauzy Desktop'
DESKTOP_APP_ID: 'com.ever.gauzydesktop'
- name: Fix Node.js PATH for child processes
shell: powershell
run: |
$ErrorActionPreference = "Stop"
$nodeExe = (Get-Command node -ErrorAction Stop).Source
$nodePath = Split-Path $nodeExe -Parent
$npmGlobalBin = & npm config get prefix
$localBin = Join-Path $PWD "node_modules\.bin"
$yarnCmd = Get-Command yarn -ErrorAction SilentlyContinue
$yarnPath = if ($yarnCmd) { Split-Path $yarnCmd.Source -Parent } else { "" }
$npmNodeExe = Join-Path $npmGlobalBin "node.exe"
if (-not (Test-Path $npmNodeExe)) { Copy-Item $nodeExe $npmNodeExe -Force }
$localNodeExe = Join-Path $localBin "node.exe"
if (-not (Test-Path $localNodeExe)) { Copy-Item $nodeExe $localNodeExe -Force }
$newPath = "$nodePath;$npmGlobalBin;$localBin;$yarnPath;$($env:PATH)"
"PATH=$newPath" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
@($nodePath, $npmGlobalBin, $localBin, $yarnPath) | Where-Object { $_ } | ForEach-Object {
$_ | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8
}
"NODE=$nodeExe" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
"NODE_PATH=$nodePath" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
$env:PATH = $newPath
[System.Environment]::SetEnvironmentVariable("PATH", $newPath, "Process")
- name: Ensure dist directory exists
shell: bash
run: mkdir -p dist/packages
- name: Increase file handle limits
shell: powershell
run: |
# Increase Node.js UV threadpool for parallel I/O (default is 4)
"UV_THREADPOOL_SIZE=32" | Out-File -FilePath $env:GITHUB_ENV -Append
# Patch graceful-fs to retry EMFILE errors with backoff
node -e "try { var gfs = require('graceful-fs'); gfs.gracefulify(require('fs')); console.log('graceful-fs patched'); } catch(e) { console.log('graceful-fs not available, skipping'); }"
- name: Reset NX
shell: powershell
run: npx nx reset
- name: Build Desktop App
shell: cmd
run: 'yarn build:desktop:windows:release:gh:x64'
env:
USE_HARD_LINKS: false
ELECTRON_BUILDER_CACHE: ${{ github.workspace }}\.cache\electron-builder
GH_TOKEN: ${{ secrets.GH_TOKEN }}
EP_GH_IGNORE_TIME: true
SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
SENTRY_TRACES_SAMPLE_RATE: '${{ secrets.SENTRY_TRACES_SAMPLE_RATE }}'
SENTRY_PROFILE_SAMPLE_RATE: '${{ secrets.SENTRY_PROFILE_SAMPLE_RATE }}'
SENTRY_HTTP_TRACING_ENABLED: '${{ secrets.SENTRY_HTTP_TRACING_ENABLED }}'
SENTRY_POSTGRES_TRACKING_ENABLED: '${{ secrets.SENTRY_POSTGRES_TRACKING_ENABLED }}'
SENTRY_PROFILING_ENABLED: '${{ secrets.SENTRY_PROFILING_ENABLED }}'
NX_NO_CLOUD: true
NX_PLUGIN_NO_TIMEOUTS: true
# Run Nx plugins in-process: isolated plugin workers intermittently fail to spawn on the
# self-hosted Windows runners ("Failed to start plugin worker") -> random package build
# failures. Matches the e2e workflow's proven setting on the same box.
NX_ISOLATE_PLUGINS: false
NX_DAEMON: false
PROJECT_REPO: 'https://github.com/ever-co/ever-gauzy.git'
# Demo electron releases go to the same repo
DESKTOP_APP_REPO_OWNER: 'ever-co'
DESKTOP_APP_REPO_NAME: 'ever-gauzy'
DESKTOP_APP_NAME: 'gauzy-desktop'
DESKTOP_APP_DESCRIPTION: 'Gauzy Desktop'
DESKTOP_APP_ID: 'com.ever.gauzydesktop'
COMPANY_SITE_LINK: 'https://gauzy.co'
- name: Scrub registry credentials
if: always()
shell: bash
# DELIBERATELY INLINE, not a composite action. A local action is resolved from the
# workspace, so a failed checkout means it cannot load and this step errors instead of
# running. 30 of these jobs check out with `clean: false`, where the previous run's
# workspace - and any live _authToken in it - survives; that is the exact case this step
# exists to cover. Configure Registry is a composite action because it genuinely needs the checkout.
run: |
# The auth token must not outlive the job. These runners check out with clean: false and
# clean only dist/ and node_modules/, so a workspace .npmrc carrying
# //packages.ever.co/:_authToken=... would sit on disk after the job ends - readable by
# anything scheduled on this runner before the next Configure Registry step resets it.
#
# This is deliberately the LAST step of the job: the build steps above run
# postinstall.electron / electron-builder install-app-deps, which resolve dependencies,
# so the credential has to survive until they are done. Only the credential lines go;
# the registry= line stays. Runs on failure too, which is when it would linger.
#
# No 'sed -i.bak': the backup would itself hold the token if this step were interrupted.
# The temp file only ever holds the SCRUBBED content, so a partial run leaks nothing.
# Cleanup policy, precisely:
# * individual cleanup ATTEMPTS are best-effort. Under `set -e` a failing sed or mv
# would abort this step before the token was removed - the exact outcome the step
# exists to prevent - so nothing is allowed to short-circuit it.
# * the POSTCONDITION is not best-effort. A surviving CREDENTIAL fails the step, because
# handing a live token to the next job on a reused clean: false runner is worse than a
# red build. Leftover registry STATE (a stale .yarnrc, a yarn.lock still rewritten to
# the VIP) only warns: it is a correctness nuisance for an unrelated workflow, not a
# secret, and the next Configure Registry step resets it anyway.
# Anything that cannot be DETERMINED counts as dirty, so an unreadable file is never
# mistaken for a clean one (grep exits 2 on a read error, which is not "no token").
set +e
# Restoring the tracked files from git is the primary mechanism: it reverts the whole
# file, so the credential, the appended registry= line and the yarn.lock rewrite all go
# in one operation.
git checkout -- .npmrc yarn.lock 2>/dev/null
# Fallback for a workspace where git cannot run at all.
if [ -f .npmrc ]; then
sed -e '/_authToken=/d' -e '/always-auth=/d' .npmrc > .npmrc.scrubbed 2>/dev/null && mv -f .npmrc.scrubbed .npmrc
fi
rm -f .npmrc.bak .npmrc.scrubbed .yarnrc yarn.lock.bak yarn.lock.rewritten
# Prove the credential is gone, starting from "undetermined" rather than "absent" so no
# inconclusive result can pass. Two ways to be inconclusive: grep exits 2 when a file
# cannot be READ, and [ -f ] answers false for both "missing" and "cannot stat", so a
# bare existence test cannot tell an absent file from an unreachable one.
cred_state="undetermined"
if [ -e .npmrc ] || [ -L .npmrc ]; then
grep -q '_authToken=' .npmrc
case "$?" in
0) cred_state="present" ;;
1) cred_state="absent" ;;
*) cred_state="undetermined" ;;
esac
elif [ -r . ] && [ -x . ]; then
# The directory is both readable AND searchable and neither a file nor a symlink named
# .npmrc exists, so the absence is proven rather than merely unobservable. Without the
# -x test a stat could fail in a directory that still answers -r, and without the -L
# test above a dangling symlink would read as "missing" while its target held a token.
cred_state="absent"
fi
# Artifacts that can also carry the token: .npmrc.bak is written by older revisions of
# this workflow, and a surviving .npmrc.scrubbed means the mv above did not complete.
# Present-but-clean is only clutter; present-and-carrying-a-token (or unreadable) is not.
for stray in .npmrc.bak .npmrc.scrubbed; do
if [ -e "$stray" ] || [ -L "$stray" ]; then
grep -q '_authToken=' "$stray"
if [ "$?" -ne 1 ]; then
cred_state="present in $stray"
fi
fi
done
if [ "$cred_state" != "absent" ]; then
echo "::error title=Registry credential may still be present::Auth token is $cred_state after cleanup on ${RUNNER_NAME:-this runner}."
exit 1
fi
# Report - but do not fail on - leftover registry state.
leftover=""
[ -e .yarnrc ] && leftover="$leftover .yarnrc"
git diff --quiet -- .npmrc yarn.lock 2>/dev/null
case "$?" in
0) ;;
1) leftover="$leftover .npmrc/yarn.lock(modified)" ;;
*) leftover="$leftover .npmrc/yarn.lock(unverifiable)" ;;
esac
if [ -n "$leftover" ]; then
echo "::warning title=Registry state left behind::Cleanup could not fully restore:$leftover on ${RUNNER_NAME:-this runner}. The next Configure Registry step resets it, but a job from another workflow could inherit it first."
else
echo "Registry credential removed; .npmrc, .yarnrc and yarn.lock restored to HEAD."
fi