Files
Ruslan KonviserandClaude Opus 5.5 cec254cd35 ci(desktop): release prod snaps to the Snap Store stable channel
Every Gauzy app snap (desktop, desktop-timer, server, api-server, agent,
mcp-server; amd64 and arm64) went to the Snap Store 'edge' channel only,
from both the stage lane (stage-apps) and the prod lane (apps). The
electron-builder snap target publishes with its own snapStore config.
When the build config has no snapStore entry, that config has no
channels, and the Snap Store publisher then defaults to 'edge'.

Prod builds now release to 'stable' and stage builds to 'edge'. Each
Linux build step appends
-c.snap.publish.provider=snapStore -c.snap.publish.channels=<channel>
to its yarn command. yarn adds extra arguments to the end of the script,
which is the electron-builder call. The override is in the workflows
because the root build scripts are shared by both lanes. Only
snap.publish is overridden: a -c.publish override would be merged into
the GitHub publish entry. GitHub releases, update channels and every
other target are unchanged. Stage names 'edge' explicitly so each
prod/stage pair still differs only in the channel.

The generated snaps already use grade stable and strict confinement,
and they need no store-approved plugs, so the store accepts them on
stable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 01:10:55 +02:00

1430 lines
75 KiB
YAML

name: API Server Build Prod
# The packaged desktop & server apps are built ONLY when 'master' is promoted to the 'apps' branch
# (branch flow: develop -> stage -> master -> apps).
# 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 Prod' creates on the merge to 'master' - so app releases
# always carry the same version as the corresponding platform release and publish to the
# same targets (each app repo's GitHub Releases + DigitalOcean Spaces).
on:
push:
branches:
- 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). 'apps' is promoted from 'master' either by
# fast-forwarding to the tagged 'master' commit (tag on HEAD) or by merging the
# 'master' -> 'apps' promotion PR (the tag then points at the merged
# 'master' tip, HEAD^2). Retries absorb the short delay until 'Release Prod' tags
# the 'master' 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" != "apps" ]; then
echo "::error::This workflow only releases from the '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 'master' 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/master 'refs/tags/*'); then
SRC_TIP=$(printf '%s\n' "$REMOTE_REFS" | awk -v ref="refs/heads/master" '$2 == ref { print $1 }')
TAG=$(resolve_tag "$HEAD_SHA")
# Accept the merge-parent tag only when HEAD^2 is the current 'master' tip -
# i.e. this is the promotion merge of 'master', not an arbitrary tagged branch
# merged in, nor a fast-forward racing 'Release Prod' (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 'master' tip. Promote by merging the 'master' -> 'apps' PR (or fast-forwarding: git push origin origin/master:apps) after 'Release Prod' has created the tag; if 'master' has moved since the promotion PR was opened, re-promote."
exit 1
release-linux:
needs: check-release-tag
runs-on: ${{ matrix.os }}
timeout-minutes: 300
strategy:
matrix:
# Flatpak (bwrap) and Snapcraft (snapd) cannot run inside the k8s ARC container
# runners - this packaging job needs a VM-class runner. Override with the
# RUNNER_LINUX_APPS_X64 org/repo variable to use a self-hosted VM.
os: ["${{ vars.RUNNER_LINUX_APPS_X64 || 'ubuntu-latest' }}"]
steps:
- name: Check out Git repository
uses: actions/checkout@v5
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v6
with:
node-version: 24.17.0
- name: Get yarn cache directory path
id: yarn-cache-dir-path
shell: bash
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v5
id: yarn-cache
with:
path: |
${{ steps.yarn-cache-dir-path.outputs.dir }}
.nx/cache
key: ${{ runner.os }}-${{ runner.arch }}-yarn-nx-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-yarn-nx-
${{ runner.os }}-${{ runner.arch }}-yarn-
- name: Change permissions
run: 'sudo chown -R $(whoami) ./*'
- name: Install system dependencies
run: 'sudo apt-get update && sudo env DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt install -y curl gnupg git libappindicator3-1 ca-certificates binutils icnsutils graphicsmagick flatpak flatpak-builder'
- name: Initialize Flatpak
run: |
export XDG_DATA_DIRS=$XDG_DATA_DIRS:/var/lib/flatpak/exports/share:$HOME/.local/share/flatpak/exports/share
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install --user -y --noninteractive flathub \
org.freedesktop.Platform//24.08 \
org.freedesktop.Sdk//24.08 \
org.electronjs.Electron2.BaseApp//24.08
- name: Install Snapcraft
# Pin snapcraft 7.x: 8.0+ renamed the `snap` command to `pack`, but electron-builder's
# app-builder still invokes `snapcraft snap` (ERR_ELECTRON_BUILDER_CANNOT_EXECUTE). 7.x keeps
# the `snap` command and still supports the core22 base.
run: sudo snap install snapcraft --classic --channel=7.x/stable
- name: Use Python 3.11 for native rebuilds (Linux)
# node-gyp's gyp eval()-parses Electron 38's common.gypi; Python 3.12's stricter tokenizer
# rejects it ("unterminated string literal"), breaking better-sqlite3's source rebuild
# (no prebuilt exists for Electron 38's ABI). Python 3.11 parses it fine. Pinned to a SHA.
id: py311
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.11'
- name: Fix node-gyp and Python
# Install build deps into, and point node-gyp at, the SAME 3.11 interpreter (not a stray python3).
run: |
"${{ steps.py311.outputs.python-path }}" -m pip install packaging setuptools
echo "npm_config_python=${{ steps.py311.outputs.python-path }}" >> "$GITHUB_ENV"
echo "PYTHON=${{ steps.py311.outputs.python-path }}" >> "$GITHUB_ENV"
- name: Install latest version of NPM
run: 'sudo npm install -g npm@11.6.2'
- name: Install globally node-gyp, ts-node and nx packages
run: 'sudo npm install --quiet -g node-gyp@10.2.0 ts-node@10.9.2 nx@^22.5.2'
- 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 server version
uses: actions/github-script@v8
with:
script: |
const script = require('./.scripts/bump-version-electron.js')
console.log(script.serverapi(true))
env:
GAUZY_RELEASE_TAG: ${{ needs.check-release-tag.outputs.tag }}
PROJECT_REPO: 'https://github.com/ever-co/ever-gauzy.git'
DESKTOP_API_SERVER_APP_NAME: 'gauzy-api-server'
DESKTOP_API_SERVER_REPO_NAME: 'ever-gauzy-api-server'
DESKTOP_API_SERVER_REPO_OWNER: 'ever-co'
COMPANY_SITE_LINK: 'https://gauzy.co'
DESKTOP_API_SERVER_APP_DESCRIPTION: 'Gauzy API Server'
DESKTOP_API_SERVER_APP_ID: 'com.ever.gauzyapiserver'
- name: Ensure dist directory exists
shell: bash
run: mkdir -p dist/packages
- name: Build Server
# Snap Store channel: prod ('apps') releases to 'stable', stage ('stage-apps') to 'edge' (the
# electron-builder default). yarn appends these flags to the script's last command, the
# electron-builder call. Override snap.publish only: a -c.publish override lands in the GitHub entry.
run: 'yarn build:gauzy-api-server:linux:release:gh:x64 -c.snap.publish.provider=snapStore -c.snap.publish.channels=stable'
env:
USE_HARD_LINKS: false
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_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
NX_NO_CLOUD: true
NX_DAEMON: false
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
- 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
release-linux-arm64:
needs: check-release-tag
runs-on: ${{ matrix.os }}
timeout-minutes: 300
strategy:
matrix:
os: ["${{ vars.RUNNER_LINUX_ARM64 || 'ubuntu-24.04-arm' }}"]
steps:
- name: Check out Git repository
uses: actions/checkout@v5
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v6
with:
node-version: 24.17.0
- name: Get yarn cache directory path
id: yarn-cache-dir-path
shell: bash
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v5
id: yarn-cache
with:
path: |
${{ steps.yarn-cache-dir-path.outputs.dir }}
.nx/cache
key: ${{ runner.os }}-${{ runner.arch }}-yarn-nx-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-yarn-nx-
${{ runner.os }}-${{ runner.arch }}-yarn-
- name: Change permissions
run: 'sudo chown -R $(whoami) ./*'
- name: Install system dependencies
run: |
sudo apt-get update
sudo env DEBIAN_FRONTEND=noninteractive NEEDRESTART_MODE=a apt install -y curl gnupg git libappindicator3-1 ca-certificates binutils icnsutils graphicsmagick libx11-dev libxtst-dev libxt-dev libxinerama-dev libx11-xcb-dev libxkbcommon-dev libxkbcommon-x11-dev libxkbfile-dev libxrandr-dev ruby ruby-dev rubygems build-essential flatpak flatpak-builder
sudo gem install --no-document fpm
- name: Initialize Flatpak
run: |
export XDG_DATA_DIRS=$XDG_DATA_DIRS:/var/lib/flatpak/exports/share:$HOME/.local/share/flatpak/exports/share
flatpak remote-add --user --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
flatpak install -y --user --noninteractive flathub \
org.freedesktop.Platform//24.08 \
org.freedesktop.Sdk//24.08 \
org.electronjs.Electron2.BaseApp//24.08
- name: Install Snapcraft
# Pin snapcraft 7.x: 8.0+ renamed the `snap` command to `pack`, but electron-builder's
# app-builder still invokes `snapcraft snap` (ERR_ELECTRON_BUILDER_CANNOT_EXECUTE). 7.x keeps
# the `snap` command and still supports the core22 base.
run: sudo snap install snapcraft --classic --channel=7.x/stable
# Pre-install snapcraft's build snaps WITH sudo. In host (destructive) mode snapcraft installs
# any build snap it is missing by running `snap install` as the unprivileged runner user, and
# on the arm64 images snapd intermittently refuses that: "error: access denied (try with sudo)"
# -> "Error installing snap 'gnome-3-28-1804'" -> ERR_ELECTRON_BUILDER_CANNOT_EXECUTE. Because
# it is intermittent, arm64 snap passed in May 2026, failed from June, and on 2026-09-23 failed
# and passed within the same hour with no change. With these already present snapcraft performs
# ZERO snap installs of its own, so the failing operation is never reached. This exact list was
# proven on branch exp/arm64-snap-snapcraft-version (runs 35895302909, 35895798132, 35896229973).
- name: Pre-install snapcraft build snaps (arm64)
run: sudo snap install core18 core20 core22 gtk-common-themes gnome-3-28-1804 gnome-42-2204
# On arm64 electron-builder has no template snap, so it builds WITHOUT one: snapcraft pulls
# stage-packages, and in host (destructive) mode that runs a bare `apt-get update`. As the
# runner user that fails with "Could not open lock file /var/lib/apt/lists/lock - open (13:
# Permission denied)" -> "Failed to refresh package list: failed to run apt update." (amd64 never
# hits this: it uses the template snap and runs no apt). This shim, first on PATH, runs ONLY
# snapcraft as root and then hands the files it wrote back to the runner user.
- name: Run snapcraft as root (arm64 host-mode snap)
run: |
shim_dir="$HOME/.local/snapcraft-root-shim"
mkdir -p "$shim_dir"
cat > "$shim_dir/snapcraft" <<'SH'
#!/bin/bash
sudo --preserve-env env PATH="$PATH" /snap/bin/snapcraft "$@"
rc=$?
sudo chown -R "$(id -u):$(id -g)" "$PWD" "$HOME/.cache" "$HOME/.local/state" 2>/dev/null || true
exit $rc
SH
chmod +x "$shim_dir/snapcraft"
echo "$shim_dir" >> "$GITHUB_PATH"
- name: Install Multipass
run: 'sudo snap install multipass'
- name: Use Python 3.11 for native rebuilds (Linux)
# node-gyp's gyp eval()-parses Electron 38's common.gypi; Python 3.12's stricter tokenizer
# rejects it ("unterminated string literal"), breaking better-sqlite3's source rebuild
# (no prebuilt exists for Electron 38's ABI). Python 3.11 parses it fine. Pinned to a SHA.
id: py311
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
with:
python-version: '3.11'
- name: Fix node-gyp and Python
# Install build deps into, and point node-gyp at, the SAME 3.11 interpreter (not a stray python3).
run: |
"${{ steps.py311.outputs.python-path }}" -m pip install packaging setuptools
echo "npm_config_python=${{ steps.py311.outputs.python-path }}" >> "$GITHUB_ENV"
echo "PYTHON=${{ steps.py311.outputs.python-path }}" >> "$GITHUB_ENV"
- name: Install latest version of NPM
run: 'sudo npm install -g npm@11.6.2'
- name: Install globally node-gyp, ts-node and nx packages
run: 'sudo npm install --quiet -g node-gyp@10.2.0 ts-node@10.9.2 nx@^22.5.2'
- 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 server api app
uses: actions/github-script@v8
with:
script: |
const script = require('./.scripts/bump-version-electron.js')
console.log(script.serverapi(true))
env:
GAUZY_RELEASE_TAG: ${{ needs.check-release-tag.outputs.tag }}
PROJECT_REPO: 'https://github.com/ever-co/ever-gauzy.git'
DESKTOP_API_SERVER_APP_NAME: 'gauzy-api-server'
DESKTOP_API_SERVER_REPO_NAME: 'ever-gauzy-api-server'
DESKTOP_API_SERVER_REPO_OWNER: 'ever-co'
COMPANY_SITE_LINK: 'https://gauzy.co'
DESKTOP_API_SERVER_APP_DESCRIPTION: 'Gauzy API Server'
DESKTOP_API_SERVER_APP_ID: 'com.ever.gauzyapiserver'
- name: Ensure dist directory exists
shell: bash
run: mkdir -p dist/packages
- name: Build Server API
# Snap Store channel: prod ('apps') releases to 'stable', stage ('stage-apps') to 'edge' (the
# electron-builder default). yarn appends these flags to the script's last command, the
# electron-builder call. Override snap.publish only: a -c.publish override lands in the GitHub entry.
run: 'yarn build:gauzy-api-server:linux:release:gh:arm64 -c.snap.publish.provider=snapStore -c.snap.publish.channels=stable'
env:
USE_HARD_LINKS: false
USE_SYSTEM_FPM: true
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_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
NX_NO_CLOUD: true
NX_DAEMON: false
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_TOKEN }}
SNAPCRAFT_BUILD_ENVIRONMENT: host
- 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
release-mac:
needs: check-release-tag
runs-on: ${{ matrix.os }}
timeout-minutes: 300
strategy:
matrix:
os: [ghcr.io/cirruslabs/macos-runner:tahoe]
steps:
- name: Check out Git repository
uses: actions/checkout@v5
- name: Install Node.js, NPM and Yarn
uses: actions/setup-node@v6
with:
node-version: 24.17.0
- name: Get yarn cache directory path
id: yarn-cache-dir-path
shell: bash
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v5
id: yarn-cache
with:
path: |
${{ steps.yarn-cache-dir-path.outputs.dir }}
.nx/cache
key: ${{ runner.os }}-${{ runner.arch }}-yarn-nx-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-yarn-nx-
${{ runner.os }}-${{ runner.arch }}-yarn-
- name: Fix node-gyp and Python
run: python3 -m pip install --break-system-packages packaging setuptools || python3 -m pip install packaging setuptools
- name: Install latest version of NPM
run: 'sudo npm install -g npm@11.6.2'
- name: Install globally node-gyp, ts-node and nx packages
run: 'sudo npm install --quiet -g node-gyp@10.2.0 ts-node@10.9.2 nx@^22.5.2'
- 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 Server version
uses: actions/github-script@v8
with:
script: |
const script = require('./.scripts/bump-version-electron.js')
console.log(script.serverapi(true))
env:
GAUZY_RELEASE_TAG: ${{ needs.check-release-tag.outputs.tag }}
PROJECT_REPO: 'https://github.com/ever-co/ever-gauzy.git'
DESKTOP_API_SERVER_APP_NAME: 'gauzy-api-server'
DESKTOP_API_SERVER_REPO_NAME: 'ever-gauzy-api-server'
DESKTOP_API_SERVER_REPO_OWNER: 'ever-co'
COMPANY_SITE_LINK: 'https://gauzy.co'
DESKTOP_API_SERVER_APP_DESCRIPTION: 'Gauzy API Server'
DESKTOP_API_SERVER_APP_ID: 'com.ever.gauzyapiserver'
- name: Prepare Apple API Key
run: |
echo "${{ secrets.APPLE_API_KEY_BASE64 }}" | base64 --decode > /tmp/AuthKey_${{ secrets.APPLE_API_KEY_ID }}.p8
chmod 600 /tmp/AuthKey_${{ secrets.APPLE_API_KEY_ID }}.p8
- name: Ensure dist directory exists
shell: bash
run: mkdir -p dist/packages
# macOS signing: build the keychain ourselves instead of letting electron-builder do it.
# app-builder-lib passes the p12 password to `security set-key-partition-list -k`, which
# expects the KEYCHAIN password — fatal on the current runner image, and still unfixed in
# the latest release. With CSC_LINK unset, electron-builder skips its own keychain code and
# uses CSC_KEYCHAIN (macPackager.js:25-48), so this sidesteps the bug without patching deps.
- name: Import Apple signing certificate into a keychain
env:
CSC_LINK_BASE64: ${{ secrets.CSC_LINK_BASE64 }}
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
run: |
set -euo pipefail
KEYCHAIN="$RUNNER_TEMP/ever-signing.keychain-db"
KEYCHAIN_PASSWORD="$(openssl rand -base64 32)"
CERT="$RUNNER_TEMP/ever-signing-cert.p12"
printf '%s' "$CSC_LINK_BASE64" | base64 --decode > "$CERT"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security set-keychain-settings -lut 21600 "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
security import "$CERT" -k "$KEYCHAIN" -P "$CSC_KEY_PASSWORD" \
-T /usr/bin/codesign -T /usr/bin/productbuild -T /usr/bin/security
# The KEYCHAIN password here — this is the exact call app-builder-lib gets wrong.
security set-key-partition-list -S apple-tool:,apple:,codesign: -s \
-k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" > /dev/null
security list-keychains -d user -s "$KEYCHAIN" $(security list-keychains -d user | xargs)
rm -f "$CERT"
# Fail loudly here rather than silently shipping an unsigned app later.
security find-identity -v -p codesigning "$KEYCHAIN" | tee /tmp/identities.txt
grep -q "Developer ID Application" /tmp/identities.txt
- name: Build Server
run: 'yarn build:gauzy-api-server:mac:release'
env:
USE_HARD_LINKS: false
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_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }}
NX_NO_CLOUD: true
NX_DAEMON: false
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_ID_APP_PASSWORD: ${{ secrets.APPLE_ID_APP_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
# CSC_LINK deliberately NOT set: that is what makes electron-builder build its own
# (broken) keychain. Point it at the one imported above instead.
CSC_KEYCHAIN: ${{ runner.temp }}/ever-signing.keychain-db
CSC_KEY_PASSWORD: ${{ secrets.CSC_KEY_PASSWORD }}
APPLE_API_KEY: /tmp/AuthKey_${{ secrets.APPLE_API_KEY_ID }}.p8
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
- 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
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 Server version
uses: actions/github-script@v8
with:
script: |
const script = require('./.scripts/bump-version-electron.js')
console.log(script.serverapi(true))
env:
# Windows signing config must be visible to the BUMP step: this script writes build.win.azureSignOptions into package.json, which electron-builder reads later.
WINDOWS_PUBLISHER_NAME: ${{ secrets.WINDOWS_PUBLISHER_NAME }}
AZURE_CERT_PROFILE_NAME: ${{ secrets.AZURE_CERT_PROFILE_NAME }}
AZURE_CODE_SIGNING_ACCOUNT: ${{ vars.AZURE_CODE_SIGNING_ACCOUNT || 'ever' }}
AZURE_CODE_SIGNING_ENDPOINT: ${{ vars.AZURE_CODE_SIGNING_ENDPOINT || 'https://eus.codesigning.azure.net/' }}
GAUZY_RELEASE_TAG: ${{ needs.check-release-tag.outputs.tag }}
PROJECT_REPO: 'https://github.com/ever-co/ever-gauzy.git'
DESKTOP_API_SERVER_APP_NAME: 'gauzy-api-server'
DESKTOP_API_SERVER_REPO_NAME: 'ever-gauzy-api-server'
DESKTOP_API_SERVER_REPO_OWNER: 'ever-co'
COMPANY_SITE_LINK: 'https://gauzy.co'
DESKTOP_API_SERVER_APP_DESCRIPTION: 'Gauzy API Server'
DESKTOP_API_SERVER_APP_ID: 'com.ever.gauzyapiserver'
- 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
# Azure Trusted Signing runs Invoke-TrustedSigning, which installs the `sign` dotnet
# global tool. The self-hosted Windows runners have no .NET SDK, so that install fails
# ("sdk-not-found") and signing is skipped. Provision it here rather than on the host,
# so the requirement lives in Git and applies to every runner.
- name: Install .NET SDK (required by Azure Trusted Signing)
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Build Server
shell: cmd
run: 'yarn build:gauzy-api-server:windows:release:gh:x64'
env:
USE_HARD_LINKS: false
ELECTRON_BUILDER_CACHE: ${{ github.workspace }}\.cache\electron-builder
GH_TOKEN: ${{ secrets.GH_TOKEN }}
# Windows Authenticode signing (electron-builder auto-signs when WIN_CSC_LINK is set;
# empty secret => skipped). Verification engages only when WINDOWS_PUBLISHER_NAME is set.
WIN_CSC_LINK: ${{ secrets.WINDOWS_CERT_PFX_BASE64 }}
WIN_CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }}
WINDOWS_PUBLISHER_NAME: ${{ secrets.WINDOWS_PUBLISHER_NAME }}
# Azure Artifact Signing (preferred once a certificate profile exists). Engaged only when
# AZURE_CERT_PROFILE_NAME is set; otherwise the PFX path above is used.
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
AZURE_CERT_PROFILE_NAME: ${{ secrets.AZURE_CERT_PROFILE_NAME }}
AZURE_CODE_SIGNING_ACCOUNT: ${{ vars.AZURE_CODE_SIGNING_ACCOUNT || 'ever' }}
AZURE_CODE_SIGNING_ENDPOINT: ${{ vars.AZURE_CODE_SIGNING_ENDPOINT || 'https://eus.codesigning.azure.net/' }}
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
NX_DAEMON: false
- 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
release-windows-arm64:
needs: check-release-tag
runs-on: ${{ matrix.os }}
timeout-minutes: 300
strategy:
matrix:
os: [windows-11-arm]
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
architecture: arm64
- name: Get yarn cache directory path
id: yarn-cache-dir-path
shell: bash
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v5
id: yarn-cache
with:
path: |
${{ steps.yarn-cache-dir-path.outputs.dir }}
.nx/cache
key: ${{ runner.os }}-${{ runner.arch }}-yarn-nx-${{ hashFiles('yarn.lock') }}
restore-keys: |
${{ runner.os }}-${{ runner.arch }}-yarn-nx-
${{ runner.os }}-${{ runner.arch }}-yarn-
- name: Install Visual Studio 2022 Build Tools (VCTools with ARM64)
shell: powershell
run: |
# The runner image normally ships VS with the VC ARM64 toolset. Only reach for
# Chocolatey if it is genuinely missing — community.chocolatey.org returning 504
# has failed this job before ("Chocolatey installed 0/0 packages"), and installing
# something already present costs ~6 min for nothing.
$ErrorActionPreference = "Continue"
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
$have = $false
if (Test-Path $vswhere) {
$found = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.ARM64 -property installationPath 2>$null
if ($found) { $have = $true; Write-Host "VC ARM64 toolset already present: $found" }
}
if (-not $have) {
Write-Host "VC ARM64 toolset not found - installing via Chocolatey"
choco install -y visualstudio2022buildtools --execution-timeout=21600 --package-parameters "--add Microsoft.VisualStudio.Workload.VCTools --add Microsoft.VisualStudio.Component.VC.Tools.ARM64 --includeRecommended --passive --norestart"
if ($LASTEXITCODE -ne 0) {
# Do not fail the job on a Chocolatey feed outage; the next step (msvc-dev-cmd)
# will either find a usable toolchain or fail with an unambiguous message.
Write-Warning "Chocolatey install failed (exit $LASTEXITCODE) - continuing; the MSVC setup step will report definitively."
}
}
exit 0
- 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: arm64
- 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 server api app
uses: actions/github-script@v8
with:
script: |
const script = require('./.scripts/bump-version-electron.js')
console.log(script.serverapi(true))
env:
# Windows signing is deliberately NOT configured for ARM64. Azure Trusted Signing
# ships no ARM64 tooling (Microsoft.Trusted.Signing.Client 1.0.95 contains only
# bin/x64 and bin/x86), so Invoke-TrustedSigning loads the x64 dlib and fails with
# "SignTool failed with exit code 3", taking the whole build down. Without
# WINDOWS_PUBLISHER_NAME the bump script emits no signer, so ARM64 builds
# unsigned and succeeds; electron-updater verification stays off for it. x64 signs.
GAUZY_RELEASE_TAG: ${{ needs.check-release-tag.outputs.tag }}
PROJECT_REPO: 'https://github.com/ever-co/ever-gauzy.git'
DESKTOP_API_SERVER_APP_NAME: 'gauzy-api-server'
DESKTOP_API_SERVER_REPO_NAME: 'ever-gauzy-api-server'
DESKTOP_API_SERVER_REPO_OWNER: 'ever-co'
COMPANY_SITE_LINK: 'https://gauzy.co'
DESKTOP_API_SERVER_APP_DESCRIPTION: 'Gauzy API Server'
DESKTOP_API_SERVER_APP_ID: 'com.ever.gauzyapiserver'
- 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'); }"
# Azure Trusted Signing runs Invoke-TrustedSigning, which installs the `sign` dotnet
# global tool. The self-hosted Windows runners have no .NET SDK, so that install fails
# ("sdk-not-found") and signing is skipped. Provision it here rather than on the host,
# so the requirement lives in Git and applies to every runner.
- name: Install .NET SDK (required by Azure Trusted Signing)
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
- name: Build Server API
shell: cmd
run: yarn build:gauzy-api-server:windows:release:gh:arm64
env:
USE_HARD_LINKS: false
ELECTRON_BUILDER_CACHE: ${{ github.workspace }}\.cache\electron-builder
GH_TOKEN: ${{ secrets.GH_TOKEN }}
# WIN_CSC_LINK is deliberately NOT passed on ARM64. electron-builder auto-signs
# whenever it is set, independently of azureSignOptions, and then spawns a signtool
# that does not exist for ARM64 in its bundled winCodeSign package:
# spawn ...\winCodeSign-2.6.0\windows-10\arm64\signtool.exe ENOENT
# ARM64 therefore ships unsigned (see the Bump step). x64 still signs via Azure.
WINDOWS_PUBLISHER_NAME: ${{ secrets.WINDOWS_PUBLISHER_NAME }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
AZURE_CERT_PROFILE_NAME: ${{ secrets.AZURE_CERT_PROFILE_NAME }}
AZURE_CODE_SIGNING_ACCOUNT: ${{ vars.AZURE_CODE_SIGNING_ACCOUNT || 'ever' }}
AZURE_CODE_SIGNING_ENDPOINT: ${{ vars.AZURE_CODE_SIGNING_ENDPOINT || 'https://eus.codesigning.azure.net/' }}
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
NX_DAEMON: false
- 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