From 87300d4a631bf15a3d70a1dcf7b3194e05eaea05 Mon Sep 17 00:00:00 2001 From: Den Kong Date: Sat, 5 Sep 2026 17:29:20 +0800 Subject: [PATCH] Add a windows package job to the release workflow Windows is the one platform code-server has no build for, so `tode`, and anything else that wants a server on that machine, has nowhere to get one. This adds a third job beside package-linux and package-macos that produces a windows-x64 tarball the same way they produce theirs: on that platform's own runner, with the native modules compiled there, uploaded to the same draft release. Most of it is the macos job with a different runner. What follows is every place windows needed something else, and why. The default shell is set to bash for the job rather than on each step, since windows is the one runner whose default is not bash. Git rewrites line endings on checkout on windows. That turns every shell script the build is made of into one bash cannot read, and every name in patches/series into one with a stray return on the end. OS is answered up front. ci/lib.sh works the system out only when OS is empty, and windows sets OS to Windows_NT for every process, so the detection never runs and every question the build asks about the system gets Windows_NT instead -- which launchers to fix up, what the archive is called. The check for an existing value is what makes answering it up front the intended way; a fix inside ci/lib.sh is possible but belongs in its own change. quilt has no windows build, so the patches are applied with git. They are ordinary -p1 diffs against the repository root and go on in the order series names them. Worth knowing for anyone who prefers the symmetry: msys2 packages quilt, and this job already installs one package from it, so `quilt push -a` here is plausible. It is untried on a runner, so it is not what this does. npm hands every script it runs to cmd, which cannot run the shell scripts this repository is built out of, so npm_config_script_shell points it at the same bash the steps use. jq is handed a process substitution when the build merges json, which bash presents as a file under /dev/fd. The jq on this image is a windows program and cannot open those: it reads the second input as nothing and the merge fails silently. The shim copies those arguments to real files. signtool has to be findable. Stamping version details into the native binaries clears any signature first and asks signtool whether there is one, which only reads and removes -- no certificate, nothing signed. rsync exists in the MSYS2 already on the image but not on the path, and it is reached through a forwarder rather than by putting msys2's /usr/bin in front. Measured, because the obvious way fails strangely: npm on the path is a shell script whose shebang reads /usr/bin/env bash, so with msys2 first it is msys2's bash that runs it, and crossing into a second msys runtime does not carry the environment -- 93 variables arrived as 7, PATH rebuilt from msys2's defaults. npm then saw no script-shell and fell back to cmd, and KEEP_MODULES was dropped by the same crossing, which would have produced a release tree with no node_modules and no node beside it and said nothing about it. The archive step asserts it has GNU tar. Of the two on this image only git bash's can rename the tree's top directory as it archives; the windows bsdtar is built without substitution support and refuses -s outright. They are interchangeable everywhere except in exactly this, so it is checked rather than assumed. This depends on the windows arms of the build scripts being correct. Without those fixes the release step fails on a missing node, and with them this job has produced a tarball that unpacks and runs: the server serves, the extension host starts, and a terminal in the workbench round-trips a command. Two things deliberately left out. There is no node-gyp header cache, which existed while this was being brought up and only saved downloads. And npm run test:native is not here, because it has not been run on windows; it would be a small addition once the job exists to run it in. --- .github/workflows/release.yaml | 153 +++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 177528aab5f4..e7a63f947c8b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -196,3 +196,156 @@ jobs: files: ./release-packages/* tag_name: v${{ env.VERSION }} name: v${{ env.VERSION }} + + package-windows: + name: win32-x64 + runs-on: windows-2022 + if: >- + (github.event_name == 'workflow_dispatch') || + (github.event_name == 'pull_request_target' && github.event.pull_request.merged == true && startsWith(github.head_ref, 'update/')) + + defaults: + run: + shell: bash + + env: + VSCODE_TARGET: win32-x64 + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ inputs.version || github.event.pull_request.head.ref || github.ref_name }} + # Ensure native modules are built from source to avoid prebuilds. + npm_config_build_from_source: true + # Windows sets OS to Windows_NT, and ci/lib.sh works the system out + # only when OS is empty, so every question the build asks about it + # gets that answer instead: which launchers to fix up, what the + # archive is called. + OS: windows + + steps: + # Git rewrites line endings on windows by default, which turns every + # shell script the build is made of into one bash cannot read, and + # every name in patches/series into one with a stray return. + - name: Keep line endings as they are in the repository + run: git config --global core.autocrlf false + + - name: Strip update/ and v from tag and set major version + run: | + version=${TAG#update/} + version=${version#v} + version=4${version:1} + echo "VERSION=$version" >> $GITHUB_ENV + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v6 + with: + submodules: true + + # quilt has no windows build. The patches are ordinary -p1 diffs + # against the repository root, so git applies them in series order. + - name: Apply patches + run: | + while read -r patch; do + case "$patch" in '' | '#'*) continue ;; esac + echo "applying $patch" + git apply --whitespace=nowarn "patches/$patch" + done < patches/series + + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v6 + with: + node-version-file: .node-version + cache: npm + cache-dependency-path: | + package-lock.json + test/package-lock.json + + # npm hands every script it runs to cmd, which cannot run the shell + # scripts this repository is built out of. Point it at the same bash + # the steps here use rather than at a path, which moves between + # images. + - name: Let npm run shell scripts + run: echo "npm_config_script_shell=$(cygpath -w "$(command -v bash)")" >> $GITHUB_ENV + + # The build merges json by handing jq a process substitution, which + # bash presents as a file under /dev/fd. The jq on this image is a + # windows program and cannot open those, so it reads the second input + # as nothing and the merge fails. Both the product and the package + # merge go through here. + - name: Let jq read what bash hands it + run: | + mkdir -p "$RUNNER_TEMP/shim" + cat > "$RUNNER_TEMP/shim/jq" <<'SHIM' + #!/usr/bin/env bash + set -euo pipefail + args=() + for arg in "$@"; do + case $arg in + /dev/fd/* | /proc/*/fd/*) + copy=$(mktemp) + cat "$arg" > "$copy" + args+=("$copy") + ;; + *) args+=("$arg") ;; + esac + done + exec jq.exe "${args[@]}" + SHIM + chmod +x "$RUNNER_TEMP/shim/jq" + echo "$RUNNER_TEMP/shim" >> $GITHUB_PATH + + # Stamping version details into the native binaries clears any + # signature they arrived with and asks signtool whether there is one. + # That only reads and removes, so it wants no certificate and signs + # nothing. It just has to be findable, and the sdk carrying it is not + # on the path. + - name: Put signtool on the path + run: | + sdk=$(ls -d "/c/Program Files (x86)/Windows Kits/10/bin"/*/x64 | sort -V | tail -1) + test -x "$sdk/signtool.exe" + cygpath -w "$sdk" >> $GITHUB_PATH + + # build-release.sh copies the tree with rsync, which neither windows + # nor the git bash on this image has. MSYS2 is already here, just not + # on the path. + - name: Install rsync + shell: cmd + run: C:\msys64\usr\bin\pacman -Sy --noconfirm --needed rsync + + # Only rsync crosses over. Putting msys2's /usr/bin in front instead + # breaks the release step: npm on the path is a shell script whose + # shebang reads /usr/bin/env bash, so with msys2 first it is msys2's + # bash that runs it, and crossing into a second msys runtime does not + # carry the environment. npm then sees no script-shell and falls back + # to cmd, which cannot run ./ci/build/build-release.sh, and + # KEEP_MODULES is dropped on the way. A forwarder avoids the whole + # class: rsync is a native exe that loads its runtime from beside + # itself, and nothing else on the path moves. + - name: Reach rsync without moving the path + run: | + cat > "$RUNNER_TEMP/shim/rsync" <<'SHIM' + #!/usr/bin/env bash + exec /c/msys64/usr/bin/rsync.exe "$@" + SHIM + chmod +x "$RUNNER_TEMP/shim/rsync" + + - run: npm ci + - run: npm run build + - run: npm run build:vscode + - run: KEEP_MODULES=1 npm run release + + # Of the two tars on this image it is git bash's GNU one that can + # rename the tree's top directory as it archives; the windows bsdtar + # is built without substitution support. Asserted rather than + # assumed, since the two are interchangeable everywhere except here. + - name: Package + run: | + case "$(tar --version | head -1)" in + *GNU*) ;; + *) echo "expected GNU tar for --transform, got $(tar --version | head -1)" >&2; exit 1 ;; + esac + npm run package + + - uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 + with: + draft: true + discussion_category_name: "📣 Announcements" + files: ./release-packages/* + tag_name: v${{ env.VERSION }} + name: v${{ env.VERSION }}