rdeploy 0.2.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,34 @@
1
1
  ----
2
+ Version 0.4.0, Fri 10 Jul 2026
3
+ ===============================
4
+ - `rdeploy upgrade` now shows live rollout progress while the atomic helm
5
+ upgrade waits: it polls `kubectl get pods` and shows the pod status table
6
+ (prefixed with `[rollout]`), so you can watch pods being created, pulled,
7
+ started and terminated (including helm hook pods such as migration jobs)
8
+ instead of blocking silently. On a terminal the table is pinned to the
9
+ bottom of the screen and updates in place while helm's output scrolls
10
+ above it; on a non-TTY (e.g. CI logs) it prints the table once, then any
11
+ row that changes.
12
+ - Added `--no-progress` to disable the live progress display. The display is
13
+ purely informational and runs concurrently with helm; helm `--atomic`
14
+ remains the sole arbiter of success, failure and rollback, and the watcher
15
+ never affects the command's exit code.
16
+
17
+ ----
18
+ Version 0.3.0, Thu 09 Jul 2026
19
+ ===============================
20
+ - `rdeploy upgrade` now runs helm with `--atomic` by default: helm waits for
21
+ the rollout to complete (up to `--timeout`, default 15m) and automatically
22
+ rolls back to the previous release if it fails, and the command exits with
23
+ an error instead of reporting success.
24
+ - Added `--no-atomic` to restore the previous fire-and-forget behaviour and
25
+ `--timeout` to adjust how long helm waits.
26
+
27
+ BREAKING CHANGES:
28
+ - `rdeploy upgrade` now blocks until the rollout completes and exits non-zero
29
+ on failure. Scripts that relied on it returning immediately should pass
30
+ `--no-atomic`.
31
+
2
32
  Version 0.2.0, Thu 09 Jul 2026
3
33
  ===============================
4
34
  - Upgraded all dependencies to current versions: invoke 2.2+/3.x, semver 3.x,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rdeploy
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: CLI that makes it easy to build and deploy an application to k8s.
5
5
  Author-email: Rehive <info@rehive.com>
6
6
  License: MIT
@@ -73,7 +73,7 @@ rdeploy git-release patch
73
73
  | `set-context` | Switch cluster and namespace context |
74
74
  | `activate` | Fetch and set project, cluster, and namespace |
75
75
  | `install` | Install a Helm chart deployment |
76
- | `upgrade` | Upgrade a Helm deployment to a new image tag |
76
+ | `upgrade` | Upgrade a Helm deployment to a new image tag (atomic by default: waits for the rollout and rolls back on failure; use `--no-atomic` to skip, `--timeout` to adjust the wait, default `15m`) |
77
77
  | `helm` | Run arbitrary Helm commands |
78
78
  | `helm-setup` | Download and configure a local Helm binary |
79
79
  | `git-release` | Bump version and push a git tag |
@@ -36,7 +36,7 @@ rdeploy git-release patch
36
36
  | `set-context` | Switch cluster and namespace context |
37
37
  | `activate` | Fetch and set project, cluster, and namespace |
38
38
  | `install` | Install a Helm chart deployment |
39
- | `upgrade` | Upgrade a Helm deployment to a new image tag |
39
+ | `upgrade` | Upgrade a Helm deployment to a new image tag (atomic by default: waits for the rollout and rolls back on failure; use `--no-atomic` to skip, `--timeout` to adjust the wait, default `15m`) |
40
40
  | `helm` | Run arbitrary Helm commands |
41
41
  | `helm-setup` | Download and configure a local Helm binary |
42
42
  | `git-release` | Bump version and push a git tag |
rdeploy-0.4.0/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.4.0
@@ -3,6 +3,10 @@ import tarfile
3
3
  import zipfile
4
4
  import urllib.request
5
5
  import io
6
+ import shutil
7
+ import threading
8
+ import time
9
+ import subprocess
6
10
 
7
11
  from invoke import task
8
12
  import semver
@@ -370,10 +374,194 @@ def install(ctx, config):
370
374
  echo=True)
371
375
 
372
376
 
377
+ class _RolloutDisplay:
378
+ """Pin a live status region to the bottom of a TTY while other output
379
+ scrolls above it.
380
+
381
+ The cursor arithmetic only stays consistent if every terminal write goes
382
+ through this object, so helm's stdout/stderr are routed here as well (via
383
+ invoke's out_stream/err_stream) and rendered above the pinned region.
384
+ Uses plain ANSI: ESC[nA moves the cursor up n lines, ESC[K clears to the
385
+ end of the line, ESC[J clears to the end of the screen.
386
+ """
387
+
388
+ def __init__(self, stream):
389
+ self._stream = stream
390
+ self._lock = threading.Lock()
391
+ self._region = []
392
+ self._drawn = 0
393
+
394
+ def log(self, text):
395
+ """Write scrolling output (complete lines) above the status region."""
396
+ with self._lock:
397
+ self._render(log_lines=text.splitlines())
398
+
399
+ def update(self, lines):
400
+ """Replace the pinned status region with the given lines."""
401
+ with self._lock:
402
+ self._region = list(lines)
403
+ self._render()
404
+
405
+ def _render(self, log_lines=()):
406
+ # Cursor rests at column 0 just below the region; move to the
407
+ # region's top, write the scrolling lines (overwriting old region
408
+ # rows), redraw the region beneath them, and clear any leftover rows
409
+ # from a previously taller region.
410
+ if self._drawn:
411
+ self._stream.write('\x1b[{n}A'.format(n=self._drawn))
412
+ for line in log_lines:
413
+ self._stream.write(line + '\x1b[K\n')
414
+ # Region lines must not wrap or the cursor-up count breaks, so
415
+ # truncate them to the terminal width. Scrolling lines may wrap
416
+ # freely: they end up above the region either way.
417
+ width = shutil.get_terminal_size((120, 24)).columns
418
+ for line in self._region:
419
+ self._stream.write(line[:width - 1] + '\x1b[K\n')
420
+ self._stream.write('\x1b[J')
421
+ self._stream.flush()
422
+ self._drawn = len(self._region)
423
+
424
+
425
+ class _LogStream(io.TextIOBase):
426
+ """File-like stream for invoke's out_stream/err_stream that forwards
427
+ complete lines to a _RolloutDisplay, buffering partial lines so the
428
+ display's line accounting stays exact."""
429
+
430
+ def __init__(self, display):
431
+ self._display = display
432
+ self._buf = ''
433
+
434
+ def write(self, s):
435
+ self._buf += s
436
+ if '\n' in self._buf:
437
+ complete, self._buf = self._buf.rsplit('\n', 1)
438
+ self._display.log(complete + '\n')
439
+ return len(s)
440
+
441
+ def flush(self):
442
+ pass
443
+
444
+ def drain(self):
445
+ """Emit any trailing partial line (call once the command is done)."""
446
+ if self._buf:
447
+ self._display.log(self._buf + '\n')
448
+ self._buf = ''
449
+
450
+
451
+ def _watch_rollout(stop_event, poll_interval=2, display=None):
452
+ """Show live `kubectl get pods` progress while the atomic helm upgrade
453
+ waits, so you can see pods being created, pulled, started and terminated
454
+ instead of a silent wait.
455
+
456
+ Pods (not deployments) are polled because for single-replica services the
457
+ deployment counters are blind to almost the whole rollout: while the new
458
+ pod is created and pulls its image the row reads 1/1, identical to steady
459
+ state, and only blinks to 2/1 for the second or two between the new pod
460
+ becoming ready and the old one being deleted. Pod rows change visibly for
461
+ the entire rollout (ContainerCreating during the pull, Running,
462
+ Terminating, CrashLoopBackOff on a bad release) and also surface helm
463
+ hook pods such as migration jobs.
464
+
465
+ With a display (stdout is a TTY) the whole pod table is pinned to the
466
+ bottom of the terminal and updated in place every poll, while helm's own
467
+ output scrolls above it. Without one (non-TTY, e.g. CI logs) it prints
468
+ append-only lines: the table once as a baseline, then only rows whose
469
+ READY/STATUS/RESTARTS change (ignoring the ticking AGE and restart-age
470
+ columns).
471
+
472
+ Runs in a background daemon thread and is fully decoupled from the
473
+ upgrade: its output is informational only and its exit codes are ignored.
474
+ Every poll reflects the cluster's current state -- pods created by the
475
+ upgrade itself show up automatically, and there are no long-lived child
476
+ processes to tear down: each poll is one short, bounded `kubectl get`.
477
+
478
+ set_context() has already pointed kubectl at the right context and
479
+ namespace. The whole body is wrapped in a broad except so a broken
480
+ watcher (kubectl missing, permission error) can never raise into the
481
+ main flow or change the exit code.
482
+ """
483
+ last_rows = {}
484
+ header_printed = False
485
+ failures = 0
486
+
487
+ def _note(message):
488
+ line = '[rollout] {message}'.format(message=message)
489
+ if display is not None:
490
+ display.update([])
491
+ display.log(line + '\n')
492
+ else:
493
+ print(line, flush=True)
494
+
495
+ try:
496
+ while not stop_event.is_set():
497
+ status = subprocess.run(['kubectl', 'get', 'pods',
498
+ '--request-timeout=10s'],
499
+ capture_output=True, text=True)
500
+ if status.returncode == 0:
501
+ failures = 0
502
+ lines = status.stdout.splitlines()
503
+ if display is not None:
504
+ # Live region: redraw the whole current table in place.
505
+ region = ['[rollout] {line}'.format(line=line)
506
+ for line in lines]
507
+ if region:
508
+ region.append('[rollout] (live view, updated {ts})'
509
+ .format(ts=time.strftime('%H:%M:%S')))
510
+ display.update(region)
511
+ else:
512
+ # Append-only: baseline table once, then changed rows.
513
+ to_print = []
514
+ if lines and not header_printed:
515
+ header_printed = True
516
+ to_print.append(lines[0])
517
+ for row in lines[1:]:
518
+ fields = row.split()
519
+ if not fields:
520
+ continue
521
+ # Compare only NAME READY STATUS RESTARTS so the
522
+ # ticking AGE and the '(Xm ago)' restart annotation
523
+ # don't reprint an otherwise unchanged row.
524
+ name, key = fields[0], ' '.join(fields[:4])
525
+ if last_rows.get(name) != key:
526
+ last_rows[name] = key
527
+ to_print.append(row)
528
+ for line in to_print:
529
+ print('[rollout] {line}'.format(line=line),
530
+ flush=True)
531
+ else:
532
+ # Tolerate transient API errors, but give up (quietly, with a
533
+ # single note) if kubectl keeps failing.
534
+ failures += 1
535
+ if failures >= 3:
536
+ error = (status.stderr or '').strip().splitlines()
537
+ _note('progress display unavailable: {err}'
538
+ .format(err=error[0] if error else
539
+ 'kubectl exited with code {rc}'
540
+ .format(rc=status.returncode)))
541
+ return
542
+ stop_event.wait(poll_interval)
543
+ except Exception as err:
544
+ _note('progress display unavailable: {err}'.format(err=err))
545
+
546
+
373
547
  @task
