packages feed

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 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")