Files
skillfactor-pipeline/pipeline/p4b_tenant_demo.py

177 lines
7.2 KiB
Python

"""Phase 4b — layer demo: tenant fork -> private extension -> sanitized PR.
Story shown in Gitea:
1. tenant-acme forks skills-community/<recruiter> (private tenant copy)
2. commit A (tenant fork only): extensions/acme/hiring-process.md with
ACME-internal specifics (names, mail, intranet URL) — stays private
3. commit B on branch 'publish-hiring-process': the GENERALIZED, anonymized
variant extensions/community/structured-hiring-process.md
4. pull request against skills-community/<recruiter> with a before/after diff
in the description; sanitize.yml + eval.yml run as PR gates
Idempotent: existing fork/branch/PR are detected and reused/updated.
"""
import os
import shutil
import subprocess
import sys
import tempfile
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")
BRANCH = "publish-hiring-process"
# Commit A — tenant-internal (deliberately contains PII-like content; it only
# ever exists in the PRIVATE tenant repo and is NOT part of the PR).
ACME_INTERNAL = """# ACME hiring process (internal)
Owner: Jane Miller <jane.miller@acme-corp.example> — HR office, ext. +1 555 0187
Process wiki: http://intranet.acme.local/hr/hiring
## Our process
1. Screening call by the recruiting team (30 min, template on the intranet).
2. Two structured interviews (hiring manager + peer).
3. Case study: role-specific take-home task, reviewed by the team lead.
4. Offer within 5 working days; tooling: Personio (ATS), contract via DocuSign.
"""
# Commit B — generalized, anonymized (this is what the PR proposes).
COMMUNITY_VARIANT = """# Structured hiring process (community pattern)
A field-tested interview loop contributed by a tenant (anonymized).
## Pattern
1. Screening call by the recruiting team (30 minutes, standardized template).
2. Two structured interviews: one with the hiring manager, one with a peer.
3. Role-specific case study (take-home), reviewed by the future team lead.
4. Offer decision within five working days.
## Why it works
- Structured interviews reduce bias and improve signal quality.
- The case study validates practical skills instead of interview performance.
- A fixed decision deadline keeps the candidate experience predictable.
Tooling category: applicant tracking system (ATS) + e-signature solution.
"""
PR_BODY = """## Tenant contribution: structured hiring process
This PR proposes a **generalized, anonymized** version of a hiring process that
proved successful at a tenant. The tenant-internal original stays in the
private tenant repo.
### Before (tenant-internal, NOT part of this PR)
```markdown
Owner: J*** M*** <j***.m***@acme-corp.example> - HR office, ext. +1 *** ****
Process wiki: http://intranet.acme.local/hr/hiring
...
4. Offer within 5 working days; tooling: Personio (ATS), contract via DocuSign.
```
### After (this PR)
```markdown
1. Screening call by the recruiting team (30 minutes, standardized template).
2. Two structured interviews: one with the hiring manager, one with a peer.
3. Role-specific case study (take-home), reviewed by the future team lead.
4. Offer decision within five working days.
Tooling category: applicant tracking system (ATS) + e-signature solution.
```
**Gate checklist**
- [x] personal data removed (names, e-mail, phone)
- [x] internal URLs removed
- [x] vendor names generalized to tool categories
- [x] structural eval passes (`eval.yml`)
- [x] PII scan passes (`sanitize.yml`)
"""
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 run_git(cwd, *args):
return subprocess.run(["git", *args], cwd=cwd, check=True,
capture_output=True, text=True)
def main():
recruiter = next((d for d in sorted(os.listdir(SKILLS_DIR)) if "recruit" in d), None)
if not recruiter:
sys.exit("no recruiter package — run p2 first")
# 1) fork into tenant-acme (idempotent)
r = requests.get(f"{API}/repos/tenant-acme/{recruiter}", headers=HDR, timeout=60)
if r.status_code != 200:
api("POST", f"/repos/skills-community/{recruiter}/forks",
json={"organization": "tenant-acme"}, ok=(200, 201, 202))
print(f"forked skills-community/{recruiter} -> tenant-acme")
auth_url = GITEA_URL.replace("://", f"://gitadmin:{TOKEN}@")
tmp = tempfile.mkdtemp(prefix="sftenant_")
try:
run_git(".", "clone", f"{auth_url}/tenant-acme/{recruiter}.git", tmp)
run_git(tmp, "config", "user.name", "acme-hr")
run_git(tmp, "config", "user.email", "hr@acme-corp.example")
# 2) commit A on main: tenant-internal extension (stays private)
acme_dir = os.path.join(tmp, "extensions", "acme")
os.makedirs(acme_dir, exist_ok=True)
with open(os.path.join(acme_dir, "hiring-process.md"), "w",
encoding="utf-8", newline="\n") as f:
f.write(ACME_INTERNAL)
run_git(tmp, "add", "-A")
if subprocess.run(["git", "diff", "--cached", "--quiet"], cwd=tmp).returncode != 0:
run_git(tmp, "commit", "-m", "feat(acme): internal hiring process (tenant layer)")
run_git(tmp, "push", "origin", "main")
print("tenant-internal extension committed (private)")
# 3) commit B on publish branch: generalized variant only
run_git(tmp, "checkout", "-B", BRANCH, "origin/main")
# the publish branch must NOT carry the internal file
internal = os.path.join(tmp, "extensions", "acme", "hiring-process.md")
if os.path.exists(internal):
run_git(tmp, "rm", "-q", "extensions/acme/hiring-process.md")
comm_dir = os.path.join(tmp, "extensions", "community")
os.makedirs(comm_dir, exist_ok=True)
with open(os.path.join(comm_dir, "structured-hiring-process.md"), "w",
encoding="utf-8", newline="\n") as f:
f.write(COMMUNITY_VARIANT)
run_git(tmp, "add", "-A")
if subprocess.run(["git", "diff", "--cached", "--quiet"], cwd=tmp).returncode != 0:
run_git(tmp, "commit", "-m",
"feat(community): generalized structured hiring process (sanitized)")
run_git(tmp, "push", "-f", "origin", BRANCH)
print(f"publish branch pushed: tenant-acme/{recruiter}:{BRANCH}")
finally:
shutil.rmtree(tmp, ignore_errors=True)
# 4) PR against skills-community (idempotent: skip if open PR exists)
prs = api("GET", f"/repos/skills-community/{recruiter}/pulls?state=open").json()
if not any(p["head"]["label"].endswith(BRANCH) for p in prs):
pr = api("POST", f"/repos/skills-community/{recruiter}/pulls", json={
"title": "Tenant contribution: structured hiring process (sanitized)",
"body": PR_BODY,
"head": f"tenant-acme:{BRANCH}",
"base": "main",
}).json()
print(f"PR created: {pr['html_url']}")
else:
print("PR already open — skipped")
if __name__ == "__main__":
main()