gowalk-cicd 1.0.152 → 1.0.154
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.
- package/CLAUDE.md +3 -0
- package/action/.daemux-version +1 -1
- package/action/scripts/delivery_icons.py +33 -0
- package/action/scripts/delivery_preflight.py +9 -3
- package/action/scripts/fixtures/single-size-ios-icon.json +38 -0
- package/action/scripts/test_delivery_icons.py +76 -0
- package/android-action/.daemux-version +1 -1
- package/backend-action/.daemux-version +1 -1
- package/backend-action/network_pools.py +3 -1
- package/backend-action/test_network_rule_diagnostics.py +16 -0
- package/docs/BACKEND_NETWORKS.md +3 -0
- package/package.json +1 -1
package/CLAUDE.md
CHANGED
|
@@ -265,6 +265,9 @@ node /path/to/gowalk-cicd/bin/cli.mjs
|
|
|
265
265
|
list is unclassified, never a guessed cause; matching signals are observations, not a retry
|
|
266
266
|
decision. Minute progress notices replace raw compiler output. Preserve the actual exit,
|
|
267
267
|
command arguments, signing and symbol checks.
|
|
268
|
+
- Offline delivery preflight recognizes compiled single-size icons through `delivery_icons.py`.
|
|
269
|
+
Require matching 1024 `Icon Image` and `MultiSized Image` index records per declared device idiom;
|
|
270
|
+
do not treat an arbitrary 1024 image as coverage or weaken missing-family checks.
|
|
268
271
|
- iOS plist stamping reads complete JSON build settings through `resolve_info_plist.py`,
|
|
269
272
|
preserving Xcode's exit status and bounded sanitized stderr. Select exactly the configured
|
|
270
273
|
bundle's application target and resolve paths from that target's source root; never take
|
package/action/.daemux-version
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.154
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Recognize Xcode's compiled single-size iOS app-icon representation."""
|
|
2
|
+
import re
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def single_size_idioms(renditions: list[dict]) -> set[str]:
|
|
6
|
+
"""Require a 1024 icon image AND its matching multi-size index for each idiom.
|
|
7
|
+
|
|
8
|
+
Xcode 26.3 expands a universal iOS source slot into phone/pad records. It
|
|
9
|
+
does not retain the source's universal idiom or emit every smaller square.
|
|
10
|
+
A bare large image is insufficient evidence of this compiled icon format.
|
|
11
|
+
"""
|
|
12
|
+
images = {
|
|
13
|
+
(row.get("Name"), row.get("Idiom"), row.get("Icon Index"))
|
|
14
|
+
for row in renditions
|
|
15
|
+
if row.get("AssetType") == "Icon Image"
|
|
16
|
+
and row.get("PixelWidth") == 1024 and row.get("PixelHeight") == 1024
|
|
17
|
+
and row.get("Scale") == 1 and row.get("Idiom") in {"phone", "pad"}
|
|
18
|
+
and type(row.get("Icon Index")) is int
|
|
19
|
+
}
|
|
20
|
+
covered = set()
|
|
21
|
+
for row in renditions:
|
|
22
|
+
if row.get("AssetType") != "MultiSized Image" or row.get("Scale") != 1:
|
|
23
|
+
continue
|
|
24
|
+
sizes = row.get("Sizes")
|
|
25
|
+
if not isinstance(sizes, list):
|
|
26
|
+
continue
|
|
27
|
+
for size in sizes:
|
|
28
|
+
match = re.fullmatch(r"1024x1024 index:(\d+) idiom:(phone|pad)", str(size))
|
|
29
|
+
if not match or row.get("Idiom") != match[2]:
|
|
30
|
+
continue
|
|
31
|
+
if (row.get("Name"), match[2], int(match[1])) in images:
|
|
32
|
+
covered.add(match[2])
|
|
33
|
+
return covered
|
|
@@ -33,6 +33,8 @@ import subprocess
|
|
|
33
33
|
import sys
|
|
34
34
|
import zipfile
|
|
35
35
|
|
|
36
|
+
from delivery_icons import single_size_idioms
|
|
37
|
+
|
|
36
38
|
SCHEMA = "gowalk-cicd/apple-delivery-preflight.v1"
|
|
37
39
|
|
|
38
40
|
#: Apple's `assetutil` lives outside the selected Xcode and needs no developer dir.
|
|
@@ -51,8 +53,9 @@ REQUIRED_ICONS = {
|
|
|
51
53
|
#: as a missing icon — that would refuse every correct build.
|
|
52
54
|
FLATTENED_ICON_SIZES = frozenset({120, 152})
|
|
53
55
|
|
|
54
|
-
#: Xcode 14+ lets one `universal` 1024 entry stand for every device family
|
|
55
|
-
#:
|
|
56
|
+
#: Xcode 14+ lets one `universal` 1024 entry stand for every device family.
|
|
57
|
+
#: Older compiled catalogs can retain a universal rendition. Newer Xcode also
|
|
58
|
+
#: emits indexed phone/pad single-size records, recognized by delivery_icons.
|
|
56
59
|
#: Demanding `pad` renditions of such a catalog would refuse a correct app, so a
|
|
57
60
|
#: universal app-icon rendition satisfies every family.
|
|
58
61
|
UNIVERSAL_IDIOMS = frozenset({"universal"})
|
|
@@ -261,10 +264,13 @@ def check_app_icons(ctx: dict) -> list[dict]:
|
|
|
261
264
|
# One universal icon covers every family; per-idiom renditions do not exist
|
|
262
265
|
# for it and demanding them would refuse a correct Xcode 14+ catalog.
|
|
263
266
|
return []
|
|
267
|
+
single_size = single_size_idioms(_icon_renditions(renditions or []))
|
|
264
268
|
for family in sorted(families):
|
|
265
269
|
if family not in REQUIRED_ICONS:
|
|
266
270
|
continue
|
|
267
271
|
idiom, required = REQUIRED_ICONS[family]
|
|
272
|
+
if idiom in single_size:
|
|
273
|
+
continue
|
|
268
274
|
device = "iPad" if family == 2 else "iPhone"
|
|
269
275
|
for pixels, label in required:
|
|
270
276
|
if renditions is not None:
|
|
@@ -307,7 +313,7 @@ def check_marketing_icon(ctx: dict) -> list[dict]:
|
|
|
307
313
|
present = any(item.get("PixelWidth") == 1024 and item.get("PixelHeight") == 1024
|
|
308
314
|
and str(item.get("Idiom") or "") in UNIVERSAL_IDIOMS | {"marketing"}
|
|
309
315
|
for item in icons)
|
|
310
|
-
if present:
|
|
316
|
+
if present or single_size_idioms(icons):
|
|
311
317
|
return []
|
|
312
318
|
return [{"itms": "ITMS-90704",
|
|
313
319
|
"message": "no 1024x1024 App Store marketing icon in the compiled asset catalog",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"AssetType": "Icon Image",
|
|
4
|
+
"Icon Index": 1,
|
|
5
|
+
"Idiom": "phone",
|
|
6
|
+
"Name": "AppIcon",
|
|
7
|
+
"PixelHeight": 1024,
|
|
8
|
+
"PixelWidth": 1024,
|
|
9
|
+
"Scale": 1
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"AssetType": "Icon Image",
|
|
13
|
+
"Icon Index": 1,
|
|
14
|
+
"Idiom": "pad",
|
|
15
|
+
"Name": "AppIcon",
|
|
16
|
+
"PixelHeight": 1024,
|
|
17
|
+
"PixelWidth": 1024,
|
|
18
|
+
"Scale": 1
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"AssetType": "MultiSized Image",
|
|
22
|
+
"Idiom": "phone",
|
|
23
|
+
"Name": "AppIcon",
|
|
24
|
+
"Scale": 1,
|
|
25
|
+
"Sizes": [
|
|
26
|
+
"1024x1024 index:1 idiom:phone"
|
|
27
|
+
]
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"AssetType": "MultiSized Image",
|
|
31
|
+
"Idiom": "pad",
|
|
32
|
+
"Name": "AppIcon",
|
|
33
|
+
"Scale": 1,
|
|
34
|
+
"Sizes": [
|
|
35
|
+
"1024x1024 index:1 idiom:pad"
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
]
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Regression for Xcode 26.3 compiling one universal iOS 1024 source icon.
|
|
2
|
+
|
|
3
|
+
The fixture retains only structural assetutil fields from a real signed archive;
|
|
4
|
+
it contains neither artwork nor signing data. Catalog mutations must fail closed.
|
|
5
|
+
"""
|
|
6
|
+
import json
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import sys
|
|
9
|
+
import unittest
|
|
10
|
+
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
12
|
+
import delivery_preflight as preflight
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def context():
|
|
16
|
+
rows = json.loads((Path(__file__).parent / "fixtures/single-size-ios-icon.json").read_text())
|
|
17
|
+
return {"families": {1, 2}, "renditions": rows, "flattened": {120, 152}}
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SingleSizeIconTests(unittest.TestCase):
|
|
21
|
+
def test_real_compiled_single_size_catalog_covers_phone_pad_and_marketing(self):
|
|
22
|
+
ctx = context()
|
|
23
|
+
self.assertEqual(preflight.check_app_icons(ctx), [])
|
|
24
|
+
self.assertEqual(preflight.check_marketing_icon(ctx), [])
|
|
25
|
+
|
|
26
|
+
def test_one_idiom_does_not_cover_the_other_declared_family(self):
|
|
27
|
+
ctx = context()
|
|
28
|
+
ctx["renditions"] = [r for r in ctx["renditions"] if r["Idiom"] == "phone"]
|
|
29
|
+
failures = preflight.check_app_icons(ctx)
|
|
30
|
+
self.assertEqual(len(failures), 2)
|
|
31
|
+
self.assertTrue(all("iPad" in item["message"] for item in failures))
|
|
32
|
+
ctx["families"] = {1}
|
|
33
|
+
self.assertEqual(preflight.check_app_icons(ctx), [])
|
|
34
|
+
|
|
35
|
+
def test_large_image_without_multi_size_index_does_not_prove_coverage(self):
|
|
36
|
+
ctx = context()
|
|
37
|
+
ctx["renditions"] = [r for r in ctx["renditions"] if r["AssetType"] == "Icon Image"]
|
|
38
|
+
self.assertEqual(len(preflight.check_app_icons(ctx)), 4)
|
|
39
|
+
self.assertEqual(len(preflight.check_marketing_icon(ctx)), 1)
|
|
40
|
+
|
|
41
|
+
def test_index_without_icon_image_does_not_prove_coverage(self):
|
|
42
|
+
ctx = context()
|
|
43
|
+
ctx["renditions"] = [r for r in ctx["renditions"] if r["AssetType"] == "MultiSized Image"]
|
|
44
|
+
self.assertEqual(len(preflight.check_app_icons(ctx)), 4)
|
|
45
|
+
self.assertEqual(len(preflight.check_marketing_icon(ctx)), 1)
|
|
46
|
+
|
|
47
|
+
def test_mismatched_catalog_pairs_do_not_cover_the_changed_idiom(self):
|
|
48
|
+
changes = [{"Icon Index": 2}, {"Name": "AppIconOther"}, {"PixelHeight": 512},
|
|
49
|
+
{"PixelWidth": 512}, {"Scale": 2}, {"AssetType": "Image"},
|
|
50
|
+
{"Idiom": "unrelated"}]
|
|
51
|
+
for change in changes:
|
|
52
|
+
with self.subTest(change=change):
|
|
53
|
+
ctx = context()
|
|
54
|
+
ctx["renditions"][0].update(change)
|
|
55
|
+
failures = preflight.check_app_icons(ctx)
|
|
56
|
+
self.assertEqual(len(failures), 2)
|
|
57
|
+
self.assertTrue(all("iPhone" in item["message"] for item in failures))
|
|
58
|
+
|
|
59
|
+
def test_unrelated_asset_name_cannot_supply_an_app_icon(self):
|
|
60
|
+
ctx = context()
|
|
61
|
+
for row in ctx["renditions"]:
|
|
62
|
+
row["Name"] = "HeroImage"
|
|
63
|
+
self.assertEqual(len(preflight.check_app_icons(ctx)), 4)
|
|
64
|
+
|
|
65
|
+
def test_malformed_or_wrong_index_metadata_is_not_coverage(self):
|
|
66
|
+
invalid = [None, "1024x1024 index:1 idiom:phone", ["1024x1024 index:2 idiom:phone"],
|
|
67
|
+
["1024x1024 index:1 idiom:pad"], ["512x512 index:1 idiom:phone"]]
|
|
68
|
+
for sizes in invalid:
|
|
69
|
+
with self.subTest(sizes=sizes):
|
|
70
|
+
ctx = context()
|
|
71
|
+
ctx["renditions"][2]["Sizes"] = sizes
|
|
72
|
+
self.assertEqual(len(preflight.check_app_icons(ctx)), 2)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
if __name__ == "__main__":
|
|
76
|
+
unittest.main()
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.154
|
|
@@ -1 +1 @@
|
|
|
1
|
-
1.0.
|
|
1
|
+
1.0.154
|
|
@@ -204,7 +204,9 @@ def validate_rules(rows):
|
|
|
204
204
|
expected += [dict(marked, priority=5210, table='main'), dict(marked, priority=5230, table='default'),
|
|
205
205
|
dict(marked, priority=5250, action='unreachable'), {'priority': 5270, 'src': 'all', 'table': 52}]
|
|
206
206
|
actual = []
|
|
207
|
-
|
|
207
|
+
# iproute2 iprule.c prints table names/numbers as JSON strings, including Tailscale's "52".
|
|
208
|
+
tables = {255: 'local', 254: 'main', 253: 'default',
|
|
209
|
+
'255': 'local', '254': 'main', '253': 'default', '52': 52}
|
|
208
210
|
for row in rows:
|
|
209
211
|
if not isinstance(row, dict) or set(row) - {'priority', 'src', 'table', 'protocol',
|
|
210
212
|
'fwmark', 'fwmask', 'action'}:
|
|
@@ -12,6 +12,22 @@ from test_network_inventory import FakeHost, RULES
|
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
class RuleDiagnosticTests(unittest.TestCase):
|
|
15
|
+
def test_iproute_decimal_table_strings_keep_exact_policy_semantics(self):
|
|
16
|
+
marked = {'src': 'all', 'fwmark': '0x80000', 'fwmask': '0xff0000'}
|
|
17
|
+
rules = [dict(row, table=str({'local': 255, 'main': 254, 'default': 253}[row['table']]))
|
|
18
|
+
for row in RULES]
|
|
19
|
+
validate_rules(rules)
|
|
20
|
+
rules += [dict(marked, priority=5210, table='254'), dict(marked, priority=5230, table='253'),
|
|
21
|
+
dict(marked, priority=5250, action='unreachable'),
|
|
22
|
+
{'priority': 5270, 'src': 'all', 'table': '52'}]
|
|
23
|
+
validate_rules(rules)
|
|
24
|
+
for invalid in ('53', '052', '0x34', 'private table alias', 53, True):
|
|
25
|
+
changed = copy.deepcopy(rules)
|
|
26
|
+
changed[-1]['table'] = invalid
|
|
27
|
+
with self.subTest(table=invalid), self.assertRaises(InventoryFailure) as caught:
|
|
28
|
+
validate_rules(changed)
|
|
29
|
+
self.assertEqual(caught.exception.code, 'host_rules_unsupported_table')
|
|
30
|
+
|
|
15
31
|
def test_each_rejected_shape_keeps_a_closed_reason_without_values(self):
|
|
16
32
|
marked = {'src': 'all', 'fwmark': '0x80000', 'fwmask': '0xff0000'}
|
|
17
33
|
valid = RULES + [dict(marked, priority=5210, table='main'), dict(marked, priority=5230, table='default'),
|
package/docs/BACKEND_NETWORKS.md
CHANGED
|
@@ -82,6 +82,9 @@ Raw container data, labels, environment, daemon configuration and command errors
|
|
|
82
82
|
Policy-rule refusals use `host_rules_unsupported_<reason>` to distinguish count, fields, priority,
|
|
83
83
|
source, table, structure, fwmark, fwmask, action and duplicate mismatches. These fixed classifications
|
|
84
84
|
retain the strict supported-rule comparison and publish no rejected keys or values.
|
|
85
|
+
The exact decimal-string spellings of supported table IDs are normalized because
|
|
86
|
+
[iproute2 prints rule tables as JSON strings](https://github.com/iproute2/iproute2/blob/v6.8.0/ip/iprule.c#L370).
|
|
87
|
+
This does not admit additional routing tables, aliases, priorities or forwarding policies.
|
|
85
88
|
|
|
86
89
|
The deployment emits its own `gowalk-cicd/backend-networks.v1` receipt. The existing
|
|
87
90
|
`backend_deploy_failed` phase remains `compose_up` on a preparation refusal. Normal public health,
|