name: MCP 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 apt install -y curl gnupg git libappindicator3-1 ca-certificates binutils icnsutils graphicsmagick' - 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.servermcp(true)) env: GAUZY_RELEASE_TAG: ${{ needs.check-release-tag.outputs.tag }} PROJECT_REPO: 'https://github.com/ever-co/ever-gauzy.git' DESKTOP_MCP_SERVER_APP_NAME: 'gauzy-mcp-server' DESKTOP_MCP_SERVER_REPO_NAME: 'ever-gauzy-mcp-server' DESKTOP_MCP_SERVER_REPO_OWNER: 'ever-co' COMPANY_SITE_LINK: 'https://gauzy.co' DESKTOP_MCP_SERVER_APP_DESCRIPTION: 'Gauzy MCP Server' DESKTOP_MCP_SERVER_APP_ID: 'com.ever.gauzymcpserver' - 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-mcp-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 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 sudo gem install --no-document fpm - 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 mcp app uses: actions/github-script@v8 with: script: | const script = require('./.scripts/bump-version-electron.js') console.log(script.servermcp(true)) env: GAUZY_RELEASE_TAG: ${{ needs.check-release-tag.outputs.tag }} PROJECT_REPO: 'https://github.com/ever-co/ever-gauzy.git' DESKTOP_MCP_SERVER_APP_NAME: 'gauzy-mcp-server' DESKTOP_MCP_SERVER_REPO_NAME: 'ever-gauzy-mcp-server' DESKTOP_MCP_SERVER_REPO_OWNER: 'ever-co' COMPANY_SITE_LINK: 'https://gauzy.co' DESKTOP_MCP_SERVER_APP_DESCRIPTION: 'Gauzy MCP Server' DESKTOP_MCP_SERVER_APP_ID: 'com.ever.gauzymcpserver' - name: Ensure dist directory exists shell: bash run: mkdir -p dist/packages - name: Build Server MCP # 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-mcp-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.servermcp(true)) env: GAUZY_RELEASE_TAG: ${{ needs.check-release-tag.outputs.tag }} PROJECT_REPO: 'https://github.com/ever-co/ever-gauzy.git' DESKTOP_MCP_SERVER_APP_NAME: 'gauzy-mcp-server' DESKTOP_MCP_SERVER_REPO_NAME: 'ever-gauzy-mcp-server' DESKTOP_MCP_SERVER_REPO_OWNER: 'ever-co' COMPANY_SITE_LINK: 'https://gauzy.co' DESKTOP_MCP_SERVER_APP_DESCRIPTION: 'Gauzy MCP Server' DESKTOP_MCP_SERVER_APP_ID: 'com.ever.gauzymcpserver' - 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-mcp-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.servermcp(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_MCP_SERVER_APP_NAME: 'gauzy-mcp-server' DESKTOP_MCP_SERVER_REPO_NAME: 'ever-gauzy-mcp-server' DESKTOP_MCP_SERVER_REPO_OWNER: 'ever-co' COMPANY_SITE_LINK: 'https://gauzy.co' DESKTOP_MCP_SERVER_APP_DESCRIPTION: 'Gauzy MCP Server' DESKTOP_MCP_SERVER_APP_ID: 'com.ever.gauzymcpserver' - 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-mcp-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 mcp app uses: actions/github-script@v8 with: script: | const script = require('./.scripts/bump-version-electron.js') console.log(script.servermcp(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_MCP_SERVER_APP_NAME: 'gauzy-mcp-server' DESKTOP_MCP_SERVER_REPO_NAME: 'ever-gauzy-mcp-server' DESKTOP_MCP_SERVER_REPO_OWNER: 'ever-co' COMPANY_SITE_LINK: 'https://gauzy.co' DESKTOP_MCP_SERVER_APP_DESCRIPTION: 'Gauzy MCP Server' DESKTOP_MCP_SERVER_APP_ID: 'com.ever.gauzymcpserver' - 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 MCP shell: cmd run: yarn build:gauzy-mcp-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