Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
5 changes: 3 additions & 2 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# Build with the tar context shown in Dockerfile: Dockerfile and the selected
# release tarball only. A checkout context excludes all files by default.
# Build with the tar context shown in Dockerfile: Dockerfile, the matching
# release lockfile, and the selected release tarball only. A checkout context
# excludes all files by default.
*
579 changes: 579 additions & 0 deletions .github/consolidated-audit-dispositions.md

Large diffs are not rendered by default.

77 changes: 50 additions & 27 deletions .github/workflows/channel-republish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,11 @@ on:
permissions:
contents: read

env:
NPM_CONFIG_FETCH_RETRIES: '3'
NPM_CONFIG_FETCH_RETRY_MINTIMEOUT: '10000'
NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT: '60000'

# The same group `release.yml` and `container-republish.yml` use, so a repair
# cannot race a release. Never cancelled: this writes to other people's
# repositories and to a moderated feed, and being interrupted partway is the
Expand Down Expand Up @@ -284,22 +289,29 @@ jobs:
# is the whole question: the release exists, so if the formula points at
# something that is not there, this is where it is caught.
- name: Audit the formula
env:
VERSION: ${{ needs.prepare.outputs.version }}
BUNDLE_URL: ${{ needs.prepare.outputs.url }}
BUNDLE_SHA256: ${{ needs.prepare.outputs.sha256 }}
run: >
node scripts/audit-formula.ts --online
--version "${{ needs.prepare.outputs.version }}"
--url "${{ needs.prepare.outputs.url }}"
--sha256 "${{ needs.prepare.outputs.sha256 }}"
--version "${VERSION}"
--url "${BUNDLE_URL}"
--sha256 "${BUNDLE_SHA256}"

- name: Push the formula
env:
GH_TOKEN: ${{ secrets.TAP_BUCKET_TOKEN }}
run: >
node scripts/channel-push.ts --channel homebrew
--repo looptroop-ai/homebrew-tap
--version "${{ needs.prepare.outputs.version }}"
--url "${{ needs.prepare.outputs.url }}"
--sha256 "${{ needs.prepare.outputs.sha256 }}"
${{ needs.prepare.outputs.force == 'true' && '--force' || '' }}
VERSION: ${{ needs.prepare.outputs.version }}
BUNDLE_URL: ${{ needs.prepare.outputs.url }}
BUNDLE_SHA256: ${{ needs.prepare.outputs.sha256 }}
FORCE: ${{ needs.prepare.outputs.force }}
run: |
set -euo pipefail
args=(--channel homebrew --repo looptroop-ai/homebrew-tap
--version "${VERSION}" --url "${BUNDLE_URL}" --sha256 "${BUNDLE_SHA256}")
if [ "${FORCE}" = 'true' ]; then args+=(--force); fi
node scripts/channel-push.ts "${args[@]}"

push-manifest:
name: Republish the Scoop manifest
Expand All @@ -324,13 +336,16 @@ jobs:
- name: Push the manifest
env:
GH_TOKEN: ${{ secrets.TAP_BUCKET_TOKEN }}
run: >
node scripts/channel-push.ts --channel scoop
--repo looptroop-ai/scoop-bucket
--version "${{ needs.prepare.outputs.version }}"
--url "${{ needs.prepare.outputs.url }}"
--sha256 "${{ needs.prepare.outputs.sha256 }}"
${{ needs.prepare.outputs.force == 'true' && '--force' || '' }}
VERSION: ${{ needs.prepare.outputs.version }}
BUNDLE_URL: ${{ needs.prepare.outputs.url }}
BUNDLE_SHA256: ${{ needs.prepare.outputs.sha256 }}
FORCE: ${{ needs.prepare.outputs.force }}
run: |
set -euo pipefail
args=(--channel scoop --repo looptroop-ai/scoop-bucket
--version "${VERSION}" --url "${BUNDLE_URL}" --sha256 "${BUNDLE_SHA256}")
if [ "${FORCE}" = 'true' ]; then args+=(--force); fi
node scripts/channel-push.ts "${args[@]}"

