gowalk-cicd 1.0.170 → 1.0.171

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.
@@ -99,3 +99,12 @@ An unknown layout or overlapping custom code edit returns a JSON
99
99
  `gowalk-cicd/migration-repair.v1` receipt with its code, file and YAML path. The
100
100
  receipt never includes the conflicting value. Resolve that specific customization
101
101
  in the same repository; unrelated repositories can continue upgrading.
102
+
103
+ 1.0.168 deleted the Android `mode` step (Bitrise delivery). No app enabled Bitrise, so it
104
+ always answered `local`; a deleted step answers null. An upgrade therefore drops a
105
+ `steps.mode.outputs.mode == 'local'` (or `!= 'bitrise'`) conjunct from an app's
106
+ `jobs.android` step `if`, and an `if` left empty goes. Any other use of `steps.mode` in a
107
+ `jobs.android` step `if` returns `android_mode_condition_requires_edit`: a Bitrise-only step
108
+ never ran (delete it), and beside `always()`, `failure()` or `cancelled()` the conjunct also
109
+ meant that the steps before `mode` succeeded (restate that). `env`, `run` and job outputs are
110
+ not checked; there `steps.mode` renders empty.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gowalk-cicd",
3
- "version": "1.0.170",
3
+ "version": "1.0.171",
4
4
  "description": "Zero-config GitHub Actions delivery for iOS TestFlight and Android Google Play.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -86,9 +86,53 @@ function iosChange(file, before, after, change) {
86
86
  return change;
87
87
  }
88
88
 
89
+ // 1.0.168 deleted the Android step `mode`, which answered 'local' on every app (none enabled
90
+ // Bitrise). A missing step answers null, so an app step still guarded by
91
+ // `steps.mode.outputs.mode == 'local'` silently stopped running. That always-true conjunct is
92
+ // dropped (&& binds tighter than ||, so any isolated conjunct can go); any other use refuses.
93
+ // Beside always(), failure() or cancelled() it also meant "nothing before mode failed", so it refuses.
94
+ const MODE = /^steps\.mode\.outputs\.mode\s*(?:==\s*'local'|!=\s*'bitrise')$/;
95
+ const USES_MODE = /\bsteps\.mode(?![\w-])/, STATUS = /\b(?:always|failure|cancelled)\(/;
96
+
97
+ function conjuncts(body) {
98
+ const terms = [];
99
+ let depth = 0, quoted = false, start = 0;
100
+ for (let index = 0; index < body.length; index++) {
101
+ const char = body[index];
102
+ if (char === "'") quoted = !quoted;
103
+ else if (!quoted && (char === '(' || char === ')')) depth += char === '(' ? 1 : -1;
104
+ else if (!quoted && !depth && body.startsWith('&&', index)) {
105
+ terms.push(body.slice(start, index).trim());
106
+ start = ++index + 1;
107
+ }
108
+ }
109
+ return [...terms, body.slice(start).trim()];
110
+ }
111
+
112
+ function modeChange(file, change, ownMode) {
113
+ const [jobs, job, steps, items, name, key] = change.path;
114
+ if (file !== 'deploy.yml' || jobs !== 'jobs' || job !== 'android' || steps !== 'steps' || items !== 'items'
115
+ || ownMode) return change;
116
+ const step = change.path.length === 5 && change.value && typeof change.value === 'object';
117
+ const condition = step ? change.value.if : change.path.length === 6 && key === 'if' ? change.value : null;
118
+ if (typeof condition !== 'string' || !USES_MODE.test(condition)) return change;
119
+ const terms = conjuncts(condition.trim().replace(/^\$\{\{([\s\S]*)\}\}$/, '$1').trim())
120
+ .filter(term => !MODE.test(term));
121
+ if (terms.some(term => USES_MODE.test(term) || STATUS.test(term))) {
122
+ throw new MigrationRepair('android_mode_condition_requires_edit', file, change.path);
123
+ }
124
+ console.log(` Migrated ${file} android step ${name}: dropped steps.mode (step deleted in 1.0.168)`);
125
+ const migrated = terms.length ? `\${{ ${terms.join(' && ')} }}` : undefined;
126
+ if (!step) return migrated === undefined ? { path: change.path, remove: true } : { ...change, value: migrated };
127
+ return { ...change, value: Object.fromEntries(Object.entries(change.value).flatMap(([field, value]) =>
128
+ field !== 'if' ? [[field, value]] : migrated === undefined ? [] : [[field, migrated]])) };
129
+ }
130
+
89
131
  export function mergeChanges(base, incoming, changes, file) {
90
132
  const result = structuredClone(incoming);
91
- for (const change of changes) {
133
+ const ownMode = changes.some(change => equal(change.path, ['jobs', 'android', 'steps', 'items', 'id:mode']));
134
+ for (const raw of changes) {
135
+ const change = modeChange(file, raw, ownMode);
92
136
  const before = at(base, change.path), after = at(incoming, change.path);
93
137
  if (change.path.at(-1) === 'order' && Array.isArray(before) && Array.isArray(after)) {
94
138
  assign(result, { path: change.path, value: mergeOrder(before, change.value, after, file, change.path) });