gowalk-cicd 1.0.126 → 1.0.127

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 +1 @@
1
- 1.0.126
1
+ 1.0.127
@@ -1 +1 @@
1
- 1.0.126
1
+ 1.0.127
@@ -12,18 +12,39 @@ function evidence(status) {
12
12
  return 'release_write_status=error';
13
13
  }
14
14
 
15
+ // Does the record name THIS run? That one question settles both halves of the
16
+ // protocol -- a claim is won when the answer is yes, released when it is no --
17
+ // and the answer does not depend on which write produced it.
18
+ //
19
+ // Both halves used to demand their OWN revision back from the read that
20
+ // follows their write. GitHub's contents API is read-after-write eventual on a
21
+ // branch ref, so that read can serve the previous blob for a moment, and a
22
+ // write that had already landed then looked like a failure. Foundy run
23
+ // 34345638488 (2026-09-09) released ownership twice -- both commits are on
24
+ // app-robot-play-coordination, both wrote `owner: null` -- and still failed
25
+ // `play_owner_release_unverified`, taking the app's first Play release with it.
26
+ const held = (row, selected) => record.same(row.value.owner, selected.owner);
27
+
28
+ // Seal owes two things, not one: the record must not name this run, AND no
29
+ // claim write of ours may still be able to land. A record another holder has
30
+ // taken settles both -- their write moved the blob, so a late CAS of ours can
31
+ // only conflict. An UNOWNED record settles only the first, so seal still writes
32
+ // once to move the revision itself, and then accepts any readback that no
33
+ // longer names this run. (That fence used to be a side effect of releasing a
34
+ // record we never held; a lost claim response relies on it.)
35
+ const fenced = (row, selected) => row.value.owner !== null && !held(row, selected);
36
+
15
37
  async function seal(client, selected) {
16
38
  let detail = 'release_write_status=none';
17
39
  for (let attempt = 0; attempt < 3; attempt++) {
18
40
  const row = record.validate(await client.read(selected.path), selected);
19
- if (row.value.owner !== null && !record.same(row.value.owner, selected.owner)) return;
41
+ if (fenced(row, selected)) return;
20
42
  const idle = record.replacement(row, null);
21
43
  // Reconcile a possibly applied write; keep only its status as evidence.
22
44
  try { detail = evidence(await client.write(selected.path, row, idle)); }
23
45
  catch { detail = evidence(undefined); }
24
46
  const after = record.validate(await client.read(selected.path), selected);
25
- if (after.value.revision === idle.revision && after.value.owner === null) return;
26
- if (after.value.owner !== null && !record.same(after.value.owner, selected.owner)) return;
47
+ if (!held(after, selected)) return;
27
48
  }
28
49
  const failure = new Error('play_owner_release_unverified');
29
50
  failure.detail = detail;
@@ -40,6 +61,12 @@ async function acquire(client, selected, attempted = () => {}, waiting = {}) {
40
61
  for (let attempt = 0; attempt < 3;) {
41
62
  if (waiting.signal?.aborted) throw new Error('play_owner_cancelled');
42
63
  const row = record.validate(await client.read(selected.path), selected);
64
+ // A record already naming THIS run is this run's own claim: the owner id is
65
+ // a UUID minted once per process, so no other writer can have put it there.
66
+ // Without this, a claim whose write landed but whose readback lagged came
67
+ // back on the next pass as a foreign live holder and the run waited out its
68
+ // own deadline against itself.
69
+ if (held(row, selected)) return;
43
70
  if (row.value.owner !== null) {
44
71
  const owner = row.value.owner;
45
72
  if (owner.kind !== 'ci' || !record.terminal(await client.run(owner), owner)) {
@@ -55,7 +82,7 @@ async function acquire(client, selected, attempted = () => {}, waiting = {}) {
55
82
  attempted();
56
83
  try { await client.write(selected.path, row, desired); } catch { /* Only exact readback may adopt it. */ }
57
84
  const after = record.validate(await client.read(selected.path), selected);
58
- if (after.value.revision === desired.revision && record.same(after.value.owner, selected.owner)) return;
85
+ if (held(after, selected)) return;
59
86
  }
60
87
  throw new Error('play_owner_busy');
61
88
  }
@@ -1 +1 @@
1
- 1.0.126
1
+ 1.0.127
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.126",
3
+ "version": "1.0.127",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {