gowalk-cicd 1.0.195 → 1.0.197

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.
@@ -50,7 +50,7 @@ def candidate(engine, old, body, name, staged, reservation):
50
50
  current = engine.inspect(old['Id'])
51
51
  unchanged(old, current)
52
52
  if current['State']['Running']:
53
- engine.request('POST', '/containers/' + old['Id'] + '/stop?t=10')
53
+ engine.stop(old['Id'])
54
54
  replacement = engine.inspect(name, missing=True)
55
55
  if replacement is None:
56
56
  result = engine.request('POST', '/containers/create?name=' + quote(name, safe=''), body)
@@ -71,6 +71,8 @@ def confirmed(engine, method, path, proof):
71
71
 
72
72
 
73
73
  def rollback(engine, old, name, reservation):
74
+ if engine.pending_stop is not None:
75
+ engine.stop(old['Id'])
74
76
  replacement = engine.inspect(name, missing=True)
75
77
  if replacement is not None and replacement['Id'] != old['Id']:
76
78
  if (replacement['Config'].get('Labels', {}).get(MARKER) != old['Id']
@@ -13,14 +13,15 @@ SOCKET = '/var/run/docker.sock'
13
13
  class Engine:
14
14
  def __init__(self):
15
15
  self.deadline = time.monotonic() + 90
16
+ self.pending_stop = None
16
17
 
17
- def request(self, method, path, body=None, missing=False, raw=False):
18
+ def request(self, method, path, body=None, missing=False, raw=False, timeout=10):
18
19
  remaining = self.deadline - time.monotonic()
19
20
  if remaining <= 0:
20
21
  raise ValueError('rollout_deadline')
21
- connection = http.client.HTTPConnection('localhost', timeout=min(10, remaining))
22
+ connection = http.client.HTTPConnection('localhost', timeout=min(timeout, remaining))
22
23
  connection.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
23
- connection.sock.settimeout(min(10, remaining))
24
+ connection.sock.settimeout(min(timeout, remaining))
24
25
  try:
25
26
  connection.sock.connect(SOCKET)
26
27
  data = None if body is None else json.dumps(body).encode()
@@ -38,6 +39,15 @@ class Engine:
38
39
  def inspect(self, identifier, missing=False):
39
40
  return self.request('GET', '/containers/' + quote(identifier, safe='') + '/json', missing=missing)
40
41
 
42
+ def stop(self, identifier):
43
+ # Docker continues an accepted stop after a lost HTTP reply. Keep that uncertainty
44
+ # until the same-ID stop completes and its stopped state is positively observed.
45
+ self.pending_stop = identifier
46
+ self.request('POST', '/containers/' + identifier + '/stop?t=10', timeout=20)
47
+ if self.inspect(identifier)['State']['Running']:
48
+ raise ValueError('container_stop_unconfirmed')
49
+ self.pending_stop = None
50
+
41
51
 
42
52
  def unshared(engine, original):
43
53
  identifiers = [row['Id'] for row in engine.request('GET', '/containers/json?all=1')]
@@ -67,9 +77,16 @@ def probe(engine, identifier, test):
67
77
  created = engine.request('POST', '/containers/' + identifier + '/exec',
68
78
  {'AttachStdout': False, 'AttachStderr': False, 'Cmd': command})
69
79
  engine.request('POST', '/exec/' + created['Id'] + '/start', {'Detach': False, 'Tty': False}, raw=True)
70
- result = engine.request('GET', '/exec/' + created['Id'] + '/json')
71
- if result['Running'] or result['ExitCode'] != 0:
72
- raise ValueError('original_probe_failed')
80
+ deadline = min(time.monotonic() + 5, engine.deadline)
81
+ while time.monotonic() < deadline:
82
+ result = engine.request('GET', '/exec/' + created['Id'] + '/json',
83
+ timeout=max(0.001, deadline - time.monotonic()))
84
+ if not result['Running']:
85
+ if result['ExitCode'] != 0:
86
+ raise ValueError('original_probe_failed')
87
+ return
88
+ time.sleep(min(0.1, max(0, deadline - time.monotonic())))
89
+ raise ValueError('original_probe_timeout')
73
90
 
74
91
 
75
92
  def healthy(engine, identifier, deadline):
@@ -113,7 +113,12 @@ def unchanged(before, after, reservation_changes=False):
113
113
 
114
114
  def verified(row, body, original):
115
115
  config = {key: value for key, value in body.items() if key not in ('HostConfig', 'NetworkingConfig')}
116
- if (row['Config'] != config or row['Image'] != original['Image'] or row['HostConfig'] != body['HostConfig']):
116
+ hosts = [dict(value) for value in (row['HostConfig'], body['HostConfig'])]
117
+ # Docker reports the default OOM setting as null while running and false before start.
118
+ for host in hosts:
119
+ if host.get('OomKillDisable') is None:
120
+ host['OomKillDisable'] = False
121
+ if row['Config'] != config or row['Image'] != original['Image'] or hosts[0] != hosts[1]:
117
122
  raise ValueError('replacement_config_changed')
118
123
  actual = row['NetworkSettings']['Networks']
119
124
  expected = body['NetworkingConfig']['EndpointsConfig']
@@ -5,6 +5,7 @@ import re
5
5
  import subprocess
6
6
 
7
7
  from runtime_healthcheck import bounded_tmpfs, cache_healthcheck
8
+ from runtime_model import escape_interpolation
8
9
 
9
10
 
10
11
  def eligible(service):
@@ -17,7 +18,14 @@ def eligible(service):
17
18
  def image_health(command, name, service):
18
19
  reference = service.get('image')
19
20
  if not reference:
20
- result = subprocess.run(command + ['config', '--images', name],
21
+ resolver = list(command)
22
+ option = next((flag for flag in ('--file', '-f') if flag in resolver), None)
23
+ if option:
24
+ resolver[resolver.index(option) + 1] = '-'
25
+ else:
26
+ resolver.extend(['--file', '-'])
27
+ selected = {'services': {name: {'build': '.'}}}
28
+ result = subprocess.run(resolver + ['config', '--images', name], input=json.dumps(selected),
21
29
  capture_output=True, text=True, timeout=30, check=True)
22
30
  references = result.stdout.split()
23
31
  if len(references) != 1:
@@ -44,6 +52,7 @@ def inherit_healthchecks(model, command):
44
52
  candidate = copy.deepcopy(service)
45
53
  candidate.setdefault('healthcheck', {})['test'] = health['Test']
46
54
  if cache_healthcheck(candidate):
55
+ candidate['healthcheck']['test'] = escape_interpolation(candidate['healthcheck']['test'])
47
56
  service['healthcheck'] = candidate['healthcheck']
48
57
  service.update(image=image['Id'], pull_policy='never')
49
58
  changed = True
@@ -36,6 +36,10 @@ wraps only a missing Compose test, retaining explicit timing and disabled/custom
36
36
  launch model pins the transformed service to that inspected image ID with pulling disabled, so a
37
37
  moving tag cannot substitute another image's healthcheck. Explicit registry-refresh policies and
38
38
  non-build services retain their existing image/pull behavior. No pre-build image healthcheck is reused.
39
+ Imported image commands escape literal dollars once at the Compose boundary, preserving Python
40
+ strings and container-shell expansion without interpreting them against the deployment host environment.
41
+ Default image names resolve from an isolated service model with the same Compose project name;
42
+ dependency images cannot make that selection ambiguous.
39
43
 
40
44
  ## Adopt an existing API
41
45
 
@@ -81,6 +85,12 @@ marker-bound replacement, restore/start the original and verify its original hea
81
85
  by identity/state readback. `rollback_unconfirmed` means recovery needs another exact readback/replay;
82
86
  retained containers and records stay available, and unrelated containers are never removed to clear a name.
83
87
 
88
+ Docker may report the default `OomKillDisable` as either null or false; these alone compare equivalently,
89
+ while an enabled OOM-kill override must match exactly. Stop requests retain their ten-second grace with
90
+ a twenty-second transport allowance inside the transaction budget. An uncertain stop must settle on
91
+ that exact container before rollback can restart it or certify its health. Original-probe verification
92
+ waits for the exec's confirmed exit status, for at most five seconds within the remaining transaction.
93
+
84
94
  Replay with the same original ID resumes an interrupted retained-container transaction. After successful
85
95
  cleanup it finds the marker-bound healthy replacement and returns `already_applied`. It refuses absent,
86
96
  ambiguous or changed runtime evidence. Start with one canary, verify application health externally and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.195",
3
+ "version": "1.0.197",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {