Do not require os in recipe manifest Platform blocks (#974) - #1160
Open
allanli4 wants to merge 1 commit into
Open
Do not require os in recipe manifest Platform blocks (#974)#1160allanli4 wants to merge 1 commit into
allanli4 wants to merge 1 commit into
Conversation
allanli4
force-pushed
the
dev/recipe-platform-os-optional
branch
from
July 29, 2026 01:02
4740f7b to
a068edf
Compare
The recipe reference documents os and architecture as optional, but manifest_selection() used the presence of the os key to gate the entire platform-match and lifecycle-selection body. A manifest omitting os was therefore skipped rather than matching any OS, so such a recipe failed artifact resolution, unit file generation and lifecycle execution. Treat an absent os as a wildcard, matching how an absent architecture is already handled. This also makes the architecture type check reachable when os is omitted.
allanli4
force-pushed
the
dev/recipe-platform-os-optional
branch
from
August 4, 2026 23:10
a068edf to
4677c29
Compare
allanli4
marked this pull request as ready for review
August 5, 2026 00:06
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Issue
#974 —
"ggdeploymentd: Do not require os and architecture fields in component recipes"
Reported by: @aws-kevinrickard
Problem
The AWS component recipe reference documents
osandarchitectureas optionalattributes of a manifest
Platformblock, but nucleus lite behaves as ifosisrequired. A manifest such as:
is silently treated as incompatible with the current platform. Expected
behaviour, per the reporter, is to match the non-lite nucleus and impose no
requirement when the fields are absent.
Scope — this fix is NECESSARY BUT NOT SUFFICIENT for the reported symptom
This change does not fix the failure the reporter observed. The error they
quoted —
doesn't claim platform {runtime=aws_nucleus_lite, os=linux, architecture=amd64} compatibility— is produced by cloud-side
ResolveComponentCandidatesin the AWS IoTGreengrass service. That matching logic is not in this repository, is not
reachable from this code, and is not reproducible on a developer host. Nothing in
this patch can change it. A reporter who applies this change and retries a cloud
deployment of a recipe with only
runtime: "*"will see the same error.What this change does fix is the on-device manifestation of the same
documented-contract violation: nucleus lite's own local platform matching also
treated
osas required, so a recipe omitting it failed artifact resolution,systemd unit-file generation, and lifecycle execution locally. That is a real,
independently observable defect against the same public documentation, and it is
the part that lives in this repository. Fixing it is a prerequisite for the
end-to-end behaviour the issue asks for — hence necessary — but the cloud-side
resolver must be addressed separately for the reporter's scenario to work.
The platform attributes nucleus lite sends to the cloud
(
modules/ggdeploymentd/src/deployment_handler.c:1152-1157) are unchanged; theyalready populate all three fields, so they are not the cause either.
Recommendation: do not close #974 on this change alone. Either keep it open
pending the service-side fix, or split the cloud-side half into its own tracking
item.
Acceptance criteria
All criteria are DERIVED — the issue states none explicitly.
Platformwith a lite-compatibleruntimebut noosis compatible on Linuxrecipe.c:399-411; testselect_manifest_platform_without_osPlatformwith noarchitectureimposes no architecture requirement (no regression)architecture.len == 0branch preserved byte-identical; testselect_manifest_platform_without_architecturePlatform: { runtime: "*" }(both omitted — the issue's exact input) is selected on Linuxselect_manifest_platform_without_os_or_architecture— assertsGG_ERR_OKand deep map equality of the selected manifestruntime; non-literuntime; explicit non-matchingos; explicit non-matchingarchitecture— all still skippedwithout_runtime,with_other_runtime,with_other_os,with_other_architectureos/architecturestill yieldsGG_ERR_INVALIDwith_invalid_os,with_invalid_architecture. See "Behaviour changes" — forarchitecturethis is a strictness increaseGG_TEST_DEFINEin the module's own#ifdef GG_SDK_TESTINGblock, run byctestasggl-recipe-inline-test. No new framework or targetRoot cause
manifest_selection()inmodules/ggl-recipe/src/recipe.ccarries a two-channelcontract: the return value answers "did I see malformed input?", while the
out-parameter answers "did this manifest match?".
GG_ERR_OKis thereforeoverloaded — it is returned both on a match and on a deliberate skip — so any
path that exits without writing the out-parameter is read by callers as "skip
this manifest". Skip is reached by omission rather than by decision.
At the base revision,
if (gg_map_get(platform, GG_STR("os"), &os_obj))(
recipe.c:400) used the presence of theoskey as the gate around the entireremaining compatibility-and-selection body (
recipe.c:401-484) — which containsevery statement that can assign the out-parameter. With
osabsent, the wholebody was skipped, the function returned
GG_ERR_OKhaving selected nothing, thecaller's error guard did not fire, and the post-loop
NULLcheck producedNo Manifest was found for linux(recipe.c:609) →GG_ERR_FAILURE.The comment at
recipe.c:398—// If OS is not provided then do nothing—described the code accurately and the requirement inaccurately: "impose no OS
requirement" was implemented as "do nothing", and in this function doing nothing
means being skipped. Tellingly,
architectureone level down was alreadyimplemented correctly (absent → zero-length buffer → accepted at
recipe.c:434),but only reachable when
oswas present.The prediction this hypothesis made, and it held: making an absent
osyielda wildcard instead of skipping the body, with everything downstream semantically
unchanged, makes the failure disappear. It did, on the first attempt.
Set aside, one line each:
platform_is_compatible()predicate (the alternative fix shapeproposed during localization). Rejected: it bundles a refactor with a bug fix,
which
docs/CONTRIBUTING.mdexplicitly asks contributors not to do, andmanifest_selection()already carries// TODO: Refactor it, so maintainershave scoped that separately. Its one correctness argument — the unreachable
architecturetype check — is delivered by this fix anyway. Offered as afollow-up below.
manifest_selection()never readsarchitecture.detail, though nucleus liteadvertises it. A real defect, but a different one — not folded in.
The fix
One file:
modules/ggl-recipe/src/recipe.c.The production change is four edits inside
manifest_selection():GgObject *os_obj;→GgObject *os_obj = NULL;so absence is representable.gg_map_get(... "os" ...)block now contains only the non-string typecheck; it no longer gates anything.
osdefaults toGG_STR("*"), i.e. "no OS requirement", appliedwith the same optional-fetch shape the adjacent
architectureblock alreadyuses.
recipe.c:332already doesGgBuffer arch_detail = GG_STR("");, so thisis established local style.
Why this is minimal: the wildcard default means the OS predicate itself needs
no edit at all. The runtime gate, the OS predicate, the architecture
fetch/validate/default, the architecture predicate, the
Lifecyclepick, theSelectionspick, and thealldefault are all byte-identical apart fromindentation —
git diff -wreduces the production change to exactly the fouritems above. The remainder of the diff is the unavoidable re-indent plus tests.
No public signature changes;
manifest_selectionisstatic, andmodules/ggl-recipe/include/ggl/recipe.his untouched.Blast radius of the bug being fixed — one root cause, four entry points:
modules/ggdeploymentd/src/deployment_handler.c:722ggl_get_recipe_artifacts_for_platformmodules/recipe2unit/src/unit_file_generator.c:499select_linux_lifecyclemodules/recipe-runner/src/runner.c:385select_linux_lifecyclemodules/ggl-docker-client/src/docker_artifact_cleanup.c:116,:260ggl_get_recipe_artifacts_for_platformDiff accounting, hunk by hunk
git diff --shortstat main..HEADreports +188/-69 on one file, which is a lotfor a validation relaxation.
CONTRIBUTINGrequires narrow diffs, so here isevery line accounted for. Ignoring whitespace the same diff is +137/-18, so
roughly 51 insertions and 51 deletions are whitespace-only.
GgObject *os_obj = NULL;, the wildcard defaultGgBuffer os = GG_STR("*");, theif (os_obj != NULL) { os = ... }guard, and moving the closing brace so the type check no longer gates the body. This is the entire semantic change.// If OS is not provided then do nothing→...it imposes no requirement. The old comment documented the bug; leaving it would mislead the next reader.if (gg_map_get(... "os" ...))wrapper eliminates one nesting level, so its 84-line body shifts left by 4 columns. Unavoidable: the body must become unconditional, and in C that means removing a brace level. Statement order and content are byte-identical —git diff -wshows nothing here.ColumnLimit: 80, and.clang-formatrequires joining them. Verified:clang-format --dry-run -Werrorpasses on the changed region, and would fail if these were left wrapped. Not discretionary reformatting — the repository's own formatter mandates it.GG_TEST_DEFINEcases plus three fixture macros. This is the oracle and the criterion coverage; see below.On the alternative that would have avoided group C entirely. The re-indent can
be dodged by keeping the wrapper
ifand making its condition always true —synthesise a default
GgObjectfor the absent case, then testos_obj != NULLinstead of the map lookup. That yields a ~7-line total diff. It was rejected:
it leaves a permanently-true conditional in the code, which is worse to read and
maintain than a correct nesting level, and
readability-*clang-tidy checks areactive. Trading permanent code quality for a one-time smaller diff is the wrong
trade; group C is whitespace-only and reviewable with
git diff -w.On the test volume. Groups A-D total ~23 non-whitespace lines; the tests are
the bulk of the diff. They are not optional — the oracle-first design makes the
failing-test-turned-green the entire basis of the claim, and each of the six
acceptance criteria needs a test that fails if the behaviour regresses. Three
fixture macros already cut roughly 90 lines of duplication versus writing the
recipe literals out longhand (the idiom used by
modules/ggdeploymentd/src/component_config.c). If a maintainer wants the setnarrowed, the two most trimmable are
select_manifest_platform_without_os(partially subsumed by the oracle) and
select_lifecycle_platform_without_os_or_architecture(covers the second entrypoint, whose assertion is the weakest) — but that trades regression coverage for
line count, so it is offered rather than assumed.
Note on "zero test files". This diff touches no file named
*test*, and thatis correct for this repository: module unit tests live inside the module's own
.cfile under#ifdef GG_SDK_TESTING, and rootCMakeLists.txt:377registerswith
add_testonly the${name}-inline-testtargets built from those blocks.Ten other modules follow this idiom. The
test_modules/*directories buildstandalone binaries that
ctestnever runs, so a test placed there would notexecute in CI and could not serve as an oracle. A filename-based "does this change
include tests?" check will report a false negative here.
Device verification on real hardware
Beyond the unit suite and CI, the change was proved end to end on a real device,
in both directions.
Host: EC2
i-0f9a24a9c5535efcc,c6i.xlarge, Ubuntu 24.04.4 (glibc 2.39, gcc 13),us-west-2. Reached over SSMsend-commandonly — zero inbound security-grouprules, no SSH key. Both
.debs were built on the device, and each build'sHEADwas asserted equal to the expected commit before the artifact was trusted.Component deployed, whose manifest
Platformomits bothosandarchitecture— the issue's exact reproduction:Driven with
ggl-cli deploy -r <recipes> -a <artifacts> -c com.example.NoOsPlatform=1.0.0,identically in both directions, with full device state reset in between
(
purge+rm -rf /var/lib/greengrass, verifiedresidual_units=0).a068edfab33c3682Nucleus version: 2.6.0-a068edfNucleus version: 2.6.0-b33c368GG_TEST_DEFINEGG_TEST_DEFINE/var/lib/greengrass/ggl.com.example.NoOsPlatform.serviceafter 5 sVerbatim rejection from the unpatched base build:
Health was gated per-unit on
ggl.core.ggconfigd.serviceandggl.core.ggdeploymentd.service, bothactivewithNRestarts=0in bothdirections. Worth flagging for anyone testing this runtime:
systemctl is-active greengrass-lite.targetreportedactivewhile six unitswere
failed, so the aggregate target is not a usable health signal. The sixfailures are all credential-dependent units (
iotcored,tesd,gg-fleet-statusdand their sockets), expected because the device wasdeliberately brought up with no cloud identity.
One finding here is worth a reviewer's attention. On a real deployment the
failure surfaces at
recipe.c:532—select_linux_lifecycle, reached viarecipe2unit— not atrecipe.c:609(select_linux_manifest) where the unit-testoracle fails. Both entry points share the single root cause, and this independently
justifies the
select_lifecycle_platform_without_os_or_architecturetest in thispatch:
select_linux_lifecycleis the path real deployments take and it had noregression test at all before this change.
All off-tree resources were torn down and the teardown verified by re-query:
instance
terminated, no tagged instances, no orphan volumes, security groupInvalidGroup.NotFound, IAM role and instance profileNoSuchEntity.Rebase onto current
mainRebased from the original base
b33c3682ontod3c2fa3f, picking up twomerged sibling PRs (#1161, #1162). No conflicts — those changes touch
modules/ggipcd/,modules/ggl-config-interpolation/, andmodules/recipe2unit/; this patch touches onlymodules/ggl-recipe/src/recipe.c.Verified rather than assumed: the merged file list and this patch's file list are
disjoint. The diff is byte-identical in shape after the rebase (+188/-69, one
file).
Everything below was re-verified against the new baseline, not carried over:
4677c296100% tests passed, 0 tests failed out of 11(11 not 10 — a sibling added a test target)clang-format19.1.7d3c2fa3fThe 4 warnings were attributed by building
d3c2fa3falone in a separateworktree: it produces the identical 4 at the identical lines —
modules/iotcored/src/tls.c:183and:184(-Wconversion), and_deps/unity-src/src/unity.c:2158and:2174('noreturn' function does return,vendored third-party). Zero warnings arise in the changed file. They only became
visible now because the rebase forced a fuller rebuild than the previous
incremental one.
The load-bearing test, and why it was strengthened
Device verification changed which test matters. On a real deployment the failure
surfaces at
recipe.c:532—select_linux_lifecycle, reached viarecipe2unit—not at
recipe.c:609(select_linux_manifest) where the unit oracle fails.select_linux_lifecycleis the path real deployments take, and it had noregression test at all before this change. That makes
select_lifecycle_platform_without_os_or_architecturethe load-bearing test, notthe incidental one.
It originally asserted only
selected.len > 0, which is too loose for a testcarrying that weight — it would accept a lifecycle rather than the right one.
It now asserts deep map equality:
TEST_LIFECYCLE_MAPwas factored out so the fixture and the expectation cannotdrift apart; the property asserted is "the returned lifecycle is the manifest's
lifecycle", which is exactly right and survives a future change to the fixture.
Mutation-tested, so it is not tautological. Changing only the expectation (not
the shared fixture macro) to a different literal map makes the test fail with
FAIL: Values were not equal; restoring it — file checksum verified identical —returns it to
PASS.GG_TEST_ASSERT_MAP_EQUALis a deep comparison, not alength check.
Verified against the non-lite nucleus
The issue asks for behaviour that matches the classic nucleus, so that claim was
checked against the reference implementation rather than inferred from the
documentation. The authoritative matcher is
PlatformHelper.isRequirementSatisfied/isAttributeSatisfiedinaws-greengrass-component-common,reached from the nucleus via
PlatformResolver.findBestMatch→RecipeLoader.loadFromFile.Platforminputoskey omittedplatformRequirement.entrySet(), i.e. only keys the recipe declares, so an omitted key is never testedarchitecturekey omittedos: "*"os: "all"os: ""(present, empty)^[a-zA-Z0-9]"simple label" guardos: "windows"on LinuxarchitectureSo the change this PR makes — an omitted
osimposes no requirement, while anexplicitly empty one does not match — is what the classic nucleus does. The
wildcard-for-absent choice was not merely a plausible reading of the docs.
Divergences that remain, all pre-existing and out of scope here:
Platformobject. Classic returnstrueoutright —// no platform is considered a wild-card. Lite still returnsGG_ERR_INVALID("Platform not provided"). Tracked separately; this also settles the open design
question there, in favour of "matches everything" rather than "skip as
nucleus-only".
architecture: "". Lite treats a present-but-empty value as a wildcard viaits
architecture.len == 0branch; classic rejects it by the same simple-labelguard that rejects
os: "". This is the one place lite is more permissive thanclassic, and it is untouched by this patch.
anyis a legacy wildcard in classic foros/architecture; lite accepts*andallbut notany./regex/templates are supported by classic and not by lite.runtimehas no special meaning in classic — it is matched like any otherattribute. Lite's rule that a manifest omitting
runtimeis nucleus-only andmust be skipped is a deliberate lite-specific divergence, and this patch leaves
it exactly as it was.
Behaviour changes worth accepting knowingly
Both follow from the fix being correct, and both are user-visible:
Manifests: [{runtime: "*"}, {runtime: "*", os: linux, architecture: amd64}]the first entry used to be skipped and the second selected; the first is now
selected. That is correct first-match semantics and matches classic nucleus,
but it changes which
Lifecycle/Artifactsblock a real deployment picks.Platform: {runtime: "*", architecture: 42}with noosused to be silentlyskipped, letting a later valid manifest win. Because the
architecturetypecheck was also nested inside the
osgate, de-nesting makes it reachable, sothis now returns
GG_ERR_INVALIDand fails the recipe. Consistent with how amalformed
osalready behaved, and it closes criterion 5, but it is astrictness increase beyond the reported defect.
Evidence
Oracle — the failing test that now passes:
Test:
modules/ggl-recipe/src/recipe.c::select_manifest_platform_without_os_or_architectureCommand:
ctest --test-dir <build> -R ggl-recipe-inline-test --output-on-failureBefore the patch, at base revision
b33c36822f2f5f9b8ea873954142bfa98dff7aaa:After the patch: passes. That symptom no longer occurs.
ggl-recipe-inline-testreports19 Tests 0 Failures 0 Ignored / OK.Reproduction was deterministic (1/1); the defect is a pure control-flow branch on
map-key presence, with no timing or I/O dependence.
Regression run — build and test:
cmake --build <build> -j$(nproc)ctest --test-dir <build> --output-on-failure100% tests passed, 0 tests failed out of 10nix flake check -Lsuite was clean at base, so there are none in the recorded command set. Two
environment-specific ones were found by extra checks and left alone; see below.
CI results on this branch — the checks have now actually run, so the earlier
"could not be verified locally" caveats are superseded. Run: CI
30412935626ona068edfa, conclusion success.formattingclang-tidyunit-testsbuild-clangbuild-musl-piiwyucmake-lintspellingeditorconfigshellcheckpackages-defaultnix flake check --no-buildCodeQL,Analyze (c-cpp / python / actions)uat-test(47-job matrix)All 64 checks on this PR pass. Zero failures.
Two things this settles that were previously only reasoned about:
clang-tidypasses. The concern wasreadability-function-cognitive-complexity(active in
.clang-tidy) against theNOLINTNEXTLINEthatmanifest_selection()still carries. The patch removes a nesting level, so the suppression remains
valid. No suppression was added, loosened, or moved to obtain this.
spellingpasses, so the new identifiers are acceptable tocspell.The one
formattingfailure, and the fix. First push (4740f7b9) failed with:Patch-induced, not pre-existing: CI on
mainat the base revisionb33c36822f2fis green forformatting. Cause: CI formats with clang-format19.1.7 (nix
clang-19.1.7), and after the de-indent that version prefersover the wrapped form. Fixed by applying exactly that, and nothing else — one
whitespace-only hunk, inside the region this patch already touches. Verified by
re-running clang-format 19.1.7 over the whole file (clean) and re-running the
build and full test suite before re-pushing.
Worth recording for future contributors: the version matters. An earlier local
check used clang-format 22.1.8, which disagreed with 19.1.7 in both
directions — it demanded a change 19.1.7 does not want in
is_recipe_variable_valid_three_part(an untouched, already-committed line, so"pre-existing violation" was an artifact of the wrong tool version, not a real
finding), and it accepted the
GG_LOGEwrapping that 19.1.7 rejects.uat-test— 47/47 pass (run30412935701, conclusionsuccess, ona068edfa). Worth noting how it got there, since it is a repository-wideconstraint any contributor will meet:
uat.ymlserialises UAT across the wholerepository:
One global slot, newer runs queue. Three
dev/*branches pushed within tenminutes cancelled each other's UAT runs, and this commit's run was initially
cancelled with 0 jobs executed. It was re-dispatched once, after confirming
the global group was empty, and then passed cleanly. It was not re-triggered in a
loop — that would cancel a sibling branch's pending run and add load to the
shared, rate-limited AWS account the concurrency group exists to protect.
No UAT job failed. For context, a sibling branch in the same batch observed an
environmental UAT failure mode unrelated to any patch (
misc/run_nucleusrunssystemd-tmpfiles --createunderset -eand exits 73 where/etcand/etc/defaultownership mismatch on the runner); it did not occur in this run.Also confirmed empirically: UAT is skipped entirely for fork PRs — the
list-testsjob is gated ongithub.event.pull_request.head.repo.full_name == 'aws-greengrass/aws-greengrass-lite',and the two earlier fork-based runs show conclusion
skipped. Had this work stayedon a fork, all 47 UAT jobs would have been silently skipped and the PR would have
looked green while never being exercised. That is why it is on an
origindev/branch.
Local verification (retained, since it is what drove the patch): build clean
with 0 errors and 0 warnings;
ctest100% tests passed, 0 tests failed out of 10;ggl-recipe-inline-test19 Tests 0 Failures. A clean local build with-D ENABLE_WERROR=1additionally surfaces 2 errors —'noreturn' function does returnin_deps/unity-src/src/unity.c, vendored third-party Unity source,not repository code. Proven pre-existing by re-running the identical build with
the patch stashed at base revision: same 2 errors, same 2 lines. It is
gcc-specific and invisible upstream because CI's
-Werrorchecks use clang/muslwhile
unit-testssetsBUILD_TESTINGwithoutENABLE_WERROR, so thatcombination never occurs in CI. No repository source file emitted a warning under
-Werror, including the patched file.Reviewer verdict
An independent critic reviewed the diff, the acceptance criteria, the rejected
alternative hypothesis, and the test results, without access to the patch
author's self-assessment.
concurAdvisory notes, surfaced rather than acted on. They did not block delivery by
design — the reviewer is advisory, and it is the test run that verified the patch.
An explicitly empty
os:is still skipped. The reviewer reported that theYAML decoder (
modules/ggl-yaml/src/yaml_decode.c) renders every scalar as abuffer, so
os:with no value becomes a zero-length buffer, fails the OSpredicate, and still produces the exact error from this issue — while
architecture:left empty is honoured as "no requirement" — and suggestedaccepting
os.len == 0for consistency.This note has since been resolved against it, and the current behaviour is
correct. The classic nucleus also rejects an explicitly empty
os: ""(see"Verified against the non-lite nucleus" above — it fails the
^[a-zA-Z0-9]simple-label guard). Treating absent as a wildcard while treating empty as a
non-match is exactly the classic semantics, so no change was made here. The
reviewer's underlying observation was still valuable, but it points the other
way: it is lite's
architecture: ""→ wildcard branch that diverges fromclassic, not its handling of
os. That branch is pre-existing and deliberatelyuntouched by this patch.
Add a two-manifest test to pin skip-versus-abort. The four negative tests
use single-manifest fixtures and
GG_TEST_ASSERT_BAD(which only asserts!= GG_ERR_OK), so nothing asserts that a non-matching manifest is skippedwhile the loop continues rather than aborting the whole recipe. A
[non-matching, matching]fixture would close that and lock in first-matchprecedence.
Silent skips have no diagnostics. The runtime gate logs why it skipped; the
osmismatch and the architecture mismatch say nothing, and the architecturemismatch exits by falling through ~50 lines to the terminal
return GG_ERR_OK.A
GG_LOGDon each skip would have made this issue self-diagnosing.The fall-through shape that caused this bug is still present. Converting
the implicit fall-through into an explicit
else { return GG_ERR_OK; }wouldremove it at near-zero diff cost — without adopting the full predicate
extraction.
Nits: assertion style is mixed across the new tests
(
GG_TEST_ASSERT_BADfor platform-mismatch negatives, exactTEST_ASSERT_EQUAL_INT(GG_ERR_INVALID, ...)for type negatives — the exact formis better); and encoding "no
osrequirement" as the sentinel"*"makes thefix depend on
"*"remaining in the predicate, where abool os_specifiedwould state the intent directly (a deliberate trade for a smaller diff).
The reviewer's first nit — that
select_lifecycle_platform_without_os_or_architectureasserted onlyselected.len > 0— has been addressed, see below.The reviewer was barred from modifying the tree, so it could not mutation-test
the new assertions; its discrimination claims rest on control-flow reasoning
plus per-test log paths, not on observed red-on-mutation.
For the human reviewer
Focus here:
os:question is now settled, not open. An earlier draft of thisPR left it for a maintainer to decide. Checking the classic nucleus resolved it:
classic rejects
os: ""too, so lite's current behaviour is right and no changewas made. See "Verified against the non-lite nucleus".
hard-failing malformed recipe — are correct consequences of the fix, but they
affect real deployments. Please accept them knowingly.
split into its own change.
clang-tidy,unit-tests, and the full 47-jobuat-testmatrix. No suppression was added, no test weakened, and no checknarrowed to obtain green.
CONTRIBUTINGstep 3's "confirm the CI UAT run on yourdev/branch is green" is satisfied.the device verification section. The unpatched base build rejects the same recipe
that the patched build accepts, so the device green is evidence rather than a
scenario that would have passed anyway.
docs/CONTRIBUTING.md's AWS-internal path isfollowed — a
dev/-prefixed branch (dev/recipe-platform-os-optional) onorigin, per "Create a branch offmainprefixed withdev/".Branch:
dev/recipe-platform-os-optionalonorigin— commit4677c296,rebased onto
mainatd3c2fa3f. Originally developed againstb33c3682, whichremains the base revision cited in the device-verification and oracle sections
below because that is the revision those comparisons were run against.
Relationship to #1158
#1158 carries this identical commit from a fork branch. It is superseded by this
PR and is being left open for the operator to close; nothing in this run opened,
closed, or modified it. The fork branch
allanli4:wt/LiAllanPersonalAICapabilities-t6was deliberately left in place,because deleting it would auto-close #1158.
Why this PR rather than that one:
docs/CONTRIBUTING.mdputs AWS-internal work ona
dev/-prefixed branch onorigin, and.github/workflows/uat.ymltriggers UATon
dev/*branches — so theorigin+dev/route is what gets this changeactually exercised in CI. A fork-based PR does not.