# ---------------------------------------------------------------------------
# Chocolatey, whose recovery states are its own.
Expand Down Expand Up @@ -405,27 +420,33 @@ jobs:
- name: Pack and prove it locally
if: steps.feed.outputs.published == 'false'
shell: bash
env:
BUNDLE: ${{ needs.prepare.outputs.bundle }}
BUNDLE_URL: ${{ needs.prepare.outputs.url }}
VERSION: ${{ needs.prepare.outputs.version }}
BUNDLE_SHA256: ${{ needs.prepare.outputs.sha256 }}
run: |
set -euo pipefail
node scripts/build-choco.ts \
--bundle "${{ needs.prepare.outputs.bundle }}" \
--url "${{ needs.prepare.outputs.url }}" \
--version "${{ needs.prepare.outputs.version }}" \
--expect-sha256 "${{ needs.prepare.outputs.sha256 }}" \
--bundle "${BUNDLE}" \
--url "${BUNDLE_URL}" \
--version "${VERSION}" \
--expect-sha256 "${BUNDLE_SHA256}" \
--out dist-choco
node scripts/smoke-choco.ts \
--nupkg "dist-choco/looptroop.${{ needs.prepare.outputs.version }}.nupkg" \
--version "${{ needs.prepare.outputs.version }}"
--nupkg "dist-choco/looptroop.${VERSION}.nupkg" \
--version "${VERSION}"

- name: Submit it
if: steps.feed.outputs.published == 'false'
shell: bash
env:
CHOCOLATEY_API_KEY: ${{ secrets.CHOCOLATEY_API_KEY }}
VERSION: ${{ needs.prepare.outputs.version }}
run: >
node scripts/choco-push.ts
--nupkg "dist-choco/looptroop.${{ needs.prepare.outputs.version }}.nupkg"
--version "${{ needs.prepare.outputs.version }}"
--nupkg "dist-choco/looptroop.${VERSION}.nupkg"
--version "${VERSION}"

# The only check that what Chocolatey serves is what this release meant to
# publish. Everything before moderation proves the package we built; this
Expand All @@ -441,9 +462,11 @@ jobs:
- name: Install it from the feed and prove it
if: steps.feed.outputs.published == 'true'
shell: bash
env:
VERSION: ${{ needs.prepare.outputs.version }}
run: >
node scripts/smoke-choco.ts --from-feed
--version "${{ needs.prepare.outputs.version }}"
--version "${VERSION}"

# ---------------------------------------------------------------------------
# WinGet, whose recovery is a pull request rather than a write.
Expand Down Expand Up @@ -538,7 +561,7 @@ jobs:
set -euo pipefail
gh workflow run published-smoke.yml \
--repo "${GITHUB_REPOSITORY}" \
--ref main \
--ref "v${VERSION}" \
-f version="${VERSION}" \
-f tier=weekly \
-f only="${CHANNEL}" \
Expand Down
152 changes: 140 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ env:
# Silences npm's funding/update chatter so failures stand out in logs.
NPM_CONFIG_FUND: 'false'
NPM_CONFIG_AUDIT: 'false'
# Bounded registry fetch retries cover transient CDN failures without
# retrying tests or masking a failed build.
NPM_CONFIG_FETCH_RETRIES: '3'
NPM_CONFIG_FETCH_RETRY_MINTIMEOUT: '10000'
NPM_CONFIG_FETCH_RETRY_MAXTIMEOUT: '60000'

jobs:
# 4,400+ lines of workflow YAML, most of it inline bash and PowerShell that no
Expand Down Expand Up @@ -214,6 +219,93 @@ jobs:
- name: Test
run: npm run test

# A small Windows PR gate for changes that can alter executable resolution or
# the published-smoke workflow. The normal matrix is broad, but it is not a
# substitute for this profile: the extensionless-first shim regression only
# exists on Windows and must run against the real win32 path rules.
windows-gate:
name: Windows gate profile (affected files)
if: github.event_name == 'pull_request'
runs-on: windows-latest
timeout-minutes: 25
steps:
- name: Checkout the pull request
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
persist-credentials: false

- name: Check whether the Windows profile is affected
id: scope
shell: bash
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
set -euo pipefail
affected=false
while IFS= read -r path; do
case "${path}" in
.github/workflows/*|package.json|package-lock.json|install.sh|install.ps1|\
scripts/smoke-lib.mjs|scripts/smoke-lib.d.mts|scripts/smoke-published.mjs|scripts/smoke-published.d.mts|scripts/tool-path.ts|\
server/lib/executablePath.ts|server/lib/__tests__/executablePath.test.ts|\
tests/installer.test.ts|tests/smokePublished.test.ts)
affected=true
break
;;
esac
done < <(git diff --name-only "${BASE_SHA}" "${HEAD_SHA}")
echo "affected=${affected}" >> "$GITHUB_OUTPUT"
echo "Windows gate affected: ${affected}"

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
if: steps.scope.outputs.affected == 'true'
with:
node-version: 24.18.1
cache: npm

- name: Pin npm to the declared version
if: steps.scope.outputs.affected == 'true'
run: node scripts/pin-npm.mjs
shell: bash

- name: Install dependencies
if: steps.scope.outputs.affected == 'true'
run: npm ci

# The driver installs this published candidate itself. Resolve the stable
# package that is actually available instead of the checkout version,
# which may be a release candidate that has not reached npm yet.
- name: Resolve a published npm candidate
if: steps.scope.outputs.affected == 'true'
id: candidate
shell: bash
run: |
set -euo pipefail
version="$(npm view looptroop version)"
test -n "${version}"
echo "version=${version}" >> "$GITHUB_OUTPUT"

# `installer.test.ts` includes the extensionless-first shim regression:
# on win32, `npm` must resolve to npm.cmd when an extensionless shim comes
# first in PATH. The executablePath suite checks the same resolver's
# ENOENT path preservation.
- name: Run the Windows resolver and published-smoke gates
if: steps.scope.outputs.affected == 'true'
shell: bash
env:
VERSION: ${{ steps.candidate.outputs.version }}
run: |
set -euo pipefail
npm test -- tests/installer.test.ts server/lib/__tests__/executablePath.test.ts tests/smokePublished.test.ts
node scripts/smoke-published.mjs \
--channel npm \
--version "${VERSION}" \
--pin \
--opencode mock \
--profile gate \
--leg "npm (windows-latest)"

# Everything above runs against the source tree, where the working directory
# already holds every file the daemon needs. This installs the tarball the way
# a user would and drives the CLI against it, which is the only place a
Expand Down Expand Up @@ -313,8 +405,10 @@ jobs:
VERSION: ${{ steps.pack.outputs.version }}
run: |
set -euo pipefail
tar -cf - Dockerfile "${TARBALL}" | docker build \
LOCKFILE=package-lock.json
tar -cf - Dockerfile "${LOCKFILE}" "${TARBALL}" | docker build \
--build-arg TARBALL="${TARBALL}" \
--build-arg LOCKFILE="${LOCKFILE}" \
--build-arg VERSION="${VERSION}" \
--build-arg REVISION="${GITHUB_SHA}" \
--tag looptroop:smoke \
Expand Down Expand Up @@ -649,7 +743,7 @@ jobs:
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
iex "& {$(irm get.scoop.sh)} -RunAsAdmin"
iex "& {$(irm https://get.scoop.sh)} -RunAsAdmin"
# The installer adds the shim directory to the *user* PATH, which this
# process has already read; without this the next step's `scoop` is
# not a command.
Expand Down Expand Up @@ -724,17 +818,17 @@ jobs:

# The standalone binaries: LoopTroop with a Node runtime inside it, for
# machines that have none. Built on the runner that matches the target,
# because a SEA embeds the running Node and a cross-built one would carry the
# wrong runtime.
# because native SEA embeds the selected Node runtime and a cross-built one
# would carry the wrong runtime.
#
# macOS x64 is absent deliberately: Node supports single-executable
# applications on arm64 only there, so Intel Macs keep Homebrew and npm.
#
# The macOS runner is pinned rather than floating so the OS the binary is
# built on is a decision rather than something that moves underneath it. It is
# not what decides the binary's minimum macOS version — the build copies the
# running Node executable and re-signs it, so that comes from Node, which
# `setup-node` pins. Bump this deliberately when the image is retired;
# not what decides the binary's minimum macOS version — the native builder
# writes the executable from the Node runtime `setup-node` pins. Bump this
# deliberately when the image is retired;
# `macos-14` was withdrawn on 2 Nov 2026, and a retired image fails the whole
# release, because `build` requires all four archives.
binary:
Expand All @@ -753,7 +847,9 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
with:
node-version: 24.18.1
# Node 26.9.0 is the standalone builder runtime. Package, npm and
# container jobs deliberately keep the Node 24.18.1 application floor.
node-version: 26.9.0
cache: npm

- name: Pin npm to the declared version
Expand All @@ -773,10 +869,11 @@ jobs:
if: runner.os == 'macOS'
run: brew list --versions gnu-tar >/dev/null || brew install gnu-tar

# Twice, requiring identical bytes. The SEA blob embeds the absolute path
# of the script it was built from, which made the first version of this
# non-reproducible for a reason nothing downstream would have explained —
# a manifest records this hash and the descriptors point at it.
# Twice, requiring identical bytes. The native SEA config embeds the
# absolute staging path of the script, which made the first version of
# this non-reproducible for a reason nothing downstream would have
# explained — a manifest records this hash and the descriptors point at
# it.
- name: Build the binary twice and require identical bytes
shell: bash
run: |
Expand All @@ -791,6 +888,37 @@ jobs:
fi
echo "Reproducible."

# This is a blocking application check of the runtime that will ship in
# the executable. The ordinary Node 26 early-warning lane is advisory;
# this command runs the built artifact itself and therefore cannot be
# replaced by a compatible-but-different host runtime.
- name: Run blocking application checks on the embedded runtime
shell: bash
run: |
set -euo pipefail
binary=$(find "${RUNNER_TEMP}/bin-a" -maxdepth 1 -type f \
-name 'looptroop-*-${{ matrix.target }}*' \
! -name '*.tar.gz' ! -name '*.zip' | head -1)
if [ -z "${binary}" ]; then
echo "::error::The binary build produced no executable for ${{ matrix.target }}."
exit 1
fi
test "$("${binary}" --version)" = "$(node -p 'require("./package.json").version')"
doctor_dir="${RUNNER_TEMP}/embedded-doctor"
mkdir -p "${doctor_dir}"
doctor_status=0
doctor=$(LOOPTROOP_CONFIG_DIR="${doctor_dir}" \
LOOPTROOP_OPENCODE_MODE=mock "${binary}" doctor --json) || doctor_status=$?
# Doctor can report an environment-only failure (for example, no
# GitHub CLI login on a pull-request runner); this check is about the
# embedded runtime, which is asserted from its structured report.
if [ "${doctor_status}" -ne 0 ]; then
echo "Doctor reported environment status ${doctor_status}; checking the embedded runtime field."
fi
echo "${doctor}" | grep -F '"name": "node"'
echo "${doctor}" | grep -F 'v26.9.0 (latest'
echo "Embedded runtime application checks passed."

# With Node removed from PATH, which is the only thing that proves the
# claim: every runner has Node, so a binary that quietly used it would
# pass any weaker test and fail for the user this artefact exists for.
Expand Down
Loading
Loading