@intentius/chant 0.105.1 → 0.106.1
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/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/pr.d.ts +16 -2
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +7 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/changed-files.d.ts +8 -2
- package/dist/components/changed-files.d.ts.map +1 -1
- package/dist/components/fan-out-record.d.ts +24 -0
- package/dist/components/fan-out-record.d.ts.map +1 -1
- package/dist/components/pr-pipeline.d.ts +60 -4
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-run.d.ts +92 -3
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/gated-waves.d.ts +4 -1
- package/dist/gated-waves.d.ts.map +1 -1
- package/dist/lexicon.d.ts +13 -2
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/op/gate-name.d.ts +7 -0
- package/dist/op/gate-name.d.ts.map +1 -1
- package/dist/op/gate-summary.d.ts +2 -0
- package/dist/op/gate-summary.d.ts.map +1 -1
- package/dist/op/gate.d.ts +10 -2
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/index.d.ts +2 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-output.d.ts +2 -2
- package/dist/op/local-output.d.ts.map +1 -1
- package/dist/op/runtime.d.ts +19 -2
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/pr-loop.d.ts +29 -4
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -1
- package/dist/workspace/member-pipeline.d.ts +14 -1
- package/dist/workspace/member-pipeline.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/__fixtures__/change-set/warden.change-set.golden.json +2 -2
- package/src/__fixtures__/change-set/warden.plan.json +47 -40
- package/src/__fixtures__/plan-summary/normalization-vectors.json +275 -2
- package/src/change-set.test.ts +7 -8
- package/src/cli/commands/lint.ts +34 -1
- package/src/cli/handlers/build-lexicon-output.test.ts +94 -0
- package/src/cli/handlers/build.ts +65 -0
- package/src/cli/handlers/fan-out.test.ts +12 -1
- package/src/cli/handlers/fan-out.ts +7 -3
- package/src/cli/handlers/operator.ts +1 -2
- package/src/cli/handlers/pr.ts +220 -35
- package/src/cli/handlers/promote.test.ts +15 -1
- package/src/cli/handlers/promote.ts +5 -3
- package/src/cli/handlers/run.test.ts +164 -1
- package/src/cli/handlers/run.ts +22 -11
- package/src/cli/main.ts +13 -1
- package/src/cli/mcp/never-over-mcp.test.ts +74 -0
- package/src/cli/registry.ts +7 -0
- package/src/components/changed-files.test.ts +58 -0
- package/src/components/changed-files.ts +19 -4
- package/src/components/fan-out-record.test.ts +47 -2
- package/src/components/fan-out-record.ts +53 -0
- package/src/components/pr-pipeline.ts +89 -6
- package/src/components/pr-run.test.ts +300 -3
- package/src/components/pr-run.ts +196 -5
- package/src/gated-waves.test.ts +11 -0
- package/src/gated-waves.ts +7 -2
- package/src/lexicon.ts +13 -1
- package/src/lifecycle/gate-ledger.ts +4 -3
- package/src/op/gate-name.ts +17 -0
- package/src/op/gate-summary.test.ts +12 -0
- package/src/op/gate-summary.ts +3 -1
- package/src/op/gate.test.ts +15 -1
- package/src/op/gate.ts +21 -4
- package/src/op/index.ts +2 -2
- package/src/op/local-executor.test.ts +35 -0
- package/src/op/local-executor.ts +2 -2
- package/src/op/local-output.test.ts +20 -0
- package/src/op/local-output.ts +4 -4
- package/src/op/runtime.ts +20 -2
- package/src/op/runtimes/local.test.ts +15 -0
- package/src/op/runtimes/local.ts +3 -1
- package/src/pr-loop.test.ts +41 -1
- package/src/pr-loop.ts +38 -8
- package/src/workspace/lineage-upgrade-cli.ts +4 -3
- package/src/workspace/lineage-upgrade.test.ts +40 -0
- package/src/workspace/member-pipeline.test.ts +28 -0
- package/src/workspace/member-pipeline.ts +20 -3
- package/src/workspace/pr-report.schema.json +14 -1
|
@@ -1,44 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"
|
|
10
|
-
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"org": "acme",
|
|
4
|
+
"entries": [
|
|
5
|
+
{
|
|
6
|
+
"kind": "delete",
|
|
7
|
+
"resourceType": "team",
|
|
8
|
+
"key": "contractors",
|
|
9
|
+
"before": {
|
|
10
|
+
"privacy": "closed",
|
|
11
|
+
"description": "Old contractors"
|
|
12
|
+
},
|
|
13
|
+
"verb": "membership"
|
|
11
14
|
},
|
|
12
|
-
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
+
{
|
|
16
|
+
"kind": "update",
|
|
17
|
+
"resourceType": "team",
|
|
18
|
+
"key": "platform",
|
|
19
|
+
"before": {
|
|
20
|
+
"privacy": "secret",
|
|
21
|
+
"description": "Platform team"
|
|
22
|
+
},
|
|
23
|
+
"after": {
|
|
24
|
+
"privacy": "closed",
|
|
25
|
+
"description": "Platform team"
|
|
26
|
+
},
|
|
27
|
+
"fields": [
|
|
28
|
+
{
|
|
29
|
+
"field": "privacy",
|
|
30
|
+
"before": "secret",
|
|
31
|
+
"after": "closed"
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"verb": "membership"
|
|
15
35
|
},
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
"kind": "create",
|
|
26
|
-
"resourceType": "team",
|
|
27
|
-
"key": "release",
|
|
28
|
-
"after": {
|
|
29
|
-
"privacy": "secret",
|
|
30
|
-
"description": "Release managers"
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"kind": "delete",
|
|
35
|
-
"resourceType": "team",
|
|
36
|
-
"key": "contractors",
|
|
37
|
-
"before": {
|
|
38
|
-
"privacy": "closed",
|
|
39
|
-
"description": "Old contractors"
|
|
36
|
+
{
|
|
37
|
+
"kind": "create",
|
|
38
|
+
"resourceType": "team",
|
|
39
|
+
"key": "release",
|
|
40
|
+
"after": {
|
|
41
|
+
"privacy": "secret",
|
|
42
|
+
"description": "Release managers"
|
|
43
|
+
},
|
|
44
|
+
"verb": "membership"
|
|
40
45
|
}
|
|
46
|
+
],
|
|
47
|
+
"managedCounts": {
|
|
48
|
+
"team": 2
|
|
41
49
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
50
|
+
}
|
|
51
|
+
]
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
"Each repo keeps its own grouping code and runs every row in CI.",
|
|
6
6
|
"rootPairs: a and b are two roots of a choudoufu live-plan-set -json document; summarize a set holding exactly the two, and they must land in one group iff group is true.",
|
|
7
7
|
"instancePairs: a and b are two resource_changes of one terraform show -json plan; summarize that plan, and the two instances must land in one group iff group is true.",
|
|
8
|
-
"Red first: a naive grouper (equal raw resource_changes) must fail every rootPairs row with group true, and a grouper that blanks every string value in before and after must falsely group exactly the rootPairs rows named in overEagerFalselyGroups."
|
|
8
|
+
"Red first: a naive grouper (equal raw resource_changes) must fail every rootPairs row with group true, and a grouper that blanks every string value in before and after must falsely group exactly the rootPairs rows named in overEagerFalselyGroups.",
|
|
9
|
+
"sensitive attribute: chant's change-set carries no value for an attribute marked in before_sensitive or after_sensitive, so two sensitive attributes group by path alone, and a sensitive attribute never groups with an unmarked one. read entry: a data source read carries no attributes, so reads of one address group whatever they returned."
|
|
9
10
|
],
|
|
10
11
|
"rootPairs": [
|
|
11
12
|
{
|
|
@@ -1379,6 +1380,277 @@
|
|
|
1379
1380
|
]
|
|
1380
1381
|
}
|
|
1381
1382
|
}
|
|
1383
|
+
},
|
|
1384
|
+
{
|
|
1385
|
+
"name": "sensitive attribute with different values",
|
|
1386
|
+
"group": true,
|
|
1387
|
+
"a": {
|
|
1388
|
+
"root": "roots/acme",
|
|
1389
|
+
"estate": "acme",
|
|
1390
|
+
"status": "planned",
|
|
1391
|
+
"plan": {
|
|
1392
|
+
"format_version": "1.2",
|
|
1393
|
+
"resource_changes": [
|
|
1394
|
+
{
|
|
1395
|
+
"address": "aws_db_instance.main",
|
|
1396
|
+
"change": {
|
|
1397
|
+
"actions": [
|
|
1398
|
+
"update"
|
|
1399
|
+
],
|
|
1400
|
+
"before": {
|
|
1401
|
+
"password": "old-a",
|
|
1402
|
+
"instance_class": "db.t3.small"
|
|
1403
|
+
},
|
|
1404
|
+
"after": {
|
|
1405
|
+
"password": "new-a",
|
|
1406
|
+
"instance_class": "db.t3.small"
|
|
1407
|
+
},
|
|
1408
|
+
"before_sensitive": {
|
|
1409
|
+
"password": true
|
|
1410
|
+
},
|
|
1411
|
+
"after_sensitive": {
|
|
1412
|
+
"password": true
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
]
|
|
1417
|
+
}
|
|
1418
|
+
},
|
|
1419
|
+
"b": {
|
|
1420
|
+
"root": "roots/globex",
|
|
1421
|
+
"estate": "globex",
|
|
1422
|
+
"status": "planned",
|
|
1423
|
+
"plan": {
|
|
1424
|
+
"format_version": "1.2",
|
|
1425
|
+
"resource_changes": [
|
|
1426
|
+
{
|
|
1427
|
+
"address": "aws_db_instance.main",
|
|
1428
|
+
"change": {
|
|
1429
|
+
"actions": [
|
|
1430
|
+
"update"
|
|
1431
|
+
],
|
|
1432
|
+
"before": {
|
|
1433
|
+
"password": "old-b",
|
|
1434
|
+
"instance_class": "db.t3.small"
|
|
1435
|
+
},
|
|
1436
|
+
"after": {
|
|
1437
|
+
"password": "new-b",
|
|
1438
|
+
"instance_class": "db.t3.small"
|
|
1439
|
+
},
|
|
1440
|
+
"before_sensitive": {
|
|
1441
|
+
"password": true
|
|
1442
|
+
},
|
|
1443
|
+
"after_sensitive": {
|
|
1444
|
+
"password": true
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
]
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
},
|
|
1452
|
+
{
|
|
1453
|
+
"name": "sensitive against not sensitive on the same attribute",
|
|
1454
|
+
"group": false,
|
|
1455
|
+
"a": {
|
|
1456
|
+
"root": "roots/acme",
|
|
1457
|
+
"estate": "acme",
|
|
1458
|
+
"status": "planned",
|
|
1459
|
+
"plan": {
|
|
1460
|
+
"format_version": "1.2",
|
|
1461
|
+
"resource_changes": [
|
|
1462
|
+
{
|
|
1463
|
+
"address": "aws_db_instance.main",
|
|
1464
|
+
"change": {
|
|
1465
|
+
"actions": [
|
|
1466
|
+
"update"
|
|
1467
|
+
],
|
|
1468
|
+
"before": {
|
|
1469
|
+
"password": "same-old",
|
|
1470
|
+
"instance_class": "db.t3.small"
|
|
1471
|
+
},
|
|
1472
|
+
"after": {
|
|
1473
|
+
"password": "same-new",
|
|
1474
|
+
"instance_class": "db.t3.small"
|
|
1475
|
+
},
|
|
1476
|
+
"before_sensitive": {
|
|
1477
|
+
"password": true
|
|
1478
|
+
},
|
|
1479
|
+
"after_sensitive": {
|
|
1480
|
+
"password": true
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
]
|
|
1485
|
+
}
|
|
1486
|
+
},
|
|
1487
|
+
"b": {
|
|
1488
|
+
"root": "roots/globex",
|
|
1489
|
+
"estate": "globex",
|
|
1490
|
+
"status": "planned",
|
|
1491
|
+
"plan": {
|
|
1492
|
+
"format_version": "1.2",
|
|
1493
|
+
"resource_changes": [
|
|
1494
|
+
{
|
|
1495
|
+
"address": "aws_db_instance.main",
|
|
1496
|
+
"change": {
|
|
1497
|
+
"actions": [
|
|
1498
|
+
"update"
|
|
1499
|
+
],
|
|
1500
|
+
"before": {
|
|
1501
|
+
"password": "same-old",
|
|
1502
|
+
"instance_class": "db.t3.small"
|
|
1503
|
+
},
|
|
1504
|
+
"after": {
|
|
1505
|
+
"password": "same-new",
|
|
1506
|
+
"instance_class": "db.t3.small"
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
]
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
1513
|
+
},
|
|
1514
|
+
{
|
|
1515
|
+
"name": "read entries whose results differ",
|
|
1516
|
+
"group": true,
|
|
1517
|
+
"a": {
|
|
1518
|
+
"root": "roots/acme",
|
|
1519
|
+
"estate": "acme",
|
|
1520
|
+
"status": "planned",
|
|
1521
|
+
"plan": {
|
|
1522
|
+
"format_version": "1.2",
|
|
1523
|
+
"resource_changes": [
|
|
1524
|
+
{
|
|
1525
|
+
"address": "data.aws_ami.base",
|
|
1526
|
+
"change": {
|
|
1527
|
+
"actions": [
|
|
1528
|
+
"read"
|
|
1529
|
+
],
|
|
1530
|
+
"after": {
|
|
1531
|
+
"id": "ami-0aaa111",
|
|
1532
|
+
"image_id": "ami-0aaa111",
|
|
1533
|
+
"owner": "amazon"
|
|
1534
|
+
},
|
|
1535
|
+
"after_unknown": {}
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1538
|
+
]
|
|
1539
|
+
}
|
|
1540
|
+
},
|
|
1541
|
+
"b": {
|
|
1542
|
+
"root": "roots/globex",
|
|
1543
|
+
"estate": "globex",
|
|
1544
|
+
"status": "planned",
|
|
1545
|
+
"plan": {
|
|
1546
|
+
"format_version": "1.2",
|
|
1547
|
+
"resource_changes": [
|
|
1548
|
+
{
|
|
1549
|
+
"address": "data.aws_ami.base",
|
|
1550
|
+
"change": {
|
|
1551
|
+
"actions": [
|
|
1552
|
+
"read"
|
|
1553
|
+
],
|
|
1554
|
+
"after": {
|
|
1555
|
+
"id": "ami-0bbb222",
|
|
1556
|
+
"image_id": "ami-0bbb222",
|
|
1557
|
+
"owner": "amazon"
|
|
1558
|
+
},
|
|
1559
|
+
"after_unknown": {}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
]
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
},
|
|
1566
|
+
{
|
|
1567
|
+
"name": "a read of a different data source",
|
|
1568
|
+
"group": false,
|
|
1569
|
+
"a": {
|
|
1570
|
+
"root": "roots/acme",
|
|
1571
|
+
"estate": "acme",
|
|
1572
|
+
"status": "planned",
|
|
1573
|
+
"plan": {
|
|
1574
|
+
"format_version": "1.2",
|
|
1575
|
+
"resource_changes": [
|
|
1576
|
+
{
|
|
1577
|
+
"address": "data.aws_ami.base",
|
|
1578
|
+
"change": {
|
|
1579
|
+
"actions": [
|
|
1580
|
+
"read"
|
|
1581
|
+
],
|
|
1582
|
+
"after": {
|
|
1583
|
+
"id": "ami-0aaa111",
|
|
1584
|
+
"image_id": "ami-0aaa111",
|
|
1585
|
+
"owner": "amazon"
|
|
1586
|
+
},
|
|
1587
|
+
"after_unknown": {}
|
|
1588
|
+
}
|
|
1589
|
+
}
|
|
1590
|
+
]
|
|
1591
|
+
}
|
|
1592
|
+
},
|
|
1593
|
+
"b": {
|
|
1594
|
+
"root": "roots/globex",
|
|
1595
|
+
"estate": "globex",
|
|
1596
|
+
"status": "planned",
|
|
1597
|
+
"plan": {
|
|
1598
|
+
"format_version": "1.2",
|
|
1599
|
+
"resource_changes": [
|
|
1600
|
+
{
|
|
1601
|
+
"address": "data.aws_ami.other",
|
|
1602
|
+
"change": {
|
|
1603
|
+
"actions": [
|
|
1604
|
+
"read"
|
|
1605
|
+
],
|
|
1606
|
+
"after": {
|
|
1607
|
+
"id": "ami-0aaa111",
|
|
1608
|
+
"image_id": "ami-0aaa111",
|
|
1609
|
+
"owner": "amazon"
|
|
1610
|
+
},
|
|
1611
|
+
"after_unknown": {}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
]
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
},
|
|
1618
|
+
{
|
|
1619
|
+
"name": "a read against a plan with no entry for it",
|
|
1620
|
+
"group": false,
|
|
1621
|
+
"a": {
|
|
1622
|
+
"root": "roots/acme",
|
|
1623
|
+
"estate": "acme",
|
|
1624
|
+
"status": "planned",
|
|
1625
|
+
"plan": {
|
|
1626
|
+
"format_version": "1.2",
|
|
1627
|
+
"resource_changes": [
|
|
1628
|
+
{
|
|
1629
|
+
"address": "data.aws_ami.base",
|
|
1630
|
+
"change": {
|
|
1631
|
+
"actions": [
|
|
1632
|
+
"read"
|
|
1633
|
+
],
|
|
1634
|
+
"after": {
|
|
1635
|
+
"id": "ami-0aaa111",
|
|
1636
|
+
"image_id": "ami-0aaa111",
|
|
1637
|
+
"owner": "amazon"
|
|
1638
|
+
},
|
|
1639
|
+
"after_unknown": {}
|
|
1640
|
+
}
|
|
1641
|
+
}
|
|
1642
|
+
]
|
|
1643
|
+
}
|
|
1644
|
+
},
|
|
1645
|
+
"b": {
|
|
1646
|
+
"root": "roots/globex",
|
|
1647
|
+
"estate": "globex",
|
|
1648
|
+
"status": "planned",
|
|
1649
|
+
"plan": {
|
|
1650
|
+
"format_version": "1.2",
|
|
1651
|
+
"resource_changes": []
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1382
1654
|
}
|
|
1383
1655
|
],
|
|
1384
1656
|
"instancePairs": [
|
|
@@ -1588,6 +1860,7 @@
|
|
|
1588
1860
|
"different policy document",
|
|
1589
1861
|
"different tag value that does not carry the estate",
|
|
1590
1862
|
"a tofu-estate marker naming some other estate",
|
|
1591
|
-
"the estate name inside a longer word is not the estate"
|
|
1863
|
+
"the estate name inside a longer word is not the estate",
|
|
1864
|
+
"sensitive against not sensitive on the same attribute"
|
|
1592
1865
|
]
|
|
1593
1866
|
}
|
package/src/change-set.test.ts
CHANGED
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
* update. The update, delete, effect and hole mappings are pinned on typed
|
|
11
11
|
* entries below.
|
|
12
12
|
*
|
|
13
|
-
* `warden.plan.json` is
|
|
14
|
-
*
|
|
15
|
-
* update and one owned delete
|
|
16
|
-
*
|
|
13
|
+
* `warden.plan.json` is what github-warden's `reconcile --plan-json` wrote
|
|
14
|
+
* (github-warden#66) for a teams cycle in an org that declares `owned: true`:
|
|
15
|
+
* one create, one update and one owned delete, as a JSON array of reconcile
|
|
16
|
+
* change sets. It was produced by warden's own `runReconcile` and
|
|
17
|
+
* `teamsCycle` against a stubbed live state, not built here.
|
|
17
18
|
*
|
|
18
19
|
* Goldens regenerate with `UPDATE_GOLDEN=1`.
|
|
19
20
|
*/
|
|
@@ -140,10 +141,8 @@ describe("lifecyclePlanPart", () => {
|
|
|
140
141
|
});
|
|
141
142
|
|
|
142
143
|
describe("reconcilePlanPart", () => {
|
|
143
|
-
test("a warden's teams cycle, as
|
|
144
|
-
const
|
|
145
|
-
golden("warden.plan.json", plan);
|
|
146
|
-
const part = reconcilePlanPart({ member: "warden", plan: read(DIR, "warden.plan.json") as ReconcileChangeSet });
|
|
144
|
+
test("a warden's teams cycle, as `reconcile --plan-json` wrote it", () => {
|
|
145
|
+
const part = reconcilePlanPart({ member: "warden", plan: read(DIR, "warden.plan.json") as ReconcileChangeSet[] });
|
|
147
146
|
expect(part.entries.map((e) => [e.address, e.action, e.disruption ?? null])).toEqual([
|
|
148
147
|
["team.contractors", "delete", "destroy"],
|
|
149
148
|
["team.platform", "update", "unknown"],
|
package/src/cli/commands/lint.ts
CHANGED
|
@@ -4,7 +4,8 @@ import { readFileSync, writeFileSync, readdirSync } from "fs";
|
|
|
4
4
|
import { runLint, parseDisableComments } from "../../lint/engine";
|
|
5
5
|
import type { LintRule, LintDiagnostic, LintFix } from "../../lint/rule";
|
|
6
6
|
import type { IntrinsicDef, LexiconPlugin } from "../../lexicon";
|
|
7
|
-
import { loadPlugins, resolveProjectLexicons } from "../plugins";
|
|
7
|
+
import { loadPlugin, loadPlugins, resolveProjectLexicons } from "../plugins";
|
|
8
|
+
import { findInfraFiles, detectLexicons } from "../../index";
|
|
8
9
|
import { formatStylish, formatJson, formatSarif } from "../reporters/stylish";
|
|
9
10
|
import { loadLocalRules } from "../../lint/rule-loader";
|
|
10
11
|
import { loadCoreRules } from "../../lint/rules/index";
|
|
@@ -129,6 +130,29 @@ function lexiconResolutionDiagnostic(projectRoot: string, error: Error): LintDia
|
|
|
129
130
|
};
|
|
130
131
|
}
|
|
131
132
|
|
|
133
|
+
/**
|
|
134
|
+
* The plugins of lexicons that the project's source imports from and that are not in `configured`, for their
|
|
135
|
+
* property-kind class names (#3184). A package that is not installed, or that fails to load, is skipped.
|
|
136
|
+
*/
|
|
137
|
+
async function loadImportedPropertyPlugins(projectPath: string, configured: readonly string[]): Promise<LexiconPlugin[]> {
|
|
138
|
+
let imported: string[];
|
|
139
|
+
try {
|
|
140
|
+
imported = await detectLexicons(await findInfraFiles(projectPath));
|
|
141
|
+
} catch {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
const plugins: LexiconPlugin[] = [];
|
|
145
|
+
for (const name of imported) {
|
|
146
|
+
if (configured.includes(name)) continue;
|
|
147
|
+
try {
|
|
148
|
+
plugins.push(await loadPlugin(name));
|
|
149
|
+
} catch {
|
|
150
|
+
// Not installed here; the import is the build's problem to report, not lint's.
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return plugins;
|
|
154
|
+
}
|
|
155
|
+
|
|
132
156
|
/**
|
|
133
157
|
* Load all lint rules: core COR/EVL rules, then lexicon plugin rules.
|
|
134
158
|
*
|
|
@@ -210,6 +234,15 @@ async function loadAllPluginRules(
|
|
|
210
234
|
// them as resources.
|
|
211
235
|
const propertyClasses = new Set(plugins.flatMap((plugin) => plugin.propertyClassNames?.() ?? []));
|
|
212
236
|
|
|
237
|
+
// chant #3184: a lexicon the source imports from but the config does not list (grafana in a k8s project that
|
|
238
|
+
// holds a dashboard ConfigMap, after `chant import`) still declares which of its classes are property-kind. Only
|
|
239
|
+
// those names are taken from it; its rules and intrinsics stay out, since the project did not ask for them.
|
|
240
|
+
if (!lexiconError) {
|
|
241
|
+
for (const plugin of await loadImportedPropertyPlugins(projectPath, lexiconNames)) {
|
|
242
|
+
for (const name of plugin.propertyClassNames?.() ?? []) propertyClasses.add(name);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
213
246
|
for (const plugin of plugins) {
|
|
214
247
|
if (plugin.lintRules) {
|
|
215
248
|
for (const r of plugin.lintRules()) {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant build --lexicon-output <lexicon>=<path>`: each named lexicon is built
|
|
3
|
+
* into its own file, so one project can declare otel and prometheus. The build
|
|
4
|
+
* itself is mocked; this checks how the handler splits the serializers and
|
|
5
|
+
* the output paths.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, test, expect, vi, beforeEach } from "vitest";
|
|
8
|
+
import type { ParsedArgs, CommandContext } from "../registry";
|
|
9
|
+
import type { Serializer } from "../../serializer";
|
|
10
|
+
|
|
11
|
+
const buildCommandMock = vi.fn();
|
|
12
|
+
|
|
13
|
+
vi.mock("../commands/build", async () => {
|
|
14
|
+
const actual = await vi.importActual<typeof import("../commands/build")>("../commands/build");
|
|
15
|
+
return { ...actual, buildCommand: (...args: unknown[]) => buildCommandMock(...args) };
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const { runBuild } = await import("./build");
|
|
19
|
+
|
|
20
|
+
const ser = (name: string) => ({ name }) as unknown as Serializer;
|
|
21
|
+
|
|
22
|
+
function ctx(overrides: Partial<ParsedArgs>, names = ["otel", "prometheus", "grafana"]): CommandContext {
|
|
23
|
+
const args = {
|
|
24
|
+
command: "build",
|
|
25
|
+
path: ".",
|
|
26
|
+
format: "",
|
|
27
|
+
fix: false,
|
|
28
|
+
watch: false,
|
|
29
|
+
verbose: false,
|
|
30
|
+
help: false,
|
|
31
|
+
live: false,
|
|
32
|
+
...overrides,
|
|
33
|
+
} as ParsedArgs;
|
|
34
|
+
return { args, plugins: [], serializers: names.map(ser) } as unknown as CommandContext;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("runBuild --lexicon-output", () => {
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
buildCommandMock.mockReset().mockResolvedValue({ success: true, errors: [], warnings: [], resourceCount: 1, fileCount: 1 });
|
|
40
|
+
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("builds each named lexicon into its own file", async () => {
|
|
44
|
+
const code = await runBuild(ctx({ lexiconOutput: ["otel=out/collector.yml", "prometheus=out/prometheus.yml"] }));
|
|
45
|
+
expect(code).toBe(0);
|
|
46
|
+
expect(buildCommandMock).toHaveBeenCalledTimes(2);
|
|
47
|
+
const calls = buildCommandMock.mock.calls.map(([o]) => [o.serializers.map((s: Serializer) => s.name), o.output, o.format]);
|
|
48
|
+
expect(calls).toEqual([
|
|
49
|
+
[["otel"], "out/collector.yml", "yaml"],
|
|
50
|
+
[["prometheus"], "out/prometheus.yml", "yaml"],
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("lexicons not named go to --output when given, and are skipped otherwise", async () => {
|
|
55
|
+
await runBuild(ctx({ lexiconOutput: ["otel=a.yml"], output: "rest.json" }));
|
|
56
|
+
expect(buildCommandMock.mock.calls.map(([o]) => [o.serializers.map((s: Serializer) => s.name), o.output])).toEqual([
|
|
57
|
+
[["otel"], "a.yml"],
|
|
58
|
+
[["prometheus", "grafana"], "rest.json"],
|
|
59
|
+
]);
|
|
60
|
+
buildCommandMock.mockClear();
|
|
61
|
+
await runBuild(ctx({ lexiconOutput: ["otel=a.yml"] }));
|
|
62
|
+
expect(buildCommandMock).toHaveBeenCalledTimes(1);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("drops the 'No serializer found' warning for lexicons another file carries, and repeats nothing", async () => {
|
|
66
|
+
const warn = "No serializer found for lexicon \"grafana\"";
|
|
67
|
+
buildCommandMock.mockResolvedValue({ success: true, errors: [], warnings: [warn, "something else"], resourceCount: 1, fileCount: 1 });
|
|
68
|
+
const err = vi.spyOn(console, "error").mockImplementation(() => {});
|
|
69
|
+
await runBuild(ctx({ lexiconOutput: ["otel=a.yml", "prometheus=b.yml"] }));
|
|
70
|
+
const printed = err.mock.calls.map((c) => String(c[0])).join("\n");
|
|
71
|
+
expect(printed).not.toContain("No serializer found");
|
|
72
|
+
expect(printed.match(/something else/g)).toHaveLength(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("a failed build fails the command, and the other files are still written", async () => {
|
|
76
|
+
buildCommandMock.mockResolvedValueOnce({ success: false, errors: ["boom"], warnings: [], resourceCount: 0, fileCount: 0 });
|
|
77
|
+
const code = await runBuild(ctx({ lexiconOutput: ["otel=a.yml", "prometheus=b.yml"] }));
|
|
78
|
+
expect(code).toBe(1);
|
|
79
|
+
expect(buildCommandMock).toHaveBeenCalledTimes(2);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("refuses a malformed spec, an unknown lexicon, a repeat, and --lexicon", async () => {
|
|
83
|
+
for (const a of [
|
|
84
|
+
{ lexiconOutput: ["otel"] },
|
|
85
|
+
{ lexiconOutput: ["otel="] },
|
|
86
|
+
{ lexiconOutput: ["nope=a.yml"] },
|
|
87
|
+
{ lexiconOutput: ["otel=a.yml", "otel=b.yml"] },
|
|
88
|
+
{ lexiconOutput: ["otel=a.yml"], lexicon: "otel" },
|
|
89
|
+
]) {
|
|
90
|
+
expect(await runBuild(ctx(a))).toBe(1);
|
|
91
|
+
}
|
|
92
|
+
expect(buildCommandMock).not.toHaveBeenCalled();
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -65,6 +65,8 @@ async function runGenerateComponents(ctx: CommandContext): Promise<number> {
|
|
|
65
65
|
output: args.output,
|
|
66
66
|
params: args.param,
|
|
67
67
|
paramsFile: args.paramsFile,
|
|
68
|
+
...(args.waveGate ? { waveGate: { gate: args.waveGate, ...(args.canary?.length ? { canary: args.canary } : {}), ...(args.base ? { base: args.base } : {}) } } : {}),
|
|
69
|
+
...(args.prLoop ? { prLoop: { ...(args.gate ? { gate: args.gate } : {}), ...(args.branch ? { branch: args.branch } : {}) } } : {}),
|
|
68
70
|
});
|
|
69
71
|
} catch (err) {
|
|
70
72
|
console.error(formatError({ message: memberPipeline.describeError(err) }));
|
|
@@ -177,6 +179,69 @@ export async function runBuild(ctx: CommandContext): Promise<number> {
|
|
|
177
179
|
// #1064 — `--param name=value`, repeated, into a flat { name: value } record.
|
|
178
180
|
const params = parseParamFlags(args.param);
|
|
179
181
|
|
|
182
|
+
// `--lexicon-output <lexicon>=<path>`: each named lexicon builds into its own
|
|
183
|
+
// file. One build per file, over the same project, so every write, check and
|
|
184
|
+
// diagnostic path is the one a single-lexicon `--lexicon` build already takes.
|
|
185
|
+
if (args.lexiconOutput?.length) {
|
|
186
|
+
if (args.watch || args.lexicon) {
|
|
187
|
+
console.error(formatError({ message: "--lexicon-output cannot be combined with --watch or --lexicon" }));
|
|
188
|
+
return 1;
|
|
189
|
+
}
|
|
190
|
+
const targets = new Map<string, string>();
|
|
191
|
+
for (const spec of args.lexiconOutput) {
|
|
192
|
+
const eq = (spec ?? "").indexOf("=");
|
|
193
|
+
if (eq <= 0 || eq === spec.length - 1) {
|
|
194
|
+
console.error(formatError({ message: `Invalid --lexicon-output "${spec}". Expected <lexicon>=<path>.` }));
|
|
195
|
+
return 1;
|
|
196
|
+
}
|
|
197
|
+
const name = spec.slice(0, eq);
|
|
198
|
+
if (targets.has(name)) {
|
|
199
|
+
console.error(formatError({ message: `--lexicon-output names lexicon "${name}" twice.` }));
|
|
200
|
+
return 1;
|
|
201
|
+
}
|
|
202
|
+
if (!serializers.some((s) => s.name === name)) {
|
|
203
|
+
console.error(formatError({ message: `No serializer found for lexicon "${name}". Available: ${serializers.map((s) => s.name).join(", ")}. List the lexicon in chant.config.ts.` }));
|
|
204
|
+
return 1;
|
|
205
|
+
}
|
|
206
|
+
targets.set(name, spec.slice(eq + 1));
|
|
207
|
+
}
|
|
208
|
+
const groups: { serializers: typeof serializers; output: string }[] = [...targets].map(([name, output]) => ({
|
|
209
|
+
serializers: serializers.filter((s) => s.name === name),
|
|
210
|
+
output,
|
|
211
|
+
}));
|
|
212
|
+
const rest = serializers.filter((s) => !targets.has(s.name));
|
|
213
|
+
if (args.output && rest.length > 0) groups.push({ serializers: rest, output: args.output });
|
|
214
|
+
|
|
215
|
+
const seenWarnings = new Set<string>();
|
|
216
|
+
let failed = false;
|
|
217
|
+
for (const group of groups) {
|
|
218
|
+
const { format, warning } = resolveBuildFormat(args.format, group.output);
|
|
219
|
+
if (warning) console.error(formatInfo(warning));
|
|
220
|
+
const result = await buildCommand({
|
|
221
|
+
path: args.path,
|
|
222
|
+
output: group.output,
|
|
223
|
+
format,
|
|
224
|
+
serializers: group.serializers,
|
|
225
|
+
plugins,
|
|
226
|
+
verbose: args.verbose,
|
|
227
|
+
env: args.env,
|
|
228
|
+
fold: args.fold,
|
|
229
|
+
sandbox: args.sandbox,
|
|
230
|
+
foldRank: args.foldRank,
|
|
231
|
+
foldRankCollapsedFile: args.foldRankCollapsedFile,
|
|
232
|
+
params: parseParamFlags(args.param),
|
|
233
|
+
paramsFile: args.paramsFile,
|
|
234
|
+
});
|
|
235
|
+
// The lexicons built by another group are not missing serializers.
|
|
236
|
+
const own = result.warnings.filter((w) => !w.includes("No serializer found for lexicon") && !seenWarnings.has(w));
|
|
237
|
+
for (const w of own) seenWarnings.add(w);
|
|
238
|
+
printWarnings(own);
|
|
239
|
+
printErrors(result.errors);
|
|
240
|
+
if (!result.success) failed = true;
|
|
241
|
+
}
|
|
242
|
+
return failed ? 1 : 0;
|
|
243
|
+
}
|
|
244
|
+
|
|
180
245
|
if (args.watch) {
|
|
181
246
|
const cleanup = buildCommandWatch({
|
|
182
247
|
path: args.path,
|