114 lines
8.5 KiB
Python
114 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Project integration, opt-in updates and user-started issue queues."""
|
|
import argparse
|
|
import json
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
from pda.transaction import recover, InstallError
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument('action', choices=('init', 'doctor', 'recover', 'check-update', 'notify-question',
|
|
'update', 'skip-update', 'remind-later',
|
|
'permissions-status', 'permissions-grant', 'permissions-revoke', 'permissions-check',
|
|
'queue-start', 'queue-stop', 'queue-status', 'queue-next', 'queue-claim',
|
|
'queue-resource-acquire', 'queue-resource-release', 'queue-legacy-stop', 'queue-transfer',
|
|
'queue-guard', 'queue-cleanup-guard', 'queue-checkpoint', 'queue-block', 'queue-retry', 'queue-finish',
|
|
'delivery-status', 'delivery-record', 'delivery-check',
|
|
'watch-start', 'watch-bind', 'watch-tick', 'watch-stop', 'watch-status'))
|
|
parser.add_argument('--project', required=True, type=Path)
|
|
parser.add_argument('--apply', action='store_true', help='apply previewed init/recover/update or queue-start/watch-start/queue-transfer/permissions-grant/permissions-revoke changes')
|
|
parser.add_argument('--force', action='store_true', help='check again without daily cache')
|
|
parser.add_argument('--revision', help='full commit SHA offered by check-update')
|
|
parser.add_argument('--owner', help='unique current conversation ID for queue ownership')
|
|
parser.add_argument('--stage', help='queue or delivery checkpoint stage')
|
|
parser.add_argument('--branch', help='task branch checkpoint')
|
|
parser.add_argument('--worktree', help='task worktree checkpoint')
|
|
parser.add_argument('--commit', help='full task or integration commit SHA')
|
|
parser.add_argument('--reason', help='queue blocking reason code')
|
|
parser.add_argument('--issue', type=int, help='issue number for scoped authorization, queue work or delivery')
|
|
parser.add_argument('--evidence', help='test/review evidence locator; never credential text')
|
|
parser.add_argument('--thread', help='bound host conversation ID for continuous watch')
|
|
parser.add_argument('--scheduler', help='verified host schedule ID')
|
|
parser.add_argument('--generation', help='generation returned by watch-start')
|
|
parser.add_argument('--interval', type=int, help='watch check cadence in seconds (default 300)')
|
|
parser.add_argument('--target-owner', help='explicit transfer destination conversation ID')
|
|
parser.add_argument('--resource', help='environment:NAME resource; blocked integration:BRANCH supports release only')
|
|
parser.add_argument('--all', action='store_true', help='stop all local workers and watch')
|
|
parser.add_argument('--allow', help='comma-separated capabilities explicitly granted for one issue')
|
|
parser.add_argument('--deny', help='comma-separated capabilities denied for one issue')
|
|
parser.add_argument('--capability', help='comma-separated capabilities to check')
|
|
args = parser.parse_args()
|
|
queue_fields = {'thread': ('watch-start', 'watch-tick'), 'scheduler': ('watch-bind', 'watch-tick'),
|
|
'generation': ('watch-bind', 'watch-tick'), 'interval': ('watch-start',), 'owner': ('permissions-status','permissions-grant','permissions-revoke','permissions-check','queue-start','queue-next','queue-guard','queue-cleanup-guard','queue-checkpoint','queue-block','queue-retry','queue-finish','queue-status','queue-stop','queue-claim','queue-resource-acquire','queue-resource-release','queue-transfer'),
|
|
'stage': ('queue-checkpoint', 'delivery-record'), 'branch': ('queue-checkpoint',),
|
|
'worktree': ('queue-checkpoint',), 'commit': ('queue-checkpoint','queue-finish','delivery-record','delivery-check'),
|
|
'reason': ('queue-block',), 'issue': ('permissions-grant','permissions-revoke','permissions-check','queue-start','queue-claim','queue-retry','delivery-status','delivery-record','delivery-check'),
|
|
'evidence': ('permissions-grant','permissions-revoke','queue-finish','delivery-record','queue-transfer'), 'target_owner': ('queue-transfer',),
|
|
'resource': ('queue-resource-acquire','queue-resource-release'),
|
|
'allow': ('permissions-grant',), 'deny': ('permissions-grant',), 'capability': ('permissions-check',)}
|
|
for field, actions in queue_fields.items():
|
|
if getattr(args, field) is not None and args.action not in actions:
|
|
parser.error('--' + field + ' is not valid for this action')
|
|
if args.action in queue_fields['owner'] and args.action not in ('queue-status','queue-stop') and not args.owner:
|
|
parser.error('--owner is required for this workflow action')
|
|
if args.all and (args.action != 'queue-stop' or args.owner):
|
|
parser.error('--all requires queue-stop without --owner')
|
|
required = {'permissions-grant': ('issue','evidence'), 'permissions-revoke': ('issue','evidence'), 'permissions-check': ('issue','capability'), 'queue-transfer': ('target_owner','evidence'), 'queue-claim': ('issue',), 'queue-resource-acquire': ('resource',), 'queue-resource-release': ('resource',),'watch-start': ('thread',), 'watch-bind': ('generation','scheduler'),
|
|
'watch-tick': ('generation','scheduler','thread'), 'queue-checkpoint': ('stage',), 'queue-block': ('reason',),
|
|
'queue-retry': ('issue',), 'queue-finish': ('commit','evidence'),
|
|
'delivery-record': ('issue','commit','stage','evidence'), 'delivery-check': ('issue','commit')}
|
|
if any(getattr(args, field) is None for field in required.get(args.action, ())):
|
|
parser.error('missing required workflow action arguments')
|
|
root = args.project.expanduser().resolve()
|
|
if not root.is_dir():
|
|
parser.error('project must be an existing directory')
|
|
if args.apply and args.action not in ('init', 'recover', 'update', 'queue-start', 'watch-start', 'queue-transfer', 'permissions-grant', 'permissions-revoke'):
|
|
parser.error('--apply is only for init, recover, update or queue-start/watch-start/queue-transfer/permissions-grant/permissions-revoke')
|
|
if args.force and args.action != 'check-update':
|
|
parser.error('--force only bypasses check-update cache; it never overwrites files')
|
|
needs_revision = args.action in ('update', 'skip-update', 'remind-later')
|
|
if bool(args.revision) != needs_revision:
|
|
parser.error('--revision is required only for update, skip-update and remind-later')
|
|
package = Path(__file__).resolve().parents[1]
|
|
try:
|
|
if args.action == 'notify-question':
|
|
from pda.notifications import notify
|
|
print(json.dumps(notify(package), ensure_ascii=False, indent=2))
|
|
return 0
|
|
if args.action.startswith('watch-'):
|
|
from pda.issue_watch import dispatch
|
|
print(json.dumps(dispatch(root, args), ensure_ascii=False, indent=2))
|
|
return 0
|
|
if args.action.startswith(('queue-', 'delivery-', 'permissions-')):
|
|
from pda.worker_dispatch import dispatch
|
|
print(json.dumps(dispatch(root, args), ensure_ascii=False, indent=2))
|
|
return 0
|
|
if args.action == 'recover':
|
|
recover(root, args.apply)
|
|
return 0
|
|
from pda.install import doctor, initialize
|
|
from pda.updates import check_update, choose, update
|
|
if args.action in ('init', 'doctor') and not (package / 'templates/project.json').is_file():
|
|
raise InstallError('init/doctor require a reviewed source checkout; installed runtime supports update and recover')
|
|
if args.action == 'doctor':
|
|
print(json.dumps(doctor(root, package), ensure_ascii=False, indent=2))
|
|
elif args.action == 'init':
|
|
initialize(root, package, args.apply)
|
|
elif args.action == 'update':
|
|
update(root, args.revision, args.apply)
|
|
else:
|
|
result = check_update(root, args.force) if args.action == 'check-update' else choose(
|
|
root, 'skip' if args.action == 'skip-update' else 'later', args.revision)
|
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
except (InstallError, OSError, ValueError, KeyError, TypeError) as exc:
|
|
print('ERROR: ' + str(exc), file=sys.stderr)
|
|
return 1
|
|
return 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|