docs(examples): add the examples gallery (scenarios, screenshots, bui… #261
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 and Release AgentBridge | |
| # The release is now AUTOMATIC: pushing master with IsPrerelease=false in AgentBridge.csproj | |
| # (in the pushed commit) triggers the wait for the dependency NuGet packages, the build of the | |
| # 5 platform archives and the GitHub release — the tag v1.yy.MM.dd is created on the fly. No | |
| # tag push and no release.ps1 invocation are needed anymore. With IsPrerelease=true, or when | |
| # today's tag already exists, the run is skipped. See docs-dev/RELEASING.md. | |
| on: | |
| push: | |
| branches: [master] | |
| workflow_dispatch: | |
| inputs: | |
| # force_release lets the daily auto-release (daily-release.yml) produce a release even | |
| # while the csproj gate is IsPrerelease=true. Without it the gate would skip the build. | |
| # The dispatch never edits the csproj or the working tree: the override is threaded | |
| # through job outputs (see check-version -> publish_props) so the built binary reports | |
| # the clean release version the tag will carry. | |
| force_release: | |
| description: 'Force a release even when IsPrerelease=true (used by the daily auto-release)' | |
| required: false | |
| type: boolean | |
| default: false | |
| version: | |
| description: 'Release version (1.yy.MM.dd). Used with force_release; empty = csproj version.' | |
| required: false | |
| type: string | |
| default: '' | |
| nuget_wait: | |
| description: 'Run the NuGet dependency wait (true/false). Used with force_release.' | |
| required: false | |
| type: string | |
| default: '' | |
| nuget_wait_packages: | |
| description: 'Comma-separated packages to wait for (used with force_release).' | |
| required: false | |
| type: string | |
| default: '' | |
| permissions: | |
| contents: write | |
| # DO NOT add a "workflows" key here — it is NOT a valid value in a workflow's permissions | |
| # block (only PATs / GitHub Apps have it). Doing so makes the whole file invalid and every | |
| # run fails with 0 jobs ("Invalid workflow file ... Unexpected value 'workflows'", | |
| # 2026-09-04). If the tag-pin push is ever refused for a workflow-file change, pin the tag | |
| # to a commit whose .github/workflows matches the default branch instead. | |
| # Serializes runs of this workflow per branch: with cancel-in-progress: false a NEW run (the | |
| # gate-restore push, a manual push during the build) never cancels the QUEUED or RUNNING | |
| # release run. The back-to-back double push of release.ps1 raced GitHub's Actions queue on | |
| # 2026-08-26: the runs were created in inverted order and the gate-off (release) run was | |
| # failed while still queued — no release was produced. The restore commit carries [skip ci] | |
| # (release.ps1) so it creates no run at all; this group is the structural guard for every | |
| # other quick double push. | |
| concurrency: | |
| group: release-${{ github.ref }} | |
| cancel-in-progress: false | |
| jobs: | |
| # Release gate: when IsPrerelease=true in AgentBridge.csproj the version carries a | |
| # "-prerelease" suffix and no GitHub release is created. See AGENTS.md. | |
| check-version: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.ver.outputs.version }} | |
| do_release: ${{ steps.gate.outputs.do_release }} | |
| need_nuget_wait: ${{ steps.nuget.outputs.need_nuget_wait }} | |
| wait_packages: ${{ steps.nuget.outputs.wait_packages }} | |
| publish_props: ${{ steps.ver.outputs.publish_props }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| path: AgentBridge | |
| - uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: '10.0.x' | |
| - name: Detect version + release gate | |
| id: ver | |
| shell: bash | |
| env: | |
| FORCE: ${{ github.event.inputs.force_release }} | |
| IN_VERSION: ${{ github.event.inputs.version }} | |
| run: | | |
| # -getProperty:Version prints the raw value (single property) — grep handles both | |
| # raw ("1.26.08.09") and JSON ({"Properties":{"Version":"..."}}) output shapes. | |
| VER=$(dotnet msbuild AgentBridge/AgentBridge.csproj -getProperty:Version -nologo | grep -oE '[0-9]+(\.[0-9]+)*(-[a-z0-9]+)?' | head -n1) | |
| # IsPrerelease is the source of truth for the gate (the "-prerelease" suffix derives | |
| # from it in the csproj). Read the property directly: if Version ever fails to carry | |
| # a full date (e.g. an empty <ReleaseDate> makes the grep above extract a bare "1"), | |
| # the suffix-based check alone would silently treat a prerelease push as a release | |
| # and run the whole build for a garbage version (2026-09-04 incident). | |
| PREREL=$(dotnet msbuild AgentBridge/AgentBridge.csproj -getProperty:IsPrerelease -nologo | grep -oE 'true|false' | head -n1) | |
| PREREL=${PREREL:-false} | |
| PUBLISH_PROPS="" | |
| if [ "$FORCE" = "true" ]; then | |
| # Forced release (daily auto-release): the csproj still carries IsPrerelease=true | |
| # and a dynamic ReleaseDate. Override the version from the dispatch input and force | |
| # the clean (non-prerelease) shape via msbuild -p so the built binary reports the | |
| # exact version the tag will carry. The csproj itself is never modified. | |
| if [ -n "$IN_VERSION" ]; then VER="$IN_VERSION"; fi | |
| PREREL="false" | |
| RDATE=$(printf '%s' "$VER" | sed -E 's/^1\.//; s/-prerelease$//') | |
| PUBLISH_PROPS="-p:IsPrerelease=false -p:ReleaseDate=$RDATE" | |
| echo "force_release=true -> version=$VER (csproj gate overridden, not edited)" | |
| fi | |
| echo "prerelease=$PREREL" >> "$GITHUB_OUTPUT" | |
| echo "version=$VER" >> "$GITHUB_OUTPUT" | |
| echo "publish_props=$PUBLISH_PROPS" >> "$GITHUB_OUTPUT" | |
| - name: Decide whether to release | |
| id: gate | |
| shell: bash | |
| # The checkout above uses path: AgentBridge, so git must run from inside the repo | |
| # (the workspace root is not a git repository — 'origin' does not exist there). | |
| working-directory: AgentBridge | |
| env: | |
| VERSION: ${{ steps.ver.outputs.version }} | |
| PRERELEASE: ${{ steps.ver.outputs.prerelease }} | |
| run: | | |
| if [ "$PRERELEASE" = "true" ]; then | |
| echo "IsPrerelease=true -> prerelease build, no GitHub release" | |
| echo "do_release=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| if git ls-remote --tags origin "refs/tags/v$VERSION" | grep -q .; then | |
| echo "Tag v$VERSION already exists -> no second release for this version" | |
| echo "do_release=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "do_release=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| # NuGetWait marker (set by release.ps1 from its core-repo pre-check): false means no | |
| # core repo changed since its last tag → the 30-min dependency wait is skipped entirely | |
| # (release.ps1 aborts the release if a changed core repo has no today-tag, so a missing | |
| # marker can never silently ship a stale engine). | |
| - name: Read NuGet wait marker | |
| id: nuget | |
| shell: bash | |
| env: | |
| FORCE: ${{ github.event.inputs.force_release }} | |
| IN_WAIT: ${{ github.event.inputs.nuget_wait }} | |
| IN_PKGS: ${{ github.event.inputs.nuget_wait_packages }} | |
| run: | | |
| if [ "$FORCE" = "true" ] && [ -n "$IN_WAIT" ]; then | |
| # Forced release: the daily auto-release already verified the changed packages are | |
| # visible on nuget.org (that is its change signal), so it passes the wait decision | |
| # explicitly instead of reading the csproj marker (which release.ps1 owns). | |
| echo "need_nuget_wait=$IN_WAIT" >> "$GITHUB_OUTPUT" | |
| echo "wait_packages=$IN_PKGS" >> "$GITHUB_OUTPUT" | |
| else | |
| WAIT=$(dotnet msbuild AgentBridge/AgentBridge.csproj -getProperty:NuGetWait -nologo | grep -oE 'true|false' | head -n1) | |
| echo "need_nuget_wait=${WAIT:-true}" >> "$GITHUB_OUTPUT" | |
| # release.ps1 narrows the list to the packages whose repos actually changed today, | |
| # so the wait below exits as soon as THEY propagate (repos that did not change never | |
| # publish today's version and must not hold the wait). Default = the full set. | |
| PKGS=$(dotnet msbuild AgentBridge/AgentBridge.csproj -getProperty:NuGetWaitPackages -nologo | head -n1) | |
| echo "wait_packages=${PKGS:-graphene.aiorchestrator,alltomarkdown,mermaidrendering,graphene.reversemarkdown,uisupportgeneric,systemextra}" >> "$GITHUB_OUTPUT" | |
| fi | |
| build: | |
| needs: check-version | |
| if: needs.check-version.outputs.do_release == 'true' | |
| name: Build ${{ matrix.rid }} | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: windows-latest | |
| rid: win-x64 | |
| ext: .exe | |
| - os: ubuntu-latest | |
| rid: linux-x64 | |
| ext: '' | |
| # linux-arm64: supported since KokoroSharp 0.8.4 (phonemizer is the pure-managed | |
| # MisakiSharp — no espeak-ng binary needed) + the native Microsoft.ML.OnnxRuntime | |
| # package ships libonnxruntime.so for linux-arm64. Cross-compiled from the x64 | |
| # runner (pure managed code + RID-specific NuGet assets). | |
| - os: ubuntu-latest | |
| rid: linux-arm64 | |
| ext: '' | |
| - os: macos-latest | |
| rid: osx-x64 | |
| ext: '' | |
| - os: macos-latest | |
| rid: osx-arm64 | |
| ext: '' | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| path: AgentBridge | |
| - name: Setup .NET | |
| uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: '10.0.x' | |
| # The dependency NuGet packages are date-versioned (1.yy.MM.dd) and published by each | |
| # repo's publish.yml when a v* tag is pushed; nuget.org propagation is not instant | |
| # (official figures: up to 30 minutes). The wait runs ONLY when release.ps1 detected a | |
| # changed core repo whose today's package is still propagating (<NuGetWait> marker): | |
| # with no core change the wait would burn 30 minutes for packages that will never exist. | |
| # release.ps1 ALSO narrows the list (NuGetWaitPackages) to the packages of the repos | |
| # that changed today, so the window ends as soon as THEY become visible — repos that | |
| # did not change never publish today's version and would otherwise hold the loop for the | |
| # full 30 minutes. After the window, a still-missing package is a HARD FAILURE: the wait | |
| # list only contains packages that SHOULD be at today's version, so expiry means that | |
| # repo's publish.yml run failed or nuget.org exceeded its documented window — proceeding | |
| # would restore YESTERDAY's engine and either crash the build with misleading errors | |
| # (2026-09-06: "CrashReporter does not exist" after the AIOrchestrator publish died | |
| # waiting for uisupportgeneric) or, worse, ship it silently. Fix the failed publish and | |
| # re-run release.ps1 — the wait is skipped as soon as the package is visible. | |
| # The list is ONLY the packages the build actually restores (the Graphene.AIOrchestrator | |
| # closure + SystemExtra): the tool plugins (DocumentTool, SpreadsheetTool, OfficeTool, | |
| # PresentationTool, OfficeSupportTool, PodcastTool, FreeCADTool) are NOT build | |
| # dependencies — they are fetched as self-contained zips from their own GitHub Releases | |
| # below, never from NuGet. | |
| - name: Wait for dependency packages on NuGet | |
| if: needs.check-version.outputs.need_nuget_wait == 'true' | |
| shell: bash | |
| env: | |
| VERSION: ${{ needs.check-version.outputs.version }} | |
| WAIT_PACKAGES: ${{ needs.check-version.outputs.wait_packages }} | |
| run: | | |
| NVER=$(printf '%s' "$VERSION" | awk -F. '{for(i=1;i<=NF;i++){gsub(/^0+/,"",$i); if($i=="")$i=0; printf "%s%s",(i>1?".":""),$i} print ""}') | |
| echo "Waiting for dependency packages at version $NVER ..." | |
| for i in $(seq 1 60); do | |
| missing="" | |
| for pkg in $(printf '%s' "$WAIT_PACKAGES" | tr ',' ' '); do | |
| if curl -fsS --max-time 20 "https://api.nuget.org/v3-flatcontainer/$pkg/index.json" | grep -q "\"$NVER\""; then | |
| echo "$pkg $NVER available (attempt $i)" | |
| else | |
| missing="$missing $pkg" | |
| fi | |
| done | |
| if [ -z "$missing" ]; then break; fi | |
| sleep 30 | |
| done | |
| if [ -n "$missing" ]; then | |
| echo "::error::after 30 min these packages are still not at $NVER:$missing" | |
| echo "::error::this release REQUIRES today's version of every package in <NuGetWaitPackages> — release.ps1 narrows it to the repos that changed today. A missing package means that repo's 'Publish NuGet Package' run failed or nuget.org propagation exceeded its documented window; proceeding would restore yesterday's engine (misleading build crash or silent stale release, 2026-09-06). Check the failed publish run of the repo above, fix it, then re-run release.ps1 — the wait is skipped as soon as the package is visible." | |
| exit 1 | |
| fi | |
| # Builds against the published NuGet packages (Graphene.AIOrchestrator 1.* and its | |
| # dependencies) — the sibling repos are private and never checked out here. | |
| - name: Publish | |
| shell: bash | |
| run: | | |
| dotnet publish AgentBridge/AgentBridge.csproj -c Release -r ${{ matrix.rid }} \ | |
| --self-contained true \ | |
| -p:PublishSingleFile=true \ | |
| -p:IncludeNativeLibrariesForSelfExtract=true \ | |
| -p:DebugType=None -p:DebugSymbols=false \ | |
| ${{ needs.check-version.outputs.publish_props }} \ | |
| -o ./publish | |
| # The archive deliberately ships kokoro.onnx (325 MB) plus the KokoroSharp | |
| # voices/ + voices-zh/ content: single-file publish bundles managed code only, | |
| # and the TTS endpoints need those assets next to the exe. The native | |
| # onnxruntime engine is included per-RID by the Microsoft.ML.OnnxRuntime | |
| # package (libonnxruntime.so / onnxruntime.dll). | |
| # | |
| # Tool plugins (Graphene.DocumentTool, Graphene.SpreadsheetTool, Graphene.OfficeTool, | |
| # Graphene.PresentationTool, Graphene.OfficeSupportTool, PodcastTool, FreeCADTool) are | |
| # loaded DYNAMICALLY from Tools/ by ToolPluginHost (byte-loaded, never referenced): the | |
| # single-file publish cannot bundle them. Fetch each plugin's self-contained release zip (plugin + non-host | |
| # deps, minus the AIOrchestrator graph — produced by the plugin repos' standard | |
| # plugin-release.yml) from its PUBLIC GitHub release and ship it into publish/Tools/<Plugin>/. | |
| # Each payload's assets/ (OfficeSupportTool templates, PresentationTool bg/js, icons) is | |
| # merged into publish/assets/ — the plugins resolve host-level assets from | |
| # AppContext.BaseDirectory\assets (same convention as the ShipWithAssets dev target and | |
| # PluginUpdater.MergeHostAssets at update time). NuGet is not part of the plugin | |
| # deployment channel. | |
| - name: Fetch tool plugins into Tools/ | |
| shell: bash | |
| run: | | |
| mkdir -p /tmp/plugins publish/Tools publish/assets | |
| for tool in DocumentTool SpreadsheetTool OfficeTool PresentationTool OfficeSupportTool PodcastTool FreeCADTool; do | |
| # Latest release tag via the /releases/latest redirect (no API call, no rate limit). | |
| tag=$(curl -fsSL -o /dev/null -w '%{url_effective}' "https://github.com/Graphene-Lab/$tool/releases/latest" \ | |
| | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+[^/]*$' | head -n1 || true) | |
| if [ -z "$tag" ]; then | |
| echo "::warning::$tool has no GitHub release yet — tool will be missing from Tools/" | |
| continue | |
| fi | |
| ver="${tag#v}" | |
| zip="$tool-$ver.zip" | |
| if ! curl -fsSL -o "/tmp/plugins/$zip" "https://github.com/Graphene-Lab/$tool/releases/download/$tag/$zip"; then | |
| echo "::warning::cannot download $zip for $tool — tool will be missing from Tools/" | |
| continue | |
| fi | |
| (cd /tmp/plugins && unzip -qo "$zip") | |
| mkdir -p "publish/Tools/$tool" | |
| cp -r "/tmp/plugins/$tool/." "publish/Tools/$tool/" | |
| if [ -d "/tmp/plugins/$tool/assets" ]; then | |
| cp -r "/tmp/plugins/$tool/assets/." "publish/assets/" | |
| fi | |
| done | |
| # The host always ships the ONNX Runtime engine (per-RID GPU build); a plugin's | |
| # transitive CPU onnxruntime must never shadow it (version conflicts, CPU-only | |
| # QwenTTS) — drop the plugin copies here (same rule as ShipOnePlugin.targets). | |
| find publish/Tools -name 'onnxruntime*' -delete | |
| find publish/Tools -name 'libonnxruntime*' -delete | |
| find publish/Tools -name 'Microsoft.ML.OnnxRuntime*.dll' -delete | |
| find publish/Tools -type f 2>/dev/null | sort || true | |
| # Windows voice bridge: fetch the AIOffice.VoiceAgent.Win self-contained win-x64 zip | |
| # from its OWN GitHub release (public repo, same channel as the tool plugins) into | |
| # publish/voiceagent/ — POST /v1/voice/listen spawns AIOffice.VoiceAgent.Win.exe from | |
| # there. The local csproj target CopyVoiceAgentOutput covers developer machines that | |
| # have the sibling VoiceAgent build; CI has no sibling, so this step is what makes the | |
| # released Windows archive work out of the box. The zip contains a top-level | |
| # AIOffice.VoiceAgent.Win/ folder (exe + voices/ + kokoro.onnx + native runtimes). | |
| # Other RIDs do not ship voice recognition (Windows-only feature). | |
| - name: Fetch VoiceAgent.Win into voiceagent/ | |
| if: matrix.rid == 'win-x64' | |
| shell: bash | |
| run: | | |
| tag=$(curl -fsSL -o /dev/null -w '%{url_effective}' "https://github.com/Graphene-Lab/AIOffice.VoiceAgent.Win/releases/latest" \ | |
| | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+[^/]*$' | head -n1 || true) | |
| if [ -z "$tag" ]; then | |
| echo "::warning::AIOffice.VoiceAgent.Win has no release yet — /voice will be unavailable in this archive" | |
| exit 0 | |
| fi | |
| ver="${tag#v}" | |
| zip="AIOffice.VoiceAgent.Win-$ver.zip" | |
| mkdir -p /tmp/voiceagent | |
| if ! curl -fsSL -o "/tmp/voiceagent/$zip" "https://github.com/Graphene-Lab/AIOffice.VoiceAgent.Win/releases/download/$tag/$zip"; then | |
| echo "::warning::cannot download $zip — /voice will be unavailable in this archive" | |
| exit 0 | |
| fi | |
| (cd /tmp/voiceagent && unzip -qo "$zip") | |
| mkdir -p publish/voiceagent | |
| cp -r /tmp/voiceagent/AIOffice.VoiceAgent.Win/. publish/voiceagent/ | |
| test -f publish/voiceagent/AIOffice.VoiceAgent.Win.exe \ | |
| && echo "voiceagent payload ready: $(du -sh publish/voiceagent | cut -f1)" \ | |
| || echo "::warning::voiceagent/ does not contain AIOffice.VoiceAgent.Win.exe" | |
| - name: Package | |
| shell: bash | |
| run: | | |
| cd publish | |
| tar -czf ../agentbridge-${{ matrix.rid }}.tar.gz . | |
| cd .. | |
| ls -lh agentbridge-${{ matrix.rid }}.tar.gz | |
| - name: Upload archive | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: agentbridge-${{ matrix.rid }} | |
| path: agentbridge-${{ matrix.rid }}.tar.gz | |
| # Windows Store MSI: separate job (not inside the win-x64 matrix build) so an | |
| # installer/WiX problem can never block the archives + GitHub release — and | |
| # continue-on-error keeps the run green when the MSI step fails (the MSI is an | |
| # extra delivery channel; the release itself must not depend on it). Payload = | |
| # the win-x64 archive the build already produced (single source of truth: the | |
| # exact tree that ships). The Store submission (store-submit job below) points | |
| # Partner Center at the VPS streaming proxy URL (see tools/store). | |
| store-msi: | |
| needs: [check-version, build] | |
| if: needs.check-version.outputs.do_release == 'true' | |
| runs-on: windows-latest | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| path: AgentBridge | |
| - uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: '10.0.x' | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| name: agentbridge-win-x64 | |
| path: win-payload | |
| - name: Extract win-x64 payload | |
| shell: pwsh | |
| run: | | |
| New-Item -ItemType Directory -Path payload | Out-Null | |
| tar -xzf win-payload/agentbridge-win-x64.tar.gz -C payload | |
| - name: Build Windows Store MSI | |
| shell: pwsh | |
| env: | |
| VERSION: ${{ needs.check-version.outputs.version }} | |
| # Store policy 10.2.9: the MSI and every PE file it ships must be signed by a | |
| # certificate chaining to a Microsoft Trusted Root CA. Either a base64 PFX or a | |
| # thumbprint of a certificate present in LocalMachine\My (token/HSM middleware). | |
| SIGN_PFX_BASE64: ${{ secrets.SIGN_PFX_BASE64 }} | |
| SIGN_PFX_PASSWORD: ${{ secrets.SIGN_PFX_PASSWORD }} | |
| SIGN_THUMBPRINT: ${{ secrets.SIGN_THUMBPRINT }} | |
| run: | | |
| $signArgs = @() | |
| $pfx = $null | |
| if ($env:SIGN_PFX_BASE64) { | |
| $pfx = Join-Path $env:RUNNER_TEMP 'store-sign.pfx' | |
| [IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:SIGN_PFX_BASE64)) | |
| $signArgs += @('-SignPfx', $pfx) | |
| if ($env:SIGN_PFX_PASSWORD) { $signArgs += @('-SignPfxPassword', $env:SIGN_PFX_PASSWORD) } | |
| } | |
| elseif ($env:SIGN_THUMBPRINT) { | |
| $signArgs += @('-SignThumbprint', $env:SIGN_THUMBPRINT) | |
| } | |
| else { | |
| Write-Warning 'No signing certificate (SIGN_* secrets) - the MSI will be UNSIGNED and store certification will fail (policy 10.2.9).' | |
| } | |
| try { | |
| & "$env:GITHUB_WORKSPACE/AgentBridge/tools/store/New-StoreInstaller.ps1" ` | |
| -PayloadDir "$env:GITHUB_WORKSPACE/payload" ` | |
| -Version $env:VERSION ` | |
| -OutDir "$env:GITHUB_WORKSPACE/store-msi" ` | |
| @signArgs | |
| } | |
| finally { | |
| # Never leave the private key on the runner disk. | |
| if ($pfx -and (Test-Path $pfx)) { Remove-Item $pfx -Force } | |
| } | |
| - name: Upload MSI | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: store-msi | |
| path: store-msi | |
| # MSIX package for the Microsoft Store — the one Store channel where Microsoft signs for us | |
| # ("The Microsoft Store will automatically re-sign your MSIX/AppX packages with a Microsoft | |
| # certificate"), so no code-signing certificate is needed. Same shape as store-msi: separate job, | |
| # continue-on-error, so a packaging problem can never block or fail the GitHub release. | |
| # | |
| # IdentityName/Publisher MUST match Partner Center (View app identity details) or the upload is | |
| # rejected: set the STORE_IDENTITY_NAME and STORE_PUBLISHER secrets. Without them the package is | |
| # still built (placeholder identity) and uploaded as a CI artifact, but it is not submittable. | |
| # The package is intentionally NOT attached to the GitHub release: an unsigned MSIX cannot be | |
| # sideloaded by users, so publishing it would only create support noise. | |
| # See docs-dev/STORE-PUBLISHING.md section 8. | |
| store-msix: | |
| needs: [check-version, build] | |
| if: needs.check-version.outputs.do_release == 'true' | |
| runs-on: windows-latest | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| path: AgentBridge | |
| - uses: actions/setup-dotnet@v4 | |
| with: | |
| dotnet-version: '10.0.x' | |
| - uses: actions/download-artifact@v4 | |
| with: | |
| name: agentbridge-win-x64 | |
| path: win-payload | |
| - name: Extract win-x64 payload | |
| shell: pwsh | |
| run: | | |
| New-Item -ItemType Directory -Path payload | Out-Null | |
| tar -xzf win-payload/agentbridge-win-x64.tar.gz -C payload | |
| - name: Add MSBuild to PATH | |
| uses: microsoft/setup-msbuild@v2 | |
| - name: Compile PSF (x64 Release) instead of taking it from NuGet | |
| shell: pwsh | |
| run: | | |
| # Why from source: Microsoft ties PSF telemetry collection to the binaries taken from the | |
| # NuGet package -- those are built with Microsoft's real telemetry provider GUID injected | |
| # into include/Telemetry.h (see psf-public-nuget-build.yml in the PSF repo). Building from | |
| # the repo leaves that provider id as the zeroed placeholder, so the packaged app has | |
| # nowhere to send telemetry. There is no runtime switch for it: PsfRuntime reads only | |
| # 'fixups' from config.json. | |
| $ErrorActionPreference = 'Continue' | |
| $psfTag = '1.0.240212.1' | |
| git clone --depth 1 --branch $psfTag ` | |
| https://github.com/microsoft/MSIX-PackageSupportFramework.git psf-src | |
| if ($LASTEXITCODE -ne 0) { Write-Warning 'PSF clone failed - falling back to NuGet'; exit 0 } | |
| Invoke-WebRequest 'https://dist.nuget.org/win-x86-commandline/v6.9.1/nuget.exe' ` | |
| -OutFile nuget.exe -UseBasicParsing | |
| & .\nuget.exe restore "psf-src\CentennialFixups.sln" | |
| if ($LASTEXITCODE -ne 0) { Write-Warning 'PSF restore failed - falling back to NuGet'; exit 0 } | |
| # The projects pin the VS2019 toolset (v142); windows-latest ships v143. | |
| msbuild "psf-src\CentennialFixups.sln" -p:Configuration=Release -p:Platform=x64 ` | |
| -p:PlatformToolset=v143 -m /v:minimal | |
| if ($LASTEXITCODE -ne 0) { | |
| Write-Warning "PSF build failed (exit $LASTEXITCODE) - falling back to NuGet"; exit 0 | |
| } | |
| New-Item -ItemType Directory -Force -Path psf-bin | Out-Null | |
| foreach ($f in 'PsfLauncher64.exe','PsfRuntime64.dll','FileRedirectionFixup64.dll') { | |
| $src = Get-ChildItem -Path psf-src -Recurse -Filter $f -ErrorAction SilentlyContinue | | |
| Where-Object { $_.FullName -match '\\x64\\Release\\' } | | |
| Select-Object -First 1 | |
| if (-not $src) { | |
| Write-Warning "PSF build did not produce $f - falling back to NuGet" | |
| Remove-Item psf-bin -Recurse -Force -ErrorAction SilentlyContinue | |
| exit 0 | |
| } | |
| Copy-Item $src.FullName psf-bin\ -Force | |
| } | |
| Write-Host "PSF built from source into psf-bin (no Microsoft telemetry provider id)" | |
| - name: Build MSIX | |
| shell: pwsh | |
| env: | |
| VERSION: ${{ needs.check-version.outputs.version }} | |
| STORE_IDENTITY_NAME: ${{ secrets.STORE_IDENTITY_NAME }} | |
| STORE_PUBLISHER: ${{ secrets.STORE_PUBLISHER }} | |
| run: | | |
| if (-not $env:STORE_IDENTITY_NAME -or -not $env:STORE_PUBLISHER) { | |
| Write-Warning 'STORE_IDENTITY_NAME / STORE_PUBLISHER are not set: the package carries placeholder identity values and Partner Center would reject the upload.' | |
| } | |
| $msixArgs = @{} | |
| if ($env:STORE_IDENTITY_NAME) { $msixArgs.IdentityName = $env:STORE_IDENTITY_NAME } | |
| if ($env:STORE_PUBLISHER) { $msixArgs.Publisher = $env:STORE_PUBLISHER } | |
| if (Test-Path "$env:GITHUB_WORKSPACE/psf-bin/PsfLauncher64.exe") { | |
| $msixArgs.PsfBinDir = "$env:GITHUB_WORKSPACE/psf-bin" | |
| Write-Host 'Packaging with the from-source PSF binaries.' | |
| } else { | |
| Write-Warning 'From-source PSF unavailable: falling back to the NuGet package, whose binaries carry Microsoft''s telemetry provider id.' | |
| } | |
| & "$env:GITHUB_WORKSPACE/AgentBridge/tools/store/New-StoreMsix.ps1" ` | |
| -PayloadDir "$env:GITHUB_WORKSPACE/payload" ` | |
| -Version $env:VERSION ` | |
| -OutDir "$env:GITHUB_WORKSPACE/store-msix" ` | |
| @msixArgs | |
| - name: Upload MSIX | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: store-msix | |
| path: store-msix/*.msix | |
| release: | |
| needs: [check-version, build, store-msi] | |
| if: needs.check-version.outputs.do_release == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Download archives | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: artifacts | |
| # Pin the tag to the commit that triggered this run (github.sha = the gate-off commit) so | |
| # a later master push (e.g. the IsPrerelease restore after a button release) cannot move | |
| # it. The GITHUB_TOKEN push does not re-trigger workflows. The tag is informational: | |
| # nothing listens on it. | |
| - name: Tag the triggering commit | |
| shell: bash | |
| run: | | |
| git tag "v${{ needs.check-version.outputs.version }}" "$GITHUB_SHA" | |
| git push origin "v${{ needs.check-version.outputs.version }}" | |
| # Creates the GitHub release (the tag already exists) with all 5 platform | |
| # archives + the Windows Store MSI when store-msi produced one (the job is | |
| # continue-on-error, so the archives release must not depend on it — an | |
| # absent MSI artifact just uploads the archives). generate_release_notes: | |
| # auto-notes from merged PRs/commits — every release ships with a changelog | |
| # (read by users and AI engines alike). | |
| # | |
| # `body` is PRE-PENDED to the auto-generated notes (softprops/action-gh-release: | |
| # "If body is specified, the body will be pre-pended to the automatically generated | |
| # notes"), so this is the per-release note. It MUST be updated in the gate-off | |
| # commit — see docs-dev/RELEASE-CHECKLIST.md: a stale note ships the previous | |
| # release's news to every user and to the AI engines that read release pages. | |
| - name: Create GitHub Release v${{ needs.check-version.outputs.version }} | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: v${{ needs.check-version.outputs.version }} | |
| files: | | |
| artifacts/**/*.tar.gz | |
| artifacts/**/*.msi | |
| body: | | |
| ### Settings, rebuilt around separate panels | |
| The single tabbed "Main settings" window is gone. Settings now opens as focused | |
| panels — **LLM & Provider**, **Email (SMTP + IMAP)** and **General** — each with | |
| its own **Save** that closes only when its values validate, so a bad value in one | |
| area never blocks the others. | |
| ### Provider management is clearer | |
| The **active-provider dropdown** is now the single way to change which provider is | |
| active: pick one, press Save, and the choice persists across restarts. The active | |
| provider is marked **(attivo)** in the list, the dropdown and the list always agree, | |
| and the old "Set default" button is gone. **Edit** opens the provider selected in | |
| the list, and the API-key field shows the stored key masked. | |
| ### SIP telephony is now a real settings panel | |
| The SIP page used to be read-only. It is now a full editable panel: enable/disable, | |
| listen port, registrar, credentials, answer mode and PIN, plus call / hang-up / | |
| reload actions. | |
| ### Voice moved to the Session menu | |
| Voice starts a one-shot dictation, so it is no longer a "setting": it now lives | |
| under **Session**, alongside the model switch and session features. The TTS engine | |
| panel gained a **reset to default**. | |
| ### Documentation on the wiki | |
| The user guides now live on the GitHub wiki, kept in sync automatically from the | |
| repository. **Help → Documentation** (and `/docs`) opens | |
| https://github.com/Graphene-Lab/AgentBridge/wiki. | |
| generate_release_notes: true | |
| # Microsoft Store submission: once the release exists with the MSI attached, point the | |
| # Partner Center draft package at the VERSIONED MSI URL (the AIOffice VPS streaming proxy | |
| # answers 200 without redirects for /agentbridge/msi/<version>, which is pinned to release | |
| # tag v<version> and never changes — Partner Center rejects redirecting URLs and requires | |
| # a URL whose binary stays frozen, policy 10.2.9, see tools/store/vps/) and submit for | |
| # certification. The real gate is inside the run script (the secrets context is not | |
| # allowed in job/step if): the STORE_* secrets exist | |
| # only for accounts that could register the Entra app the Store API requires — individual | |
| # developer accounts have none, so the step exits early and the Store update is manual | |
| # (see tools/store/README.md "Manual fallback"); continue-on-error keeps the run green | |
| # in any case: the GitHub release is already out and a failed Store submission must | |
| # never look like a failed release. | |
| store-submit: | |
| needs: [check-version, store-msi, release] | |
| if: needs.store-msi.result == 'success' | |
| runs-on: ubuntu-latest | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| path: AgentBridge | |
| - name: Submit to Microsoft Store | |
| shell: pwsh | |
| env: | |
| VERSION: ${{ needs.check-version.outputs.version }} | |
| STORE_TENANT_ID: ${{ secrets.STORE_TENANT_ID }} | |
| STORE_CLIENT_ID: ${{ secrets.STORE_CLIENT_ID }} | |
| STORE_CLIENT_SECRET: ${{ secrets.STORE_CLIENT_SECRET }} | |
| STORE_PRODUCT_ID: ${{ secrets.STORE_PRODUCT_ID }} | |
| STORE_SELLER_ID: ${{ secrets.STORE_SELLER_ID }} | |
| run: | | |
| # The secrets context is not usable in if: conditions — gate on the env var | |
| # instead. Without the Entra app credentials (individual accounts) the Store | |
| # update stays manual (see tools/store/README.md "Manual fallback"). | |
| if ([string]::IsNullOrEmpty($env:STORE_TENANT_ID)) { | |
| Write-Host 'STORE_TENANT_ID not configured - skipping Store submission (manual fallback).' | |
| exit 0 | |
| } | |
| & "$env:GITHUB_WORKSPACE/AgentBridge/tools/store/Submit-Store.ps1" ` | |
| -PackageUrl "https://aitechnology.it/agentbridge/msi/$env:VERSION" ` | |
| -Version $env:VERSION |