"""Phase 4 — publish the skill library into the running Gitea instance. Creates (idempotent, safe to re-run): - orgs: skills-core (public), skills-community (public), tenant-acme (private) - one repo per package in skills-core, pushed via git (real commits) - skills-community/recruitment-consultant (community edition, PR target) - skills-core/marketplace with marketplace.json + README (logo, mermaid) The tenant fork/PR demo lives in p4b_tenant_demo.py. Gitea runs on MSSQL behind Caddy; pushes are serialized with a small pause (single-core SQL Express — be gentle). """ import json import os import shutil import subprocess import sys import tempfile import time import requests from dotenv import load_dotenv BASE = os.path.join(os.path.dirname(__file__), "..") load_dotenv(os.path.join(BASE, ".env")) GITEA_URL = os.environ["GITEA_URL"].rstrip("/") TOKEN = os.environ["GITEA_TOKEN"] API = f"{GITEA_URL}/api/v1" HDR = {"Authorization": f"token {TOKEN}"} SKILLS_DIR = os.path.join(BASE, "skills") PUSH_PAUSE_S = 1.5 def api(method, path, ok=(200, 201, 204), **kw): r = requests.request(method, f"{API}{path}", headers=HDR, timeout=120, **kw) if r.status_code not in ok: raise RuntimeError(f"{method} {path} -> {r.status_code}: {r.text[:300]}") return r def ensure_org(name, visibility): r = requests.get(f"{API}/orgs/{name}", headers=HDR, timeout=60) if r.status_code == 200: return api("POST", "/orgs", json={"username": name, "visibility": visibility, "full_name": name, "description": f"skillfactor — {name}"}) print(f"org created: {name} ({visibility})") def ensure_repo(org, name, description=""): r = requests.get(f"{API}/repos/{org}/{name}", headers=HDR, timeout=60) if r.status_code == 200: return False api("POST", f"/orgs/{org}/repos", json={"name": name, "description": description[:255], "private": False, "default_branch": "main", "auto_init": False}) print(f"repo created: {org}/{name}") return True def push_dir(org, repo, src_dir, message): """Push src_dir content as single commit to main (force: package is source of truth).""" push_url = GITEA_URL.replace("://", f"://gitadmin:{TOKEN}@") + f"/{org}/{repo}.git" tmp = tempfile.mkdtemp(prefix="sfpush_") try: shutil.copytree(src_dir, tmp, dirs_exist_ok=True) def g(*args): subprocess.run(["git", *args], cwd=tmp, check=True, capture_output=True, text=True) g("init", "-b", "main") g("config", "user.name", "skillfactor-pipeline") g("config", "user.email", "pipeline@noreply.zeiterfassung.cloud") g("add", "-A") g("commit", "-m", message) g("push", "--force", push_url, "main") finally: shutil.rmtree(tmp, ignore_errors=True) def build_marketplace_dir(slugs): tmp = tempfile.mkdtemp(prefix="sfmarket_") # globale Provenienz aus den Manifests aggregieren (fuer das README-Pie) prov_labels = {"esco": "ESCO (occupations & competences)", "onet": "O*NET (tasks & tools)", "jobads": "Job boards (market evidence)", "wiki_ai": "Wikipedia & AI expert curation"} prov = {k: 0 for k in prov_labels} for slug in slugs: m = json.load(open(os.path.join(SKILLS_DIR, slug, "manifest.json"), encoding="utf-8")) for k in prov: prov[k] += (m.get("provenance", {}).get("items", {}) or {}).get(k, 0) prov_rows = "\n".join(f' "{prov_labels[k]}" : {v}' for k, v in prov.items() if v) index = [] for slug in slugs: manifest = json.load(open(os.path.join(SKILLS_DIR, slug, "manifest.json"), encoding="utf-8")) index.append({ "name": manifest["name"], "title": manifest["title"], "slug": slug, "esco_uri": manifest["ids"]["esco_uri"], "onet_soc": manifest["ids"]["onet_soc"], "version": manifest["version"], "layer": manifest["layer"], "repo": f"{GITEA_URL}/skills-core/{slug}", }) json.dump(index, open(os.path.join(tmp, "marketplace.json"), "w", encoding="utf-8"), indent=2) os.makedirs(os.path.join(tmp, "assets"), exist_ok=True) for logo in ("skillfactor_logo_red.png", "skillfactor_mark_red.png"): shutil.copy(os.path.join(BASE, "assets", logo), os.path.join(tmp, "assets", logo)) readme = f"""

