feat: add Fly sync target and CLI commands#86
Conversation
…arned patterns Update architecture doc with FetchResult type, DiffCommonArgs split, error classification rules, and 3 new anti-patterns from Epic 2 retrospective. Add sprint change proposal documenting the analysis.
Implement FlySyncTarget in zopp-sync crate with FlyApi trait abstraction for testability. Wire up `zopp sync fly --app <name>` and `zopp diff fly --app <name>` CLI commands following the established AWS pattern. Fly secrets are write-only (API returns labels but not values), so fetch_current returns empty-string values. The diff engine detects add/remove by key presence; use --force to push all values. Includes unit tests for FlySyncTarget (mock-based) and CLI parsing tests for both sync and diff Fly subcommands.
…Fly sync - Add send_with_retry() with exponential backoff for 429/5xx responses (max 3 retries, 1s/2s/4s delays) to satisfy AC7 - Percent-encode app names and secret labels in URL paths to prevent path injection with special characters - Wrap FLY_API_TOKEN in ZeroizeOnDrop struct per project security standards - Add unit tests for URL encoding and retryable status classification
There was a problem hiding this comment.
2 issues found across 16 files
Confidence score: 3/5
- There is concrete regression risk in
apps/zopp-cli/src/commands/sync_fly.rs:sync flycurrently ignores--force, so update operations run unconditionally and can rewrite secrets on every execution. apps/zopp-cli/src/commands/diff_fly.rscan under-report changes when zopp values are empty strings, which meansdiff flymay miss required updates for write-only Fly targets.- Given both issues are medium severity (6/10) with high confidence, this is a meaningful user-facing behavior risk rather than a minor housekeeping concern.
- Pay close attention to
apps/zopp-cli/src/commands/sync_fly.rsandapps/zopp-cli/src/commands/diff_fly.rs- force-flag handling and empty-value diff logic can cause incorrect update behavior.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/zopp-cli/src/commands/sync_fly.rs">
<violation number="1" location="apps/zopp-cli/src/commands/sync_fly.rs:121">
P2: `sync fly` ignores `--force` and always includes update operations, causing unconditional secret rewrites on every run.</violation>
</file>
<file name="apps/zopp-cli/src/commands/diff_fly.rs">
<violation number="1" location="apps/zopp-cli/src/commands/diff_fly.rs:121">
P2: `diff fly` can miss updates for keys whose zopp value is empty, because Fly placeholder values are also empty strings. For write-only targets, overlapping keys should be treated as updates regardless of value equality.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
2 issues found across 16 files
Confidence score: 3/5
- There is concrete regression risk in
apps/zopp-cli/src/commands/sync_fly.rs:sync flycurrently ignores--force, so update operations run unconditionally and can rewrite secrets on every execution. apps/zopp-cli/src/commands/diff_fly.rscan under-report changes when zopp values are empty strings, which meansdiff flymay miss required updates for write-only Fly targets.- Given both issues are medium severity (6/10) with high confidence, this is a meaningful user-facing behavior risk rather than a minor housekeeping concern.
- Pay close attention to
apps/zopp-cli/src/commands/sync_fly.rsandapps/zopp-cli/src/commands/diff_fly.rs- force-flag handling and empty-value diff logic can cause incorrect update behavior.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/zopp-cli/src/commands/sync_fly.rs">
<violation number="1" location="apps/zopp-cli/src/commands/sync_fly.rs:121">
P2: `sync fly` ignores `--force` and always includes update operations, causing unconditional secret rewrites on every run.</violation>
</file>
<file name="apps/zopp-cli/src/commands/diff_fly.rs">
<violation number="1" location="apps/zopp-cli/src/commands/diff_fly.rs:121">
P2: `diff fly` can miss updates for keys whose zopp value is empty, because Fly placeholder values are also empty strings. For write-only targets, overlapping keys should be treated as updates regardless of value equality.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
|
@cubic-dev-ai Please do a full re-review of the PR. |
@vieiralucas I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
3 issues found across 17 files
Confidence score: 2/5
- There is a high regression risk in
crates/zopp-sync/src/fly/client.rs: targeting/secretsinstead of documented/secretkeysis likely to break Fly sync/diff behavior against the live API. apps/zopp-cli/src/commands/sync_fly.rshas a concrete behavior bug where--forceis ignored, so existing Fly keys are always reapplied, which can cause unexpected repeated updates.apps/zopp-cli/src/commands/diff_fly.rscan miss real changes when desired values are empty strings, because comparison against Fly placeholder""values hides updates; this is user-facing diff accuracy risk.- Pay close attention to
crates/zopp-sync/src/fly/client.rs,apps/zopp-cli/src/commands/sync_fly.rs,apps/zopp-cli/src/commands/diff_fly.rs- endpoint mismatch and sync/diff correctness issues should be resolved before relying on this behavior.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/zopp-cli/src/commands/sync_fly.rs">
<violation number="1" location="apps/zopp-cli/src/commands/sync_fly.rs:121">
P2: `--force` is ignored: existing Fly keys are always treated as updates and reapplied on every sync.</violation>
</file>
<file name="apps/zopp-cli/src/commands/diff_fly.rs">
<violation number="1" location="apps/zopp-cli/src/commands/diff_fly.rs:121">
P2: `diff fly` can miss updates for keys whose desired value is empty string because it relies on value equality against Fly’s placeholder `""` values.</violation>
</file>
<file name="crates/zopp-sync/src/fly/client.rs">
<violation number="1" location="crates/zopp-sync/src/fly/client.rs:182">
P1: The client targets `/secrets` endpoints, but current Machines API docs expose app secret management under `/secretkeys`; this likely breaks Fly sync/diff behavior against the live API.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| impl FlyApi for FlyClient { | ||
| async fn list_secrets(&self, app: &str) -> Result<Vec<SecretEntry>, SyncError> { | ||
| let app_encoded = encode_path_segment(app); | ||
| let url = format!("{BASE_URL}/v1/apps/{app_encoded}/secrets"); |
There was a problem hiding this comment.
P1: The client targets /secrets endpoints, but current Machines API docs expose app secret management under /secretkeys; this likely breaks Fly sync/diff behavior against the live API.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At crates/zopp-sync/src/fly/client.rs, line 182:
<comment>The client targets `/secrets` endpoints, but current Machines API docs expose app secret management under `/secretkeys`; this likely breaks Fly sync/diff behavior against the live API.</comment>
<file context>
@@ -0,0 +1,306 @@
+impl FlyApi for FlyClient {
+ async fn list_secrets(&self, app: &str) -> Result<Vec<SecretEntry>, SyncError> {
+ let app_encoded = encode_path_segment(app);
+ let url = format!("{BASE_URL}/v1/apps/{app_encoded}/secrets");
+
+ let resp = send_with_retry(|| self.http.get(&url).bearer_auth(&self.token.0)).await?;
</file context>
…rets Fly API returns empty-string values for secrets. Without filtering, the diff engine treats all overlapping keys as Updates. Now: - sync fly: only applies Updates when --force is set - diff fly: filters out phantom Updates entirely
|
@cubic-dev-ai Please do a full re-review of the PR. |
@vieiralucas I have started the AI code review. It will take a few minutes to complete. |
| // Note: Fly secrets are write-only, so values from fetch are empty strings. | ||
| // Diff can reliably detect add/remove but not value changes. | ||
| // Filter out Update ops since they're phantom diffs (empty vs actual value). | ||
| // Users should use `zopp sync fly --force` to push all values. |
There was a problem hiding this comment.
i think we should just overwrite by default instead
Fly secrets are write-only so we can't compare values. Instead of filtering out updates and requiring --force, always treat overlapping keys as updates to ensure Fly has current values.
|
Addressed review findings in 87bb800:
|
There was a problem hiding this comment.
1 issue found across 2 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/zopp-cli/src/commands/sync_fly.rs">
<violation number="1" location="apps/zopp-cli/src/commands/sync_fly.rs:121">
P1: `sync fly` no longer respects `--force`: all overlapping secrets are treated as updates and applied unconditionally.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| // Note: Fly secrets are write-only (values are empty strings from fetch). | ||
| // All overlapping keys show as updates since we can't compare values. | ||
| // This is correct behavior — we always overwrite to ensure Fly has current values. | ||
| let operations = zopp_sync::diff(&zopp_map, &fetch_result.secrets); |
There was a problem hiding this comment.
P1: sync fly no longer respects --force: all overlapping secrets are treated as updates and applied unconditionally.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/zopp-cli/src/commands/sync_fly.rs, line 121:
<comment>`sync fly` no longer respects `--force`: all overlapping secrets are treated as updates and applied unconditionally.</comment>
<file context>
@@ -116,18 +116,9 @@ pub async fn cmd_sync_fly(
- };
+ // All overlapping keys show as updates since we can't compare values.
+ // This is correct behavior — we always overwrite to ensure Fly has current values.
+ let operations = zopp_sync::diff(&zopp_map, &fetch_result.secrets);
// 6. Dry-run or no changes: show diff output
</file context>
| let operations = zopp_sync::diff(&zopp_map, &fetch_result.secrets); | |
| let all_operations = zopp_sync::diff(&zopp_map, &fetch_result.secrets); | |
| let operations: Vec<DiffOperation> = if common.force { | |
| all_operations | |
| } else { | |
| all_operations | |
| .into_iter() | |
| .filter(|op| !matches!(op, DiffOperation::Update { .. })) | |
| .collect() | |
| }; |
|
Cubic incremental review flagged that |
Summary
FlySyncTargetinzopp-synccrate withFlyApitrait abstraction for testabilityzopp sync fly --app <name>andzopp diff fly --app <name>CLI commands following the established AWS patternfetch_current()returns labels with empty-string values since Fly API doesn't expose secret valuesKey Design Decisions
--forceto ensure all values are current.set_secretsAPI call; removes processed individually viaunset_secretsync statusis AWS-specific (requires--region)Test Plan
FlySyncTarget(fetch, apply add/remove/update, auth error, empty state, mixed errors)FlyClient(URL encoding, retryable status codes)cargo fmt --checkcleancargo clippy --workspace --all-targets --all-featurescleanSummary by cubic
Adds a Fly sync target and CLI commands to diff and sync zopp secrets with Fly apps for Story 3.1. Sync now always overwrites overlapping keys to handle Fly’s write‑only secrets, with retries, TLS hardening, and a small CI timeout tweak.
New Features
FlySyncTargetinzopp-syncwith aFlyApiclient; new commands:zopp sync fly --app <name>andzopp diff fly --app <name>.--forceneeded).FLY_API_TOKENzeroized in memory,reqwestviarustls-tlswithwebpki-roots, bumpedquinn-prototo0.11.14, and increased Playwright dev server timeout to 5m for CI.Migration
FLY_API_TOKEN(create at https://fly.io/user/personal_access_tokens).zopp diff fly --app <name>to preview, thenzopp sync fly --app <name>to apply.Written for commit 87bb800. Summary will update on new commits.