fix(ssh): address codex review on MFA secondary password prefill #4093
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: build-packages | |
| # Trigger philosophy | |
| # - Any push to any branch + any PR -> run the build matrix so CI is | |
| # always testable. Same-repo PR runs own package validation; matching | |
| # branch push runs become a lightweight mirror only after a current | |
| # open PR run for the same commit is visible. If lookup is slow or | |
| # unavailable, the push run falls back to the full matrix. Artifacts | |
| # upload as workflow artifacts only; *no* GitHub Release is published. | |
| # - Tag push matching `v<MAJOR>.<MINOR>.<PATCH>` (with optional | |
| # pre-release suffix like `v1.2.3-rc.1`) -> run the matrix and | |
| # publish a GitHub Release. Loose tags like `v-test`, `vNEXT`, or | |
| # `v1.0` no longer auto-publish. | |
| # - Manual `workflow_dispatch` -> run the matrix on the selected ref. | |
| # `publish_release` only publishes when the selected ref is also a | |
| # strict version tag. | |
| # | |
| # The release job validates the exact same rule before publishing, so | |
| # adding branches/PRs above is safe; accidental tag-like branch names | |
| # won't leak a release. | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| publish_release: | |
| description: "Publish GitHub Release after build" | |
| type: boolean | |
| default: false | |
| mosh_bin_release: | |
| description: "Release tag containing bundled mosh-client binaries" | |
| type: string | |
| default: "" | |
| et_bin_release: | |
| description: "Release tag containing bundled et (EternalTerminal) binaries" | |
| type: string | |
| default: "" | |
| push: | |
| branches: | |
| - "**" | |
| tags: | |
| - "v[0-9]+.[0-9]+.[0-9]+" | |
| - "v[0-9]+.[0-9]+.[0-9]+-[0-9A-Za-z]*" | |
| pull_request: | |
| # A newer run for the same push branch or PR cancels older in-progress | |
| # work. Push and PR events stay in separate groups so deduped push runs | |
| # can mirror PR results cleanly instead of leaving cancelled checks on | |
| # the PR. Publishing tag runs share a release group across push and | |
| # manual dispatch; non-publishing manual tag runs use their own group. | |
| concurrency: | |
| group: build-packages-${{ github.workflow }}-${{ startsWith(github.ref, 'refs/tags/') && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish_release)) && 'release' || github.event_name }}-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.ref_type }}-${{ github.event.pull_request.head.ref || github.ref_name }} | |
| cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }} | |
| permissions: | |
| actions: read | |
| contents: read | |
| pull-requests: read | |
| env: | |
| MOSH_BIN_RELEASE: ${{ github.event.inputs.mosh_bin_release || vars.MOSH_BIN_RELEASE || '' }} | |
| BUNDLE_MOSH: ${{ (startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish_release))) || (github.event_name == 'workflow_dispatch' && inputs.mosh_bin_release != '') }} | |
| ET_BIN_RELEASE: ${{ github.event.inputs.et_bin_release || vars.ET_BIN_RELEASE || '' }} | |
| BUNDLE_ET: ${{ (startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish_release))) || (github.event_name == 'workflow_dispatch' && inputs.et_bin_release != '') }} | |
| STRICT_VERSION_REF_RE: '^refs/tags/v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-((0|[1-9][0-9]*|[A-Za-z][0-9A-Za-z-]*|[0-9A-Za-z][0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)(\.(0|[1-9][0-9]*|[A-Za-z][0-9A-Za-z-]*|[0-9A-Za-z][0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*))*))?$' | |
| jobs: | |
| dedupe: | |
| name: dedupe push run | |
| runs-on: ubuntu-latest | |
| outputs: | |
| skip_heavy_ci: ${{ steps.detect.outputs.skip_heavy_ci }} | |
| heavy_ci_pr_run_id: ${{ steps.detect.outputs.heavy_ci_pr_run_id }} | |
| steps: | |
| - name: Detect duplicate heavy CI | |
| id: detect | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPOSITORY: ${{ github.repository }} | |
| REPOSITORY_OWNER: ${{ github.repository_owner }} | |
| EVENT_NAME: ${{ github.event_name }} | |
| REF: ${{ github.ref }} | |
| HEAD_REF: ${{ github.ref_name }} | |
| HEAD_SHA: ${{ github.sha }} | |
| run: | | |
| skip_heavy_ci=false | |
| if [[ "$EVENT_NAME" == "push" && "$REF" == refs/heads/* ]]; then | |
| pr_count=0 | |
| if ! pr_count="$(gh api --method GET "repos/${REPOSITORY}/pulls" \ | |
| -f state=open \ | |
| -f "head=${REPOSITORY_OWNER}:${HEAD_REF}" \ | |
| -F per_page=1 \ | |
| --jq 'length')"; then | |
| echo "::warning::Could not check open PRs; running full push CI." | |
| pr_count=0 | |
| fi | |
| pr_run_id="" | |
| if [[ "$pr_count" != "0" ]]; then | |
| cutoff="$(date -u -d '20 minutes ago' +'%Y-%m-%dT%H:%M:%SZ')" | |
| for attempt in {1..18}; do | |
| if ! pr_run_id="$(gh api --method GET "repos/${REPOSITORY}/actions/workflows/build.yml/runs" \ | |
| -f event=pull_request \ | |
| -f "branch=${HEAD_REF}" \ | |
| -f "head_sha=${HEAD_SHA}" \ | |
| -F per_page=20 \ | |
| --jq "[.workflow_runs[] | select(.created_at >= \"${cutoff}\" and .conclusion != \"cancelled\" and .conclusion != \"skipped\")] | sort_by(.created_at, .id) | .[0].id // \"\"")"; then | |
| echo "::warning::Could not check PR workflow runs; running full push CI." | |
| pr_run_id="" | |
| break | |
| fi | |
| if [[ -n "$pr_run_id" ]]; then | |
| skip_heavy_ci=true | |
| break | |
| fi | |
| if [[ "$attempt" == "18" ]]; then | |
| break | |
| fi | |
| sleep 10 | |
| done | |
| fi | |
| if [[ -n "$pr_run_id" ]]; then | |
| echo "heavy_ci_pr_run_id=${pr_run_id}" >> "$GITHUB_OUTPUT" | |
| echo "heavy_ci_pr_run_id=${pr_run_id}" | |
| fi | |
| fi | |
| echo "skip_heavy_ci=${skip_heavy_ci}" >> "$GITHUB_OUTPUT" | |
| echo "skip_heavy_ci=${skip_heavy_ci}" | |
| dedupe-result: | |
| name: dedupe result | |
| needs: dedupe | |
| if: needs.dedupe.outputs.skip_heavy_ci == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Mirror PR build result | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPOSITORY: ${{ github.repository }} | |
| PR_RUN_ID: ${{ needs.dedupe.outputs.heavy_ci_pr_run_id }} | |
| run: | | |
| if [[ -z "$PR_RUN_ID" ]]; then | |
| echo "::error::No PR workflow run was selected for dedupe." | |
| exit 1 | |
| fi | |
| for attempt in {1..360}; do | |
| if ! result="$(gh run view "$PR_RUN_ID" --repo "$REPOSITORY" --json status,conclusion --jq '.status + "|" + (.conclusion // "")')"; then | |
| echo "::warning::Could not read PR workflow run ${PR_RUN_ID}; retrying." | |
| sleep 30 | |
| continue | |
| fi | |
| status="${result%%|*}" | |
| conclusion="${result#*|}" | |
| echo "PR run ${PR_RUN_ID}: status=${status} conclusion=${conclusion:-pending}" | |
| if [[ "$status" == "completed" ]]; then | |
| if [[ "$conclusion" == "success" ]]; then | |
| exit 0 | |
| fi | |
| echo "::error::PR workflow run ${PR_RUN_ID} completed with conclusion '${conclusion}'." | |
| exit 1 | |
| fi | |
| sleep 30 | |
| done | |
| echo "::error::Timed out waiting for PR workflow run ${PR_RUN_ID}." | |
| exit 1 | |
| resolve-mosh: | |
| name: resolve bundled mosh-client | |
| needs: dedupe | |
| if: | | |
| needs.dedupe.outputs.skip_heavy_ci != 'true' | |
| && ( | |
| (startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish_release))) | |
| || (github.event_name == 'workflow_dispatch' && inputs.mosh_bin_release != '') | |
| ) | |
| runs-on: ubuntu-latest | |
| outputs: | |
| mosh_bin_release: ${{ steps.resolve.outputs.mosh_bin_release }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Resolve bundled mosh-client release | |
| id: resolve | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: | | |
| node scripts/resolve-mosh-bin-release.cjs | |
| release="$(grep '^MOSH_BIN_RELEASE=' "$GITHUB_ENV" | tail -n 1 | cut -d= -f2-)" | |
| if [[ -z "$release" ]]; then | |
| echo "::error::MOSH_BIN_RELEASE was not resolved." | |
| exit 1 | |
| fi | |
| echo "mosh_bin_release=${release}" >> "$GITHUB_OUTPUT" | |
| resolve-et: | |
| name: resolve bundled et-client | |
| needs: dedupe | |
| if: | | |
| needs.dedupe.outputs.skip_heavy_ci != 'true' | |
| && ( | |
| (startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish_release))) | |
| || (github.event_name == 'workflow_dispatch' && inputs.et_bin_release != '') | |
| ) | |
| runs-on: ubuntu-latest | |
| outputs: | |
| et_bin_release: ${{ steps.resolve.outputs.et_bin_release }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Resolve bundled et-client release | |
| id: resolve | |
| env: | |
| GITHUB_TOKEN: ${{ github.token }} | |
| run: | | |
| node scripts/resolve-et-bin-release.cjs | |
| release="$(grep '^ET_BIN_RELEASE=' "$GITHUB_ENV" | tail -n 1 | cut -d= -f2-)" | |
| if [[ -z "$release" ]]; then | |
| echo "::error::ET_BIN_RELEASE was not resolved." | |
| exit 1 | |
| fi | |
| echo "et_bin_release=${release}" >> "$GITHUB_OUTPUT" | |
| build: | |
| name: ${{ needs.dedupe.outputs.skip_heavy_ci == 'true' && format('deduped build-{0}', matrix.name) || format('build-{0}', matrix.name) }} | |
| needs: [dedupe, resolve-mosh, resolve-et] | |
| if: | | |
| always() | |
| && needs.dedupe.result == 'success' | |
| && needs.dedupe.outputs.skip_heavy_ci != 'true' | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - name: macos | |
| os: macos-latest | |
| pack_script: pack:mac | |
| - name: windows | |
| os: windows-latest | |
| # The mosh binary workflow currently produces win32-x64 only. | |
| # Keep official packages aligned with bundled-mosh coverage | |
| # until Cygwin arm64 is stable enough to build win32-arm64. | |
| pack_script: pack:win-x64 | |
| env: | |
| MOSH_BIN_RELEASE: ${{ needs.resolve-mosh.outputs.mosh_bin_release }} | |
| ET_BIN_RELEASE: ${{ needs.resolve-et.outputs.et_bin_release }} | |
| VITE_SYNC_GITHUB_CLIENT_ID: ${{ secrets.VITE_SYNC_GITHUB_CLIENT_ID }} | |
| VITE_SYNC_GOOGLE_CLIENT_ID: ${{ secrets.VITE_SYNC_GOOGLE_CLIENT_ID }} | |
| VITE_SYNC_GOOGLE_CLIENT_SECRET: ${{ secrets.VITE_SYNC_GOOGLE_CLIENT_SECRET }} | |
| VITE_SYNC_ONEDRIVE_CLIENT_ID: ${{ secrets.VITE_SYNC_ONEDRIVE_CLIENT_ID }} | |
| steps: | |
| - name: Validate bundled mosh-client release | |
| if: env.BUNDLE_MOSH == 'true' | |
| shell: bash | |
| env: | |
| RESOLVE_MOSH_RESULT: ${{ needs.resolve-mosh.result }} | |
| run: | | |
| if [[ "$RESOLVE_MOSH_RESULT" != "success" || -z "$MOSH_BIN_RELEASE" ]]; then | |
| echo "::error::Bundled mosh-client release was not resolved for this package build." | |
| exit 1 | |
| fi | |
| - name: Validate bundled et-client release | |
| if: env.BUNDLE_ET == 'true' | |
| shell: bash | |
| env: | |
| RESOLVE_ET_RESULT: ${{ needs.resolve-et.result }} | |
| run: | | |
| if [[ "$RESOLVE_ET_RESULT" != "success" || -z "$ET_BIN_RELEASE" ]]; then | |
| echo "::error::Bundled et-client release was not resolved for this package build." | |
| exit 1 | |
| fi | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| cache: npm | |
| - name: Install deps | |
| run: npm ci | |
| - name: Fetch bundled mosh-client | |
| if: env.BUNDLE_MOSH == 'true' | |
| shell: bash | |
| run: | | |
| if [[ "${{ matrix.name }}" == "macos" ]]; then | |
| npm run fetch:mosh -- --platform=darwin --arch=universal | |
| elif [[ "${{ matrix.name }}" == "windows" ]]; then | |
| npm run fetch:mosh -- --platform=win32 --arch=x64 | |
| fi | |
| - name: Fetch bundled et-client | |
| if: env.BUNDLE_ET == 'true' | |
| shell: bash | |
| run: | | |
| if [[ "${{ matrix.name }}" == "macos" ]]; then | |
| npm run fetch:et -- --platform=darwin --arch=universal | |
| elif [[ "${{ matrix.name }}" == "windows" ]]; then | |
| npm run fetch:et -- --platform=win32 --arch=x64 | |
| fi | |
| - name: Set version | |
| shell: bash | |
| run: | | |
| # Strict semver matches v<MAJOR>.<MINOR>.<PATCH>[-pre]; loose | |
| # tags / branches / PRs fall through to a semver-pre-release | |
| # form (`0.0.0-sha-<short-sha>`) so npm pkg / electron-builder | |
| # accept it. Non-semver versions (e.g. bare "abc1234") cause | |
| # downstream tooling to error or pick weird codepaths. | |
| if [[ "$GITHUB_REF" =~ $STRICT_VERSION_REF_RE ]]; then | |
| VERSION="${GITHUB_REF_NAME#v}" | |
| else | |
| VERSION="0.0.0-sha-${GITHUB_SHA:0:7}" | |
| fi | |
| echo "Setting version to ${VERSION}" | |
| npm pkg set version="${VERSION}" | |
| - name: Build package | |
| env: | |
| ELECTRON_BUILDER_PUBLISH: "never" | |
| # macOS code signing & notarization (only for macOS builds) | |
| CSC_LINK: ${{ matrix.name == 'macos' && secrets.MAC_CSC_LINK || '' }} | |
| CSC_KEY_PASSWORD: ${{ matrix.name == 'macos' && secrets.MAC_CSC_KEY_PASSWORD || '' }} | |
| APPLE_ID: ${{ matrix.name == 'macos' && secrets.APPLE_ID || '' }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ matrix.name == 'macos' && secrets.APPLE_APP_SPECIFIC_PASSWORD || '' }} | |
| APPLE_TEAM_ID: ${{ matrix.name == 'macos' && secrets.APPLE_TEAM_ID || '' }} | |
| run: npm run ${{ matrix.pack_script }} | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: netcatty-${{ matrix.name }} | |
| path: | | |
| release/*.dmg | |
| release/*.zip | |
| release/*.exe | |
| release/*.msi | |
| release/*.AppImage | |
| release/*.deb | |
| release/*.rpm | |
| release/*.pacman | |
| release/*.tar.gz | |
| release/*.yml | |
| release/*.blockmap | |
| if-no-files-found: ignore | |
| # Dedicated job for Linux x64 — builds inside AlmaLinux 8 (GLIBC 2.28) | |
| # so native modules (node-pty, serialport) stay compatible with RHEL 8 / | |
| # CentOS 8 / UOS / Deepin. Ubuntu 22.04 links against GLIBC 2.32–2.35, | |
| # which crashes on those older hosts (see #2062). | |
| # | |
| # We previously used debian:buster for the same glibc floor, but Buster's | |
| # g++ 8 cannot compile Electron 42 natives (`-std=gnu++20`). AlmaLinux 8 | |
| # keeps glibc 2.28 while gcc-toolset-13 provides a real C++20 toolchain. | |
| # Link natives with -static-libstdc++ so the result does not require a | |
| # newer system libstdc++ on RHEL 8 derivatives. | |
| # Key: GLIBC < 2.34 also avoids the libpthread-merge symbol requirement. | |
| build-linux-x64: | |
| name: ${{ needs.dedupe.outputs.skip_heavy_ci == 'true' && 'deduped build-linux-x64' || 'build-linux-x64' }} | |
| needs: [dedupe, resolve-mosh, resolve-et] | |
| if: | | |
| always() | |
| && needs.dedupe.result == 'success' | |
| && needs.dedupe.outputs.skip_heavy_ci != 'true' | |
| runs-on: ubuntu-latest | |
| container: | |
| image: almalinux:8 | |
| env: | |
| MOSH_BIN_RELEASE: ${{ needs.resolve-mosh.outputs.mosh_bin_release }} | |
| ET_BIN_RELEASE: ${{ needs.resolve-et.outputs.et_bin_release }} | |
| npm_config_arch: x64 | |
| npm_config_target_arch: x64 | |
| # Keep rebuilt natives portable to stock RHEL 8 libstdc++. | |
| LDFLAGS: "-static-libstdc++ -static-libgcc" | |
| VITE_SYNC_GITHUB_CLIENT_ID: ${{ secrets.VITE_SYNC_GITHUB_CLIENT_ID }} | |
| VITE_SYNC_GOOGLE_CLIENT_ID: ${{ secrets.VITE_SYNC_GOOGLE_CLIENT_ID }} | |
| VITE_SYNC_GOOGLE_CLIENT_SECRET: ${{ secrets.VITE_SYNC_GOOGLE_CLIENT_SECRET }} | |
| VITE_SYNC_ONEDRIVE_CLIENT_ID: ${{ secrets.VITE_SYNC_ONEDRIVE_CLIENT_ID }} | |
| steps: | |
| - name: Validate bundled mosh-client release | |
| if: env.BUNDLE_MOSH == 'true' | |
| shell: bash | |
| env: | |
| RESOLVE_MOSH_RESULT: ${{ needs.resolve-mosh.result }} | |
| run: | | |
| if [[ "$RESOLVE_MOSH_RESULT" != "success" || -z "$MOSH_BIN_RELEASE" ]]; then | |
| echo "::error::Bundled mosh-client release was not resolved for this package build." | |
| exit 1 | |
| fi | |
| - name: Validate bundled et-client release | |
| if: env.BUNDLE_ET == 'true' | |
| shell: bash | |
| env: | |
| RESOLVE_ET_RESULT: ${{ needs.resolve-et.result }} | |
| run: | | |
| if [[ "$RESOLVE_ET_RESULT" != "success" || -z "$ET_BIN_RELEASE" ]]; then | |
| echo "::error::Bundled et-client release was not resolved for this package build." | |
| exit 1 | |
| fi | |
| - name: Install build dependencies | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| dnf -y install epel-release | |
| dnf -y install \ | |
| curl ca-certificates \ | |
| gcc-toolset-13-gcc gcc-toolset-13-gcc-c++ make \ | |
| python3.11 python3.11-pip \ | |
| git file rpm-build binutils cpio elfutils \ | |
| libarchive gzip bzip2 xz zstd tar which findutils \ | |
| dpkg fakeroot \ | |
| fuse fuse-libs gtk3 nss libXScrnSaver libXtst alsa-lib \ | |
| at-spi2-atk libdrm mesa-libgbm libX11 libX11-xcb libxcb \ | |
| mesa-libGL pango cairo | |
| # gcc-toolset-13: C++20 for Electron 42 natives; still links glibc 2.28. | |
| GCC_TOOLSET_ROOT="/opt/rh/gcc-toolset-13/root" | |
| echo "PATH=${GCC_TOOLSET_ROOT}/usr/bin:${PATH}" >> "${GITHUB_ENV}" | |
| echo "CC=${GCC_TOOLSET_ROOT}/usr/bin/gcc" >> "${GITHUB_ENV}" | |
| echo "CXX=${GCC_TOOLSET_ROOT}/usr/bin/g++" >> "${GITHUB_ENV}" | |
| echo "DEVTOOLSET_ROOTPATH=${GCC_TOOLSET_ROOT}" >> "${GITHUB_ENV}" | |
| export PATH="${GCC_TOOLSET_ROOT}/usr/bin:${PATH}" | |
| gcc --version | |
| g++ --version | |
| # AlmaLinux 8's libarchive package is often library-only in minimal | |
| # images (no /usr/bin/bsdtar). electron-builder pacman packaging needs | |
| # bsdtar; pull a Debian tools package that runs on glibc 2.28. | |
| # Prefer Buster (published as +deb10u1 on archive.debian.org). | |
| if [[ ! -x /usr/bin/bsdtar ]]; then | |
| echo "bsdtar missing; installing CLI from Debian libarchive-tools" | |
| mkdir -p /tmp/bsdtar-deb | |
| BSDTAR_DEB_URLS=( | |
| "http://archive.debian.org/debian/pool/main/liba/libarchive/libarchive-tools_3.3.3-4+deb10u1_amd64.deb" | |
| "http://archive.debian.org/debian/pool/main/liba/libarchive/libarchive-tools_3.2.2-2+deb9u2_amd64.deb" | |
| ) | |
| BSDTAR_DEB_OK=0 | |
| for url in "${BSDTAR_DEB_URLS[@]}"; do | |
| echo "trying ${url}" | |
| if curl -fsSL -o /tmp/libarchive-tools.deb "${url}"; then | |
| BSDTAR_DEB_OK=1 | |
| break | |
| fi | |
| echo "download failed for ${url}" | |
| done | |
| if [[ "${BSDTAR_DEB_OK}" -ne 1 ]]; then | |
| echo "::error::Could not download Debian libarchive-tools for bsdtar" | |
| exit 1 | |
| fi | |
| dpkg-deb -x /tmp/libarchive-tools.deb /tmp/bsdtar-deb | |
| install -m 0755 /tmp/bsdtar-deb/usr/bin/bsdtar /usr/local/bin/bsdtar-bin | |
| # Prefer system libarchive.so if present; fall back to the deb's. | |
| if [[ ! -e /usr/lib64/libarchive.so.13 && -d /tmp/bsdtar-deb/usr/lib/x86_64-linux-gnu ]]; then | |
| mkdir -p /usr/local/lib/bsdtar-libs | |
| cp -a /tmp/bsdtar-deb/usr/lib/x86_64-linux-gnu/libarchive.so* /usr/local/lib/bsdtar-libs/ 2>/dev/null || true | |
| fi | |
| printf '%s\n' \ | |
| '#!/bin/bash' \ | |
| 'unset LD_LIBRARY_PATH' \ | |
| 'if [[ -d /usr/local/lib/bsdtar-libs ]]; then' \ | |
| ' export LD_LIBRARY_PATH=/usr/local/lib/bsdtar-libs' \ | |
| 'fi' \ | |
| 'exec /usr/local/bin/bsdtar-bin "$@"' \ | |
| > /usr/local/bin/bsdtar | |
| chmod +x /usr/local/bin/bsdtar | |
| fi | |
| # Some fpm paths still look for xzmt even when payload compression | |
| # is gzip; provide a multi-thread xz shim. | |
| printf '%s\n' '#!/bin/sh' 'exec /usr/bin/xz -T0 "$@"' > /usr/local/bin/xzmt | |
| chmod +x /usr/local/bin/xzmt | |
| # electron-builder's portable fpm injects its own LD_LIBRARY_PATH. | |
| # System tools (rpmbuild, bsdtar, ...) then exit 127 under that path. | |
| # Wrap every packaging helper so child processes see a clean env. | |
| # Use printf (not a bare heredoc): unindented heredoc body breaks GHA YAML. | |
| wrap_clean_env() { | |
| local name="$1" | |
| local real="" | |
| if [[ -x "/usr/bin/${name}" ]]; then | |
| real="/usr/bin/${name}" | |
| elif [[ -x "/usr/local/bin/${name}-bin" ]]; then | |
| real="/usr/local/bin/${name}-bin" | |
| elif command -v "${name}" >/dev/null 2>&1; then | |
| real="$(command -v "${name}")" | |
| else | |
| return 0 | |
| fi | |
| # Don't wrap our own wrapper recursively. | |
| if [[ "${real}" == /usr/local/bin/* && "${name}" != "bsdtar" ]]; then | |
| return 0 | |
| fi | |
| if [[ "${name}" == "bsdtar" && -x /usr/local/bin/bsdtar-bin ]]; then | |
| real="/usr/local/bin/bsdtar-bin" | |
| fi | |
| printf '%s\n' \ | |
| '#!/bin/bash' \ | |
| 'unset LD_LIBRARY_PATH' \ | |
| "exec ${real} \"\$@\"" \ | |
| > "/usr/local/bin/${name}" | |
| chmod +x "/usr/local/bin/${name}" | |
| } | |
| for cmd in rpmbuild bsdtar bsdcpio tar gzip xz strip cpio; do | |
| wrap_clean_env "${cmd}" | |
| done | |
| # Re-apply bsdtar wrapper with optional lib path for the deb-extracted binary. | |
| if [[ -x /usr/local/bin/bsdtar-bin ]]; then | |
| printf '%s\n' \ | |
| '#!/bin/bash' \ | |
| 'unset LD_LIBRARY_PATH' \ | |
| 'if [[ -d /usr/local/lib/bsdtar-libs ]]; then' \ | |
| ' export LD_LIBRARY_PATH=/usr/local/lib/bsdtar-libs' \ | |
| 'fi' \ | |
| 'exec /usr/local/bin/bsdtar-bin "$@"' \ | |
| > /usr/local/bin/bsdtar | |
| chmod +x /usr/local/bin/bsdtar | |
| fi | |
| # Fail early with diagnostics if packaging tools are missing. | |
| for tool in rpmbuild bsdtar strip gzip xz xzmt; do | |
| if ! command -v "${tool}" >/dev/null 2>&1; then | |
| echo "::error::Required packaging tool not found: ${tool}" | |
| ls -la /usr/bin/bsd* /usr/local/bin/bsd* 2>/dev/null || true | |
| rpm -ql libarchive 2>/dev/null | head -n 40 || true | |
| exit 1 | |
| fi | |
| echo "found ${tool}: $(command -v "${tool}")" | |
| done | |
| rpmbuild --version | |
| bsdtar --version | head -n 1 | |
| # Official Node 22 linux-x64 tarball (same major as other package jobs). | |
| NODE_VERSION="$(curl -fsSL https://nodejs.org/dist/latest-v22.x/SHASUMS256.txt \ | |
| | awk '/node-v[0-9.]+-linux-x64\.tar\.xz$/{print $2; exit}' \ | |
| | sed -E 's/^node-(v[0-9.]+)-linux-x64\.tar\.xz$/\1/')" | |
| if [[ -z "${NODE_VERSION}" ]]; then | |
| echo "::error::Could not resolve latest Node 22 linux-x64 tarball version." | |
| exit 1 | |
| fi | |
| echo "Installing Node ${NODE_VERSION} from nodejs.org" | |
| curl -fsSL "https://nodejs.org/dist/${NODE_VERSION}/node-${NODE_VERSION}-linux-x64.tar.xz" \ | |
| | tar -xJ -C /usr/local --strip-components=1 | |
| node -v | |
| npm -v | |
| # node-gyp 12 needs Python >=3.8; AlmaLinux 8 default is older. | |
| alternatives --set python3 /usr/bin/python3.11 || ln -sfn /usr/bin/python3.11 /usr/local/bin/python3 | |
| python3 --version | |
| echo "PYTHON=/usr/bin/python3.11" >> "${GITHUB_ENV}" | |
| echo "npm_config_python=/usr/bin/python3.11" >> "${GITHUB_ENV}" | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install deps | |
| run: | | |
| python3 --version | |
| g++ --version | |
| npm ci | |
| - name: Set version | |
| shell: bash | |
| run: | | |
| # See matrix job's Set version step for the strict-semver | |
| # rationale; identical logic, duplicated because the Linux | |
| # legs are standalone jobs. | |
| if [[ "$GITHUB_REF" =~ $STRICT_VERSION_REF_RE ]]; then | |
| VERSION="${GITHUB_REF_NAME#v}" | |
| else | |
| VERSION="0.0.0-sha-${GITHUB_SHA:0:7}" | |
| fi | |
| echo "Setting version to ${VERSION}" | |
| npm pkg set version="${VERSION}" | |
| - name: Prepare node-pty Linux runtime | |
| env: | |
| npm_config_arch: x64 | |
| run: bash scripts/ensure-node-pty-linux.sh prepare x64 | |
| - name: Fetch bundled mosh-client | |
| if: env.BUNDLE_MOSH == 'true' | |
| run: npm run fetch:mosh -- --platform=linux --arch=x64 | |
| - name: Fetch bundled et-client | |
| if: env.BUNDLE_ET == 'true' | |
| run: npm run fetch:et -- --platform=linux --arch=x64 | |
| - name: Build package | |
| env: | |
| npm_config_arch: x64 | |
| ELECTRON_BUILDER_PUBLISH: "never" | |
| # Natives are already rebuilt. Prefer system packaging tools so | |
| # rpmbuild brp scripts do not pick up a toolset-only binary, and so | |
| # /usr/local/bin/rpmbuild (clean-env wrapper) wins over /usr/bin. | |
| PATH: /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin | |
| run: | | |
| set -euo pipefail | |
| command -v node | |
| command -v rpmbuild | |
| command -v bsdtar | |
| command -v gzip | |
| command -v xzmt | |
| for cmd in rpmbuild bsdtar; do | |
| if ! head -n 2 "$(command -v "${cmd}")" | grep -q 'unset LD_LIBRARY_PATH'; then | |
| echo "::warning::${cmd} on PATH is not the clean-env wrapper; packaging may fail with exit 127" | |
| fi | |
| done | |
| npm run pack:linux-x64 | |
| - name: Verify packaged node-pty Linux runtime | |
| run: bash scripts/ensure-node-pty-linux.sh verify x64 | |
| - name: Verify packaged deb artifact | |
| run: bash scripts/verify-linux-deb-artifact.sh amd64 | |
| - name: Verify packaged rpm artifact | |
| run: bash scripts/verify-linux-rpm-artifact.sh x86_64 | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: netcatty-linux-x64 | |
| path: | | |
| release/*.AppImage | |
| release/*.deb | |
| release/*.rpm | |
| release/*.pacman | |
| release/*.yml | |
| release/*.blockmap | |
| if-no-files-found: ignore | |
| # Dedicated job for Linux ARM64 — builds inside Debian Bullseye (GLIBC 2.31) | |
| # to ensure compatibility with older distros like UOS/Deepin (GLIBC 2.28). | |
| # Key: GLIBC < 2.34 avoids the libpthread-merge symbol requirement. | |
| build-linux-arm64: | |
| name: ${{ needs.dedupe.outputs.skip_heavy_ci == 'true' && 'deduped build-linux-arm64' || 'build-linux-arm64' }} | |
| needs: [dedupe, resolve-mosh, resolve-et] | |
| if: | | |
| always() | |
| && needs.dedupe.result == 'success' | |
| && needs.dedupe.outputs.skip_heavy_ci != 'true' | |
| runs-on: ubuntu-24.04-arm | |
| container: | |
| image: debian:bullseye | |
| env: | |
| MOSH_BIN_RELEASE: ${{ needs.resolve-mosh.outputs.mosh_bin_release }} | |
| ET_BIN_RELEASE: ${{ needs.resolve-et.outputs.et_bin_release }} | |
| npm_config_arch: arm64 | |
| npm_config_target_arch: arm64 | |
| VITE_SYNC_GITHUB_CLIENT_ID: ${{ secrets.VITE_SYNC_GITHUB_CLIENT_ID }} | |
| VITE_SYNC_GOOGLE_CLIENT_ID: ${{ secrets.VITE_SYNC_GOOGLE_CLIENT_ID }} | |
| VITE_SYNC_GOOGLE_CLIENT_SECRET: ${{ secrets.VITE_SYNC_GOOGLE_CLIENT_SECRET }} | |
| VITE_SYNC_ONEDRIVE_CLIENT_ID: ${{ secrets.VITE_SYNC_ONEDRIVE_CLIENT_ID }} | |
| steps: | |
| - name: Validate bundled mosh-client release | |
| if: env.BUNDLE_MOSH == 'true' | |
| shell: bash | |
| env: | |
| RESOLVE_MOSH_RESULT: ${{ needs.resolve-mosh.result }} | |
| run: | | |
| if [[ "$RESOLVE_MOSH_RESULT" != "success" || -z "$MOSH_BIN_RELEASE" ]]; then | |
| echo "::error::Bundled mosh-client release was not resolved for this package build." | |
| exit 1 | |
| fi | |
| - name: Validate bundled et-client release | |
| if: env.BUNDLE_ET == 'true' | |
| shell: bash | |
| env: | |
| RESOLVE_ET_RESULT: ${{ needs.resolve-et.result }} | |
| run: | | |
| if [[ "$RESOLVE_ET_RESULT" != "success" || -z "$ET_BIN_RELEASE" ]]; then | |
| echo "::error::Bundled et-client release was not resolved for this package build." | |
| exit 1 | |
| fi | |
| - name: Install build dependencies | |
| run: | | |
| apt-get update | |
| apt-get install -y curl build-essential python3 git libfuse2 file rpm \ | |
| libarchive-tools \ | |
| libglib2.0-0 libgtk-3-0 libnss3 libxss1 libxtst6 libasound2 \ | |
| libatk-bridge2.0-0 libdrm2 libgbm1 libx11-xcb1 libxcb-dri3-0 | |
| curl -fsSL https://deb.nodesource.com/setup_22.x | bash - | |
| apt-get install -y nodejs | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install deps | |
| run: npm ci | |
| - name: Set version | |
| shell: bash | |
| run: | | |
| # See matrix job's Set version step for the strict-semver | |
| # rationale; identical logic, duplicated because the Linux | |
| # legs are standalone jobs. | |
| if [[ "$GITHUB_REF" =~ $STRICT_VERSION_REF_RE ]]; then | |
| VERSION="${GITHUB_REF_NAME#v}" | |
| else | |
| VERSION="0.0.0-sha-${GITHUB_SHA:0:7}" | |
| fi | |
| echo "Setting version to ${VERSION}" | |
| npm pkg set version="${VERSION}" | |
| - name: Prepare node-pty Linux runtime | |
| env: | |
| npm_config_arch: arm64 | |
| run: bash scripts/ensure-node-pty-linux.sh prepare arm64 | |
| - name: Fetch bundled mosh-client | |
| if: env.BUNDLE_MOSH == 'true' | |
| run: npm run fetch:mosh -- --platform=linux --arch=arm64 | |
| - name: Fetch bundled et-client | |
| if: env.BUNDLE_ET == 'true' | |
| run: npm run fetch:et -- --platform=linux --arch=arm64 | |
| - name: Build package | |
| env: | |
| npm_config_arch: arm64 | |
| ELECTRON_BUILDER_PUBLISH: "never" | |
| run: npm run pack:linux-arm64 | |
| - name: Verify packaged node-pty Linux runtime | |
| run: bash scripts/ensure-node-pty-linux.sh verify arm64 | |
| - name: Verify packaged deb artifact | |
| run: bash scripts/verify-linux-deb-artifact.sh arm64 | |
| - name: Verify packaged rpm artifact | |
| run: bash scripts/verify-linux-rpm-artifact.sh aarch64 | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: netcatty-linux-arm64 | |
| path: | | |
| release/*.AppImage | |
| release/*.deb | |
| release/*.rpm | |
| release/*.pacman | |
| release/*.yml | |
| release/*.blockmap | |
| if-no-files-found: ignore | |
| release: | |
| name: release | |
| runs-on: ubuntu-latest | |
| needs: [build, build-linux-x64, build-linux-arm64] | |
| # Only release on a strict v<MAJOR>.<MINOR>.<PATCH>[-pre] tag. | |
| # Manual workflow_dispatch can publish only when it is run from one | |
| # of those tags. PRs and branch pushes skip this job. | |
| if: | | |
| startsWith(github.ref, 'refs/tags/v') | |
| && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish_release)) | |
| permissions: | |
| contents: write | |
| actions: read | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Validate release tag | |
| shell: bash | |
| run: | | |
| if [[ ! "$GITHUB_REF" =~ $STRICT_VERSION_REF_RE ]]; then | |
| echo "::error::Release tags must be v<MAJOR>.<MINOR>.<PATCH> or v<MAJOR>.<MINOR>.<PATCH>-<prerelease>." | |
| exit 1 | |
| fi | |
| - name: Download artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| merge-multiple: true | |
| - name: List artifacts | |
| run: ls -la artifacts/ | |
| - name: Verify update metadata files | |
| run: | | |
| missing=0 | |
| for f in latest-mac.yml latest.yml latest-linux.yml latest-linux-arm64.yml; do | |
| if [ ! -f "artifacts/$f" ]; then | |
| echo "::warning::Missing $f in merged artifacts, attempting recovery..." | |
| missing=1 | |
| fi | |
| done | |
| if [ "$missing" = "1" ]; then | |
| echo "Re-downloading individual artifacts to recover missing files..." | |
| for name in netcatty-macos netcatty-windows netcatty-linux-x64 netcatty-linux-arm64; do | |
| tmpdir="/tmp/artifact-${name}" | |
| gh run download ${{ github.run_id }} --name "${name}" --dir "${tmpdir}" 2>/dev/null || true | |
| if [ -d "${tmpdir}" ]; then | |
| for yml in "${tmpdir}"/latest*.yml; do | |
| [ -f "$yml" ] && cp -v "$yml" artifacts/ | |
| done | |
| fi | |
| done | |
| echo "After recovery:" | |
| ls -la artifacts/*.yml | |
| fi | |
| # Final check — fail if any update yml is still missing | |
| for f in latest-mac.yml latest.yml latest-linux.yml latest-linux-arm64.yml; do | |
| if [ ! -f "artifacts/$f" ]; then | |
| echo "::error::$f is still missing after recovery attempt" | |
| exit 1 | |
| fi | |
| done | |
| echo "All update metadata files present." | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Verify downloaded Linux amd64 deb artifact | |
| run: | | |
| deb_file="$(find artifacts -maxdepth 1 -type f -name '*-linux-amd64.deb' -print | sort | head -n 1)" | |
| test -n "${deb_file}" | |
| bash scripts/verify-linux-deb-artifact.sh amd64 "${deb_file}" | |
| - name: Verify downloaded Linux arm64 deb artifact metadata | |
| env: | |
| VERIFY_LOAD: "0" | |
| run: | | |
| deb_file="$(find artifacts -maxdepth 1 -type f -name '*-linux-arm64.deb' -print | sort | head -n 1)" | |
| test -n "${deb_file}" | |
| bash scripts/verify-linux-deb-artifact.sh arm64 "${deb_file}" | |
| - name: Generate Release Body | |
| run: node .github/scripts/generate-release-note.js | |
| env: | |
| GITHUB_REF_NAME: ${{ github.ref_name }} | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| GITHUB_SHA: ${{ github.sha }} | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| body_path: release_notes.md | |
| prerelease: ${{ contains(github.ref_name, '-') }} | |
| files: | | |
| artifacts/*.dmg | |
| artifacts/*.zip | |
| artifacts/*.exe | |
| artifacts/*.AppImage | |
| artifacts/*.deb | |
| artifacts/*.rpm | |
| artifacts/*.pacman | |
| artifacts/*.yml | |
| artifacts/*.blockmap | |
| generate_release_notes: true | |
| fail_on_unmatched_files: false | |
| token: ${{ secrets.RELEASE_TOKEN }} | |
| update-nix-release: | |
| name: update Nix release metadata | |
| runs-on: ubuntu-latest | |
| needs: release | |
| if: | | |
| startsWith(github.ref, 'refs/tags/v') | |
| && !contains(github.ref_name, '-') | |
| && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish_release)) | |
| permissions: | |
| contents: write | |
| actions: read | |
| steps: | |
| - name: Checkout default branch | |
| uses: actions/checkout@v4 | |
| with: | |
| ref: ${{ github.event.repository.default_branch }} | |
| token: ${{ secrets.RELEASE_TOKEN }} | |
| - name: Download Linux artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| merge-multiple: true | |
| - name: Update Nix release metadata | |
| run: node .github/scripts/update-nix-release.js --artifacts artifacts --version "${GITHUB_REF_NAME#v}" | |
| - name: Commit Nix release metadata | |
| shell: bash | |
| run: | | |
| if git diff --quiet -- nix/release.nix; then | |
| echo "Nix release metadata is already current." | |
| exit 0 | |
| fi | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git add nix/release.nix | |
| git commit -m "Update Nix release metadata for ${GITHUB_REF_NAME}" | |
| git push origin HEAD:${{ github.event.repository.default_branch }} | |
| homebrew-tap: | |
| name: bump homebrew tap | |
| runs-on: ubuntu-latest | |
| needs: release | |
| # Only stable release tags update the Cask. Prerelease tags | |
| # (e.g. v1.2.0-rc.1) are skipped so brew users stay on stable. | |
| if: | | |
| startsWith(github.ref, 'refs/tags/v') | |
| && !contains(github.ref_name, '-') | |
| && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.publish_release)) | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Download macOS artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: netcatty-macos | |
| path: artifacts/ | |
| - name: Bump Cask in binaricat/homebrew-netcatty | |
| env: | |
| HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} | |
| ARTIFACTS_DIR: artifacts | |
| run: | | |
| # Strip the leading "v" — Cask version is plain semver. | |
| VERSION="${GITHUB_REF_NAME#v}" | |
| export VERSION | |
| bash .github/scripts/bump-homebrew-cask.sh |