Files
tutti/.github/workflows/sync-tutti-agent-skills.yml
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

370 lines
16 KiB
YAML

name: Sync Tutti Agent Skills
on:
push:
branches:
- main
paths:
- "services/tuttid/service/workspace/app_factory_reference/**"
- "services/tuttid/service/workspace/agent_workspace_app_reference/**"
workflow_dispatch:
inputs:
source_ref:
description: "Source ref to mirror, usually main or a commit SHA."
required: false
default: main
permissions:
contents: read
issues: write
pull-requests: write
concurrency:
group: sync-tutti-agent-skills
cancel-in-progress: false
jobs:
sync:
runs-on: ubuntu-latest
env:
SKILLS_REPO: tutti-os/tutti-agent-skills
SYNC_BRANCH: sync/workspace-app-factory-skill
steps:
- name: Checkout Tutti main repository
uses: actions/checkout@v4
with:
path: source-repository
ref: ${{ github.event.inputs.source_ref || github.sha }}
- name: Checkout skills repository
uses: actions/checkout@v4
with:
repository: ${{ env.SKILLS_REPO }}
token: ${{ secrets.TUTTI_AGENT_SKILLS_SYNC_TOKEN }}
path: tutti-agent-skills
- name: Sync built-in skills
run: |
rsync -a --delete \
source-repository/services/tuttid/service/workspace/app_factory_reference/ \
tutti-agent-skills/skills/tutti-workspace-app-factory/
rsync -a --delete \
source-repository/services/tuttid/service/workspace/app_factory_reference/ \
tutti-agent-skills/plugins/tutti/skills/tutti-workspace-app-factory/
rsync -a --delete \
source-repository/services/tuttid/service/workspace/agent_workspace_app_reference/ \
tutti-agent-skills/skills/tutti-agent-workspace-app/
rsync -a --delete \
source-repository/services/tuttid/service/workspace/agent_workspace_app_reference/ \
tutti-agent-skills/plugins/tutti/skills/tutti-agent-workspace-app/
- name: Detect mirrored changes
id: changes
working-directory: tutti-agent-skills
run: |
versions_aligned="$(python3 - <<'PY'
import json
import pathlib
import re
paths = [
pathlib.Path(".codex-plugin/plugin.json"),
pathlib.Path("plugins/tutti/.codex-plugin/plugin.json"),
pathlib.Path("plugins/tutti/.claude-plugin/plugin.json"),
]
versions = [json.loads(path.read_text()).get("version") for path in paths]
valid = all(re.fullmatch(r"\d+\.\d+\.\d+", str(version or "")) for version in versions)
print("true" if valid and len(set(versions)) == 1 else "false")
PY
)"
if git diff --quiet -- \
skills/tutti-workspace-app-factory \
skills/tutti-agent-workspace-app \
plugins/tutti/skills/tutti-workspace-app-factory \
plugins/tutti/skills/tutti-agent-workspace-app \
&& [[ "$versions_aligned" == "true" ]]; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"
fi
- name: Bump plugin patch version
if: steps.changes.outputs.has_changes == 'true'
working-directory: tutti-agent-skills
run: |
python3 - <<'PY'
import json
import pathlib
import re
import sys
def bump_patch(version):
base = str(version).split("+", 1)[0]
parts = base.split(".")
if len(parts) != 3 or not all(part.isdigit() for part in parts):
sys.exit(f"Unsupported plugin version: {version}")
parts[2] = str(int(parts[2]) + 1)
return ".".join(parts)
manifest_paths = [
pathlib.Path(".codex-plugin/plugin.json"),
pathlib.Path("plugins/tutti/.codex-plugin/plugin.json"),
pathlib.Path("plugins/tutti/.claude-plugin/plugin.json"),
]
root_manifest = json.loads(manifest_paths[0].read_text())
next_version = bump_patch(root_manifest.get("version", "0.1.0"))
for manifest_path in manifest_paths:
text = manifest_path.read_text()
text = re.sub(
r'("version"\s*:\s*")[^"]+(")',
rf"\g<1>{next_version}\2",
text,
count=1,
)
manifest_path.write_text(text)
PY
- name: Validate allowed changed paths
if: steps.changes.outputs.has_changes == 'true'
working-directory: tutti-agent-skills
run: |
git diff --name-only > /tmp/tutti-agent-skills-changed-files.txt
while IFS= read -r path; do
case "$path" in
skills/tutti-workspace-app-factory/*|skills/tutti-workspace-app-factory/**|skills/tutti-agent-workspace-app/*|skills/tutti-agent-workspace-app/**|plugins/tutti/skills/tutti-workspace-app-factory/*|plugins/tutti/skills/tutti-workspace-app-factory/**|plugins/tutti/skills/tutti-agent-workspace-app/*|plugins/tutti/skills/tutti-agent-workspace-app/**|.codex-plugin/plugin.json|plugins/tutti/.codex-plugin/plugin.json|plugins/tutti/.claude-plugin/plugin.json)
;;
*)
echo "Unexpected changed path: $path" >&2
exit 1
;;
esac
done < /tmp/tutti-agent-skills-changed-files.txt
- name: Validate skill discovery
if: steps.changes.outputs.has_changes == 'true'
working-directory: tutti-agent-skills
run: |
npx --yes skills add . --list
npx --yes skills add ./skills/tutti-workspace-app-factory --list
npx --yes skills add ./skills/tutti-agent-workspace-app --list
- name: Validate marketplace layout
if: steps.changes.outputs.has_changes == 'true'
working-directory: tutti-agent-skills
run: |
python3 - <<'PY'
import json
import pathlib
import re
import sys
marketplace = json.loads(pathlib.Path(".agents/plugins/marketplace.json").read_text())
plugins = marketplace.get("plugins")
if not isinstance(plugins, list) or not plugins:
sys.exit("marketplace must contain at least one plugin")
entry = plugins[0]
if entry.get("name") != "tutti":
sys.exit("marketplace plugin name must be tutti")
if entry.get("source", {}).get("path") != "./plugins/tutti":
sys.exit("marketplace plugin source path must be ./plugins/tutti")
for root in (pathlib.Path("."), pathlib.Path("plugins/tutti")):
manifest = json.loads((root / ".codex-plugin/plugin.json").read_text())
if manifest.get("name") != "tutti":
sys.exit(f"{root} plugin name must be tutti")
if manifest.get("skills") != "./skills/":
sys.exit(f"{root} skills path must be ./skills/")
if manifest.get("interface", {}).get("websiteURL") != "https://tutti.sh/":
sys.exit(f"{root} websiteURL must be https://tutti.sh/")
for skill_name in ["tutti-workspace-app-factory", "tutti-agent-workspace-app"]:
if not (root / f"skills/{skill_name}/SKILL.md").exists():
sys.exit(f"{root} is missing {skill_name}")
root_codex = json.loads(pathlib.Path(".codex-plugin/plugin.json").read_text())
plugin_codex = json.loads(pathlib.Path("plugins/tutti/.codex-plugin/plugin.json").read_text())
if root_codex.get("version") != plugin_codex.get("version"):
sys.exit("root and marketplace Codex plugin versions must match")
claude = json.loads(pathlib.Path("plugins/tutti/.claude-plugin/plugin.json").read_text())
if root_codex.get("version") != claude.get("version"):
sys.exit("Codex and Claude plugin versions must match")
if claude.get("name") != "tutti":
sys.exit("Claude plugin name must be tutti")
if claude.get("skills") != "./skills/":
sys.exit("Claude plugin skills path must be ./skills/")
if claude.get("hooks") != "./hooks/claude-hooks.json":
sys.exit("Claude plugin hooks path must be ./hooks/claude-hooks.json")
for label, manifest in (("Codex", root_codex), ("Claude", claude)):
if not re.fullmatch(r"\d+\.\d+\.\d+", str(manifest.get("version", ""))):
sys.exit(f"{label} plugin version must be numeric semver")
PY
- name: Validate skill frontmatter
if: steps.changes.outputs.has_changes == 'true'
working-directory: tutti-agent-skills
run: |
python3 - <<'PY'
import pathlib
import re
import sys
for expected_name in ["tutti-workspace-app-factory", "tutti-agent-workspace-app"]:
skill = pathlib.Path(f"skills/{expected_name}/SKILL.md")
text = skill.read_text()
if not text.startswith("---\n"):
sys.exit(f"{skill} must start with YAML frontmatter")
end = text.find("\n---\n", 4)
if end < 0:
sys.exit(f"{skill} must close YAML frontmatter")
frontmatter = text[4:end]
if not re.search(rf"^name:\s*{re.escape(expected_name)}\s*$", frontmatter, re.M):
sys.exit(f"{skill} missing expected name")
if not re.search(r"^description:\s*.+", frontmatter, re.M):
sys.exit(f"{skill} missing description")
allowed = {"name", "description"}
keys = {
line.split(":", 1)[0].strip()
for line in frontmatter.splitlines()
if ":" in line and not line.startswith(" ")
}
extra = keys - allowed
if extra:
sys.exit(f"{skill} frontmatter has unsupported keys: {sorted(extra)}")
PY
- name: Commit and push sync branch
if: steps.changes.outputs.has_changes == 'true'
working-directory: tutti-agent-skills
env:
TOKEN: ${{ secrets.TUTTI_AGENT_SKILLS_SYNC_TOKEN }}
run: |
git config user.name "tutti-agent-skills-sync"
git config user.email "tutti-agent-skills-sync@users.noreply.github.com"
git checkout -B "$SYNC_BRANCH"
git add \
skills/tutti-workspace-app-factory \
skills/tutti-agent-workspace-app \
plugins/tutti/skills/tutti-workspace-app-factory \
plugins/tutti/skills/tutti-agent-workspace-app \
.codex-plugin/plugin.json \
plugins/tutti/.codex-plugin/plugin.json \
plugins/tutti/.claude-plugin/plugin.json
git commit -m "Sync Tutti agent skills from Tutti main repository"
git push "https://x-access-token:${TOKEN}@github.com/${SKILLS_REPO}.git" "$SYNC_BRANCH" --force
- name: Open or update sync pull request
if: steps.changes.outputs.has_changes == 'true'
id: sync_pr
working-directory: tutti-agent-skills
env:
GH_TOKEN: ${{ secrets.TUTTI_AGENT_SKILLS_SYNC_TOKEN }}
run: |
pr_number="$(gh pr list \
--repo "$SKILLS_REPO" \
--head "$SYNC_BRANCH" \
--json number \
--jq '.[0].number // ""')"
if [ -z "$pr_number" ]; then
body="$(printf 'Mirrors Tutti main repository workspace app skills.\n\nSource commit: %s' "${GITHUB_SHA}")"
pr_url="$(gh pr create \
--repo "$SKILLS_REPO" \
--base main \
--head "$SYNC_BRANCH" \
--title "Sync Tutti agent skills" \
--body "$body")"
pr_number="${pr_url##*/}"
else
body="$(printf 'Mirrors Tutti main repository workspace app skills.\n\nSource commit: %s' "${GITHUB_SHA}")"
gh pr edit "$pr_number" \
--repo "$SKILLS_REPO" \
--title "Sync Tutti agent skills" \
--body "$body"
pr_url="https://github.com/${SKILLS_REPO}/pull/${pr_number}"
fi
echo "number=$pr_number" >> "$GITHUB_OUTPUT"
echo "url=https://github.com/${SKILLS_REPO}/pull/${pr_number}" >> "$GITHUB_OUTPUT"
- name: Wait for sync pull request checks
if: steps.changes.outputs.has_changes == 'true'
env:
GH_TOKEN: ${{ secrets.TUTTI_AGENT_SKILLS_SYNC_TOKEN }}
run: |
for attempt in {1..30}; do
checks_json="$(gh pr checks \
"${{ steps.sync_pr.outputs.number }}" \
--repo "$SKILLS_REPO" \
--json name,state,bucket 2>/dev/null || true)"
checks_count="$(printf '%s' "$checks_json" | python3 -c 'import json,sys; print(len(json.load(sys.stdin) or []))' 2>/dev/null || echo 0)"
if [ "$checks_count" -gt 0 ]; then
break
fi
if [ "$attempt" -eq 30 ]; then
echo "No checks appeared on sync PR after waiting." >&2
exit 1
fi
sleep 5
done
gh pr checks \
"${{ steps.sync_pr.outputs.number }}" \
--repo "$SKILLS_REPO" \
--watch \
--fail-fast
- name: Merge sync pull request
if: steps.changes.outputs.has_changes == 'true'
env:
GH_TOKEN: ${{ secrets.TUTTI_AGENT_SKILLS_SYNC_TOKEN }}
run: |
gh pr merge \
"${{ steps.sync_pr.outputs.number }}" \
--repo "$SKILLS_REPO" \
--squash \
--delete-branch \
--admin
- name: Find source pull request
if: always()
id: source_pr
env:
GH_TOKEN: ${{ github.token }}
run: |
pr_number="$(gh api \
-H "Accept: application/vnd.github+json" \
"repos/${GITHUB_REPOSITORY}/commits/${GITHUB_SHA}/pulls" \
--jq '.[0].number // ""')"
echo "number=$pr_number" >> "$GITHUB_OUTPUT"
- name: Comment sync result on source pull request
if: always() && steps.source_pr.outputs.number != ''
uses: actions/github-script@v8
with:
script: |
const prNumber = Number("${{ steps.source_pr.outputs.number }}");
const hasChanges = "${{ steps.changes.outputs.has_changes }}" === "true";
const syncUrl = "${{ steps.sync_pr.outputs.url }}";
const status = "${{ job.status }}";
const body = hasChanges
? `Tutti skills sync ${status}.\n\nExternal repository:\n tutti-os/tutti-agent-skills\n\nSync PR:\n ${syncUrl}\n\nMerge:\n automatically merged after external checks pass\n\nSource commit:\n ${context.sha}`
: `Tutti skills sync skipped.\n\nNo mirrored skill changes were detected for source commit ${context.sha}.`;
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
- name: Summarize sync
if: always()
run: |
{
echo "## Tutti Skills Sync"
echo
echo "- Source commit: \`${GITHUB_SHA}\`"
echo "- Mirrored changes: \`${{ steps.changes.outputs.has_changes || 'unknown' }}\`"
echo "- Sync PR: ${{ steps.sync_pr.outputs.url || 'not created' }}"
echo "- Job status: \`${{ job.status }}\`"
} >> "$GITHUB_STEP_SUMMARY"