Skip to content

fix: prevent builtin service config loss on dependency graph damage - #1824

Merged
aws-kevinrickard merged 7 commits into
aws-greengrass:mainfrom
aws-kevinrickard:v228-dependency-default-flag
Jul 20, 2026
Merged

fix: prevent builtin service config loss on dependency graph damage#1824
aws-kevinrickard merged 7 commits into
aws-greengrass:mainfrom
aws-kevinrickard:v228-dependency-default-flag

Conversation

@aws-kevinrickard

@aws-kevinrickard aws-kevinrickard commented Jul 16, 2026

Copy link
Copy Markdown
Member

Issue #, if available: n/a

Description of changes:

Defense-in-depth fix for a silent config-loss failure chain, as five small commits:

  1. Prevent (GreengrassService.setupDependencies): preserve the existing isDefault flag when re-adding a dependency listed in the dependencies topic, instead of downgrading it to false. Default (builtin) dependencies can no longer be evicted by a topic update.
  2. Tolerate (DeploymentActivator): key builtin merge protection on static builtin identity (new KernelLifecycle.AUTOSTART_BUILTIN_SERVICE_NAMES) unioned with the live dependency graph, and add the same protection to the rollback merge behavior, which previously had none. Even with a damaged dependency graph, deployments and rollbacks can no longer delete builtin services' config.
  3. Heal (KernelConfigResolver.getMainConfig): union the static autostart builtin names (name:HARD) into main's resolved dependencies, so a deployment on an already-damaged device repairs the dependency graph instead of perpetuating the damage until reboot.
  4. Heal fully (GreengrassService.setupDependencies): when main re-adds a statically known autostart builtin that is not currently a default dependency, restore launch-equivalent default protection (isDefault=true). Applies only to main — other services declaring builtin names keep normal removable-dependency behavior.
  5. Refactor: shared helper for the builtin protection name set, null-annotation guard, test import order.

Why is this change necessary:

The autostart builtin services (DeploymentService, FleetStatusService, TelemetryAgent, UpdateSystemPolicyService — the builtins with autostart=true) are injected into main's in-memory dependency map at every launch as default dependencies, protected from eviction by topic updates. That protection was fragile, and everything downstream keyed off the same mutable runtime state:

  1. On a fresh device the services/main/dependencies topic contains only the nucleus; the builtins exist only in the in-memory map.
  2. The first deployment writes the topic with the full resolved list, and the subscriber re-added every entry with isDefault=false — silently erasing the launch protection.
  3. If that first deployment fails after activation and rolls back, the rollback force-replays the pre-deployment snapshot, restoring the topic to nucleus-only — and the subscriber evicts every builtin from the dependency graph while the services keep running.
  4. Builtin merge protection was keyed on kernel.orderedDependencies(), so the next deployment deleted the evicted builtins' entire config subtrees (thing-group membership state, fleet-status sequence number). The running services kept writing to the detached config nodes, so nothing looked wrong.
  5. Every tlog truncation rewrites the file from the attached tree, omitting the detached subtrees. At the next restart the device boots from a valid config missing its thing-group state; a subsequent deployment then treats installed components as orphaned group data and removes them.

Observed in the field as: fleet-status sequence number resetting to 0 and fleetConfigArns becoming empty after a routine restart, followed by a startup-time local deployment removing the customer's components.

TokenExchangeService and DockerApplicationManager are builtins but not autostart: they are not launch-injected and not in the static set; they remain covered by the graph-derived protection as before.

How was this change tested:

  • Updated or added new unit tests.
  • Updated or added new integration tests.
  • Updated or added new end-to-end tests.
  • If my code makes a remote network call, it was tested with a proxy.

Each behavioral commit adds a regression test verified in both polarities (fails without the fix, passes with it):

  • SetupDependencyTest: a default dependency survives a topic update omitting it (real config + subscriber path); a builtin re-added to main gets launch-equivalent protection, while a non-main service declaring the same name keeps removable behavior.
  • DefaultActivatorTest: with no builtins in orderedDependencies(), a deployment merge retains builtin config (and still removes absent non-builtins); a rollback whose snapshot lacks builtin config retains it (and still discards absent non-builtins).
  • KernelConfigResolverTest: resolve() with an empty main dependency map still emits every autostart builtin as name:HARD.

Healthy-graph behavior is unchanged: all pre-existing tests in the touched areas pass unmodified (KernelConfigResolverTest, DefaultActivatorTest, KernelUpdateActivatorTest, DeploymentActivatorFactoryTest, SetupDependencyTest, GreengrassServiceTest, KernelTest, KernelLifecycleTest, LifecycleTest, MergeTest), with checkstyle/PMD active. The full failure chain was additionally reproduced end-to-end against unmodified v2.10.2 config/lifecycle classes; re-running that reproduction against this patched build confirms the chain breaks at step 2 (no flag erasure, no eviction, no config deletion, all state survives restart).

Any additional information or context required to review the change:

  • Static set = @ImplementsService(autostart=true) builtins — the same criterion launch() uses — so on a healthy device the unions add nothing. Protecting a name absent from a device's graph is harmless (MERGE only retains existing config), and statically added names always resolve since autostart builtins are compiled into the nucleus.
  • One deliberate rollback behavior change: nodes newly created under a builtin during the failed deployment are retained rather than discarded; snapshot values are still restored exactly as before.
  • createRollbackMergeBehavior widened from private to protected for testability, matching createDeploymentMergeBehavior.
  • Related PRs from the same investigation: fix: local deployments preserve running components across bookkeeping loss #1819 (regression tests for the cleanupGroupData one-way ratchet that amplifies this loss into component removal) and fix: Use USE_LONG_FOR_INTS to prevent unnecessary config.tlog writes #1820 (fix for the LogManager-driven config churn that drove near-constant tlog truncation, shrinking the loss-exposure window). This PR supersedes test: builtin service dependencies #1823 (characterization tests demonstrating the pre-fix behavior, by @saranyailla): its two bug demonstrations are covered here as inverted regression tests, and its two baseline invariant tests are incorporated (with attribution) in the final test commit.

Documentation Checklist:

  • Updated the README if applicable.

Compatibility Checklist:

  • I confirm that the change is backwards compatible.
  • Any modification or deletion of public interfaces does not impact other plugin components.
  • For external library version updates, I have reviewed its change logs and Nucleus does not consume
    any deprecated method or type.

Refer to Compatibility Guidelines for more information.

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.


"Simple" Explanation of the Situation:

Background: the builtin services

The nucleus runs four internal "builtin" services compiled into it —
DeploymentService (processes deployments), FleetStatusService
(reports device status to the cloud), TelemetryAgent, and
UpdateSystemPolicyService. They are not deployed as components;
instead, at every boot the nucleus adds them to its in-memory service
dependency graph with a "shield flag" that exists only in memory
(🛡️ below) — nothing about them is persisted to disk.

They matter here for two reasons:

  1. Their config holds critical device state. DeploymentService's config
    records which thing groups this device belongs to and which components
    each group wants running. If that record is lost, the next deployment
    treats those components as orphaned and removes them.

  2. Their protection is fragile. Two things guard them — the shield
    flag
    (🛡️), a memory-only marker guarding their place in the graph,
    and the protection list, built fresh at each deployment to guard
    their config during the merge. Both are computed from current runtime
    state
    rather than from the fixed builtin list, and breaking the first
    silently disables the second:

    shield flag erased → builtin evicted from graph
                               │
      protection list ("who's in the graph right now?") ── empty
                               ▼
             next deployment deletes the builtin's config
    

Background: deployments — the merge, and cloud vs local

Each deployment applies its configuration by merging it over the
existing config tree ("the merge" below). Two deployment types matter:
cloud (thing-group) deployments carry the target group's full
component list from the cloud; local deployments (submitted on the
device itself — some fleets run a component that auto-submits one at
every startup) carry only their own changes, and preserve every other
group's components solely from the device's local records. (Both
types fetch group membership from the cloud — lab-verified — but
membership alone cannot rebuild a missing local record of a group's
components.) That difference decides 💥 vs ✅ in the scenarios below.

Background: state - kept in three places

  • GRAPH — main's in-memory dependency list (🛡️ = protected/default entry)
  • MEM — in-memory config tree (DeploymentService's group state);
    attached/DETACHED = whether the subtree is still reachable from the
    config root (see primer below)
  • DISK — config.tlog (what survives a restart); "deps topic" = the
    persisted copy of main's dependency list stored inside it (a "topic" is
    a node in the nucleus config tree — Greengrass jargon, not MQTT). The
    file = last periodic full rewrite (a root walk, every ~21 min on this
    device) plus individual appends since — writes to detached nodes still
    append, but each rewrite discards them and omits detached data

Background: attached vs detached config

        ATTACHED (normal)                DETACHED (after a deployment
                                          merge deletes the subtree)

             root                             root
              │                                │
           services                         services ──── LogManager ...
        ┌─────┴────────┐
 DeploymentService   LogManager     DeploymentService ◄── still held by the
        │                                  │              running service's
   GroupToRootComponents              GroupToRootComponents   own pointer
        │                                  │
   thinggroup/G → components          thinggroup/G → components

 reachable from root ✓               unreachable from root ✗
 → saved by every disk rewrite       → alive and fully usable in memory,
                                       invisible to every disk rewrite

Like a reference to a linked list item which is no longer in the list:
the running service keeps reading and writing its subtree through its own
pointer — writes even still append lines to config.tlog — but anything that
persists config by walking the tree from the root (truncation rewrites,
effectiveConfig.yaml) no longer sees it. The subtree evaporates for good
when the process exits.


Scenario A — the failure path (the incident)

① FIRST BOOT (fresh device)
   GRAPH: builtins 🛡️  (injected at launch, memory-only)
   MEM:   DeploymentService config attached, no group state yet
   DISK:  deps topic = [Nucleus]           ← builtins never written to disk

② FIRST DEPLOYMENT FAILS after activation → ROLLBACK          [enters W1]
   forward merge wrote deps topic incl. builtins → shield flags erased
   (🛡️ → ✗): whenever the deps topic changes, the graph is rebuilt from
   it, and every listed service is re-registered as an ordinary
   UNSHIELDED entry — re-registration overwrites the boot-granted flag
   rollback restores deps topic = [Nucleus] → builtins EVICTED from graph
   (not listed, and no longer shielded)
   GRAPH: builtins GONE (services still running)
   MEM:   config still attached & complete
   DISK:  still complete

③ NEXT DEPLOYMENT SUCCEEDS                                    [enters W2]
   the protection list is built from the damaged graph → comes up
   empty → the merge deletes the builtins' config
   GRAPH: builtins still gone
   MEM:   group state alive but DETACHED (services hold old refs)
   DISK:  group state alive immediately following the deployment (via
          appends), but purged at the next tlog truncation (~21 min) —
          truncation rewrites the file from ATTACHED nodes only — and
          it stays gone, since only deployments write group state again
   ...device runs normally like this for days/weeks — even processing
   local deployments safely, since the group data is still alive in
   detached memory (cleanup finds the membership and keeps everything);
   nothing surfaces until a restart...

④ RESTART (e.g. operator systemctl restart)
   GRAPH: builtins 🛡️ again (boot re-injects — graph heals)
   MEM:   rebuilt from disk → group state MISSING
   DISK:  unchanged (missing group state)