skillfactor

# skillfactor marketplace **AI with work experience.** The occupational skill layer: public occupation databases (ESCO, O*NET) + live job-ad evidence + expert knowledge, compiled into one ready-to-use skill package per occupation. Every employee agent loads the package for its role — and starts with the profession built in. **{len(index)} occupations** are currently published in [`skills-core`]({GITEA_URL}/skills-core). The machine-readable index is [`marketplace.json`](marketplace.json). ## Architecture ```mermaid flowchart LR A["ESCO 1.2.1 / O*NET 30.3
+ official crosswalk"] --> DB[("evidence store
Microsoft SQL Server")] J["job ads
(JSearch / Adzuna APIs)"] --> DB W["Wikipedia / Wikidata
+ literature"] --> G DB --> G["generator"] G --> CORE["skills-core
1 repo per occupation"] CORE -->|fork| T["tenant-acme
(private extensions)"] T -->|sanitize gate:
PII scan + eval + human PR review| COMM["skills-community"] ``` ## Layer model | Layer | Content | Visibility | |---|---|---| | community | shared, anonymized best practices | public | | company | tenant processes (e.g. `tenant-acme`) | private | | role | the role's memory | tenant | | person | personal style | employee | Every boundary is a gate: PII/secret scan (`sanitize.yml`), structural eval (`eval.yml`), anonymization, and a human pull-request review. ## Data provenance Every package documents where its content comes from in its own `PROVENANCE.md` (rendered pie chart + table). Library-wide distribution of content items: ```mermaid pie showData title Content sources across all {len(slugs)} packages {prov_rows} ``` The taxonomy share dominates library-wide because job-ad and expert enrichment rolls out occupation by occupation (see the [recruiter package]({GITEA_URL}/skills-core/recruitment-consultant/src/branch/main/PROVENANCE.md) for a fully enriched split). ## Licensing This marketplace includes information from the O*NET Database (v30.3, USDOL/ETA, CC BY 4.0) and ESCO (v1.2.1, © European Union — [conditions](https://esco.ec.europa.eu/en/use-esco/download)). skillfactor is not endorsed by USDOL/ETA or the European Commission. """ open(os.path.join(tmp, "README.md"), "w", encoding="utf-8", newline="\n").write(readme) return tmp def main(): only = sys.argv[1] if len(sys.argv) > 1 else None ensure_org("skills-core", "public") ensure_org("skills-community", "public") ensure_org("tenant-acme", "private") slugs = sorted(d for d in os.listdir(SKILLS_DIR) if os.path.isdir(os.path.join(SKILLS_DIR, d))) if only: slugs = [s for s in slugs if s == only] print(f"publishing {len(slugs)} packages ...") for i, slug in enumerate(slugs, 1): manifest = json.load(open(os.path.join(SKILLS_DIR, slug, "manifest.json"), encoding="utf-8")) ensure_repo("skills-core", slug, manifest.get("title", slug)) push_dir("skills-core", slug, os.path.join(SKILLS_DIR, slug), f"feat: {slug} skill package v{manifest['version']}") print(f"[{i}/{len(slugs)}] pushed skills-core/{slug}") time.sleep(PUSH_PAUSE_S) # community edition of the recruiter package = PR target for the tenant demo recruiter = next((s for s in slugs if "recruit" in s), None) if recruiter: ensure_repo("skills-community", recruiter, "community edition — accepts sanitized tenant contributions") push_dir("skills-community", recruiter, os.path.join(SKILLS_DIR, recruiter), f"feat: community edition of {recruiter}") print(f"pushed skills-community/{recruiter}") all_slugs = sorted(d for d in os.listdir(SKILLS_DIR) if os.path.isdir(os.path.join(SKILLS_DIR, d))) market = build_marketplace_dir(all_slugs) try: ensure_repo("skills-core", "marketplace", "skillfactor marketplace index") push_dir("skills-core", "marketplace", market, "docs: marketplace index + README") print("pushed skills-core/marketplace") finally: shutil.rmtree(market, ignore_errors=True) if __name__ == "__main__": main()