keiro-ops 0.13.0.0 → 0.14.0.0
raw patch · 4 files changed
+70/−9 lines, 4 filesdep ~keirodep ~keiro-migrationsdep ~keiro-pgmqPVP ok
version bump matches the API change (PVP)
Dependency ranges changed: keiro, keiro-migrations, keiro-pgmq
API changes (from Hackage documentation)
Files
- CHANGELOG.md +12/−0
- keiro-ops.cabal +6/−6
- src/Keiro/Ops/Outbox.hs +9/−3
- test/Main.hs +43/−0
CHANGELOG.md view
@@ -6,6 +6,18 @@ ## Unreleased +## 0.14.0.0 — 2026-08-21++### Breaking Changes++- Requires the lockstep `keiro ^>=0.14.0.0`, `keiro-pgmq ^>=0.14.0.0`, and+ `keiro-migrations ^>=0.14.0.0`.++### New Features++- `outbox list` accepts `--status rejected` and renders `rejected_at`,+ `rejection_code`, and bounded `rejection_detail` in human and JSON output.+ ## 0.13.0.0 — 2026-08-17 ### Breaking Changes
keiro-ops.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: keiro-ops-version: 0.13.0.0+version: 0.14.0.0 synopsis: Operational command-line interface for Keiro deployments description: A standalone database-only console and embeddable command tree for inspecting@@ -68,9 +68,9 @@ , effectful >=2.6 && <2.7 , effectful-core >=2.6 && <2.7 , hasql >=1.10 && <1.11- , keiro ^>=0.13.0.0- , keiro-migrations ^>=0.13.0.0- , keiro-pgmq ^>=0.13.0.0+ , keiro ^>=0.14.0.0+ , keiro-migrations ^>=0.14.0.0+ , keiro-pgmq ^>=0.14.0.0 , kiroku-store >=0.8 && <0.9 , optparse-applicative >=0.18 && <0.20 , text >=2.1 && <2.2@@ -104,9 +104,9 @@ , hasql >=1.10 && <1.11 , hasql-transaction >=1.1 && <1.3 , hspec >=2.11- , keiro ^>=0.13.0.0+ , keiro ^>=0.14.0.0 , keiro-ops- , keiro-pgmq ^>=0.13.0.0+ , keiro-pgmq ^>=0.14.0.0 , keiro-test-support , kiroku-store >=0.8 && <0.9 , optparse-applicative >=0.18 && <0.20
src/Keiro/Ops/Outbox.hs view
@@ -70,7 +70,7 @@ List <$> ( ListOptions <$> textOption "source" "SOURCE" "Producing bounded-context source"- <*> optional (option statusReader (long "status" <> metavar "STATUS" <> help "pending, publishing, sent, failed, or dead"))+ <*> optional (option statusReader (long "status" <> metavar "STATUS" <> help "pending, publishing, sent, rejected, failed, or dead")) <*> optional (textOption "destination" "DESTINATION" "Destination filter") <*> option positiveIntReader (long "limit" <> metavar "N" <> Opt.value 100 <> showDefault <> help "Maximum rows") )@@ -184,7 +184,7 @@ outboxListResult :: [OutboxRow] -> OpsResult outboxListResult outboxRows = OpsResult- { headers = ["id", "source", "destination", "status", "attempts", "created_at", "last_error"],+ { headers = ["id", "source", "destination", "status", "attempts", "created_at", "last_error", "rejected_at", "rejection_code", "rejection_detail"], rows = map outboxRow outboxRows, jsonValue = Aeson.toJSON (map outboxJson outboxRows) }@@ -197,7 +197,10 @@ statusText row.status, showText row.attemptCount, timeText row.createdAt,- maybe "" (truncateCell 120) row.lastError+ maybe "" (truncateCell 120) row.lastError,+ maybe "" timeText row.rejectedAt,+ maybe "" publishRejectionCode row.rejection,+ maybe "" (truncateCell 120) (row.rejection >>= publishRejectionDetail) ] outboxJson :: OutboxRow -> Value@@ -218,6 +221,9 @@ "next_attempt_at" .= row.nextAttemptAt, "last_error" .= row.lastError, "published_at" .= row.publishedAt,+ "rejected_at" .= row.rejectedAt,+ "rejection_code" .= fmap publishRejectionCode row.rejection,+ "rejection_detail" .= (row.rejection >>= publishRejectionDetail), "created_at" .= row.createdAt, "updated_at" .= row.updatedAt ]
test/Main.hs view
@@ -135,6 +135,7 @@ isParseSuccess (parseOps embeddedHooks ["rebuild", "external-read", "counter_reader", "1"]) `shouldBe` True isParseSuccess (parseOps embeddedHooks ["rebuild", "retire-external-read", "counter_reader", "1"]) `shouldBe` True isParseSuccess (parseOps embeddedHooks ["rebuild", "reproject-stream", "ops-group", "ops-projection", "orders-1"]) `shouldBe` True+ isParseSuccess (parseOps embeddedHooks ["outbox", "list", "--source", "ops-source", "--status", "rejected"]) `shouldBe` True isParseFailure (parseOps embeddedHooks ["rebuild", "retire-external-read", "counter_reader", "0"]) `shouldBe` True isParseFailure (parseOps embeddedHooks ["rebuild", "reproject-stream", "ops-group", "ops-projection", "orders-1", "--page-size", "0"]) `shouldBe` True @@ -1028,6 +1029,48 @@ applied <- OpsOutbox.runCommand (opsEnv True store) (OpsOutbox.RequeueStuck 0 10) applied `shouldSatisfy` isSucceeded outboxStatus store outboxId `shouldReturn` Just Outbox.OutboxFailed++ it "filters and renders rejected rows with bounded audit fields" $ \store -> do+ now <- getCurrentTime+ let outboxId = testOutboxId "018f5f43-8a70-7b9a-9a9b-59d391a76811"+ event = sampleIntegrationEvent now "outbox-rejected-message"+ rejection <-+ either (fail . show) pure $+ Outbox.mkPublishRejection "authorization.denied" (Just "the sink policy refused this message")+ expectStore store (runTransaction (Outbox.enqueueOutboxTx (Outbox.OutboxMessage outboxId event)))+ summary <-+ expectStore store $+ Outbox.publishClaimedOutbox+ (\rows -> pure [(row.outboxId, Outbox.PublishRejected rejection) | row <- rows])+ Outbox.defaultPublishOptions+ Nothing+ summary.rejected `shouldBe` 1++ listed <-+ OpsOutbox.runCommand+ (opsEnv False store)+ ( OpsOutbox.List+ OpsOutbox.ListOptions+ { source = "ops-source",+ status = Just Outbox.OutboxRejected,+ destination = Nothing,+ limit = 10+ }+ )+ humanField "status" listed `shouldBe` Just "rejected"+ humanField "rejection_code" listed `shouldBe` Just "authorization.denied"+ humanField "rejection_detail" listed `shouldBe` Just "the sink policy refused this message"+ humanField "rejected_at" listed `shouldSatisfy` maybe False (not . Text.null)+ case listed of+ Succeeded OpsResult {jsonValue = Aeson.Array values} ->+ case values Vector.!? 0 of+ Just (Aeson.Object row) -> do+ KeyMap.lookup "status" row `shouldBe` Just (Aeson.String "rejected")+ KeyMap.lookup "rejection_code" row `shouldBe` Just (Aeson.String "authorization.denied")+ KeyMap.lookup "rejection_detail" row `shouldBe` Just (Aeson.String "the sink policy refused this message")+ KeyMap.lookup "rejected_at" row `shouldSatisfy` maybe False (/= Aeson.Null)+ other -> expectationFailure ("expected one rejected JSON object, got " <> show other)+ other -> expectationFailure ("expected a successful rejected-row listing, got " <> show other) it "surfaces dispatch dead letters through the supported API" $ \store -> do let sourceEvent = EventId (testUuid "018f5f43-8a70-7b9a-9a9b-59d391a76802")