mirror of
https://github.com/ever-co/ever-gauzy.git
synced 2026-10-02 01:54:50 +08:00
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>
363 lines
18 KiB
YAML
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
|