refactor: make ARNES external-repo based with ticket publish flow
This commit is contained in:
133
scripts/publish_ticket.py
Executable file
133
scripts/publish_ticket.py
Executable file
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import json
|
||||
import subprocess
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
BACKLOG = ROOT / 'backlog' / 'features.json'
|
||||
ARTIFACTS = ROOT / 'work' / 'artifacts'
|
||||
|
||||
|
||||
def now_iso():
|
||||
return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace('+00:00', 'Z')
|
||||
|
||||
|
||||
def run_git(*args, check=True):
|
||||
result = subprocess.run(
|
||||
['git', *args],
|
||||
cwd=ROOT,
|
||||
text=True,
|
||||
capture_output=True,
|
||||
)
|
||||
if check and result.returncode != 0:
|
||||
msg = result.stderr.strip() or result.stdout.strip() or f'git command failed: {args}'
|
||||
raise SystemExit(msg)
|
||||
return result
|
||||
|
||||
|
||||
def ensure_git_repo():
|
||||
result = run_git('rev-parse', '--is-inside-work-tree', check=False)
|
||||
if result.returncode != 0 or result.stdout.strip() != 'true':
|
||||
raise SystemExit('This project is not inside a git repository. Run git init or clone a repo first.')
|
||||
|
||||
|
||||
def load_backlog():
|
||||
return json.loads(BACKLOG.read_text(encoding='utf-8'))
|
||||
|
||||
|
||||
def find_feature(feature_id):
|
||||
data = load_backlog()
|
||||
for feature in data.get('features', []):
|
||||
if str(feature.get('id')) == feature_id:
|
||||
return feature
|
||||
raise SystemExit(f'Feature not found in backlog: {feature_id}')
|
||||
|
||||
|
||||
def current_branch():
|
||||
branch = run_git('symbolic-ref', '--quiet', '--short', 'HEAD', check=False).stdout.strip()
|
||||
if not branch:
|
||||
branch = run_git('rev-parse', '--abbrev-ref', 'HEAD', check=False).stdout.strip()
|
||||
if not branch or branch == 'HEAD':
|
||||
raise SystemExit('Detached HEAD is not supported for publish. Checkout a branch first.')
|
||||
return branch
|
||||
|
||||
|
||||
def ensure_remote(remote):
|
||||
remotes = [line.strip() for line in run_git('remote').stdout.splitlines() if line.strip()]
|
||||
if remote not in remotes:
|
||||
raise SystemExit(f'Remote not found: {remote}. Add it first with git remote add {remote} <url>.')
|
||||
|
||||
|
||||
def status_porcelain():
|
||||
return run_git('status', '--porcelain').stdout.strip()
|
||||
|
||||
|
||||
def default_commit_message(feature):
|
||||
feature_id = feature['id']
|
||||
ticket_type = feature.get('type')
|
||||
title = str(feature.get('title', '')).strip()
|
||||
if ticket_type:
|
||||
return f'{feature_id} {ticket_type}: {title}'
|
||||
return f'{feature_id}: {title}'
|
||||
|
||||
|
||||
def write_publish_artifact(feature_id, payload):
|
||||
feature_dir = ARTIFACTS / feature_id
|
||||
feature_dir.mkdir(parents=True, exist_ok=True)
|
||||
path = feature_dir / 'publish.json'
|
||||
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + '\n', encoding='utf-8')
|
||||
return path
|
||||
|
||||
|
||||
def ensure_git_identity():
|
||||
name = run_git('config', 'user.name', check=False).stdout.strip()
|
||||
email = run_git('config', 'user.email', check=False).stdout.strip()
|
||||
if not name or not email:
|
||||
raise SystemExit('Missing git identity. Configure git user.name and user.email before publish.')
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Commit and push one ticket, then write publish.json evidence.')
|
||||
parser.add_argument('--feature-id', required=True)
|
||||
parser.add_argument('--remote', default='origin')
|
||||
parser.add_argument('--branch', default='')
|
||||
parser.add_argument('--commit-message', default='')
|
||||
args = parser.parse_args()
|
||||
|
||||
ensure_git_repo()
|
||||
ensure_git_identity()
|
||||
feature = find_feature(args.feature_id)
|
||||
remote = args.remote.strip() or 'origin'
|
||||
branch = args.branch.strip() or current_branch()
|
||||
ensure_remote(remote)
|
||||
|
||||
if not status_porcelain():
|
||||
raise SystemExit('No git changes to publish. Nothing to commit.')
|
||||
|
||||
commit_message = args.commit_message.strip() or default_commit_message(feature)
|
||||
payload = {
|
||||
'agent': 'leader',
|
||||
'verdict': 'PUBLISHED',
|
||||
'feature_id': args.feature_id,
|
||||
'branch': branch,
|
||||
'remote': remote,
|
||||
'message': commit_message,
|
||||
'pushed': True,
|
||||
'published_at': now_iso(),
|
||||
'note': 'This artifact is committed inside the publish commit for this ticket.'
|
||||
}
|
||||
artifact_path = write_publish_artifact(args.feature_id, payload)
|
||||
|
||||
run_git('add', '-A')
|
||||
if not status_porcelain():
|
||||
raise SystemExit('No staged git changes after git add -A. Nothing to commit.')
|
||||
|
||||
run_git('commit', '-m', commit_message)
|
||||
run_git('push', remote, branch)
|
||||
print(f'done -> {artifact_path}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user