Skip to content
Merged
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .agents/skills/develop-maple-agent/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ the tree, but not for TypeScript SDK changes. Component-only
changes should not unnecessarily select Research packaging. Maintain the root
selectors, their tests, `.github/workflows/agent-ci.yml`, and unsigned PR
selection in `.github/workflows/agent-desktop-build.yml` together. The desktop
workflow intentionally builds both profiles on every `master` push.
workflow compares `master` with the last fully verified ancestor package run;
unrelated pushes skip packages, while manual master dispatch forces both profiles.

Linux Nix packages use a pure source fileset rooted at the monorepo, including
the sibling SDK/proxy source and SDK assets. Validate it when adding a new local dependency
Expand Down
23 changes: 19 additions & 4 deletions .github/workflows/agent-desktop-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ jobs:
changes:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
actions: read
outputs:
agent: ${{ steps.classify.outputs.agent }}
steps:
Expand All @@ -40,16 +43,28 @@ jobs:
ref: ${{ github.sha }}
persist-credentials: false
fetch-depth: 0
- name: Select unsigned PR packaging
- name: Select master packaging since last verified build
id: master
if: ${{ github.repository == 'MaplePrivacyLabs/Maple' && github.ref == 'refs/heads/master' && github.event_name == 'push' }}
working-directory: .
env:
GH_TOKEN: ${{ github.token }}
run: python3 scripts/ci/agent_master_selection.py >> "$GITHUB_OUTPUT"
- name: Select Agent packaging
id: classify
working-directory: .
env:
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
MASTER_AGENT: ${{ steps.master.outputs.agent }}
run: |
set -euo pipefail
if [ "$GITHUB_EVENT_NAME" != pull_request ]; then
echo 'agent=true' >> "$GITHUB_OUTPUT"
if [ "$GITHUB_EVENT_NAME" = push ] && [ "${MASTER_AGENT:-}" = false ]; then
echo 'agent=false' >> "$GITHUB_OUTPUT"
else
echo 'agent=true' >> "$GITHUB_OUTPUT"
fi
exit 0
fi
git fetch --no-tags origin "$BASE_SHA" "$HEAD_SHA"
Expand All @@ -64,7 +79,7 @@ jobs:
macos-build:
name: Build macOS binary (${{ matrix.variant }})
needs: changes
if: ${{ always() && !cancelled() && ((github.event_name == 'pull_request' && (needs.changes.result != 'success' || needs.changes.outputs.agent != 'false')) || (github.repository == 'MaplePrivacyLabs/Maple' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch'))) }}
if: ${{ always() && !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.agent != 'false') && (github.event_name == 'pull_request' || (github.repository == 'MaplePrivacyLabs/Maple' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch'))) }}
runs-on: blacksmith-6vcpu-macos-26
timeout-minutes: 120
strategy:
Expand Down Expand Up @@ -207,7 +222,7 @@ jobs:
linux:
name: Linux AppImage (${{ matrix.variant }})
needs: changes
if: ${{ always() && !cancelled() && ((github.event_name == 'pull_request' && (needs.changes.result != 'success' || needs.changes.outputs.agent != 'false')) || (github.repository == 'MaplePrivacyLabs/Maple' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch'))) }}
if: ${{ always() && !cancelled() && (needs.changes.result != 'success' || needs.changes.outputs.agent != 'false') && (github.event_name == 'pull_request' || (github.repository == 'MaplePrivacyLabs/Maple' && github.ref == 'refs/heads/master' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch'))) }}
runs-on: blacksmith-8vcpu-ubuntu-2404
timeout-minutes: 120
strategy:
Expand Down
18 changes: 15 additions & 3 deletions apps/maple-agent/docs/desktop-builds.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
# Agent desktop builds

`Maple Agent Desktop Builds` builds both hosted profiles on every protected
`master` push and on a manual dispatch from `master`. macOS ARM64 builds are
`Maple Agent Desktop Builds` builds both hosted profiles when Agent inputs change
on protected `master`, and always on a manual dispatch from `master`. It compares
the current source with the last successful ancestor run that actually completed
all four Dev/Prod macOS/Linux downloaded-package verifiers. Failed, in-flight and
selector-only runs do not advance that baseline, so a later unrelated push still
builds outstanding Agent changes. Missing history, API failures or no verified
baseline in the last 100 successful master runs select a full build. PRs continue
to select unsigned previews from their whole merge-base diff.

Unrelated master commits have no new Agent artifacts. Existing downloads keep
their original source SHA and provenance; they are never relabeled as a newer
commit. A manual dispatch from `master` forces fresh packages, including when
the previous run's 30-day artifacts have expired. macOS ARM64 builds are
Developer ID signed, notarized, and stapled. Linux x86_64 builds are AppImages
with a bundled runtime; CI also checks each downloaded AppImage's command-line
startup in an Ubuntu 24.04 container with no Nix installation and no network
Expand Down Expand Up @@ -35,7 +46,8 @@ verification. GUI startup uses empty temporary state and does not sign in.

## Download and install

Open the latest successful `Maple Agent Desktop Builds` run on GitHub Actions.
Open the latest successful `Maple Agent Desktop Builds` run with completed
package verifiers on GitHub Actions; a selector-only run has no packages.
Its artifacts include the profile, platform, and run ID in their names:

- `maple-agent-dev-macos-aarch64-RUN`
Expand Down
5 changes: 5 additions & 0 deletions docs/repository-workflows.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ its table-driven tests when the dependency graph or component layout changes.
The backend has its own root `opensecret-ci.yml` workflow and change selector;
`sdk-integration.yml` tests both SDKs against `services/opensecret/` from the
same checkout. Backend changes do not imply Research or Agent packaging.
Agent desktop packages compare master against the last successful ancestor run
with all four Dev/Prod macOS/Linux download verifiers completed; selector-only
and failed runs do not consume outstanding changes. Unrelated master pushes skip
Agent packages. Manual master dispatch always builds both profiles. PR previews
keep their merge-base selection and signing stays confined to trusted master.
The separate `opensecret-eif.yml` builds dev/prod EIF candidates on
same-repository PRs changing measured inputs without editing PCR approvals;
these diagnostic builds do not publish an artifact. It compares measurements
Expand Down
1 change: 1 addition & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -757,6 +757,7 @@
} ''
cd "$src"
python3 scripts/ci/test_agent_workflows.py
python3 scripts/ci/test_agent_master_selection.py
python3 scripts/ci/test_agent_rust_deps.py
python3 scripts/ci/test_agent_debug_plist.py
python3 apps/maple-agent/scripts/test_release_packaging.py
Expand Down
1 change: 1 addition & 0 deletions scripts/ci/agent_change_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
".github/workflows/agent-ci.yml",
".github/workflows/agent-desktop-build.yml",
"scripts/ci/agent_change_detection.py",
"scripts/ci/agent_master_selection.py",
"scripts/ci/change_detection.py",
"scripts/ci/verify-agent-rust-deps.py",
"scripts/ci/apple-toolchain.json",
Expand Down
115 changes: 115 additions & 0 deletions scripts/ci/agent_master_selection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""Compare Agent inputs with the last fully verified master package run."""

from __future__ import annotations

import argparse
import json
import os
from pathlib import Path
import re
import subprocess
import sys

from agent_change_detection import classify_paths


ROOT = Path(__file__).resolve().parents[2]
REPOSITORY = "MaplePrivacyLabs/Maple"
WORKFLOW = ".github/workflows/agent-desktop-build.yml"
VERIFIERS = frozenset(
f"Verify downloaded {platform} ({profile})"
for platform in ("macOS", "Linux") for profile in ("dev", "prod")
)


def api(path: str) -> dict:
result = subprocess.run(
["gh", "api", "--method", "GET", path],
check=True, capture_output=True, text=True, timeout=15,
)
return json.loads(result.stdout)


def git(root: Path, *args: str) -> subprocess.CompletedProcess:
return subprocess.run(
["git", *args], cwd=root, check=True, capture_output=True, timeout=15,
)


def select_master(head: str, current_run_id: int, root: Path = ROOT) -> tuple[bool, str]:
if not re.fullmatch(r"[0-9a-f]{40}", head):
raise ValueError("invalid current commit")
runs = api(
f"repos/{REPOSITORY}/actions/workflows/agent-desktop-build.yml/runs"
"?branch=master&status=success&per_page=100"
)["workflow_runs"]
for run in runs:
if (
run["id"] == current_run_id
or run["repository"]["full_name"] != REPOSITORY
or run["head_repository"]["full_name"] != REPOSITORY
or run["head_branch"] != "master"
or run["event"] not in ("push", "workflow_dispatch")
or run["path"] != WORKFLOW
or run["status"] != "completed"
or run["conclusion"] != "success"
):
continue
base = run["head_sha"]
if not re.fullmatch(r"[0-9a-f]{40}", base):
raise ValueError("invalid baseline commit")
ancestor = subprocess.run(
["git", "merge-base", "--is-ancestor", base, head],
cwd=root, capture_output=True, timeout=15,
)
if ancestor.returncode == 1:
continue
if ancestor.returncode != 0:
raise ValueError("baseline history unavailable")
jobs = api(
f"repos/{REPOSITORY}/actions/runs/{run['id']}/jobs?filter=latest&per_page=100"
)
if jobs["total_count"] != len(jobs["jobs"]):
raise ValueError("incomplete baseline job list")
verified = {
job["name"] for job in jobs["jobs"]
if job["status"] == "completed" and job["conclusion"] == "success"
}
if not VERIFIERS <= verified:
# Selector-only successes and incomplete profiles cannot advance
# the baseline past an Agent change that still needs packaging.
continue
paths = git(root, "diff", "--no-renames", "--name-only", "-z", base, head).stdout
changed = [path.decode("utf-8", errors="surrogateescape")
for path in paths.split(b"\0") if path]
selected = classify_paths(changed)
return selected, (
f"Agent inputs {'changed' if selected else 'unchanged'} since fully verified "
f"master run {run['id']} at {base}; current commit {head}."
)
return True, "No fully verified ancestor in the last 100 successful master runs; building."


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--head", default=os.environ.get("GITHUB_SHA", ""))
parser.add_argument("--run-id", type=int, default=os.environ.get("GITHUB_RUN_ID", "0"))
args = parser.parse_args()
selected = True
if (os.environ.get("GITHUB_EVENT_NAME") != "push"
or os.environ.get("GITHUB_REPOSITORY") != REPOSITORY
or os.environ.get("GITHUB_REF") != "refs/heads/master"):
reason = "Manual or non-master event; building without a baseline lookup."
else:
try:
selected, reason = select_master(args.head, args.run_id)
except (KeyError, TypeError, ValueError, OSError, subprocess.SubprocessError):
reason = "Master baseline lookup or diff unavailable; building."
print(reason, file=sys.stderr)
print(f"agent={'true' if selected else 'false'}")
return 0


if __name__ == "__main__":
raise SystemExit(main())
1 change: 1 addition & 0 deletions scripts/ci/test_agent_change_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ def test_selector_and_shared_tooling_changes_select_agent(self):
"flake.nix", "flake.lock", ".github/workflows/agent-ci.yml",
".github/workflows/agent-desktop-build.yml",
"scripts/ci/agent_change_detection.py", "scripts/ci/change_detection.py",
"scripts/ci/agent_master_selection.py",
"scripts/ci/verify-agent-rust-deps.py",
"scripts/ci/apple-toolchain.json", "scripts/ci/select-xcode.py",
):
Expand Down
Loading
Loading