agentboot: parse tool results, isolate child sessions, capture stderr… #1006
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: Github Release | |
| on: | |
| push: | |
| tags: | |
| - 'v*' | |
| branches: | |
| - 'ci/**' | |
| - 'main' | |
| workflow_dispatch: | |
| inputs: | |
| release_tag: | |
| description: 'Release tag (e.g., v0.20250101.0)' | |
| required: true | |
| type: string | |
| default: '' | |
| build_gui: | |
| description: 'Build GUI version' | |
| required: false | |
| type: boolean | |
| default: false | |
| env: | |
| GO_VERSION: '1.26' | |
| NODE_VERSION: '24' | |
| jobs: | |
| setup: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| build_gui: ${{ steps.set_vars.outputs.build_gui }} | |
| release_tag: ${{ steps.set_vars.outputs.release_tag }} | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| submodules: true | |
| - name: Set variables | |
| id: set_vars | |
| shell: bash | |
| run: | | |
| if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | |
| echo "release_tag=${{ github.event.inputs.release_tag }}" >> $GITHUB_OUTPUT | |
| echo "build_gui=${{ github.event.inputs.build_gui }}" >> $GITHUB_OUTPUT | |
| elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then | |
| echo "release_tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT | |
| echo "build_gui=false" >> $GITHUB_OUTPUT | |
| else | |
| # CI push to branch - use a dev version | |
| echo "release_tag=dev-${GITHUB_SHA:0:7}" >> $GITHUB_OUTPUT | |
| echo "build_gui=false" >> $GITHUB_OUTPUT | |
| fi | |
| build-frontend: | |
| needs: setup | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| submodules: true | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Cache pnpm modules | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.pnpm-store | |
| frontend/node_modules | |
| key: ${{ runner.os }}-pnpm-${{ hashFiles('frontend/pnpm-lock.yaml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pnpm- | |
| - name: Install pnpm | |
| run: corepack enable | |
| - name: Install frontend dependencies | |
| run: | | |
| pnpm approve-builds --all | |
| pnpm install --frozen-lockfile | |
| working-directory: ./frontend | |
| - name: Run codegen | |
| run: pnpm gen:api | |
| working-directory: ./frontend | |
| - name: Type check (non-blocking) | |
| # Legacy type errors exist; report but don't fail the release build. | |
| continue-on-error: true | |
| run: | | |
| pnpm typecheck || echo "::warning::TypeScript errors found (non-blocking, see step log)" | |
| working-directory: ./frontend | |
| - name: Build frontend | |
| run: | | |
| pnpm build && mkdir -p ../internal/web/dist && cp -R dist/* ../internal/web/dist/ | |
| working-directory: ./frontend | |
| - name: Upload frontend artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: frontend-dist | |
| path: internal/web/dist/ | |
| retention-days: 7 | |
| build-cli: | |
| needs: [setup, build-frontend] | |
| runs-on: ${{ matrix.runs-on }} | |
| strategy: | |
| matrix: | |
| include: | |
| - goos: linux | |
| goarch: amd64 | |
| suffix: linux-amd64 | |
| runs-on: ubuntu-22.04 | |
| - goos: linux | |
| goarch: arm64 | |
| suffix: linux-arm64 | |
| runs-on: ubuntu-22.04 | |
| cross_compile: true | |
| - goos: darwin | |
| goarch: amd64 | |
| suffix: macos-amd64 | |
| runs-on: macos-15-intel | |
| - goos: darwin | |
| goarch: arm64 | |
| suffix: macos-arm64 | |
| runs-on: macos-latest | |
| - goos: windows | |
| goarch: amd64 | |
| suffix: windows-amd64 | |
| exe: .exe | |
| runs-on: windows-latest | |
| - goos: windows | |
| goarch: arm64 | |
| suffix: windows-arm64 | |
| exe: .exe | |
| runs-on: ubuntu-22.04 | |
| cross_compile: true | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| submodules: true | |
| - name: Set up tag environment | |
| shell: bash | |
| run: | | |
| if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | |
| echo "RELEASE_TAG=${{ github.event.inputs.release_tag }}" >> $GITHUB_ENV | |
| echo "TAG_EXISTS=$(git tag -l "${{ github.event.inputs.release_tag }}" | wc -l)" >> $GITHUB_ENV | |
| elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then | |
| echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV | |
| echo "TAG_EXISTS=1" >> $GITHUB_ENV | |
| else | |
| # CI push to branch | |
| echo "RELEASE_TAG=${{ needs.setup.outputs.release_tag }}" >> $GITHUB_ENV | |
| echo "TAG_EXISTS=0" >> $GITHUB_ENV | |
| fi | |
| - name: Setup Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| - name: Cache Go modules | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cache/go-build | |
| ~/go/pkg/mod | |
| key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} | |
| restore-keys: | | |
| ${{ runner.os }}-go- | |
| - name: Download frontend artifact | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: frontend-dist | |
| path: internal/web/dist/ | |
| - name: Install musl toolchain (for amd64 static linking) | |
| if: matrix.goos == 'linux' && matrix.goarch == 'amd64' | |
| run: | | |
| sudo apt-get update -qq || echo "done" | |
| sudo apt-get install -y -qq musl-tools musl-dev || echo "done" | |
| - name: Install zig (for arm64 cross-compilation) | |
| if: matrix.cross_compile == true | |
| run: | | |
| wget -q https://ziglang.org/download/0.13.0/zig-linux-x86_64-0.13.0.tar.xz | |
| tar -xf zig-linux-x86_64-0.13.0.tar.xz | |
| echo "$PWD/zig-linux-x86_64-0.13.0" >> $GITHUB_PATH | |
| - name: Build Go binary (${{ matrix.suffix }}) | |
| shell: bash | |
| run: | | |
| # Create build directory | |
| mkdir -p dist | |
| # Remove dev-only files before embedding | |
| rm -f internal/web/dist/stats.html | |
| rm -f internal/web/dist/mockServiceWorker.js | |
| # Get build info | |
| VERSION="${{ env.RELEASE_TAG }}" | |
| GIT_COMMIT="$(git rev-parse HEAD 2>/dev/null || echo 'unknown')" | |
| BUILD_TIME="$(date -u '+%Y-%m-%d-%H-%M-%S' 2>/dev/null || echo 'unknown')" | |
| GO_VERSION="$(go version | awk '{print $3}' | sed 's/go//' 2>/dev/null || echo 'unknown')" | |
| PLATFORM="${{ matrix.goos }}/${{ matrix.goarch }}" | |
| # Build with optimized ldflags | |
| # -s: strip symbol table | |
| # -w: strip DWARF debug info | |
| # -X: for metadata | |
| LDFLAGS="-s -w" | |
| LDFLAGS+=" -X main.version=${VERSION}" | |
| LDFLAGS+=" -X main.gitCommit=${GIT_COMMIT}" | |
| LDFLAGS+=" -X main.buildTime=${BUILD_TIME}" | |
| LDFLAGS+=" -X main.goVersion=${GO_VERSION}" | |
| LDFLAGS+=" -X main.platform=${PLATFORM}" | |
| # Configure build flags based on platform | |
| BUILD_TAGS="" | |
| LDFLAGS_EXT="" | |
| if [ "${{ matrix.goos }}" == "linux" ]; then | |
| if [ "${{ matrix.goarch }}" == "amd64" ]; then | |
| # Static linking with musl for Linux amd64 | |
| BUILD_TAGS="-tags 'sqlite_omit_load_extension'" | |
| LDFLAGS_EXT="-linkmode external -extldflags \"-static\"" | |
| CC=musl-gcc | |
| export CC | |
| elif [ "${{ matrix.goarch }}" == "arm64" ]; then | |
| # Use zig for cross-compilation with musl (static) | |
| CC="zig cc -target aarch64-linux-musl" | |
| CXX="zig c++ -target aarch64-linux-musl" | |
| export CC CXX | |
| BUILD_TAGS="-tags 'sqlite_omit_load_extension'" | |
| LDFLAGS_EXT="-linkmode external -extldflags \"-static\"" | |
| fi | |
| elif [ "${{ matrix.goos }}" == "windows" ] && [ "${{ matrix.goarch }}" == "arm64" ]; then | |
| # Cross-compile Windows on ARM (e.g. Snapdragon) via zig. | |
| # Link statically so the bare .exe carries no mingw runtime DLL | |
| # dependency (libwinpthread/libssp), matching the linux-arm64 leg. | |
| CC="zig cc -target aarch64-windows-gnu" | |
| CXX="zig c++ -target aarch64-windows-gnu" | |
| export CC CXX | |
| BUILD_TAGS="-tags 'sqlite_omit_load_extension'" | |
| LDFLAGS_EXT="-linkmode external -extldflags \"-static\"" | |
| fi | |
| # Build with optimizations | |
| CGO_ENABLED=1 GOOS=${{ matrix.goos }} GOARCH=${{ matrix.goarch }} \ | |
| go build \ | |
| ${BUILD_TAGS} \ | |
| -ldflags="${LDFLAGS} ${LDFLAGS_EXT}" \ | |
| -trimpath \ | |
| -o ./dist/tingly-box${{ matrix.exe }} \ | |
| ./cli/tingly-box | |
| # Make binary executable for non-Windows | |
| [ "${{ matrix.goos }}" != "windows" ] && chmod +x ./dist/tingly-box${{ matrix.exe }} || true | |
| # UPX is Linux-only. Windows is deliberately excluded: UPX-packed, | |
| # unsigned .exe files are prime targets for Windows Defender / AV | |
| # heuristic false positives (e.g. Wacatac), which quarantine or lock the | |
| # binary on user machines and surface as "This app can't run on your PC". | |
| # macOS is excluded because UPX breaks Mach-O signing/notarization. | |
| - name: Install UPX 5.1.1 | |
| if: matrix.goos == 'linux' | |
| shell: bash | |
| run: | | |
| wget -q https://github.com/upx/upx/releases/download/v5.1.1/upx-5.1.1-amd64_linux.tar.xz | |
| tar -xf upx-5.1.1-amd64_linux.tar.xz | |
| export PATH="$PWD/upx-5.1.1-amd64_linux:$PATH" | |
| - name: Compress binary with UPX | |
| if: matrix.goos == 'linux' | |
| shell: bash | |
| run: | | |
| if command -v upx &> /dev/null || [ -f "./upx-5.1.1-amd64_linux/upx" ]; then | |
| UPX_CMD="upx" | |
| [ -f "./upx-5.1.1-amd64_linux/upx" ] && UPX_CMD="./upx-5.1.1-amd64_linux/upx" | |
| # Use best compression with LZMA (slower but smaller) | |
| $UPX_CMD --best --lzma ./dist/tingly-box${{ matrix.exe }} || \ | |
| $UPX_CMD --best ./dist/tingly-box${{ matrix.exe }} | |
| else | |
| echo "UPX not available, skipping compression" | |
| fi | |
| - name: Upload build artifact (${{ matrix.suffix }}) | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: cli-${{ matrix.suffix }} | |
| path: ./dist/tingly-box${{ matrix.exe }} | |
| retention-days: 7 | |
| build-gui: | |
| needs: setup | |
| if: needs.setup.outputs.build_gui == 'true' | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| matrix: | |
| include: | |
| # macOS builds | |
| - os: macos-latest | |
| platform: macos | |
| arch: arm64 | |
| goos: darwin | |
| goarch: arm64 | |
| # Windows builds | |
| - os: windows-latest | |
| platform: windows | |
| arch: amd64 | |
| goos: windows | |
| goarch: amd64 | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| submodules: true | |
| - name: Set up tag environment | |
| shell: bash | |
| run: | | |
| if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then | |
| echo "RELEASE_TAG=${{ github.event.inputs.release_tag }}" >> $GITHUB_ENV | |
| echo "TAG_EXISTS=$(git tag -l "${{ github.event.inputs.release_tag }}" | wc -l)" >> $GITHUB_ENV | |
| elif [[ "${GITHUB_REF}" == refs/tags/* ]]; then | |
| echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV | |
| echo "TAG_EXISTS=1" >> $GITHUB_ENV | |
| else | |
| # CI push to branch | |
| echo "RELEASE_TAG=${{ needs.setup.outputs.release_tag }}" >> $GITHUB_ENV | |
| echo "TAG_EXISTS=0" >> $GITHUB_ENV | |
| fi | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version: ${{ env.NODE_VERSION }} | |
| - name: Setup Go | |
| uses: actions/setup-go@v5 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| - name: Cache Go modules | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.cache/go-build | |
| ~/go/pkg/mod | |
| key: ${{ runner.os }}-go-${{ matrix.goarch }}-${{ hashFiles('**/go.sum') }} | |
| restore-keys: | | |
| ${{ runner.os }}-go-${{ matrix.goarch }}- | |
| ${{ runner.os }}-go- | |
| - name: Cache pnpm modules | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.pnpm-store | |
| frontend/node_modules | |
| key: ${{ runner.os }}-pnpm-${{ matrix.goarch }}-${{ hashFiles('frontend/pnpm-lock.yaml') }} | |
| restore-keys: | | |
| ${{ runner.os }}-pnpm-${{ matrix.goarch }}- | |
| ${{ runner.os }}-pnpm- | |
| - name: Install go-task | |
| run: go install github.com/go-task/task/v3/cmd/task@latest | |
| - name: Install wails3 | |
| # Keep in lockstep with the library version in gui/wails3/go.mod | |
| run: go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0-beta.12 | |
| - name: Install pnpm | |
| run: corepack enable | |
| - name: Install frontend dependencies | |
| shell: bash | |
| run: | | |
| pnpm approve-builds --all | |
| pnpm install --frozen-lockfile | |
| working-directory: ./frontend | |
| - name: Run codegen | |
| run: task codegen | |
| - name: Install NSIS (Windows only) | |
| if: matrix.platform == 'windows' | |
| run: choco install nsis -y | |
| - name: Build Go binary (macOS) | |
| if: matrix.platform == 'macos' | |
| run: | | |
| task darwin:package VERSION="${RELEASE_TAG}" | |
| mkdir -p dist | |
| cd bin | |
| ls -la . | |
| zip -r ../dist/tingly-box-gui-macos-arm64.zip TinglyBox.app | |
| cd .. | |
| cd dist | |
| ls -la . | |
| - name: Build Go binary (Windows) | |
| if: matrix.platform == 'windows' | |
| shell: pwsh | |
| run: | | |
| task windows:build PRODUCTION=true VERSION="$env:RELEASE_TAG" | |
| New-Item -ItemType Directory -Force -Path dist | |
| cd bin | |
| Get-ChildItem | |
| Compress-Archive -Path tingly-box.exe -DestinationPath ../dist/tingly-box-gui-windows-amd64.zip | |
| cd .. | |
| cd dist | |
| Get-ChildItem | |
| - name: Upload build artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: gui-${{ matrix.platform }}-${{ matrix.arch }} | |
| path: ./dist/ | |
| retention-days: 7 | |
| github-release: | |
| needs: [setup, build-cli, build-gui] | |
| runs-on: ubuntu-latest | |
| if: | | |
| always() && | |
| (startsWith(github.ref, 'refs/tags/v') || github.event_name == 'workflow_dispatch') && | |
| needs.build-cli.result == 'success' && | |
| needs.setup.result == 'success' && | |
| (needs.build-gui.result == 'success' || needs.build-gui.result == 'skipped') | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| submodules: true | |
| - name: Set up tag environment | |
| run: | | |
| echo "RELEASE_TAG=${{ needs.setup.outputs.release_tag }}" >> $GITHUB_ENV | |
| - name: Download CLI artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: cli-* | |
| path: ./dist/cli | |
| # Keep artifacts in separate subdirectories to avoid overwriting same-named binaries | |
| - name: Download GUI artifacts | |
| if: needs.setup.outputs.build_gui == 'true' | |
| uses: actions/download-artifact@v4 | |
| with: | |
| pattern: gui-* | |
| path: ./dist/gui | |
| # Keep artifacts in separate subdirectories | |
| - name: Package binaries with proper extensions | |
| run: | | |
| # Save original directory and use absolute path for release | |
| ORIGINAL_DIR="$(pwd)" | |
| RELEASE_DIR="${ORIGINAL_DIR}/release" | |
| mkdir -p "$RELEASE_DIR" | |
| # Process CLI binaries - each artifact is in its own subdirectory | |
| if [ -d "dist/cli" ]; then | |
| for artifact_dir in dist/cli/cli-*; do | |
| if [ -d "$artifact_dir" ]; then | |
| # Extract platform from artifact directory name | |
| platform=$(basename "$artifact_dir" | sed 's/cli-//') | |
| chmod +x "$artifact_dir"/tingly-box* || true | |
| # Package the binary with platform suffix in zip filename | |
| binary=$(ls "$artifact_dir"/tingly-box* 2>/dev/null | head -1) | |
| if [ -n "$binary" ]; then | |
| (cd "$artifact_dir" && zip "${RELEASE_DIR}/tingly-box-${platform}.zip" "$(basename "$binary")") | |
| fi | |
| fi | |
| done | |
| fi | |
| # Process GUI binaries (already zipped) | |
| if [ -d "dist/gui" ]; then | |
| # GUI artifacts are also in subdirectories | |
| for artifact_dir in dist/gui/gui-*; do | |
| if [ -d "$artifact_dir" ]; then | |
| cp "$artifact_dir"/*.zip "$RELEASE_DIR/" 2>/dev/null || true | |
| fi | |
| done | |
| # Also check for direct files | |
| find dist/gui -maxdepth 1 -name "*.zip" -exec cp {} "$RELEASE_DIR/" \; 2>/dev/null || true | |
| fi | |
| # Create checksums for all files (only if we have zip files) | |
| if ls "$RELEASE_DIR"/*.zip 1> /dev/null 2>&1; then | |
| (cd "$RELEASE_DIR" && sha256sum *.zip > checksums.txt) | |
| else | |
| echo "No zip files found in release directory" | |
| fi | |
| - name: Create GitHub Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ env.RELEASE_TAG }} | |
| files: | | |
| release/*.zip | |
| release/checksums.txt | |
| generate_release_notes: true | |
| draft: false | |
| prerelease: false | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| # GitHub never starts workflow runs for events that a workflow creates with | |
| # its own GITHUB_TOKEN, so the release published above does not fire the | |
| # `release: published` trigger in npm.yml. Dispatch it explicitly instead. | |
| # Nothing gets published by this: the npm run stops at the `production` | |
| # environment approval gate before the first `npm publish`. This only | |
| # removes the manual "Run workflow" step, not the human approval. | |
| trigger-npm-publish: | |
| needs: [setup, github-release] | |
| runs-on: ubuntu-latest | |
| if: startsWith(github.ref, 'refs/tags/v') && needs.github-release.result == 'success' | |
| permissions: | |
| actions: write | |
| contents: read | |
| steps: | |
| - name: Dispatch NPX publish workflow | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| RELEASE_TAG: ${{ needs.setup.outputs.release_tag }} | |
| run: | | |
| # Same inference npm.yml applies on a release event: stable → latest, | |
| # pre-release (v1.2.3-rc1) → rc. | |
| VERSION="${RELEASE_TAG#v}" | |
| if [[ "$VERSION" == *-* ]]; then | |
| NPM_TAG=rc | |
| else | |
| NPM_TAG=latest | |
| fi | |
| echo "Dispatching npm.yml for $RELEASE_TAG (npm dist-tag: $NPM_TAG)" | |
| gh workflow run npm.yml \ | |
| --repo "$GITHUB_REPOSITORY" \ | |
| --ref "$RELEASE_TAG" \ | |
| -f tag="$RELEASE_TAG" \ | |
| -f npm_tag="$NPM_TAG" \ | |
| -f publish_cli=true \ | |
| -f publish_gui=false \ | |
| -f build_docker=true | |
| # Best effort: link the dispatched run so approvers can find it from here. | |
| RUN_URL="" | |
| for _ in 1 2 3 4 5 6; do | |
| sleep 5 | |
| RUN_URL="$(gh run list --repo "$GITHUB_REPOSITORY" --workflow npm.yml \ | |
| --event workflow_dispatch --branch "$RELEASE_TAG" --limit 1 \ | |
| --json url -q '.[0].url' 2>/dev/null || true)" | |
| [ -n "$RUN_URL" ] && break | |
| done | |
| { | |
| echo "## 📦 NPX publish dispatched" | |
| echo "" | |
| echo "| Item | Value |" | |
| echo "| --- | --- |" | |
| echo "| Release | \`$RELEASE_TAG\` |" | |
| echo "| npm dist-tag | \`$NPM_TAG\` |" | |
| echo "| Publish | cli=true, gui=false, docker=true |" | |
| if [ -n "$RUN_URL" ]; then | |
| echo "| Run | $RUN_URL |" | |
| else | |
| echo "| Run | see the [NPX Package Publish workflow](https://github.com/$GITHUB_REPOSITORY/actions/workflows/npm.yml) |" | |
| fi | |
| echo "" | |
| echo "The run waits for approval of the \`production\` environment before publishing." | |
| } >> "$GITHUB_STEP_SUMMARY" |