yams/.github/workflows/publish-repos.yml
Trevon d0b86106d2
fix(release): ship the daemon in the MSI, unblock experimental publishing, find the Windows hang (#238)
* fix(windows): ship yams-daemon and yams-mcp-server in the MSI

The Windows release already builds both and meson install stages them next
to yams.exe, but yams.wxs only packaged yams.exe, so daemon-backed commands
and yams serve failed after an MSI install. Package both, and make
build-msi.ps1 fail when a required executable is missing from the stage or a
staged executable is not packaged.

Signed-off-by: trvon <me@trevon.dev>

* ci(release): fall back to the stable R2 and signing secrets for experimental

The experimental channel needs EXPERIMENTAL_* Cloudflare and GPG secrets that
were never created, so even a passing nightly could not publish and
repo.yamsmemory.ai/experimental/* has never existed. Fall back to the stable
token, account and key when the channel-specific secrets are unset; setting
them later still takes precedence.

Signed-off-by: trvon <me@trevon.dev>

* ci: add a manual Windows workflow that finds hanging test cases

search_service times out at 360 s on Windows in every Tests run, which keeps
experimental from going green and blocks every nightly. The meson log only
shows the timeout. This workflow builds once and runs each Catch2 case on its
own with a per-case timeout, then reports durations and the cases that hang.
It runs only on workflow_dispatch or a push to a diagnose/** branch.

Signed-off-by: trvon <me@trevon.dev>

---------

Signed-off-by: trvon <me@trevon.dev>
2026-09-30 18:11:34 -06:00

175 lines
7.3 KiB
YAML

name: Publish Package Repositories
"on":
workflow_dispatch:
inputs:
channel:
description: "Repository channel expected from the trusted release manifest"
type: choice
options:
- stable
- experimental
required: true
default: stable
run_id:
description: "Exact successful Release run ID (required for experimental)"
required: false
default: ""
permissions:
contents: read
actions: read
jobs:
publish:
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- name: Checkout publication policy
uses: actions/checkout@v4
with:
fetch-depth: 1
submodules: false
- name: Validate and resolve trusted Release run
id: resolve-run
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REQUESTED_CHANNEL: ${{ inputs.channel }}
REQUESTED_RUN_ID: ${{ inputs.run_id }}
run: |
set -euo pipefail
if [ "$REQUESTED_CHANNEL" != stable ] && [ "$REQUESTED_CHANNEL" != experimental ]; then
echo "::error::Missing or unsupported repository channel: $REQUESTED_CHANNEL"
exit 1
fi
if [ "$REQUESTED_CHANNEL" = experimental ] && [ -z "$REQUESTED_RUN_ID" ]; then
echo "::error::Experimental publication requires an exact Release run ID"
exit 1
fi
RUN_ID="$REQUESTED_RUN_ID"
if [ -z "$RUN_ID" ]; then
RUN_ID=$(gh api "repos/${{ github.repository }}/actions/workflows/release.yml/runs?per_page=100" \
--jq '.workflow_runs[] | select(.conclusion == "success" and .event == "push") | .id' | head -1)
if [ -z "$RUN_ID" ]; then
echo "::error::No successful tag-triggered Release workflow run found"
exit 1
fi
fi
if ! [[ "$RUN_ID" =~ ^[1-9][0-9]*$ ]]; then
echo "::error::run_id must be a positive numeric Actions run ID"
exit 1
fi
RUN_INFO=$(gh api "repos/${{ github.repository }}/actions/runs/${RUN_ID}" \
--jq '{conclusion: .conclusion, status: .status, event: .event, path: .path, run_attempt: .run_attempt}')
CONCLUSION=$(jq -r '.conclusion' <<<"$RUN_INFO")
STATUS=$(jq -r '.status' <<<"$RUN_INFO")
EVENT=$(jq -r '.event' <<<"$RUN_INFO")
WORKFLOW_PATH=$(jq -r '.path' <<<"$RUN_INFO")
RUN_ATTEMPT=$(jq -r '.run_attempt' <<<"$RUN_INFO")
if [ "$STATUS" != completed ] || [ "$CONCLUSION" != success ]; then
echo "::error::Release run $RUN_ID is not a completed success"
exit 1
fi
if [ "$EVENT" = pull_request ] || [ "$EVENT" = pull_request_target ]; then
echo "::error::Refusing repository artifacts from a pull-request run"
exit 1
fi
if [ "$WORKFLOW_PATH" != ".github/workflows/release.yml" ]; then
echo "::error::Run $RUN_ID belongs to $WORKFLOW_PATH, not release.yml"
exit 1
fi
if ! [[ "$RUN_ATTEMPT" =~ ^[1-9][0-9]*$ ]]; then
echo "::error::Release run has invalid attempt metadata"
exit 1
fi
echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT"
echo "run_attempt=$RUN_ATTEMPT" >> "$GITHUB_OUTPUT"
- name: Download trusted release manifest
uses: actions/download-artifact@v4
with:
run-id: ${{ steps.resolve-run.outputs.run_id }}
name: release-manifest
path: trusted-manifest
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Download APT repository artifact
uses: actions/download-artifact@v4
with:
run-id: ${{ steps.resolve-run.outputs.run_id }}
name: apt-repo
path: repository-input/aptrepo
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Download YUM repository artifact
uses: actions/download-artifact@v4
with:
run-id: ${{ steps.resolve-run.outputs.run_id }}
name: yum-repo
path: repository-input/yumrepo
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Download Arch repository artifact
uses: actions/download-artifact@v4
with:
run-id: ${{ steps.resolve-run.outputs.run_id }}
name: arch-repo
path: repository-input/archrepo
github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Publish validated channel to R2
env:
STABLE_CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN || secrets.R2_SECRET_ACCESS_KEY }}
STABLE_CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
EXPERIMENTAL_CLOUDFLARE_API_TOKEN: ${{ secrets.EXPERIMENTAL_CLOUDFLARE_API_TOKEN || secrets.CLOUDFLARE_API_TOKEN || secrets.R2_SECRET_ACCESS_KEY }}
EXPERIMENTAL_CLOUDFLARE_ACCOUNT_ID: ${{ secrets.EXPERIMENTAL_CLOUDFLARE_ACCOUNT_ID || secrets.CLOUDFLARE_ACCOUNT_ID }}
STABLE_GPG_PUBLIC_KEY_B64: ${{ secrets.GPG_PUBLIC_KEY_B64 || '' }}
EXPERIMENTAL_GPG_PUBLIC_KEY_B64: ${{ secrets.EXPERIMENTAL_GPG_PUBLIC_KEY_B64 || secrets.GPG_PUBLIC_KEY_B64 || '' }}
REQUESTED_CHANNEL: ${{ inputs.channel }}
shell: bash
run: |
set -euo pipefail
if [ "$REQUESTED_CHANNEL" = stable ]; then
CLOUDFLARE_API_TOKEN="$STABLE_CLOUDFLARE_API_TOKEN"
CLOUDFLARE_ACCOUNT_ID="$STABLE_CLOUDFLARE_ACCOUNT_ID"
GPG_PUBLIC_KEY_B64="$STABLE_GPG_PUBLIC_KEY_B64"
else
CLOUDFLARE_API_TOKEN="$EXPERIMENTAL_CLOUDFLARE_API_TOKEN"
CLOUDFLARE_ACCOUNT_ID="$EXPERIMENTAL_CLOUDFLARE_ACCOUNT_ID"
GPG_PUBLIC_KEY_B64="$EXPERIMENTAL_GPG_PUBLIC_KEY_B64"
fi
export CLOUDFLARE_API_TOKEN CLOUDFLARE_ACCOUNT_ID
unset STABLE_CLOUDFLARE_API_TOKEN STABLE_CLOUDFLARE_ACCOUNT_ID
unset EXPERIMENTAL_CLOUDFLARE_API_TOKEN EXPERIMENTAL_CLOUDFLARE_ACCOUNT_ID
unset STABLE_GPG_PUBLIC_KEY_B64 EXPERIMENTAL_GPG_PUBLIC_KEY_B64
if [ -z "$CLOUDFLARE_API_TOKEN" ] || [ -z "$CLOUDFLARE_ACCOUNT_ID" ]; then
echo "Missing channel-specific Cloudflare R2 authority" >&2
exit 1
fi
WRANGLER_ROOT="${RUNNER_TEMP:-/tmp}/wrangler-cli"
WRANGLER_BIN="$WRANGLER_ROOT/node_modules/.bin/wrangler"
if [ ! -x "$WRANGLER_BIN" ]; then
npm install --prefix "$WRANGLER_ROOT" wrangler@4.69.0 >/dev/null
fi
"$WRANGLER_BIN" --version >&2
public_key_args=()
if [ -n "$GPG_PUBLIC_KEY_B64" ]; then
echo "$GPG_PUBLIC_KEY_B64" | base64 -d > channel-gpg.key
public_key_args=(--public-key channel-gpg.key)
fi
python3 scripts/ci/publish_repository.py \
--repo-root repository-input \
--manifest trusted-manifest/latest.json \
--expected-channel "$REQUESTED_CHANNEL" \
--selected-run-id "${{ steps.resolve-run.outputs.run_id }}" \
--selected-run-attempt "${{ steps.resolve-run.outputs.run_attempt }}" \
--bucket yams-repository \
--wrangler "$WRANGLER_BIN" \
"${public_key_args[@]}"
echo "Published $REQUESTED_CHANNEL without pruning any channel objects." >&2