⑤ LOCAL DEPLOYMENT at startup (from a component that auto-submits
   one at every boot — common in managed fleets, incl. this one)
   no local record of the group's components exists → resolution has
   nothing to preserve them from (membership, though fetched, cannot
   rebuild the record)
   💥 COMPONENTS REMOVED

The two windows

Scenario A passed through two named degraded states — they differ in
whether the damage has reached the disk, which decides what a restart does:

Window 1 (A②) Window 2 (A③)
Damage graph eviction only (memory) + builtin config deleted; disk state deleted after config.tlog truncation
Device looks normal normal
A restart... heals (disk still complete) triggers the loss (memory held the last good copy)
Ends when the next deployment runs (→ W2) the restart happens (→ A④)

W1 is transient (hours, typically); W2 can persist for weeks while the
device looks perfectly healthy — one restart away from impact.

Scenario B — first deployment succeeds (the normal fleet path, safe)

① FIRST BOOT            GRAPH: builtins 🛡️   DISK: deps=[Nucleus]
② FIRST DEPLOYMENT OK   shield flags erased (🛡️ → ✗) as in A②, BUT
                        DISK: deps topic now CONTAINS builtins
                        (carried forward from the healthy graph —
                        deployments always persist the graph's builtins)
③ any LATER deployment fails → rollback restores a deps list
                        WITH builtins → nothing evicted → SAFE forever

The whole vulnerability lives in one corner: only a rollback to the
pre-first-deployment snapshot restores a builtin-less list.
After one
successful deployment, the persisted list includes the builtins and
rollbacks are harmless.

Related safe fork: a first deployment that fails without rollback
(failure handling policy DO_NOTHING) is also harmless — no snapshot
replay means no builtin-less list is ever restored, so nothing is
evicted. Scenario A specifically requires fail + rollback.

Scenario C — restart during W1 (safe, heals)

Same start as Scenario A, but the device restarts before any further
deployment runs:

① FIRST BOOT (fresh device)
   GRAPH: builtins 🛡️  (injected at launch, memory-only)
   MEM:   DeploymentService config attached, no group state yet
   DISK:  deps topic = [Nucleus]

② FIRST DEPLOYMENT FAILS after activation → ROLLBACK          [enters W1]
   shield flags erased by the forward merge, then rollback evicts the
   builtins (as in A②)
   GRAPH: builtins GONE (services still running)
   MEM:   config still attached & complete
   DISK:  still complete                   ← this is what makes the
                                             coming restart safe

③ RESTART (before any further deployment)
   GRAPH: builtins 🛡️ re-injected at launch → graph healed
   MEM:   rebuilt from disk → complete (disk was never damaged)
   DISK:  unchanged, complete
   → FULLY HEALED — device is now equivalent to Scenario B after ②:
     the next deployment writes the deps topic including the builtins,
     and later rollbacks are harmless

④ (any further deployments / restarts)
   → normal operation, no failure path remains

The heal works because in W1 the disk still holds everything: the restart
discards only the damaged in-memory graph (rebuilt at boot) and loses no
data. W1 is transient though — it ends at the next deployment (→ A③), and
fleets with auto-submitting components deploy often.

Scenario D — restart during W2, but no deployment afterward (latent)

① FIRST BOOT (fresh device)
   GRAPH: builtins 🛡️        MEM: attached, empty       DISK: deps=[Nucleus]

② FIRST DEPLOYMENT FAILS after activation → ROLLBACK          [enters W1]
   GRAPH: builtins EVICTED   MEM: attached, complete    DISK: complete

③ NEXT DEPLOYMENT SUCCEEDS                                    [enters W2]
   GRAPH: builtins gone
   MEM:   group state alive but DETACHED
   DISK:  group state purged at the next truncation, stays gone

④ RESTART
   GRAPH: builtins 🛡️ (boot heals the graph)
   MEM:   rebuilt from disk → group state MISSING
          (FSS sequence number resets to 0, membership empty)
   DISK:  unchanged (missing group state)

⑤ ...NO deployment processes...
   components keep running normally — the damage is invisible except
   via the cloud-visible symptoms: the FleetStatusService sequence
   number resets to 0 and components report empty fleetConfigArns
   (their thing-group associations)
   💤 IMPACT STAYS LATENT until ANY deployment runs (then → A⑤ 💥
      or → E⑤ ✅ depending on its type)

On the incident device, this window lasted 18 seconds — the gap between
the restart and the component's auto-submitted local deployment.

Scenario E — cloud deployment after the restart (recovers)

① FIRST BOOT (fresh device)
   GRAPH: builtins 🛡️        MEM: attached, empty       DISK: deps=[Nucleus]

② FIRST DEPLOYMENT FAILS after activation → ROLLBACK          [enters W1]
   GRAPH: builtins EVICTED   MEM: attached, complete    DISK: complete

③ NEXT DEPLOYMENT SUCCEEDS                                    [enters W2]
   GRAPH: builtins gone
   MEM:   group state alive but DETACHED
   DISK:  group state purged at the next truncation, stays gone

④ RESTART
   GRAPH: builtins 🛡️ (boot heals the graph)
   MEM:   rebuilt from disk → group state MISSING
   DISK:  unchanged (missing group state)

⑤ CLOUD DEPLOYMENT to the device's thing group G
   carries G's full component list in its deployment doc — the part a
   local deployment lacks — and fetches membership from the cloud
   resolution = G's components from the doc
   GRAPH: builtins 🛡️ (shield flags intact since ④)
   MEM:   GroupMembership + GroupToRootComponents(G) rebuilt, attached
   DISK:  group state persisted again at completion
   ✅ DEVICE FULLY RECOVERED (single-group device)

Yes — this succeeds and recovers the device, because the cloud deployment
carries the group's component list with it: it doesn't need the (missing)
local record. This is why the remediation guidance is "deploy to the
degraded device" and it is essentially how the incident device was
recovered. Lab-verified addendum: the safety of this path is inherent,
not timing luck — the resumed/processed cloud deployment persists the
group record before any startup local deployment can be processed
(deployments are handled serially, in order).

Variants: multi-group devices → Scenario F; the recovery deployment itself
failing → Scenario G.

Scenario F — multi-group device, cloud deployment after the restart (partial recovery, collateral removal)

Device belongs to TWO thing groups, Alpha and Beta, each deploying its own
components:

① FIRST BOOT (fresh device)
   GRAPH: builtins 🛡️        MEM: attached, empty       DISK: deps=[Nucleus]

② FIRST DEPLOYMENT (Alpha) FAILS after activation → ROLLBACK   [enters W1]
   GRAPH: builtins EVICTED   MEM: attached, complete    DISK: complete

③ DEPLOYMENTS for Alpha and Beta SUCCEED                       [enters W2]
   both groups' components installed and running
   MEM:   group state for Alpha AND Beta alive but DETACHED
   DISK:  group state for both GONE

④ RESTART
   GRAPH: builtins 🛡️ (boot heals the graph)
   MEM:   rebuilt from disk → BOTH groups' state missing
   DISK:  missing both

⑤ CLOUD DEPLOYMENT to Alpha
   membership fetched from cloud → shows Alpha AND Beta ✓
   resolution: Alpha's components come from the deployment doc ✓
               Beta's components are preserved ONLY if Beta's LOCAL
               GroupToRootComponents record exists — it doesn't
   💥 BETA'S COMPONENTS REMOVED, Alpha recovered
   MEM/DISK: group state for Alpha rebuilt; Beta absent

⑥ CLOUD DEPLOYMENT to Beta (a second, separate deployment)
   ✅ Beta recovered — full recovery requires one deployment PER group

The membership lookup protects a group's record from cleanup, but a
missing record is never rebuilt from the cloud — component preservation
still reads only local state. This one-way property is what we call the
"ratchet": group records are destroyed automatically, but only ever
rebuilt by a deployment targeting that specific group. So recovery is
per-group, and each group's deployment removes the still-unrecovered
groups' components in the meantime.

Scenario G — recovery deployment fails and rolls back (re-arms the bug)

Single-group device; the recovery attempt itself fails after activation:

① FIRST BOOT (fresh device)
   GRAPH: builtins 🛡️        MEM: attached, empty       DISK: deps=[Nucleus]

② FIRST DEPLOYMENT FAILS after activation → ROLLBACK          [enters W1]
   GRAPH: builtins EVICTED   MEM: attached, complete    DISK: complete

③ NEXT DEPLOYMENT SUCCEEDS                                    [enters W2]
   GRAPH: builtins gone
   MEM:   group state alive but DETACHED
   DISK:  group state purged at the next truncation, stays gone
          note: deps topic was written by this damaged-graph
          resolution, i.e. WITHOUT the builtins

④ RESTART
   GRAPH: builtins 🛡️ (boot heals the graph)
   MEM:   rebuilt from disk → group state MISSING
   DISK:  unchanged (missing group state; deps topic still
          builtin-less)

⑤ CLOUD DEPLOYMENT arrives (the recovery attempt)
   snapshot taken for rollback = current config, whose deps topic
   is WITHOUT builtins
   forward merge writes deps topic WITH builtins → boot-granted
   shield flags erased (🛡️ → ✗, as in A②)
   ...deployment FAILS after activation (e.g. a component breaks)...

⑥ ROLLBACK replays the snapshot
   deps topic restored to the builtin-less list → builtins EVICTED again
   GRAPH: builtins GONE      [degraded again: W1's structure — evicted
                              graph, builtin config still attached —
                              but group state is already lost, so it
                              lacks W1's "restart heals" property]
   MEM:   group state still missing (rollback restored pre-⑤ config,
          which had none)
   DISK:  still missing group state

⑦ from here: a later successful cloud deployment recovers it (→ E),
   another deployment while degraded re-deletes builtin config (→ A③),
   a restart re-heals the graph but restores no data (→ A④)

The pre-first-deployment corner generalizes: on an unfixed nucleus, ANY
rollback whose snapshot holds a builtin-less deps topic re-evicts the
builtins. A degraded device's snapshots hold exactly that — so every
failed deployment on a degraded device re-arms the cycle.

Scenario H — nucleus UPGRADE fails on a W2 device

A nucleus upgrade is a bootstrap deployment: it reboots the device
mid-deployment, and a failure rolls back by REBOOTING AGAIN into the old
nucleus from a rollback snapshot. On a W2 device that snapshot is
poisoned:

① – ③ as Scenario A: device is W2-degraded (builtin config detached,
   disk missing group state), still running normally — this is exactly
   the state a fleet-remediation upgrade targets

④ UPGRADE DEPLOYMENT (new nucleus version) arrives
   rollback snapshot taken = a DUMP of the config tree — walked from
   the root, so it OMITS the detached group state
   merge + bootstrap: device REBOOTS into the new nucleus...

⑤ ...UPGRADE FAILS during activation → KERNEL ROLLBACK
   device reboots AGAIN, into the OLD nucleus, loading the snapshot
   GRAPH: builtins 🛡️ (boot re-injects)
   MEM:   rebuilt from the poisoned snapshot → group state MISSING
   DISK:  missing group state
   → the device lands directly in the State-B condition (as after A④)

⑥ LOCAL DEPLOYMENT at startup (auto-submitting component)
   💥 COMPONENTS REMOVED — the failed remediation attempt itself
      triggered the incident it was meant to prevent

Unlike Scenario G (ordinary deployment rollback — no reboot, memory
survives, nothing new lost), the upgrade's rollback path REBOOTS, so the
detached in-memory group state evaporates and the loss lands immediately.

Operational implication (confirmed): upgrades of degraded devices should
be monitored, and a failed upgrade followed immediately by a fresh
thing-group deployment — lab-verified to fully restore the device
(with a component-down window in between).

Contrast: the same failed upgrade on a W1 device is harmless — its
snapshot is complete (everything still attached), the rollback reboot
heals it like any W1 restart (→ C), and a W1 device has no deployed
components to lose anyway (its first deployment never succeeded).

Status: Reproduced. A W2-degraded device upgraded
toward a fixed build (deployment also carried a broken component) hit
FAILED_ROLLBACK_COMPLETE; post-rollback, DeploymentService booted with
empty group state, and the first local deployment removed the running
component. Notably, the cloud membership fetch SUCCEEDED and did not
help — the decisive variable is the missing local record of the group's
components, which membership alone cannot rebuild. The fix status of the
upgrade target is irrelevant: the rollback executes old-nucleus code.

Scenario I — reboot(s) before the first deployment, which then fails (same as A)

① FIRST BOOT (fresh device)
   GRAPH: builtins 🛡️        MEM: attached, empty       DISK: deps=[Nucleus]

② REBOOT (no deployment has arrived yet — one or many, doesn't matter)
   GRAPH: builtins 🛡️ (re-injected, as at every boot)
   MEM:   rebuilt from disk → identical to ①
   DISK:  unchanged — deps topic still [Nucleus]

③ FIRST DEPLOYMENT FAILS after activation → ROLLBACK
   → proceeds exactly as A② (shield flags erased by the forward merge,
     rollback restores [Nucleus], builtins evicted) and onward

Pre-deployment reboots change nothing: each boot recreates the same
state. The vulnerable corner — a builtin-less deps topic on disk — exists
from provisioning until the first deployment writes builtins into the
topic
, no matter how many reboots happen in between. Only a deployment
can close the corner (→ B) or spring it (→ A).

Scenario J — the FIRST deployment is a bootstrap deployment that fails (safe!)

A bootstrap deployment (e.g. one changing the nucleus version) REBOOTS
the device mid-deployment, before activation completes. Assume the
rollback itself does not bootstrap/reboot (no bootstrap-on-rollback):

① FIRST BOOT (fresh device)
   GRAPH: builtins 🛡️        MEM: attached, empty       DISK: deps=[Nucleus]

② BOOTSTRAP DEPLOYMENT arrives (first deployment ever)
   rollback snapshot taken (deps=[Nucleus], tree complete)
   forward merge writes deps topic WITH builtins → shield flags
   erased (🛡️ → ✗), as in A②...
   ...then the device REBOOTS to continue the deployment

③ BOOT (mid-deployment, activation stage)
   GRAPH: builtins 🛡️ — the reboot RE-INJECTS the shield flags!
   the erasure from ② did not survive the restart (flags are re-granted
   at every launch)

④ DEPLOYMENT FAILS during activation → ROLLBACK (no reboot)
   snapshot replayed: deps topic restored to [Nucleus]
   the builtins are absent from the list BUT carry fresh shield
   flags → NOT evicted 🛡️
   GRAPH: builtins 🛡️        MEM: rolled back, fresh    DISK: complete
   ✅ NO DAMAGE — device is a healthy fresh device again

The eviction in Scenario A requires the flag-erasure (②) and the
snapshot replay (rollback) to happen within one uninterrupted boot
session
. A bootstrap deployment inserts a reboot between them, which
re-arms the shields — so it is immune. This is also why the incident's
trigger had to be an ordinary component/config deployment: those never
reboot, so nothing restores the flags before the rollback replays the
poisoned list.

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

Unit Tests Coverage Report

File Coverage Lines Branches
All files 67% 72% 63%
com.aws.greengrass.deployment.activator.DeploymentActivatorFactory 100% 100% 100%
com.aws.greengrass.deployment.activator.KernelUpdateActivator 60% 65% 55%
com.aws.greengrass.deployment.activator.DeploymentActivator 78% 81% 75%
com.aws.greengrass.deployment.activator.DefaultActivator 57% 53% 61%
com.aws.greengrass.authorization.AuthorizationIPCAgent$ValidateAuthorizationTokenOperationHandler 95% 90% 100%
com.aws.greengrass.authorization.AuthorizationPolicyParser$1 100% 100% 0%
com.aws.greengrass.authorization.AuthorizationPolicyParser$2 0% 0% 0%
com.aws.greengrass.authorization.WildcardTrie 97% 98% 95%
com.aws.greengrass.authorization.AuthorizationIPCAgent 100% 100% 0%
com.aws.greengrass.authorization.AuthorizationPolicyParser 84% 91% 77%
com.aws.greengrass.authorization.AuthorizationHandler$ResourceLookupPolicy 100% 100% 0%
com.aws.greengrass.authorization.AuthorizationHandler 86% 94% 78%
com.aws.greengrass.authorization.AuthorizationModule 96% 100% 93%
com.aws.greengrass.authorization.AuthorizationPolicy 100% 100% 0%
com.aws.greengrass.util.IotSdkClientFactory$EnvironmentStage 56% 63% 50%
com.aws.greengrass.util.IotSdkClientFactory 85% 88% 83%
com.aws.greengrass.util.RootCAUtils 59% 69% 50%
com.aws.greengrass.util.DependencyOrder 100% 100% 100%
com.aws.greengrass.util.SerializerFactory 100% 100% 0%
com.aws.greengrass.util.BaseRetryableAccessor 95% 90% 100%
com.aws.greengrass.util.CommitableWriter 47% 70% 25%
com.aws.greengrass.util.EncryptionUtils$PemWriter 100% 100% 100%
com.aws.greengrass.util.IamSdkClientFactory 100% 100% 0%
com.aws.greengrass.util.OrderedExecutorService$OrderedTask 81% 88% 75%
com.aws.greengrass.util.ProxyUtils 74% 74% 75%
com.aws.greengrass.util.FileSystemPermission$Option 100% 100% 0%
com.aws.greengrass.util.NucleusPaths 92% 92% 0%
com.aws.greengrass.util.Exec 62% 78% 46%
com.aws.greengrass.util.StsSdkClientFactory 100% 100% 0%
com.aws.greengrass.util.MqttChunkedPayloadPublisher 83% 72% 94%
com.aws.greengrass.util.LockFactory 77% 77% 0%
com.aws.greengrass.util.CommitableReader 66% 82% 50%
com.aws.greengrass.util.Utils$1 50% 50% 0%
com.aws.greengrass.util.Utils 80% 83% 76%
com.aws.greengrass.util.AppendableWriter 0% 0% 0%
com.aws.greengrass.util.Digest 83% 91% 75%
com.aws.greengrass.util.OrderedExecutorService 82% 81% 83%
com.aws.greengrass.util.CommitableFile 78% 85% 71%
com.aws.greengrass.util.RetryUtils$DifferentiatedRetryConfig 100% 100% 0%
com.aws.greengrass.util.Coerce 92% 93% 91%
com.aws.greengrass.util.BatchedSubscriber 87% 100% 75%
com.aws.greengrass.util.LockScope 100% 100% 0%
com.aws.greengrass.util.Exec$Copier 86% 91% 82%
com.aws.greengrass.util.S3SdkClientFactory 92% 100% 85%
com.aws.greengrass.util.LoaderLogsSummarizer 0% 0% 0%
com.aws.greengrass.util.DefaultConcurrentHashMap 100% 100% 100%
com.aws.greengrass.util.Coerce$1 100% 100% 0%
com.aws.greengrass.util.GreengrassServiceClientFactory$1 0% 0% 0%
com.aws.greengrass.util.RegionUtils 46% 46% 0%
com.aws.greengrass.util.RetryUtils 86% 92% 79%
com.aws.greengrass.util.Permissions 85% 98% 72%
com.aws.greengrass.util.EncryptionUtils 100% 100% 100%
com.aws.greengrass.util.GreengrassServiceClientFactory 23% 19% 26%
com.aws.greengrass.util.platforms.windows.WindowsPlatform$CmdDecorator 0% 0% 0%
com.aws.greengrass.util.platforms.windows.WindowsPlatform$WindowsFileSystemPermissionView 0% 0% 0%
com.aws.greengrass.util.platforms.windows.UserEnv 0% 0% 0%
com.aws.greengrass.util.platforms.windows.WindowsPlatform 0% 0% 0%
com.aws.greengrass.util.platforms.windows.WindowsPlatform$1 0% 0% 0%
com.aws.greengrass.util.platforms.windows.WindowsPlatform$2 0% 0% 0%
com.aws.greengrass.util.platforms.windows.WindowsExec 0% 0% 0%
com.aws.greengrass.util.platforms.windows.WindowsUserAttributes 0% 0% 0%
com.aws.greengrass.util.platforms.windows.UserEnv$PROFILEINFO 0% 0% 0%
com.aws.greengrass.util.platforms.windows.WindowsPlatform$RunasDecorator 0% 0% 0%
com.aws.greengrass.componentmanager.plugins.docker.DefaultDockerClient 1% 1% 0%
com.aws.greengrass.componentmanager.plugins.docker.EcrAccessor 63% 63% 0%
com.aws.greengrass.componentmanager.plugins.docker.DockerImageDownloader 79% 77% 81%
com.aws.greengrass.componentmanager.plugins.docker.Image 66% 66% 0%
com.aws.greengrass.componentmanager.plugins.docker.Registry$RegistrySource 100% 100% 0%
com.aws.greengrass.componentmanager.plugins.docker.Registry$RegistryType 100% 100% 0%
com.aws.greengrass.componentmanager.plugins.docker.Registry$Credentials 75% 75% 0%
com.aws.greengrass.componentmanager.plugins.docker.DockerApplicationManagerService 0% 0% 0%
com.aws.greengrass.componentmanager.plugins.docker.Registry 75% 100% 50%
com.aws.greengrass.componentmanager.plugins.docker.DockerImageArtifactParser 97% 98% 96%
com.aws.greengrass.builtin.services.mqttproxy.MqttProxyIPCAgent 85% 88% 83%
com.aws.greengrass.builtin.services.mqttproxy.MqttProxyIPCAgent$PublishToIoTCoreOperationHandler 56% 76% 37%
com.aws.greengrass.builtin.services.mqttproxy.MqttProxyIPCAgent$SubscribeToIoTCoreOperationHandler 44% 53% 35%
com.aws.greengrass.mqttclient.v5.PubAck 81% 100% 62%
com.aws.greengrass.mqttclient.v5.Subscribe 75% 100% 50%
com.aws.greengrass.mqttclient.v5.SubscribeResponse 83% 100% 66%
com.aws.greengrass.mqttclient.v5.Subscribe$RetainHandlingType 100% 100% 0%
com.aws.greengrass.mqttclient.v5.UnsubscribeResponse 75% 100% 50%
com.aws.greengrass.mqttclient.v5.Publish$PayloadFormatIndicator 50% 50% 0%
com.aws.greengrass.mqttclient.v5.QOS 67% 84% 50%
com.aws.greengrass.mqttclient.v5.Publish 48% 59% 37%
com.aws.greengrass.builtin.services.telemetry.ComponentMetricIPCEventStreamAgent$PutComponentMetricOperationHandler 88% 88% 0%
com.aws.greengrass.builtin.services.telemetry.ComponentMetricIPCEventStreamAgent 87% 97% 76%
com.aws.greengrass.componentmanager.models.ComponentIdentifier 100% 100% 0%
com.aws.greengrass.componentmanager.models.ComponentMetadata 0% 0% 0%
com.aws.greengrass.componentmanager.models.PermissionType 58% 66% 50%
com.aws.greengrass.componentmanager.models.Permission 70% 100% 40%
com.aws.greengrass.componentmanager.models.ComponentRequirementIdentifier 0% 0% 0%
com.aws.greengrass.util.platforms.StubResourceController 20% 20% 0%
com.aws.greengrass.util.platforms.Platform$1 100% 100% 0%
com.aws.greengrass.util.platforms.UserDecorator 100% 100% 0%
com.aws.greengrass.util.platforms.Platform 66% 75% 58%
com.aws.greengrass.util.platforms.Platform$FileSystemPermissionView 100% 100% 0%
com.aws.greengrass.dependency.Context$Value 79% 86% 72%
com.aws.greengrass.dependency.EZPlugins 43% 51% 36%
com.aws.greengrass.dependency.Context 78% 83% 72%
com.aws.greengrass.dependency.InjectionActions 100% 100% 0%
com.aws.greengrass.dependency.State 53% 75% 32%
com.aws.greengrass.dependency.ComponentStatusCode 43% 64% 22%
com.aws.greengrass.dependency.Context$1 84% 69% 100%
com.aws.greengrass.mqttclient.spool.Spool 82% 89% 75%
com.aws.greengrass.mqttclient.spool.InMemorySpool 77% 77% 0%
com.aws.greengrass.mqttclient.spool.SpoolerStorageType 100% 100% 0%
com.aws.greengrass.componentmanager.KernelConfigResolver 83% 90% 77%
com.aws.greengrass.componentmanager.Unarchiver 3% 3% 0%
com.aws.greengrass.componentmanager.ClientConfigurationUtils 11% 15% 7%
com.aws.greengrass.componentmanager.ComponentStore 62% 65% 58%
com.aws.greengrass.componentmanager.ComponentServiceHelper 65% 80% 50%
com.aws.greengrass.componentmanager.DependencyResolver 96% 98% 94%
com.aws.greengrass.componentmanager.ComponentManager 72% 73% 70%
com.aws.greengrass.util.platforms.unix.UnixRunWithGenerator 79% 74% 84%
com.aws.greengrass.util.platforms.unix.UnixPlatform$ShDecorator 68% 87% 50%
com.aws.greengrass.util.platforms.unix.UnixUserAttributes 58% 66% 50%
com.aws.greengrass.util.platforms.unix.UnixPlatform$IdOption 100% 100% 0%
com.aws.greengrass.util.platforms.unix.UnixPlatform 36% 38% 35%
com.aws.greengrass.util.platforms.unix.UnixExec 42% 43% 40%
com.aws.greengrass.util.platforms.unix.UnixGroupAttributes 0% 0% 0%
com.aws.greengrass.util.platforms.unix.QNXPlatform 0% 0% 0%
com.aws.greengrass.util.platforms.unix.UnixPlatform$1 100% 100% 0%
com.aws.greengrass.util.platforms.unix.UnixPlatform$SudoDecorator 72% 86% 58%
com.aws.greengrass.util.platforms.unix.UnixPlatform$PosixFileSystemPermissionView 100% 100% 100%
com.aws.greengrass.util.platforms.unix.DarwinPlatform 0% 0% 0%
com.aws.greengrass.config.UpdateBehaviorTree$PrunedUpdateBehaviorTree 80% 80% 0%
com.aws.greengrass.config.Node 88% 89% 87%
com.aws.greengrass.config.PlatformResolver 72% 81% 62%
com.aws.greengrass.config.ConfigurationReader$1 100% 100% 0%
com.aws.greengrass.config.Configuration 80% 89% 72%
com.aws.greengrass.config.ConfigurationReader 90% 96% 84%
com.aws.greengrass.config.UpdateBehaviorTree 100% 100% 100%
com.aws.greengrass.config.Topic 76% 84% 68%
com.aws.greengrass.config.CaseInsensitiveString 65% 70% 60%
com.aws.greengrass.config.Topics 90% 92% 88%
com.aws.greengrass.config.ConfigurationReader$ConfigurationMode 100% 100% 0%
com.aws.greengrass.config.ConfigurationWriter 74% 77% 72%
com.aws.greengrass.config.WhatHappened 100% 100% 0%
com.aws.greengrass.config.UpdateBehaviorTree$UpdateBehavior 100% 100% 0%
com.aws.greengrass.iot.IotConnectionManager 45% 67% 22%
com.aws.greengrass.iot.IotCloudHelper 78% 90% 66%
com.aws.greengrass.iot.model.IotCloudResponse 100% 100% 0%
com.aws.greengrass.deployment.bootstrap.BootstrapTaskStatus 100% 100% 0%
com.aws.greengrass.deployment.bootstrap.BootstrapSuccessCode 83% 100% 66%
com.aws.greengrass.deployment.bootstrap.BootstrapManager 78% 82% 74%
com.aws.greengrass.deployment.bootstrap.BootstrapManager$1 100% 100% 0%
com.aws.greengrass.deployment.bootstrap.BootstrapTaskStatus$ExecutionStatus 100% 100% 0%
com.aws.greengrass.deployment.model.S3EndpointType 100% 100% 0%
com.aws.greengrass.deployment.model.FailureHandlingPolicy 100% 100% 0%
com.aws.greengrass.deployment.model.DeploymentTask 100% 100% 0%
com.aws.greengrass.deployment.model.RunWith 85% 95% 75%
com.aws.greengrass.deployment.model.DeploymentPackageConfiguration 57% 57% 0%
com.aws.greengrass.deployment.model.DeploymentDocument$SDKSerializer 100% 100% 0%
com.aws.greengrass.deployment.model.Deployment$DeploymentType 100% 100% 0%
com.aws.greengrass.deployment.model.Deployment 87% 100% 75%
com.aws.greengrass.deployment.model.Deployment$DeploymentStage 100% 100% 0%
com.aws.greengrass.deployment.model.DeploymentDocument$SDKDeserializer 80% 80% 0%
com.aws.greengrass.deployment.model.DeploymentTaskMetadata 100% 100% 0%
com.aws.greengrass.deployment.model.DeploymentDocument 100% 100% 100%
com.aws.greengrass.deployment.model.DeploymentResult$DeploymentStatus 100% 100% 0%
com.aws.greengrass.status.FleetStatusService 76% 84% 69%
com.aws.greengrass.status.FleetStatusService$1 100% 100% 0%
com.aws.greengrass.mqttclient.MqttClient$1 75% 100% 50%
com.aws.greengrass.mqttclient.MqttClient$2 100% 100% 0%
com.aws.greengrass.mqttclient.AwsIotMqtt5Client 50% 69% 32%
com.aws.greengrass.mqttclient.PublishRequest 70% 90% 50%
com.aws.greengrass.mqttclient.MqttClient 74% 81% 68%
com.aws.greengrass.mqttclient.WrapperMqttClientConnection 91% 82% 100%
com.aws.greengrass.mqttclient.AwsIotMqttClient 82% 89% 75%
com.aws.greengrass.mqttclient.AwsIotMqttClient$1 71% 93% 50%
com.aws.greengrass.mqttclient.CallbackEventManager 91% 92% 91%
com.aws.greengrass.mqttclient.IotCoreTopicValidator 89% 93% 85%
com.aws.greengrass.mqttclient.StandaloneMqttConnector 68% 77% 58%
com.aws.greengrass.mqttclient.MqttTopic 97% 94% 100%
com.aws.greengrass.mqttclient.AwsIotMqtt5Client$1 48% 68% 27%
com.aws.greengrass.mqttclient.IotCoreTopicValidator$Operation 100% 100% 0%
com.aws.greengrass.network.HttpClientProvider 50% 50% 0%
com.aws.greengrass.status.model.FleetStatusDetails 100% 100% 100%
com.aws.greengrass.status.model.OverallStatus 100% 100% 0%
com.aws.greengrass.status.model.Trigger 58% 80% 37%
com.aws.greengrass.status.model.MessageType 76% 85% 66%
com.aws.greengrass.deployment.errorcode.DeploymentErrorCode 100% 100% 0%
com.aws.greengrass.deployment.errorcode.DeploymentErrorCodeUtils 75% 79% 70%
com.aws.greengrass.deployment.errorcode.DeploymentErrorType 100% 100% 0%
com.aws.greengrass.tes.CredentialRequestHandler 83% 89% 77%
com.aws.greengrass.tes.CredentialRequestHandler$TESCache 100% 100% 0%
com.aws.greengrass.tes.HttpServerImpl 100% 100% 0%
com.aws.greengrass.tes.LazyCredentialProvider 12% 12% 0%
com.aws.greengrass.tes.TokenExchangeService 55% 67% 42%
com.aws.greengrass.componentmanager.converter.RecipeLoader 75% 88% 62%
com.aws.greengrass.componentmanager.converter.RecipeLoader$RecipeFormat 100% 100% 0%
com.aws.greengrass.lifecyclemanager.Periodicity 13% 16% 11%
com.aws.greengrass.lifecyclemanager.LogManagerHelper 100% 100% 0%
com.aws.greengrass.lifecyclemanager.UnloadableService 77% 71% 83%
com.aws.greengrass.lifecyclemanager.RunWithPathOwnershipHandler 100% 100% 100%
com.aws.greengrass.lifecyclemanager.KernelAlternatives 48% 50% 47%
com.aws.greengrass.lifecyclemanager.ShellRunner$Default 69% 74% 64%
com.aws.greengrass.lifecyclemanager.GreengrassService 76% 78% 75%
com.aws.greengrass.lifecyclemanager.Lifecycle$DesiredStateUpdatedEvent 100% 100% 0%
com.aws.greengrass.lifecyclemanager.GenericExternalService 45% 49% 41%
com.aws.greengrass.lifecyclemanager.GreengrassService$RunStatus 100% 100% 0%
com.aws.greengrass.lifecyclemanager.Lifecycle 77% 80% 74%
com.aws.greengrass.lifecyclemanager.Kernel 72% 75% 68%
com.aws.greengrass.lifecyclemanager.KernelMetricsEmitter 100% 100% 100%
com.aws.greengrass.lifecyclemanager.Lifecycle$StateEvent 100% 100% 0%
com.aws.greengrass.lifecyclemanager.KernelCommandLine 77% 78% 76%
com.aws.greengrass.lifecyclemanager.GenericExternalService$RunResult 100% 100% 0%
com.aws.greengrass.lifecyclemanager.Kernel$1 78% 100% 57%
com.aws.greengrass.lifecyclemanager.KernelLifecycle 81% 85% 77%
com.aws.greengrass.lifecyclemanager.PluginService 41% 50% 33%
com.aws.greengrass.lifecyclemanager.UpdateSystemPolicyService 7% 8% 6%
com.aws.greengrass.util.platforms.unix.linux.CgroupManager 70% 86% 55%
com.aws.greengrass.util.platforms.unix.linux.LinuxSystemResourceController 40% 44% 36%
com.aws.greengrass.util.platforms.unix.linux.LinuxPlatform 100% 100% 0%
com.aws.greengrass.util.platforms.unix.linux.CgroupV1 94% 94% 0%
com.aws.greengrass.util.platforms.unix.linux.CgroupV2 76% 92% 60%
com.aws.greengrass.deployment.converter.DeploymentDocumentConverter 77% 84% 70%
com.aws.greengrass.ipc.AuthenticationHandler 16% 25% 8%
com.aws.greengrass.ipc.IPCEventStreamService 65% 80% 50%
com.aws.greengrass.jna.Kernel32Ex 0% 0% 0%
com.aws.greengrass.builtin.services.configstore.ConfigStoreIPCEventStreamAgent$UpdateConfigurationOperationHandler 76% 73% 80%
com.aws.greengrass.builtin.services.configstore.ConfigStoreIPCEventStreamAgent 63% 77% 50%
com.aws.greengrass.builtin.services.configstore.ConfigStoreIPCEventStreamAgent$ConfigurationUpdateOperationHandler 69% 79% 59%
com.aws.greengrass.builtin.services.configstore.ConfigStoreIPCEventStreamAgent$GetConfigurationOperationHandler 76% 81% 71%
com.aws.greengrass.builtin.services.configstore.ConfigStoreIPCEventStreamAgent$SendConfigurationValidityReportOperationHandler 86% 90% 83%
com.aws.greengrass.builtin.services.configstore.ConfigStoreIPCEventStreamAgent$ValidateConfigurationUpdatesOperationHandler 85% 85% 0%
com.aws.greengrass.ipc.common.DefaultOperationHandler 0% 0% 0%
com.aws.greengrass.security.SecurityService$DefaultCryptoKeyProvider 96% 93% 100%
com.aws.greengrass.security.SecurityService 78% 76% 81%
com.aws.greengrass.provisioning.ProvisioningPluginFactory 0% 0% 0%
com.aws.greengrass.provisioning.ProvisioningConfigUpdateHelper 91% 100% 83%
com.aws.greengrass.componentmanager.builtins.GreengrassRepositoryDownloader 50% 61% 39%
com.aws.greengrass.componentmanager.builtins.S3Downloader 55% 60% 50%
com.aws.greengrass.componentmanager.builtins.ArtifactDownloaderFactory 79% 77% 80%
com.aws.greengrass.componentmanager.builtins.ArtifactDownloader 82% 83% 80%
com.aws.greengrass.builtin.services.lifecycle.LifecycleIPCEventStreamAgent$UpdateStateOperationHandler 90% 90% 0%
com.aws.greengrass.builtin.services.lifecycle.LifecycleIPCEventStreamAgent$DeferComponentUpdateHandler 77% 77% 0%
com.aws.greengrass.builtin.services.lifecycle.LifecycleIPCEventStreamAgent 31% 24% 37%
com.aws.greengrass.builtin.services.lifecycle.LifecycleIPCEventStreamAgent$SubscribeToComponentUpdateOperationHandler 73% 96% 50%
com.aws.greengrass.builtin.services.lifecycle.LifecycleIPCEventStreamAgent$PauseComponentHandler 89% 90% 87%
com.aws.greengrass.builtin.services.lifecycle.LifecycleIPCEventStreamAgent$ResumeComponentHandler 89% 90% 87%
com.aws.greengrass.builtin.services.pubsub.PubSubIPCEventStreamAgent$PublishToTopicOperationHandler 90% 80% 100%
com.aws.greengrass.builtin.services.pubsub.SubscriptionTrie 97% 98% 95%
com.aws.greengrass.builtin.services.pubsub.PubSubIPCEventStreamAgent 83% 92% 73%
com.aws.greengrass.builtin.services.pubsub.PubSubIPCEventStreamAgent$SubscribeToTopicOperationHandler 68% 68% 0%
com.aws.greengrass.telemetry.MetricsPayload 100% 100% 0%
com.aws.greengrass.telemetry.MetricsAggregator 87% 90% 83%
com.aws.greengrass.telemetry.MetricsAggregator$1 100% 100% 0%
com.aws.greengrass.telemetry.AggregatedMetric 100% 100% 0%
com.aws.greengrass.telemetry.TelemetryAgent 71% 77% 66%
com.aws.greengrass.telemetry.TelemetryConfiguration 52% 65% 40%
com.aws.greengrass.telemetry.PeriodicMetricsEmitter 100% 100% 0%
com.aws.greengrass.telemetry.TelemetryAgent$1 60% 60% 0%
com.aws.greengrass.telemetry.SystemMetricsEmitter 100% 100% 100%
com.aws.greengrass.deployment.DeploymentConfigMerger 87% 87% 87%
com.aws.greengrass.deployment.IotJobsHelper$IotJobsClientFactory 100% 100% 0%
com.aws.greengrass.deployment.DeploymentConfigMerger$AggregateServicesChangeManager 73% 71% 76%
com.aws.greengrass.deployment.DeviceConfiguration 73% 78% 67%
com.aws.greengrass.deployment.DeploymentDocumentDownloader 69% 80% 58%
com.aws.greengrass.deployment.DeploymentQueue 97% 100% 95%
com.aws.greengrass.deployment.DeploymentService 59% 68% 49%
com.aws.greengrass.deployment.EndpointSwitchState 95% 100% 91%
com.aws.greengrass.deployment.IotJobsHelper$LatestQueuedJobs 69% 69% 70%
com.aws.greengrass.deployment.KernelUpdateDeploymentTask 70% 83% 57%
com.aws.greengrass.deployment.DynamicComponentConfigurationValidator 84% 94% 75%
com.aws.greengrass.deployment.DefaultDeploymentTask 66% 77% 56%
com.aws.greengrass.deployment.DeploymentDirectoryManager 71% 86% 56%
com.aws.greengrass.deployment.IotJobsHelper$WrapperMqttConnectionFactory 100% 100% 0%
com.aws.greengrass.deployment.IotJobsHelper 62% 70% 54%
com.aws.greengrass.deployment.IotJobsHelper$1 85% 85% 0%
com.aws.greengrass.deployment.EndpointSwitchPreflightValidator 73% 71% 75%
com.aws.greengrass.deployment.ThingGroupHelper 47% 61% 33%
com.aws.greengrass.deployment.ShadowDeploymentListener 37% 51% 22%
com.aws.greengrass.deployment.ShadowDeploymentListener$1 14% 14% 0%
com.aws.greengrass.deployment.DeploymentStatusKeeper 82% 93% 71%
com.aws.greengrass.deployment.IotJobsClientWrapper 15% 15% 0%
com.aws.greengrass.util.orchestration.SystemServiceUtilsFactory 0% 0% 0%
com.aws.greengrass.util.orchestration.ProcdUtils 0% 0% 0%
com.aws.greengrass.util.orchestration.SystemServiceUtils 0% 0% 0%
com.aws.greengrass.util.orchestration.InitUtils 0% 0% 0%
com.aws.greengrass.util.orchestration.SystemdUtils 0% 0% 0%
com.aws.greengrass.util.orchestration.WinswUtils 0% 0% 0%
com.aws.greengrass.testing.TestFeatureParameters 83% 100% 66%
com.aws.greengrass.testing.TestFeatureParameters$1 100% 100% 0%
com.aws.greengrass.ipc.modules.PubSubIPCService 68% 68% 0%
com.aws.greengrass.ipc.modules.AuthorizationService 75% 75% 0%
com.aws.greengrass.ipc.modules.ComponentMetricIPCService 69% 69% 0%
com.aws.greengrass.ipc.modules.MqttProxyIPCService 64% 64% 0%
com.aws.greengrass.ipc.modules.LifecycleIPCService 86% 86% 0%
com.aws.greengrass.ipc.modules.ConfigStoreIPCService 66% 66% 0%
com.aws.greengrass.easysetup.GreengrassSetup 75% 74% 76%
com.aws.greengrass.easysetup.DeviceProvisioningHelper 69% 77% 62%

Minimum allowed coverage is 65%

Generated by 🐒 cobertura-action against 7a2f726

@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

Integration Tests Coverage Report

File Coverage Lines Branches
All files 53% 57% 49%
com.aws.greengrass.deployment.activator.DeploymentActivatorFactory 100% 100% 100%
com.aws.greengrass.deployment.activator.KernelUpdateActivator 25% 29% 22%
com.aws.greengrass.deployment.activator.DeploymentActivator 79% 84% 75%
com.aws.greengrass.deployment.activator.DefaultActivator 70% 80% 61%
com.aws.greengrass.authorization.AuthorizationIPCAgent$ValidateAuthorizationTokenOperationHandler 48% 47% 50%
com.aws.greengrass.authorization.AuthorizationPolicyParser$1 100% 100% 0%
com.aws.greengrass.authorization.AuthorizationPolicyParser$2 0% 0% 0%
com.aws.greengrass.authorization.WildcardTrie 74% 79% 70%
com.aws.greengrass.authorization.AuthorizationIPCAgent 100% 100% 0%
com.aws.greengrass.authorization.AuthorizationPolicyParser 76% 80% 72%
com.aws.greengrass.authorization.AuthorizationHandler$ResourceLookupPolicy 100% 100% 0%
com.aws.greengrass.authorization.AuthorizationHandler 74% 74% 74%
com.aws.greengrass.authorization.AuthorizationModule 46% 59% 33%
com.aws.greengrass.authorization.AuthorizationPolicy 0% 0% 0%
com.aws.greengrass.util.IotSdkClientFactory$EnvironmentStage 0% 0% 0%
com.aws.greengrass.util.IotSdkClientFactory 0% 0% 0%
com.aws.greengrass.util.RootCAUtils 0% 0% 0%
com.aws.greengrass.util.DependencyOrder 100% 100% 100%
com.aws.greengrass.util.SerializerFactory 100% 100% 0%
com.aws.greengrass.util.BaseRetryableAccessor 0% 0% 0%
com.aws.greengrass.util.CommitableWriter 47% 70% 25%
com.aws.greengrass.util.EncryptionUtils$PemWriter 0% 0% 0%
com.aws.greengrass.util.IamSdkClientFactory 0% 0% 0%
com.aws.greengrass.util.OrderedExecutorService$OrderedTask 45% 66% 25%
com.aws.greengrass.util.ProxyUtils 27% 32% 21%
com.aws.greengrass.util.FileSystemPermission$Option 100% 100% 0%
com.aws.greengrass.util.NucleusPaths 100% 100% 0%
com.aws.greengrass.util.Exec 70% 85% 56%
com.aws.greengrass.util.StsSdkClientFactory 0% 0% 0%
com.aws.greengrass.util.MqttChunkedPayloadPublisher 35% 42% 27%
com.aws.greengrass.util.LockFactory 77% 77% 0%
com.aws.greengrass.util.CommitableReader 0% 0% 0%
com.aws.greengrass.util.Utils$1 87% 100% 75%
com.aws.greengrass.util.Utils 55% 60% 51%
com.aws.greengrass.util.AppendableWriter 0% 0% 0%
com.aws.greengrass.util.Digest 66% 83% 50%
com.aws.greengrass.util.OrderedExecutorService 63% 77% 50%
com.aws.greengrass.util.CommitableFile 65% 73% 57%
com.aws.greengrass.util.RetryUtils$DifferentiatedRetryConfig 60% 60% 0%
com.aws.greengrass.util.Coerce 59% 65% 53%
com.aws.greengrass.util.BatchedSubscriber 59% 68% 50%
com.aws.greengrass.util.LockScope 100% 100% 0%
com.aws.greengrass.util.Exec$Copier 86% 91% 82%
com.aws.greengrass.util.S3SdkClientFactory 38% 38% 0%
com.aws.greengrass.util.LoaderLogsSummarizer 0% 0% 0%
com.aws.greengrass.util.DefaultConcurrentHashMap 100% 100% 100%
com.aws.greengrass.util.Coerce$1 0% 0% 0%
com.aws.greengrass.util.GreengrassServiceClientFactory$1 0% 0% 0%
com.aws.greengrass.util.RegionUtils 0% 0% 0%
com.aws.greengrass.util.RetryUtils 24% 35% 12%
com.aws.greengrass.util.Permissions 72% 89% 54%
com.aws.greengrass.util.EncryptionUtils 0% 0% 0%
com.aws.greengrass.util.GreengrassServiceClientFactory 46% 32% 61%
com.aws.greengrass.util.platforms.windows.WindowsPlatform$CmdDecorator 0% 0% 0%
com.aws.greengrass.util.platforms.windows.WindowsPlatform$WindowsFileSystemPermissionView 0% 0% 0%
com.aws.greengrass.util.platforms.windows.UserEnv 0% 0% 0%
com.aws.greengrass.util.platforms.windows.WindowsPlatform 0% 0% 0%
com.aws.greengrass.util.platforms.windows.WindowsPlatform$1 0% 0% 0%
com.aws.greengrass.util.platforms.windows.WindowsPlatform$2 0% 0% 0%
com.aws.greengrass.util.platforms.windows.WindowsExec 0% 0% 0%
com.aws.greengrass.util.platforms.windows.WindowsUserAttributes 0% 0% 0%
com.aws.greengrass.util.platforms.windows.UserEnv$PROFILEINFO 0% 0% 0%
com.aws.greengrass.util.platforms.windows.WindowsPlatform$RunasDecorator 0% 0% 0%
com.aws.greengrass.componentmanager.plugins.docker.DefaultDockerClient 1% 1% 0%
com.aws.greengrass.componentmanager.plugins.docker.EcrAccessor 61% 72% 50%
com.aws.greengrass.componentmanager.plugins.docker.DockerImageDownloader 54% 61% 47%
com.aws.greengrass.componentmanager.plugins.docker.Image 66% 66% 0%
com.aws.greengrass.componentmanager.plugins.docker.Registry$RegistrySource 100% 100% 0%
com.aws.greengrass.componentmanager.plugins.docker.Registry$RegistryType 100% 100% 0%
com.aws.greengrass.componentmanager.plugins.docker.Registry$Credentials 75% 75% 0%
com.aws.greengrass.componentmanager.plugins.docker.DockerApplicationManagerService 0% 0% 0%
com.aws.greengrass.componentmanager.plugins.docker.Registry 75% 100% 50%
com.aws.greengrass.componentmanager.plugins.docker.DockerImageArtifactParser 83% 88% 78%
com.aws.greengrass.builtin.services.mqttproxy.MqttProxyIPCAgent 47% 53% 41%
com.aws.greengrass.builtin.services.mqttproxy.MqttProxyIPCAgent$PublishToIoTCoreOperationHandler 58% 78% 37%
com.aws.greengrass.builtin.services.mqttproxy.MqttProxyIPCAgent$SubscribeToIoTCoreOperationHandler 42% 49% 35%
com.aws.greengrass.mqttclient.v5.PubAck 0% 0% 0%
com.aws.greengrass.mqttclient.v5.Subscribe 0% 0% 0%
com.aws.greengrass.mqttclient.v5.SubscribeResponse 0% 0% 0%
com.aws.greengrass.mqttclient.v5.Subscribe$RetainHandlingType 87% 87% 0%
com.aws.greengrass.mqttclient.v5.UnsubscribeResponse 0% 0% 0%
com.aws.greengrass.mqttclient.v5.Publish$PayloadFormatIndicator 50% 50% 0%
com.aws.greengrass.mqttclient.v5.QOS 50% 76% 25%
com.aws.greengrass.mqttclient.v5.Publish 29% 34% 25%
com.aws.greengrass.builtin.services.telemetry.ComponentMetricIPCEventStreamAgent$PutComponentMetricOperationHandler 0% 0% 0%
com.aws.greengrass.builtin.services.telemetry.ComponentMetricIPCEventStreamAgent 16% 16% 0%
com.aws.greengrass.componentmanager.models.ComponentIdentifier 75% 75% 0%
com.aws.greengrass.componentmanager.models.ComponentMetadata 0% 0% 0%
com.aws.greengrass.componentmanager.models.PermissionType 58% 66% 50%
com.aws.greengrass.componentmanager.models.Permission 79% 100% 59%
com.aws.greengrass.componentmanager.models.ComponentRequirementIdentifier 0% 0% 0%
com.aws.greengrass.util.platforms.StubResourceController 20% 20% 0%
com.aws.greengrass.util.platforms.Platform$1 100% 100% 0%
com.aws.greengrass.util.platforms.UserDecorator 100% 100% 0%
com.aws.greengrass.util.platforms.Platform 80% 90% 70%
com.aws.greengrass.util.platforms.Platform$FileSystemPermissionView 100% 100% 0%
com.aws.greengrass.dependency.Context$Value 77% 84% 70%
com.aws.greengrass.dependency.EZPlugins 61% 68% 54%
com.aws.greengrass.dependency.Context 76% 83% 70%
com.aws.greengrass.dependency.InjectionActions 100% 100% 0%
com.aws.greengrass.dependency.State 55% 82% 28%
com.aws.greengrass.dependency.ComponentStatusCode 55% 73% 36%
com.aws.greengrass.dependency.Context$1 84% 69% 100%
com.aws.greengrass.mqttclient.spool.Spool 23% 36% 10%
com.aws.greengrass.mqttclient.spool.InMemorySpool 44% 44% 0%
com.aws.greengrass.mqttclient.spool.SpoolerStorageType 100% 100% 0%
com.aws.greengrass.componentmanager.KernelConfigResolver 76% 84% 69%
com.aws.greengrass.componentmanager.Unarchiver 72% 87% 58%
com.aws.greengrass.componentmanager.ClientConfigurationUtils 0% 0% 0%
com.aws.greengrass.componentmanager.ComponentStore 46% 48% 45%
com.aws.greengrass.componentmanager.ComponentServiceHelper 33% 52% 14%
com.aws.greengrass.componentmanager.DependencyResolver 60% 66% 53%
com.aws.greengrass.componentmanager.ComponentManager 65% 61% 69%
com.aws.greengrass.util.platforms.unix.UnixRunWithGenerator 63% 61% 65%
com.aws.greengrass.util.platforms.unix.UnixPlatform$ShDecorator 75% 100% 50%
com.aws.greengrass.util.platforms.unix.UnixUserAttributes 75% 100% 50%
com.aws.greengrass.util.platforms.unix.UnixPlatform$IdOption 100% 100% 0%
com.aws.greengrass.util.platforms.unix.UnixPlatform 64% 60% 67%
com.aws.greengrass.util.platforms.unix.UnixExec 75% 81% 68%
com.aws.greengrass.util.platforms.unix.UnixGroupAttributes 100% 100% 0%
com.aws.greengrass.util.platforms.unix.QNXPlatform 0% 0% 0%
com.aws.greengrass.util.platforms.unix.UnixPlatform$1 100% 100% 0%
com.aws.greengrass.util.platforms.unix.UnixPlatform$SudoDecorator 76% 89% 62%
com.aws.greengrass.util.platforms.unix.UnixPlatform$PosixFileSystemPermissionView 87% 91% 83%
com.aws.greengrass.util.platforms.unix.DarwinPlatform 0% 0% 0%
com.aws.greengrass.config.UpdateBehaviorTree$PrunedUpdateBehaviorTree 80% 80% 0%
com.aws.greengrass.config.Node 78% 80% 77%
com.aws.greengrass.config.PlatformResolver 38% 48% 27%
com.aws.greengrass.config.ConfigurationReader$1 100% 100% 0%
com.aws.greengrass.config.Configuration 65% 81% 50%
com.aws.greengrass.config.ConfigurationReader 66% 76% 57%
com.aws.greengrass.config.UpdateBehaviorTree 100% 100% 100%
com.aws.greengrass.config.Topic 67% 73% 62%
com.aws.greengrass.config.CaseInsensitiveString 65% 70% 60%
com.aws.greengrass.config.Topics 70% 75% 64%
com.aws.greengrass.config.ConfigurationReader$ConfigurationMode 100% 100% 0%
com.aws.greengrass.config.ConfigurationWriter 75% 73% 77%
com.aws.greengrass.config.WhatHappened 100% 100% 0%
com.aws.greengrass.config.UpdateBehaviorTree$UpdateBehavior 100% 100% 0%
com.aws.greengrass.iot.IotConnectionManager 42% 46% 38%
com.aws.greengrass.iot.IotCloudHelper 0% 0% 0%
com.aws.greengrass.iot.model.IotCloudResponse 0% 0% 0%
com.aws.greengrass.deployment.bootstrap.BootstrapTaskStatus 100% 100% 0%
com.aws.greengrass.deployment.bootstrap.BootstrapSuccessCode 0% 0% 0%
com.aws.greengrass.deployment.bootstrap.BootstrapManager 59% 65% 54%
com.aws.greengrass.deployment.bootstrap.BootstrapManager$1 0% 0% 0%
com.aws.greengrass.deployment.bootstrap.BootstrapTaskStatus$ExecutionStatus 100% 100% 0%
com.aws.greengrass.deployment.model.S3EndpointType 0% 0% 0%
com.aws.greengrass.deployment.model.FailureHandlingPolicy 100% 100% 0%
com.aws.greengrass.deployment.model.DeploymentTask 100% 100% 0%
com.aws.greengrass.deployment.model.RunWith 70% 91% 50%
com.aws.greengrass.deployment.model.DeploymentPackageConfiguration 21% 21% 0%
com.aws.greengrass.deployment.model.DeploymentDocument$SDKSerializer 100% 100% 0%
com.aws.greengrass.deployment.model.Deployment$DeploymentType 100% 100% 0%
com.aws.greengrass.deployment.model.Deployment 72% 70% 75%
com.aws.greengrass.deployment.model.Deployment$DeploymentStage 100% 100% 0%
com.aws.greengrass.deployment.model.DeploymentDocument$SDKDeserializer 20% 20% 0%
com.aws.greengrass.deployment.model.DeploymentTaskMetadata 100% 100% 0%
com.aws.greengrass.deployment.model.DeploymentDocument 91% 100% 83%
com.aws.greengrass.deployment.model.DeploymentResult$DeploymentStatus 100% 100% 0%
com.aws.greengrass.status.FleetStatusService 83% 90% 75%
com.aws.greengrass.status.FleetStatusService$1 16% 16% 0%
com.aws.greengrass.mqttclient.MqttClient$1 12% 12% 0%
com.aws.greengrass.mqttclient.MqttClient$2 100% 100% 0%
com.aws.greengrass.mqttclient.AwsIotMqtt5Client 34% 43% 25%
com.aws.greengrass.mqttclient.PublishRequest 70% 90% 50%
com.aws.greengrass.mqttclient.MqttClient 38% 46% 31%
com.aws.greengrass.mqttclient.WrapperMqttClientConnection 90% 80% 100%
com.aws.greengrass.mqttclient.AwsIotMqttClient 0% 0% 0%
com.aws.greengrass.mqttclient.AwsIotMqttClient$1 0% 0% 0%
com.aws.greengrass.mqttclient.CallbackEventManager 32% 48% 16%
com.aws.greengrass.mqttclient.IotCoreTopicValidator 61% 60% 62%
com.aws.greengrass.mqttclient.StandaloneMqttConnector 0% 0% 0%
com.aws.greengrass.mqttclient.MqttTopic 0% 0% 0%
com.aws.greengrass.mqttclient.AwsIotMqtt5Client$1 12% 19% 5%
com.aws.greengrass.mqttclient.IotCoreTopicValidator$Operation 100% 100% 0%
com.aws.greengrass.network.HttpClientProvider 50% 50% 0%
com.aws.greengrass.status.model.FleetStatusDetails 100% 100% 100%
com.aws.greengrass.status.model.OverallStatus 100% 100% 0%
com.aws.greengrass.status.model.Trigger 62% 86% 37%
com.aws.greengrass.status.model.MessageType 76% 85% 66%
com.aws.greengrass.deployment.errorcode.DeploymentErrorCode 100% 100% 0%
com.aws.greengrass.deployment.errorcode.DeploymentErrorCodeUtils 37% 44% 29%
com.aws.greengrass.deployment.errorcode.DeploymentErrorType 100% 100% 0%
com.aws.greengrass.tes.CredentialRequestHandler 0% 0% 0%
com.aws.greengrass.tes.CredentialRequestHandler$TESCache 0% 0% 0%
com.aws.greengrass.tes.HttpServerImpl 0% 0% 0%
com.aws.greengrass.tes.LazyCredentialProvider 12% 12% 0%
com.aws.greengrass.tes.TokenExchangeService 0% 0% 0%
com.aws.greengrass.componentmanager.converter.RecipeLoader 72% 86% 59%
com.aws.greengrass.componentmanager.converter.RecipeLoader$RecipeFormat 100% 100% 0%
com.aws.greengrass.lifecyclemanager.Periodicity 57% 64% 50%
com.aws.greengrass.lifecyclemanager.LogManagerHelper 100% 100% 0%
com.aws.greengrass.lifecyclemanager.UnloadableService 25% 25% 0%
com.aws.greengrass.lifecyclemanager.RunWithPathOwnershipHandler 89% 96% 83%
com.aws.greengrass.lifecyclemanager.KernelAlternatives 16% 19% 14%
com.aws.greengrass.lifecyclemanager.ShellRunner$Default 73% 76% 71%
com.aws.greengrass.lifecyclemanager.GreengrassService 89% 89% 88%
com.aws.greengrass.lifecyclemanager.Lifecycle$DesiredStateUpdatedEvent 100% 100% 0%
com.aws.greengrass.lifecyclemanager.GenericExternalService 71% 77% 65%
com.aws.greengrass.lifecyclemanager.GreengrassService$RunStatus 100% 100% 0%
com.aws.greengrass.lifecyclemanager.Lifecycle 81% 82% 79%
com.aws.greengrass.lifecyclemanager.Kernel 57% 57% 57%
com.aws.greengrass.lifecyclemanager.KernelMetricsEmitter 100% 100% 100%
com.aws.greengrass.lifecyclemanager.Lifecycle$StateEvent 100% 100% 0%
com.aws.greengrass.lifecyclemanager.KernelCommandLine 56% 61% 51%
com.aws.greengrass.lifecyclemanager.GenericExternalService$RunResult 100% 100% 0%
com.aws.greengrass.lifecyclemanager.Kernel$1 0% 0% 0%
com.aws.greengrass.lifecyclemanager.KernelLifecycle 78% 80% 77%
com.aws.greengrass.lifecyclemanager.PluginService 67% 68% 66%
com.aws.greengrass.lifecyclemanager.UpdateSystemPolicyService 84% 84% 83%
com.aws.greengrass.util.platforms.unix.linux.CgroupManager 73% 86% 60%
com.aws.greengrass.util.platforms.unix.linux.LinuxSystemResourceController 67% 72% 63%
com.aws.greengrass.util.platforms.unix.linux.LinuxPlatform 100% 100% 0%
com.aws.greengrass.util.platforms.unix.linux.CgroupV1 0% 0% 0%
com.aws.greengrass.util.platforms.unix.linux.CgroupV2 74% 89% 60%
com.aws.greengrass.deployment.converter.DeploymentDocumentConverter 72% 79% 65%
com.aws.greengrass.ipc.AuthenticationHandler 30% 35% 25%
com.aws.greengrass.ipc.IPCEventStreamService 73% 80% 66%
com.aws.greengrass.jna.Kernel32Ex 0% 0% 0%
com.aws.greengrass.builtin.services.configstore.ConfigStoreIPCEventStreamAgent$UpdateConfigurationOperationHandler 68% 73% 63%
com.aws.greengrass.builtin.services.configstore.ConfigStoreIPCEventStreamAgent 78% 81% 75%
com.aws.greengrass.builtin.services.configstore.ConfigStoreIPCEventStreamAgent$ConfigurationUpdateOperationHandler 76% 90% 62%
com.aws.greengrass.builtin.services.configstore.ConfigStoreIPCEventStreamAgent$GetConfigurationOperationHandler 67% 78% 57%
com.aws.greengrass.builtin.services.configstore.ConfigStoreIPCEventStreamAgent$SendConfigurationValidityReportOperationHandler 78% 90% 66%
com.aws.greengrass.builtin.services.configstore.ConfigStoreIPCEventStreamAgent$ValidateConfigurationUpdatesOperationHandler 95% 95% 0%
com.aws.greengrass.ipc.common.DefaultOperationHandler 0% 0% 0%
com.aws.greengrass.security.SecurityService$DefaultCryptoKeyProvider 24% 23% 25%
com.aws.greengrass.security.SecurityService 38% 51% 25%
com.aws.greengrass.provisioning.ProvisioningPluginFactory 100% 100% 0%
com.aws.greengrass.provisioning.ProvisioningConfigUpdateHelper 75% 100% 50%
com.aws.greengrass.componentmanager.builtins.GreengrassRepositoryDownloader 0% 0% 0%
com.aws.greengrass.componentmanager.builtins.S3Downloader 10% 17% 3%
com.aws.greengrass.componentmanager.builtins.ArtifactDownloaderFactory 52% 63% 42%
com.aws.greengrass.componentmanager.builtins.ArtifactDownloader 16% 21% 11%
com.aws.greengrass.builtin.services.lifecycle.LifecycleIPCEventStreamAgent$UpdateStateOperationHandler 60% 60% 0%
com.aws.greengrass.builtin.services.lifecycle.LifecycleIPCEventStreamAgent$DeferComponentUpdateHandler 88% 88% 0%
com.aws.greengrass.builtin.services.lifecycle.LifecycleIPCEventStreamAgent 59% 61% 56%
com.aws.greengrass.builtin.services.lifecycle.LifecycleIPCEventStreamAgent$SubscribeToComponentUpdateOperationHandler 57% 64% 50%
com.aws.greengrass.builtin.services.lifecycle.LifecycleIPCEventStreamAgent$PauseComponentHandler 56% 62% 50%
com.aws.greengrass.builtin.services.lifecycle.LifecycleIPCEventStreamAgent$ResumeComponentHandler 56% 62% 50%
com.aws.greengrass.builtin.services.pubsub.PubSubIPCEventStreamAgent$PublishToTopicOperationHandler 70% 90% 50%
com.aws.greengrass.builtin.services.pubsub.SubscriptionTrie 70% 76% 64%
com.aws.greengrass.builtin.services.pubsub.PubSubIPCEventStreamAgent 66% 73% 58%
com.aws.greengrass.builtin.services.pubsub.PubSubIPCEventStreamAgent$SubscribeToTopicOperationHandler 97% 94% 100%
com.aws.greengrass.telemetry.MetricsPayload 100% 100% 0%
com.aws.greengrass.telemetry.MetricsAggregator 79% 86% 72%
com.aws.greengrass.telemetry.MetricsAggregator$1 100% 100% 0%
com.aws.greengrass.telemetry.AggregatedMetric 100% 100% 0%
com.aws.greengrass.telemetry.TelemetryAgent 59% 71% 48%
com.aws.greengrass.telemetry.TelemetryConfiguration 30% 51% 10%
com.aws.greengrass.telemetry.PeriodicMetricsEmitter 100% 100% 0%
com.aws.greengrass.telemetry.TelemetryAgent$1 20% 20% 0%
com.aws.greengrass.telemetry.SystemMetricsEmitter 100% 100% 100%
com.aws.greengrass.deployment.DeploymentConfigMerger 69% 69% 68%
com.aws.greengrass.deployment.IotJobsHelper$IotJobsClientFactory 100% 100% 0%
com.aws.greengrass.deployment.DeploymentConfigMerger$AggregateServicesChangeManager 77% 74% 80%
com.aws.greengrass.deployment.DeviceConfiguration 69% 73% 65%
com.aws.greengrass.deployment.DeploymentDocumentDownloader 14% 14% 0%
com.aws.greengrass.deployment.DeploymentQueue 61% 68% 55%
com.aws.greengrass.deployment.DeploymentService 69% 71% 68%
com.aws.greengrass.deployment.EndpointSwitchState 29% 41% 16%
com.aws.greengrass.deployment.IotJobsHelper$LatestQueuedJobs 19% 19% 0%
com.aws.greengrass.deployment.KernelUpdateDeploymentTask 18% 28% 7%
com.aws.greengrass.deployment.DynamicComponentConfigurationValidator 85% 82% 87%
com.aws.greengrass.deployment.DefaultDeploymentTask 68% 76% 61%
com.aws.greengrass.deployment.DeploymentDirectoryManager 64% 78% 50%
com.aws.greengrass.deployment.IotJobsHelper$WrapperMqttConnectionFactory 100% 100% 0%
com.aws.greengrass.deployment.IotJobsHelper 34% 40% 28%
com.aws.greengrass.deployment.IotJobsHelper$1 14% 14% 0%
com.aws.greengrass.deployment.EndpointSwitchPreflightValidator 10% 10% 0%
com.aws.greengrass.deployment.ThingGroupHelper 27% 38% 16%
com.aws.greengrass.deployment.ShadowDeploymentListener 42% 51% 33%
com.aws.greengrass.deployment.ShadowDeploymentListener$1 14% 14% 0%
com.aws.greengrass.deployment.DeploymentStatusKeeper 81% 91% 71%
com.aws.greengrass.deployment.IotJobsClientWrapper 35% 41% 30%
com.aws.greengrass.util.orchestration.SystemServiceUtilsFactory 0% 0% 0%
com.aws.greengrass.util.orchestration.ProcdUtils 0% 0% 0%
com.aws.greengrass.util.orchestration.SystemServiceUtils 0% 0% 0%
com.aws.greengrass.util.orchestration.InitUtils 0% 0% 0%
com.aws.greengrass.util.orchestration.SystemdUtils 0% 0% 0%
com.aws.greengrass.util.orchestration.WinswUtils 0% 0% 0%
com.aws.greengrass.testing.TestFeatureParameters 83% 100% 66%
com.aws.greengrass.testing.TestFeatureParameters$1 100% 100% 0%
com.aws.greengrass.ipc.modules.PubSubIPCService 81% 81% 0%
com.aws.greengrass.ipc.modules.AuthorizationService 100% 100% 0%
com.aws.greengrass.ipc.modules.ComponentMetricIPCService 69% 69% 0%
com.aws.greengrass.ipc.modules.MqttProxyIPCService 78% 78% 0%
com.aws.greengrass.ipc.modules.LifecycleIPCService 86% 86% 0%
com.aws.greengrass.ipc.modules.ConfigStoreIPCService 100% 100% 0%
com.aws.greengrass.easysetup.GreengrassSetup 0% 0% 0%
com.aws.greengrass.easysetup.DeviceProvisioningHelper 0% 0% 0%

Minimum allowed coverage is 58%

Generated by 🐒 cobertura-action against 7a2f726

@aws-kevinrickard
aws-kevinrickard force-pushed the v228-dependency-default-flag branch from 240215f to 3a647ea Compare July 17, 2026 00:53
aws-kevinrickard and others added 7 commits July 17, 2026 11:17
Builtin autostart services are injected into main's dependency map at
launch as default dependencies. When a deployment writes the
services/main/dependencies topic, setupDependencies re-added every
listed service with isDefault=false, silently erasing that protection.
Any later topic update lacking the builtins then evicted them from the
dependency graph while their services kept running.

This happens in practice when a device's first-ever deployment fails
after activation and rolls back: the rollback snapshot's dependencies
topic value predates the deployment and lists only the nucleus, so
replaying it evicts every builtin service. Because deployment merge
protection for builtin services is keyed on kernel.orderedDependencies(),
the next deployment then deletes the builtins' entire config subtrees
(including DeploymentService group membership state and the
FleetStatusService sequence number) while the services keep writing to
the detached config nodes. Subsequent tlog truncations omit the detached
subtrees, so the loss surfaces only at the next restart, where the
device loses its thing group state and deployments tear down installed
components.

Preserve the existing isDefault flag when a dependency is re-added from
the topic so default dependencies can never be evicted by a topic
update. The flag was not read anywhere except the eviction filter, and
launch() re-injects builtins as default at every boot, so no behavior
other than the erroneous eviction is affected.
Deployment merges protect builtin services' config with per-service
MERGE overrides keyed on kernel.orderedDependencies(). The live
dependency graph is not a safe key: a builtin service can be
erroneously missing from it while still running, in which case the
services-level REPLACE wildcard silently deletes the builtin's entire
config subtree (deployment group state, fleet status sequence number,
telemetry state) out from under the running service.

Derive the protected name set from static builtin identity
(KernelLifecycle autostart builtin annotations) and union it with the
graph-derived set. Autostart plugin builtins, whose names are not
statically known, remain covered by the graph-derived set. Protecting
a builtin that is not currently in the graph is harmless since a MERGE
override only retains existing config.

Also add the same builtin protection to the rollback merge behavior,
which previously had none: replaying a snapshot that lacks a builtin's
config (e.g. one dumped while the builtin's subtree was detached)
discarded the builtin's entire config. Values present in the snapshot
are restored exactly as before; the only behavior change is that nodes
newly created under a builtin during the failed deployment are retained
rather than discarded.

createRollbackMergeBehavior is widened from private to protected for
testability, matching createDeploymentMergeBehavior.
KernelConfigResolver.getMainConfig carried builtin services into main's
resolved dependencies only when they were present in the live dependency
graph (kernel.getMain().getDependencies()). If a builtin was erroneously
missing from the graph while its service was still running, every
subsequent deployment resolution omitted it, so the damage was
self-sustaining: the dependencies topic never listed the builtins again
until the next restart re-injected them at launch.

Union the static autostart builtin names (as name:HARD) with the
graph-derived carry-forward. On a healthy device the graph already
contains every autostart builtin, so the resolved output is unchanged.
On a damaged device, merging the resolved config now repairs the
dependency graph — the dependencies topic subscriber re-adds the missing
builtins — instead of perpetuating the damage until reboot. Builtins
present in the graph keep their actual dependency type; the static union
only applies to names absent from the graph. Autostart builtins are
compiled into the nucleus, so a statically added name always resolves.
When a deployment resolution repairs a damaged dependency graph by
listing a previously evicted autostart builtin in main's dependencies
topic, the subscriber re-added it as a non-default dependency, since no
prior dependency info existed to preserve the flag from. The healed
state was therefore weaker than a freshly launched one: a later topic
update omitting the builtins (for example another rollback) could evict
them again.

Treat the statically known autostart builtin names as default
dependencies when main re-adds them from the topic. This is exactly the
set launch() injects as default at every boot, so healing now restores
launch-equivalent protection. Other services declaring the same names
keep the existing removable-dependency behavior.
Deduplicate the static-plus-graph builtin name union used by both the
deployment and rollback merge behaviors into a single helper so the two
protection sites cannot drift. Also guard the static name derivation
against a builtin class missing its annotation, and fix test import
ordering.
Document in getBuiltinServiceNamesToProtect the residual asymmetry for
annotation-discovered plugin autostart builtins (protected only while
present in the dependency graph and not healed by the static
carry-forward). Assert the preserved default flag directly in the
flag-preservation test, pinning that contract in isolation from the
static-name default restoration for main. Use getServiceName
consistently in the new code paths.
Add two baseline tests for the eviction-filter behavior the builtin
protection layers rest on: a default dependency which was never listed
in the dependencies topic survives a topic write omitting it (with its
default flag untouched), and re-adding an evicted dependency with
isDefault=true restores its protection against subsequent omissions —
the mechanism by which launch() repairs the dependency graph at boot.

Adapted from the characterization test suite in aws-greengrass#1823, which this
change set supersedes.

Co-authored-by: saranyailla <nukai@amazon.com>
@aws-kevinrickard
aws-kevinrickard force-pushed the v228-dependency-default-flag branch from 3a647ea to 7a2f726 Compare July 17, 2026 18:17
@aws-kevinrickard

Copy link
Copy Markdown
Member Author

Nit (non-blocking): getMainConfig uses both accessors for the same value — greengrassService.getName() for the dependency entry and greengrassService.getServiceName() for the dedup set. In production they're identical (getName() is a one-line delegate to getServiceName()), but on Mockito mocks the delegation doesn't exist, so they're separately stubbable: an unstubbed getServiceName() in the older mock-based resolver tests silently adds null to the dedup set. Harmless today since those tests assert containment, but using one accessor in both places would remove the trap.

Long term: getName()/getServiceName() is historical duplication (the alias pair predates the 2020 EvergreenService → GreengrassService rename). It would be worth deprecating getName() and migrating call sites to getServiceName() in a separate cleanup, so new code stops picking accessors at random and mock-based tests can't diverge on which one is stubbed. Fine as a follow-up; not something this PR needs to take on.

@aws-kevinrickard
aws-kevinrickard merged commit a28341c into aws-greengrass:main Jul 20, 2026
4 of 5 checks passed
yitingb pushed a commit that referenced this pull request Aug 3, 2026
…1824)

* fix: preserve default flag when re-adding dependencies from topic

Builtin autostart services are injected into main's dependency map at
launch as default dependencies. When a deployment writes the
services/main/dependencies topic, setupDependencies re-added every
listed service with isDefault=false, silently erasing that protection.
Any later topic update lacking the builtins then evicted them from the
dependency graph while their services kept running.

This happens in practice when a device's first-ever deployment fails
after activation and rolls back: the rollback snapshot's dependencies
topic value predates the deployment and lists only the nucleus, so
replaying it evicts every builtin service. Because deployment merge
protection for builtin services is keyed on kernel.orderedDependencies(),
the next deployment then deletes the builtins' entire config subtrees
(including DeploymentService group membership state and the
FleetStatusService sequence number) while the services keep writing to
the detached config nodes. Subsequent tlog truncations omit the detached
subtrees, so the loss surfaces only at the next restart, where the
device loses its thing group state and deployments tear down installed
components.

Preserve the existing isDefault flag when a dependency is re-added from
the topic so default dependencies can never be evicted by a topic
update. The flag was not read anywhere except the eviction filter, and
launch() re-injects builtins as default at every boot, so no behavior
other than the erroneous eviction is affected.

* fix: key builtin merge protection on static builtin names

Deployment merges protect builtin services' config with per-service
MERGE overrides keyed on kernel.orderedDependencies(). The live
dependency graph is not a safe key: a builtin service can be
erroneously missing from it while still running, in which case the
services-level REPLACE wildcard silently deletes the builtin's entire
config subtree (deployment group state, fleet status sequence number,
telemetry state) out from under the running service.

Derive the protected name set from static builtin identity
(KernelLifecycle autostart builtin annotations) and union it with the
graph-derived set. Autostart plugin builtins, whose names are not
statically known, remain covered by the graph-derived set. Protecting
a builtin that is not currently in the graph is harmless since a MERGE
override only retains existing config.

Also add the same builtin protection to the rollback merge behavior,
which previously had none: replaying a snapshot that lacks a builtin's
config (e.g. one dumped while the builtin's subtree was detached)
discarded the builtin's entire config. Values present in the snapshot
are restored exactly as before; the only behavior change is that nodes
newly created under a builtin during the failed deployment are retained
rather than discarded.

createRollbackMergeBehavior is widened from private to protected for
testability, matching createDeploymentMergeBehavior.

* fix: carry builtin services forward in main config by static identity

KernelConfigResolver.getMainConfig carried builtin services into main's
resolved dependencies only when they were present in the live dependency
graph (kernel.getMain().getDependencies()). If a builtin was erroneously
missing from the graph while its service was still running, every
subsequent deployment resolution omitted it, so the damage was
self-sustaining: the dependencies topic never listed the builtins again
until the next restart re-injected them at launch.

Union the static autostart builtin names (as name:HARD) with the
graph-derived carry-forward. On a healthy device the graph already
contains every autostart builtin, so the resolved output is unchanged.
On a damaged device, merging the resolved config now repairs the
dependency graph — the dependencies topic subscriber re-adds the missing
builtins — instead of perpetuating the damage until reboot. Builtins
present in the graph keep their actual dependency type; the static union
only applies to names absent from the graph. Autostart builtins are
compiled into the nucleus, so a statically added name always resolves.

* fix: restore default flag for autostart builtins re-added to main

When a deployment resolution repairs a damaged dependency graph by
listing a previously evicted autostart builtin in main's dependencies
topic, the subscriber re-added it as a non-default dependency, since no
prior dependency info existed to preserve the flag from. The healed
state was therefore weaker than a freshly launched one: a later topic
update omitting the builtins (for example another rollback) could evict
them again.

Treat the statically known autostart builtin names as default
dependencies when main re-adds them from the topic. This is exactly the
set launch() injects as default at every boot, so healing now restores
launch-equivalent protection. Other services declaring the same names
keep the existing removable-dependency behavior.

* refactor: extract shared builtin protection name set helper

Deduplicate the static-plus-graph builtin name union used by both the
deployment and rollback merge behaviors into a single helper so the two
protection sites cannot drift. Also guard the static name derivation
against a builtin class missing its annotation, and fix test import
ordering.

* refactor: address review feedback on builtin protection

Document in getBuiltinServiceNamesToProtect the residual asymmetry for
annotation-discovered plugin autostart builtins (protected only while
present in the dependency graph and not healed by the static
carry-forward). Assert the preserved default flag directly in the
flag-preservation test, pinning that contract in isolation from the
static-name default restoration for main. Use getServiceName
consistently in the new code paths.

* test: pin default dependency eviction filter invariants

Add two baseline tests for the eviction-filter behavior the builtin
protection layers rest on: a default dependency which was never listed
in the dependencies topic survives a topic write omitting it (with its
default flag untouched), and re-adding an evicted dependency with
isDefault=true restores its protection against subsequent omissions —
the mechanism by which launch() repairs the dependency graph at boot.

Adapted from the characterization test suite in #1823, which this
change set supersedes.

Co-authored-by: saranyailla <nukai@amazon.com>

---------

Co-authored-by: saranyailla <nukai@amazon.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants