Skip to content

Commit 5c18e3f

Browse files
committed
ci: scan only UI artifacts in dry runs
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
1 parent b0279a0 commit 5c18e3f

6 files changed

Lines changed: 151 additions & 47 deletions

File tree

‎.github/workflows/_build.yml‎

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Reusable: build binaries (standard + UI) on all platforms
1+
# Reusable: build release binaries on all supported platforms
22
name: Build
33

44
on:
@@ -12,6 +12,10 @@ on:
1212
description: 'Generate build provenance attestations for release artifacts'
1313
type: boolean
1414
default: false
15+
ui_only:
16+
description: 'Build only the UI runtime archives'
17+
type: boolean
18+
default: false
1519

1620
permissions:
1721
contents: read
@@ -64,6 +68,7 @@ jobs:
6468
node-version: "22"
6569

6670
- name: Build standard binary
71+
if: ${{ !inputs.ui_only }}
6772
env:
6873
VERSION: ${{ inputs.version }}
6974
CC: ${{ matrix.cc }}
@@ -75,17 +80,16 @@ jobs:
7580
scripts/build.sh "CC=$CC" "CXX=$CXX"
7681
fi
7782
78-
- name: Ad-hoc sign macOS binary
79-
if: startsWith(matrix.os, 'macos')
83+
- name: Ad-hoc sign macOS standard binary
84+
if: ${{ !inputs.ui_only && startsWith(matrix.os, 'macos') }}
8085
run: codesign --sign - --force build/c/codebase-memory-mcp
8186

82-
# Archive layout/name live in the ONE canonical script the local
83-
# artifact-flow smoke lane also runs (venue-parity contract).
8487
- name: Archive standard binary (canonical package-release.sh)
88+
if: ${{ !inputs.ui_only }}
8589
run: scripts/package-release.sh ${{ matrix.goos }} ${{ matrix.goarch }}
8690

8791
- name: Attest standard binary provenance
88-
if: ${{ inputs.attest }}
92+
if: ${{ inputs.attest && !inputs.ui_only }}
8993
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
9094
with:
9195
subject-path: codebase-memory-mcp-${{ matrix.goos }}-${{ matrix.goarch }}.tar.gz
@@ -110,6 +114,8 @@ jobs:
110114
if: matrix.goos == 'linux' && matrix.goarch == 'amd64'
111115
run: scripts/security-ui.sh
112116

117+
# Archive layout/name live in the ONE canonical script the local
118+
# artifact-flow smoke lane also runs (venue-parity contract).
113119
- name: Archive UI binary (canonical package-release.sh)
114120
run: scripts/package-release.sh ${{ matrix.goos }} ${{ matrix.goarch }} --variant ui
115121

@@ -145,6 +151,7 @@ jobs:
145151
node-version: "22"
146152

147153
- name: Build standard binary
154+
if: ${{ !inputs.ui_only }}
148155
shell: msys2 {0}
149156
env:
150157
VERSION: ${{ inputs.version }}
@@ -155,14 +162,13 @@ jobs:
155162
scripts/build.sh CC=clang CXX=clang++
156163
fi
157164
158-
# Archive layout/name live in the ONE canonical script the local
159-
# artifact-flow smoke lane also runs (venue-parity contract).
160165
- name: Archive standard binary (canonical package-release.sh)
166+
if: ${{ !inputs.ui_only }}
161167
shell: msys2 {0}
162168
run: scripts/package-release.sh windows amd64 CC=clang CXX=clang++
163169

164170
- name: Attest standard binary provenance
165-
if: ${{ inputs.attest }}
171+
if: ${{ inputs.attest && !inputs.ui_only }}
166172
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
167173
with:
168174
subject-path: codebase-memory-mcp-windows-amd64.zip
@@ -178,6 +184,8 @@ jobs:
178184
scripts/build.sh --with-ui CC=clang CXX=clang++
179185
fi
180186
187+
# Archive layout/name live in the ONE canonical script the local
188+
# artifact-flow smoke lane also runs (venue-parity contract).
181189
- name: Archive UI binary (canonical package-release.sh)
182190
shell: msys2 {0}
183191
run: scripts/package-release.sh windows amd64 --variant ui CC=clang CXX=clang++
@@ -218,6 +226,7 @@ jobs:
218226
node-version: "22"
219227

220228
- name: Build standard binary
229+
if: ${{ !inputs.ui_only }}
221230
shell: msys2 {0}
222231
env:
223232
VERSION: ${{ inputs.version }}
@@ -229,11 +238,12 @@ jobs:
229238
fi
230239
231240
- name: Archive standard binary (canonical package-release.sh)
241+
if: ${{ !inputs.ui_only }}
232242
shell: msys2 {0}
233243
run: scripts/package-release.sh windows arm64 CC=clang CXX=clang++
234244

235245
- name: Attest standard binary provenance
236-
if: ${{ inputs.attest }}
246+
if: ${{ inputs.attest && !inputs.ui_only }}
237247
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
238248
with:
239249
subject-path: codebase-memory-mcp-windows-arm64.zip
@@ -288,6 +298,7 @@ jobs:
288298
node-version: "22"
289299

290300
- name: Build standard binary (static)
301+
if: ${{ !inputs.ui_only }}
291302
env:
292303
VERSION: ${{ inputs.version }}
293304
run: |
@@ -297,16 +308,18 @@ jobs:
297308
scripts/build.sh CC=gcc CXX=g++ STATIC=1
298309
fi
299310
300-
- name: Verify static linking
311+
- name: Verify standard static linking
312+
if: ${{ !inputs.ui_only }}
301313
run: |
302314
file build/c/codebase-memory-mcp
303315
ldd build/c/codebase-memory-mcp 2>&1 | grep -q "not a dynamic executable" || ldd build/c/codebase-memory-mcp 2>&1 | grep -q "statically linked"
304316
305317
- name: Archive standard binary (canonical package-release.sh)
318+
if: ${{ !inputs.ui_only }}
306319
run: scripts/package-release.sh linux ${{ matrix.arch }}-portable
307320

308321
- name: Attest standard binary provenance
309-
if: ${{ inputs.attest }}
322+
if: ${{ inputs.attest && !inputs.ui_only }}
310323
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
311324
with:
312325
subject-path: codebase-memory-mcp-linux-${{ matrix.arch }}-portable.tar.gz
@@ -321,6 +334,11 @@ jobs:
321334
scripts/build.sh --with-ui CC=gcc CXX=g++ STATIC=1
322335
fi
323336
337+
- name: Verify UI static linking
338+
run: |
339+
file build/c/codebase-memory-mcp
340+
ldd build/c/codebase-memory-mcp 2>&1 | grep -q "not a dynamic executable" || ldd build/c/codebase-memory-mcp 2>&1 | grep -q "statically linked"
341+
324342
- name: Archive UI binary (canonical package-release.sh)
325343
run: scripts/package-release.sh linux ${{ matrix.arch }}-portable --variant ui
326344

‎.github/workflows/_smoke.yml‎

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Reusable: smoke test every binary (standard + UI, all platforms)
1+
# Reusable: smoke test shipped release binaries on all platforms
22
name: Smoke
33

44
on:
@@ -8,6 +8,10 @@ on:
88
description: 'Smoke the shipped binaries on the broad platform matrix (extra OS versions) instead of the core set'
99
type: boolean
1010
default: false
11+
ui_only:
12+
description: 'Smoke only the UI runtime archives'
13+
type: boolean
14+
default: false
1115

1216
permissions:
1317
contents: read
@@ -30,6 +34,7 @@ jobs:
3034
id: set
3135
env:
3236
BROAD: ${{ inputs.broad_platforms }}
37+
UI_ONLY: ${{ inputs.ui_only }}
3338
run: |
3439
CORE_UNIX='[
3540
{"os":"ubuntu-latest","goos":"linux","goarch":"amd64"},
@@ -72,13 +77,14 @@ jobs:
7277
WIN=$(jq -cn --argjson a "$CORE_WIN" '$a')
7378
PORTABLE=$(jq -cn --argjson a "$CORE_PORTABLE" '$a')
7479
fi
75-
# Expand each OS entry over both variants into a FLAT include list so
76-
# every {os,variant} runs as its own job. A {variant:[...],include:$LIST}
77-
# matrix collapses under GitHub's include-merge (later os entries
78-
# overwrite the variant combos last-wins), which silently dropped every
79-
# OS except the last — e.g. only windows-11-arm ran, never windows-latest.
80-
# Building the cartesian explicitly avoids that.
81-
VARIANTS='["standard","ui"]'
80+
# Keep the variant dimension explicit because the smoke wrappers use it
81+
# to require and validate the UI pack. VT-focused dry runs select only
82+
# UI; release compatibility retains both variants until migration.
83+
if [ "$UI_ONLY" = "true" ]; then
84+
VARIANTS='["ui"]'
85+
else
86+
VARIANTS='["standard","ui"]'
87+
fi
8288
UNIX_M=$(jq -cn --argjson a "$UNIX" --argjson v "$VARIANTS" '{include:[$a[] as $o | $v[] as $t | ($o + {variant:$t})]}')
8389
WIN_M=$(jq -cn --argjson a "$WIN" --argjson v "$VARIANTS" '{include:[$a[] as $o | $v[] as $t | ($o + {variant:$t})]}')
8490
PORTABLE_M=$(jq -cn --argjson a "$PORTABLE" --argjson v "$VARIANTS" '{include:[$a[] as $o | $v[] as $t | ($o + {variant:$t})]}')
@@ -133,8 +139,8 @@ jobs:
133139
scripts/security-install.sh "$BIN"
134140
scripts/security-network.sh "$BIN"
135141
136-
- name: MCP robustness test (linux-amd64 standard only)
137-
if: matrix.variant == 'standard' && matrix.goos == 'linux' && matrix.goarch == 'amd64'
142+
- name: MCP robustness test (selected linux-amd64 runtime)
143+
if: matrix.goos == 'linux' && matrix.goarch == 'amd64' && (inputs.ui_only || matrix.variant == 'standard')
138144
run: |
139145
BIN="$RUNNER_TEMP/cbm-artifact/codebase-memory-mcp"
140146
scripts/security-fuzz.sh "$BIN"

‎.github/workflows/dry-run.yml‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ jobs:
9191
uses: ./.github/workflows/_build.yml
9292
with:
9393
attest: false
94+
ui_only: true
9495

9596
# ── Smoke test every binary ────────────────────────────────────
9697
# Run unless builds were skipped or a build leg FAILED. Every build leg
@@ -102,6 +103,7 @@ jobs:
102103
uses: ./.github/workflows/_smoke.yml
103104
with:
104105
broad_platforms: true
106+
ui_only: true
105107

106108
# ── Soak tests (optional, parallel with smoke) ────────────────
107109
soak:
@@ -151,10 +153,11 @@ jobs:
151153
- name: Extract archive artifacts
152154
run: |
153155
scripts/ci/extract-release-archives.sh assets binaries \
154-
--expect-archives=16 \
155-
--expect-binaries=16 \
156+
--archive-scope=ui \
157+
--expect-archives=8 \
158+
--expect-binaries=8 \
156159
--expect-packs=8 \
157-
--expect-runtime-files=64
160+
--expect-runtime-files=32
158161
159162
- name: VirusTotal scan
160163
uses: crazy-max/ghaction-virustotal@936d8c5c00afe97d3d9a1af26d017cfdf26800a2 # v5.0.0

‎scripts/ci/extract-release-archives.sh‎

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#
55
# Usage:
66
# extract-release-archives.sh <archive-dir> <output-dir> \
7-
# [--expect-archives=N] [--expect-binaries=N] [--expect-packs=N] \
7+
# [--archive-scope=all|ui] [--expect-archives=N] [--expect-binaries=N] [--expect-packs=N] \
88
# [--expect-runtime-files=N]
99
#
1010
# The output directory is published as one atomic bundle:
@@ -18,7 +18,7 @@
1818
set -euo pipefail
1919

2020
if [ "$#" -lt 2 ]; then
21-
echo "Usage: $0 <archive-dir> <output-dir> [--expect-archives=N] [--expect-binaries=N] [--expect-packs=N] [--expect-runtime-files=N]" >&2
21+
echo "Usage: $0 <archive-dir> <output-dir> [--archive-scope=all|ui] [--expect-archives=N] [--expect-binaries=N] [--expect-packs=N] [--expect-runtime-files=N]" >&2
2222
exit 2
2323
fi
2424

@@ -70,18 +70,21 @@ UNIX_TARGETS = (
7070
"linux-arm64-portable",
7171
)
7272
WINDOWS_TARGETS = ("windows-amd64", "windows-arm64")
73-
CANONICAL_ARCHIVES = frozenset(
73+
UI_ARCHIVES = frozenset(
7474
[
75-
f"codebase-memory-mcp{suffix}-{target}.tar.gz"
76-
for suffix in ("", "-ui")
75+
f"codebase-memory-mcp-ui-{target}.tar.gz"
7776
for target in UNIX_TARGETS
7877
]
7978
+ [
80-
f"codebase-memory-mcp{suffix}-{target}.zip"
81-
for suffix in ("", "-ui")
79+
f"codebase-memory-mcp-ui-{target}.zip"
8280
for target in WINDOWS_TARGETS
8381
]
8482
)
83+
STANDARD_ARCHIVES = frozenset(
84+
[f"codebase-memory-mcp-{target}.tar.gz" for target in UNIX_TARGETS]
85+
+ [f"codebase-memory-mcp-{target}.zip" for target in WINDOWS_TARGETS]
86+
)
87+
CANONICAL_ARCHIVES = UI_ARCHIVES | STANDARD_ARCHIVES
8588
PACK_NAME = re.compile(r"cbm-ui-([0-9a-f]{64})\.pack\Z")
8689
SAFE_LABEL = re.compile(r"[^A-Za-z0-9._-]+")
8790
SAFE_ASSET_PATH = re.compile(rb"\A[A-Za-z0-9._/-]+\Z")
@@ -290,13 +293,22 @@ class ObjectStore:
290293
291294
def parse_arguments(
292295
argv: Sequence[str],
293-
) -> Tuple[pathlib.Path, pathlib.Path, Dict[str, Optional[int]]]:
296+
) -> Tuple[pathlib.Path, pathlib.Path, Dict[str, Optional[int]], str]:
294297
archive_dir = pathlib.Path(argv[1]).absolute()
295298
output_dir = pathlib.Path(argv[2]).absolute()
296299
expected: Dict[str, Optional[int]] = {value: None for value in COUNT_OPTIONS.values()}
297300
seen: set[str] = set()
301+
archive_scope = "all"
298302
for argument in argv[3:]:
299303
option, separator, raw_value = argument.partition("=")
304+
if separator and option == "--archive-scope":
305+
if "archive_scope" in seen:
306+
raise ContractError(f"duplicate option: {option}")
307+
if raw_value not in {"all", "ui"}:
308+
raise ContractError("--archive-scope must be all or ui")
309+
archive_scope = raw_value
310+
seen.add("archive_scope")
311+
continue
300312
if not separator or option not in COUNT_OPTIONS:
301313
raise ContractError(f"unknown option: {argument}")
302314
key = COUNT_OPTIONS[option]
@@ -306,7 +318,7 @@ def parse_arguments(
306318
raise ContractError(f"{option} requires a non-negative integer")
307319
expected[key] = int(raw_value)
308320
seen.add(key)
309-
return archive_dir, output_dir, expected
321+
return archive_dir, output_dir, expected, archive_scope
310322
311323
312324
def validate_namespace(archive_name: str, names: Iterable[str]) -> Tuple[str, Dict[str, str]]:
@@ -674,7 +686,7 @@ def write_tsv(
674686
675687
676688
def main(argv: Sequence[str]) -> None:
677-
archive_dir, output_dir, expected = parse_arguments(argv)
689+
archive_dir, output_dir, expected, archive_scope = parse_arguments(argv)
678690
if not archive_dir.is_dir() or archive_dir.is_symlink():
679691
raise ContractError(f"archive directory is not a regular directory: {archive_dir}")
680692
if output_dir.name in ("", ".", ".."):
@@ -687,11 +699,13 @@ def main(argv: Sequence[str]) -> None:
687699
688700
archive_paths = sorted(archive_dir.iterdir(), key=lambda candidate: candidate.name)
689701
actual_names = {path.name for path in archive_paths}
690-
if actual_names != CANONICAL_ARCHIVES or len(archive_paths) != len(CANONICAL_ARCHIVES):
691-
missing = sorted(CANONICAL_ARCHIVES - actual_names)
692-
unexpected = sorted(actual_names - CANONICAL_ARCHIVES)
702+
expected_names = UI_ARCHIVES if archive_scope == "ui" else CANONICAL_ARCHIVES
703+
if actual_names != expected_names or len(archive_paths) != len(expected_names):
704+
missing = sorted(expected_names - actual_names)
705+
unexpected = sorted(actual_names - expected_names)
693706
raise ContractError(
694-
f"archive namespace mismatch: expected exact canonical 16; missing={missing}, unexpected={unexpected}"
707+
f"archive namespace mismatch: expected exact canonical {len(expected_names)} "
708+
f"({archive_scope}); missing={missing}, unexpected={unexpected}"
695709
)
696710
for path in archive_paths:
697711
mode = path.lstat().st_mode

0 commit comments

Comments
 (0)