374
- def upgrade(ctx, config, tag):
548
+ def upgrade(ctx, config, tag, atomic=True, timeout='15m', progress=True):
375
549
  """
376
550
  Upgrades kubernetes deployment
551
+
552
+ By default the upgrade runs atomically: helm waits for the rollout to
553
+ complete and rolls back to the previous release if it fails, and the
554
+ task exits with an error. Use --no-atomic to restore the old
555
+ fire-and-forget behaviour, and --timeout to change how long helm waits.
556
+
557
+ While the atomic upgrade waits, a live progress display polls
558
+ `kubectl get pods` and shows the pod status table (prefixed with
559
+ [rollout]), so you can watch pods being created, pulled, started and
560
+ terminated instead of a silent wait. On a terminal the table is pinned
561
+ to the bottom of the screen and updates in place while helm's output
562
+ scrolls above it; on a non-TTY it prints the table once, then any row
563
+ that changes. This is purely informational and never affects the
564
+ result; use --no-progress to disable it.
377
565
  """
378
566
 
379
567
  settings_dict = get_settings()
@@ -391,17 +579,72 @@ def upgrade(ctx, config, tag):
391
579
  else:
392
580
  helm_chart = config_dict['helm_chart']
393
581
 
394
- ctx.run('{helm_bin} upgrade {project_name} '
395
- '--values {helm_values_path} '
396
- '--set image.tag={version} '
397
- '--version {helm_chart_version} {helm_chart}'
398
- .format(helm_bin=helm_bin,
399
- project_name=config_dict['project_name'],
400
- helm_chart=helm_chart,
401
- helm_values_path=config_dict['helm_values_path'],
402
- version=tag,
403
- helm_chart_version=config_dict['helm_chart_version']),
404
- echo=True)
582
+ atomic_flags = ' --atomic --timeout {timeout}'.format(timeout=timeout) if atomic else ''
583
+
584
+ # Only watch when there is an atomic wait to watch (--no-atomic returns
585
+ # immediately) and progress display is enabled.
586
+ show_progress = atomic and progress
587
+ stop_event = threading.Event()
588
+ watcher = None
589
+ display = None
590
+ run_streams = {}
591
+ if show_progress:
592
+ if sys.stdout.isatty():
593
+ # Live view: the pod table is pinned to the bottom of the
594
+ # terminal and helm's output scrolls above it. Helm's streams
595
+ # must be routed through the display for the cursor arithmetic
596
+ # to hold, so hand invoke our own out/err streams.
597
+ display = _RolloutDisplay(sys.stdout)
598
+ run_streams = {'out_stream': _LogStream(display),
599
+ 'err_stream': _LogStream(display)}
600
+ watcher = threading.Thread(target=_watch_rollout, args=(stop_event,),
601
+ kwargs={'display': display}, daemon=True)
602
+
603
+ helm_command = ('{helm_bin} upgrade {project_name} '
604
+ '--values {helm_values_path} '
605
+ '--set image.tag={version} '
606
+ '--version {helm_chart_version}{atomic_flags} {helm_chart}'
607
+ .format(helm_bin=helm_bin,
608
+ project_name=config_dict['project_name'],
609
+ helm_chart=helm_chart,
610
+ helm_values_path=config_dict['helm_values_path'],
611
+ version=tag,
612
+ helm_chart_version=config_dict['helm_chart_version'],
613
+ atomic_flags=atomic_flags))
614
+
615
+ # The helm call below is the sole source of truth for success/failure/
616
+ # rollback. The watcher runs concurrently and is stopped in finally
617
+ # whether helm succeeds or rolls back; its output never influences result.
618
+ try:
619
+ if watcher is not None:
620
+ watcher.start()
621
+
622
+ if display is not None:
623
+ # invoke's echo writes straight to stdout, which would race with
624
+ # the display's cursor arithmetic; echo through the display
625
+ # instead (same bold formatting as invoke).
626
+ display.log('\033[1;37m{cmd}\033[0m\n'.format(cmd=helm_command))
627
+ result = ctx.run(helm_command, echo=display is None, warn=True,
628
+ **run_streams)
629
+ finally:
630
+ stop_event.set()
631
+ if watcher is not None:
632
+ # A poll in flight finishes within kubectl's --request-timeout,
633
+ # so this join is bounded and leaves no child processes behind.
634
+ watcher.join(timeout=15)
635
+ for stream in run_streams.values():
636
+ stream.drain()
637
+
638
+ if result.failed:
639
+ if atomic:
640
+ sys.exit('Upgrade of {project_name} to {version} failed.'
641
+ ' Helm rolled the release back to the previous version.'
642
+ .format(project_name=config_dict['project_name'],
643
+ version=tag))
644
+ else:
645
+ sys.exit('Upgrade of {project_name} to {version} failed.'
646
+ .format(project_name=config_dict['project_name'],
647
+ version=tag))
405
648
 
406
649
 
407
650
  @task
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rdeploy
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: CLI that makes it easy to build and deploy an application to k8s.
5
5
  Author-email: Rehive <info@rehive.com>
6
6
  License: MIT
@@ -73,7 +73,7 @@ rdeploy git-release patch
73
73
  | `set-context` | Switch cluster and namespace context |
74
74
  | `activate` | Fetch and set project, cluster, and namespace |
75
75
  | `install` | Install a Helm chart deployment |
76
- | `upgrade` | Upgrade a Helm deployment to a new image tag |
76
+ | `upgrade` | Upgrade a Helm deployment to a new image tag (atomic by default: waits for the rollout and rolls back on failure; use `--no-atomic` to skip, `--timeout` to adjust the wait, default `15m`) |
77
77
  | `helm` | Run arbitrary Helm commands |
78
78
  | `helm-setup` | Download and configure a local Helm binary |
79
79
  | `git-release` | Bump version and push a git tag |