Skip to content

Feat/cloud run deploy#22

Merged
emilwojtaszek merged 14 commits into
mainfrom
feat/cloud-run-deploy
Feb 8, 2026
Merged

Feat/cloud run deploy#22
emilwojtaszek merged 14 commits into
mainfrom
feat/cloud-run-deploy

Conversation

@emilwojtaszek

@emilwojtaszek emilwojtaszek commented Feb 8, 2026

Copy link
Copy Markdown
Member

Summary by CodeRabbit

  • New Features

    • Added a lightweight /healthz health check endpoint.
  • UI Updates

    • Large theme overhaul to a unified zinc-based palette; refreshed borders, spacing, radii, and sizing.
    • Refreshed component styles across flash, headers, tables, lists, badges, cards, certificates, dashboard, and invoice views.
    • Token management: streaming-backed list, improved create/revoke flows and token UI.
  • Infrastructure

    • Added CI and deploy workflows, Cloud Run service config, GCP setup script, docker-compose for document service, ignore files, and example env vars.
  • Tests

    • Updated tests to match new styling and selectors.

emilwojtaszek and others added 9 commits February 8, 2026 11:18
Replace colorful Phoenix/Elixir themes with neutral zinc-based palette
mimicking shadcn/ui defaults. Replace all shadow-sm/shadow-xl with
border border-base-300 for a flat, clean look. Replace DaisyUI stat
component with plain bordered divs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace colorful DaisyUI component classes (badge-success, badge-warning,
alert-info, table-zebra, card-body, etc.) with subtle, bordered pill
styles using Tailwind opacity modifiers (bg-success/10, text-success,
border-success/20). This creates a consistent, flat, neutral aesthetic
across all pages while keeping DaisyUI structural components (drawer,
dropdown, menu) intact.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Reduce Docker build context and prevent secrets from leaking.
docker-compose.yml provides Gotenberg sidecar for local dev.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- KsefHub.Release: migrate/0 and rollback/2 for prod releases
- GET /healthz returns 200 JSON for Cloud Run probes
- Health route placed outside auth pipelines

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Provisions Artifact Registry, service accounts, Workload Identity
Federation for GitHub OIDC, and Secret Manager secrets.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Declarative service.yaml with ksef-hub (ingress) + gotenberg (sidecar),
health probes, secret references, and 0-3 autoscaling.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Runs on PR + push to main: deps, format check, compile with warnings
as errors, credo, and tests against PostgreSQL 16.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Authenticates via Workload Identity Federation, builds and pushes
Docker image to Artifact Registry, runs DB migrations via Cloud Run
Jobs, and deploys multi-container service.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Add PORT, PHX_SERVER, POOL_SIZE, and ECTO_IPV6 entries used
by the Cloud Run deployment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Feb 8, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Adds CI/CD and deployment workflows, GCP provisioning and Docker tooling, a Cloud Run service (with gotenberg sidecar), a health endpoint and release helpers, extensive Tailwind/DaisyUI theme and component restyling, LiveView token streaming, updated tests, and multiple ignore/config files.

Changes

Cohort / File(s) Summary
Ignore & deploy helper files
\.dockerignore, \.gcloudignore, docker-compose.yml, scripts/setup-gcp.sh
New ignore files and docker-compose entry for Gotenberg; GCP provisioning script to create Artifact Registry, service accounts, Workload Identity Federation, Secrets Manager entries, and IAM bindings.
GitHub Actions
.github/workflows/ci.yml, .github/workflows/deploy.yml
New CI workflow (Elixir/OTP matrix, Postgres service, cache, format/credo/tests) and Deploy workflow to build/push Docker image to Artifact Registry, run migrations, and deploy to Cloud Run using Workload Identity and Cloud SDK.
Cloud Run manifest
cloud-run/service.yaml
Adds Knative/Cloud Run service with main app and gotenberg sidecar, probes, resource limits, env/secret wiring, startup CPU boost and autoscaling annotations.
Release & health endpoints
lib/ksef_hub/release.ex, lib/ksef_hub_web/controllers/health_controller.ex, lib/ksef_hub_web/router.ex
Adds release helper module (migrate/rollback), HealthController with GET /healthz returning JSON {"status":"ok"}, and route registration outside browser pipelines.
CSS / theme tokens
assets/css/app.css
Large theme token overhaul: zinc-based neutral palette for dark/light themes, adjusted radii/sizes, border thickness, and depth; replaces many color tokens.
UI components & layouts
lib/ksef_hub_web/components/core_components.ex, lib/ksef_hub_web/components/invoice_components.ex, lib/ksef_hub_web/components/layouts.ex
Refactors visual classes to Tailwind/daisyUI inline-pill/bordered patterns: flashes, headers, tables, lists, badges, dropdown styling; adds a couple of @specs for private helpers.
LiveViews & templates
lib/ksef_hub_web/live/*.ex, lib/ksef_hub_web/controllers/page_html/home.html.heex
Multiple LiveViews and templates converted from shadow/card to bordered/padded cards, badge→pill style updates, stat_card refactor, token list streaming, and padding/typography tweaks.
Token streaming & UI
lib/ksef_hub_web/live/token_live.ex
Switches token list to LiveView streams (stream_insert), updates create/revoke flows to stream updates, adds @spec format_datetime/1, and restyles token forms/alerts.
Refactor helpers
lib/ksef_hub_web/live/invoice_live/index.ex
Introduces filter_assigns/2 helper to centralize assign construction (refactor only).
Cloud tooling / compose
docker-compose.yml, cloud-run/service.yaml
Gotenberg service added to compose; Cloud Run manifest references same sidecar and ports.
Tests & config examples
test/ksef_hub_web/components/invoice_components_test.exs, test/ksef_hub_web/live/*, .env.example, mix.lock, mix.exs
Updated tests to match new CSS selectors/classes; .env.example adds PORT/PHX_SERVER/POOL_SIZE/ECTO_IPV6; minor mix file/mix.lock updates.

Sequence Diagram

sequenceDiagram
    participant GHA as GitHub Actions
    participant WIF as Workload Identity (GCP)
    participant AR as Artifact Registry
    participant CR as Cloud Run
    participant DB as Postgres DB

    GHA->>WIF: Request OIDC credentials
    activate WIF
    WIF-->>GHA: Return short-lived credentials
    deactivate WIF

    GHA->>AR: Build & push Docker image
    activate AR
    AR-->>GHA: Return image URL/digest
    deactivate AR

    GHA->>CR: Trigger migration job using pushed image
    activate CR
    CR->>DB: Run Ecto migrations
    DB-->>CR: Return migration result
    deactivate CR

    GHA->>CR: Deploy service referencing image
    CR-->>GHA: Deployment complete
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 I hopped through code and clouds with glee,

zinc-colors, pills, and a CI/CD tree,
images built and migrations spun,
gotenberg steams while health checks run—
a rabbit cheers: deploy and be merry!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 38.10% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat/cloud run deploy' accurately reflects the main objective of this PR, which is adding comprehensive Cloud Run deployment infrastructure including CI/CD workflows, Docker configuration, and service manifests.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/cloud-run-deploy

No actionable comments were generated in the recent review. 🎉

🧹 Recent nitpick comments
lib/ksef_hub_web/live/certificate_live.ex (1)

242-257: Consider a more specific typespec for the assigns parameter.

The @spec active_badge(map()) is quite generic. A more precise type would improve documentation and type checking.

Suggested improvement
-  `@spec` active_badge(map()) :: Phoenix.LiveView.Rendered.t()
+  `@spec` active_badge(%{active: boolean() | nil, optional(atom()) => any()}) :: Phoenix.LiveView.Rendered.t()

Alternatively, the second clause could use a simpler catch-all pattern if no other active values are expected:

-  defp active_badge(%{active: active} = assigns) when active in [false, nil] do
+  defp active_badge(assigns) do

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @.github/workflows/deploy.yml:
- Around line 40-84: Quote all unquoted variable expansions and the image/tag
arguments in the workflow steps to satisfy shellcheck (e.g.,
"${GCP_REGION}-docker.pkg.dev", "${IMAGE_NAME}:${{ github.sha }}",
"${IMAGE_NAME}:latest", "${SERVICE_NAME}", "${GCP_PROJECT_ID}"); ensure the
docker build/push tags and sed replacement use quoted expansions; change the
gcloud run jobs --args usage to pass a single quoted comma-separated string
(e.g., --args "eval,KsefHub.Release.migrate()") both in the execute and create
branches; and quote the --set-secrets value and service-account expansion when
creating the job and in the gcloud run services add-iam-policy-binding command
to avoid word-splitting and SC2086/SC2140 warnings.
🧹 Nitpick comments (9)
.dockerignore (1)

1-19: Consider adding .github/ to reduce Docker build context.

The .gcloudignore file excludes .github/ but this file doesn't. Adding it would reduce the Docker build context size and maintain consistency between ignore files.

Suggested addition
 docs/
+.github/
lib/ksef_hub_web/live/token_live.ex (2)

119-119: Use <.form> component instead of plain <form> tag.

Per coding guidelines, always use the imported Phoenix.Component.form/1 function component for forms. This ensures consistent behavior with LiveView form handling.

Suggested fix
-        <form phx-submit="create" phx-change="validate" class="space-y-4 mt-2">
+        <.form for={`@form`} phx-submit="create" phx-change="validate" class="space-y-4 mt-2" id="create-token-form">

And close with </.form> instead of </form>.


172-173: Add @spec to private functions.

Per coding guidelines, every function (public and private) must have a @spec typespec.

Suggested fix
+  `@spec` format_datetime(DateTime.t() | NaiveDateTime.t() | nil) :: String.t()
   defp format_datetime(nil), do: "Never"
   defp format_datetime(dt), do: Calendar.strftime(dt, "%Y-%m-%d %H:%M")
test/ksef_hub_web/live/dashboard_live_test.exs (1)

38-40: Consider using more specific element IDs for stat assertions.

The current assertions all target [class*='text-2xl font-bold'] which matches multiple elements. While this works, it doesn't verify which stat card displays which count. Adding id attributes to the stat elements in the LiveView template would enable more precise assertions like has_element?(view, "#income-count", "2").

test/ksef_hub_web/live/invoice_live/show_test.exs (1)

76-76: Consider more specific selectors for status badge assertions.

The [class*=rounded-md] selector is quite broad and could match unintended elements. Since status_badge is a reusable component, consider adding a data-testid="status-badge" attribute to make tests more resilient to styling changes.

Also applies to: 90-90, 106-107, 117-117

lib/ksef_hub_web/components/invoice_components.ex (2)

12-16: Consider adding explicit fallback styling for unknown types.

If an unexpected type value is passed, only the base classes apply without any color styling. While this may be intentional (graceful degradation), you might want to add an explicit fallback for visual consistency:

       "inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium border",
       `@type` == "income" && "bg-success/10 text-success border-success/20",
-      `@type` == "expense" && "bg-warning/10 text-warning border-warning/20"
+      `@type` == "expense" && "bg-warning/10 text-warning border-warning/20",
+      `@type` not in ["income", "expense"] && "bg-base-200 text-base-content/60 border-base-300"
     ]}>

This matches the fallback pattern used in status_classes/1 in sync_live.ex.


26-31: Same consideration for status_badge fallback styling.

Similar to type_badge, consider adding explicit fallback styling for unknown status values:

Suggested change
       "inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium border",
       `@status` == "pending" && "bg-warning/10 text-warning border-warning/20",
       `@status` == "approved" && "bg-success/10 text-success border-success/20",
-      `@status` == "rejected" && "bg-error/10 text-error border-error/20"
+      `@status` == "rejected" && "bg-error/10 text-error border-error/20",
+      `@status` not in ["pending", "approved", "rejected"] && "bg-base-200 text-base-content/60 border-base-300"
     ]}>
lib/ksef_hub_web/live/dashboard_live.ex (1)

162-176: Missing @spec for stat_card/1.

Per coding guidelines, every function (public and private) must have a @spec typespec.

Add typespec for stat_card
+  `@spec` stat_card(map()) :: Phoenix.LiveView.Rendered.t()
   defp stat_card(assigns) do
     assigns = assign_new(assigns, :color, fn -> "text-base-content" end)

As per coding guidelines: "Every function (public and private) must have @spec typespec".

lib/ksef_hub_web/components/core_components.ex (1)

274-281: Missing @spec for private error/1 helper.

Per coding guidelines, every function must have a @spec typespec.

Add typespec for error
   # Helper used by inputs to generate form errors
+  `@spec` error(map()) :: Phoenix.LiveView.Rendered.t()
   defp error(assigns) do

As per coding guidelines: "Every function (public and private) must have @spec typespec".

Comment thread .github/workflows/deploy.yml
emilwojtaszek and others added 2 commits February 8, 2026 15:31
- .dockerignore: add .github/ to reduce build context
- deploy.yml: quote all variable expansions, fix --args syntax
- token_live: use <.form> component, add @SPEC on format_datetime/1
- invoice_components: add fallback styling for unknown type/status
- dashboard_live: add @SPEC on stat_card/1
- core_components: add @SPEC on error/1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/ksef_hub_web/live/token_live.ex (1)

135-167: ⚠️ Potential issue | 🟠 Major

Switch tokens list to LiveView streams to prevent memory bloat.

The LiveView assigns @tokens as a plain list in mount/3 and on create/revoke events by refetching all tokens. Per coding guidelines, collections must use streams to avoid memory ballooning. Update mount/3 to initialize the stream, use stream() or stream(..., reset: true) on create/revoke instead of full list reassignment, track tokens_count separately, and update the template to consume stream tuples with phx-update="stream" on the parent container.

🔧 Required changes

In mount/3:

tokens = Accounts.list_api_tokens(socket.assigns.current_user.id)
{:ok, socket
  |> stream(:tokens, tokens)
  |> assign(
    page_title: "API Tokens",
    tokens_count: length(tokens),
    # ... rest of assigns
  )}

In handle_event("create", ...) and handle_event("revoke", ...):
Replace assign(tokens: tokens) with:

|> stream(:tokens, tokens, reset: true)
|> assign(tokens_count: length(tokens))

In template:

<div class="mt-6 overflow-x-auto" phx-update="stream" id="tokens">
  <.table id="tokens" rows={`@streams.tokens`} row_id={fn {id, _} -> id end}>
    <:col :let={{_id, token}} label="Name">{token.name}</:col>
    <!-- Apply {_id, token} pattern to all columns -->
  </.table>
</div>

<p :if={`@tokens_count` == 0} class="text-center text-base-content/60 py-8">
  No API tokens yet. Create one to get started.
</p>
🤖 Fix all issues with AI agents
In @.github/workflows/deploy.yml:
- Around line 55-69: The migration step currently executes an existing job which
can use a stale image and hides stderr; change it to first upsert/update the job
with the new image and same flags (for job name ksef-hub-migrate, use --image
"${IMAGE_NAME}:${{ github.sha }}", --set-secrets and --service-account) so the
job definition is updated if it exists (use gcloud run jobs update or create as
appropriate), then run the job separately with gcloud run jobs execute (no
redirection of stderr like 2>/dev/null) to ensure the migration runs with the
latest image and real errors surface.

In `@lib/ksef_hub_web/live/token_live.ex`:
- Around line 172-174: Add `@doc` and `@spec` annotations for the module's public
LiveView callbacks: annotate mount/3, handle_event/3 and render/1. For each
function add a short descriptive `@doc` string and a `@spec`: e.g. mount(params ::
map(), session :: map(), socket :: Phoenix.LiveView.Socket.t()) :: {:ok,
Phoenix.LiveView.Socket.t()} | {:ok, Phoenix.LiveView.Socket.t(), keyword()},
handle_event(event :: String.t() | atom(), params :: map(), socket ::
Phoenix.LiveView.Socket.t()) :: {:noreply, Phoenix.LiveView.Socket.t()} |
{:reply, map(), Phoenix.LiveView.Socket.t()}, and render(assigns :: map()) ::
Phoenix.LiveView.Rendered.t(); place these annotations immediately above the
corresponding function definitions (mount/3, handle_event/3, render/1).
🧹 Nitpick comments (2)
.github/workflows/deploy.yml (1)

78-84: Consider logging when IAM binding fails for non-idempotency reasons.

The 2>/dev/null || true pattern silently ignores all errors. While this is appropriate for idempotency (binding already exists), it could mask permission issues or misconfigurations.

♻️ Optional: Log errors while still allowing failure
       - name: Ensure service allows traffic
         run: |
           gcloud run services add-iam-policy-binding "${SERVICE_NAME}" \
             --region "${GCP_REGION}" \
             --member="allUsers" \
             --role="roles/run.invoker" \
-            --quiet 2>/dev/null || true
+            --quiet || echo "::warning::IAM binding may already exist or failed"
lib/ksef_hub_web/live/dashboard_live.ex (1)

111-126: Extract the duplicated expense status rows into a reusable function component.

Three nearly identical row blocks (Pending, Approved, Rejected) repeat the same layout with only the label, badge color, and value changing. Consolidating these into a stateless function component improves maintainability.

♻️ Suggested refactor
          <div class="space-y-2.5">
-           <div class="flex items-center justify-between">
-             <span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium border bg-warning/10 text-warning border-warning/20">Pending</span>
-             <span class="font-mono text-sm">{`@pending_expense`}</span>
-           </div>
-           <div class="flex items-center justify-between">
-             <span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium border bg-success/10 text-success border-success/20">Approved</span>
-             <span class="font-mono text-sm">{`@approved_expense`}</span>
-           </div>
-           <div class="flex items-center justify-between">
-             <span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium border bg-error/10 text-error border-error/20">Rejected</span>
-             <span class="font-mono text-sm">{`@rejected_expense`}</span>
-           </div>
+           <.expense_status_row
+             label="Pending"
+             value={`@pending_expense`}
+             badge_class="bg-warning/10 text-warning border-warning/20"
+           />
+           <.expense_status_row
+             label="Approved"
+             value={`@approved_expense`}
+             badge_class="bg-success/10 text-success border-success/20"
+           />
+           <.expense_status_row
+             label="Rejected"
+             value={`@rejected_expense`}
+             badge_class="bg-error/10 text-error border-error/20"
+           />
          </div>
        </div>
      </div>

+  `@spec` expense_status_row(map()) :: Phoenix.LiveView.Rendered.t()
+  defp expense_status_row(assigns) do
+    ~H"""
+    <div class="flex items-center justify-between">
+      <span class={["inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium border", `@badge_class`]}>
+        {`@label`}
+      </span>
+      <span class="font-mono text-sm">{`@value`}</span>
+    </div>
+    """
+  end

Comment on lines +55 to +69
- name: Run database migrations
run: |
gcloud run jobs execute ksef-hub-migrate \
--region "${GCP_REGION}" \
--wait \
--args "eval,KsefHub.Release.migrate()" \
2>/dev/null || \
gcloud run jobs create ksef-hub-migrate \
--region "${GCP_REGION}" \
--image "${IMAGE_NAME}:${{ github.sha }}" \
--set-secrets "DATABASE_URL=database-url:latest,SECRET_KEY_BASE=secret-key-base:latest" \
--service-account "ksef-hub-runner@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
--execute-now \
--wait \
--args "eval,KsefHub.Release.migrate()"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Migration job uses stale image when the job already exists.

When gcloud run jobs execute runs on an existing job, it uses the image the job was created with—not the newly pushed image. This means migrations for new code changes won't actually run.

Additionally, 2>/dev/null suppresses all stderr, potentially masking real errors (not just "job not found").

🐛 Proposed fix: Update job image before execution
       - name: Run database migrations
         run: |
-          gcloud run jobs execute ksef-hub-migrate \
-            --region "${GCP_REGION}" \
-            --wait \
-            --args "eval,KsefHub.Release.migrate()" \
-            2>/dev/null || \
-          gcloud run jobs create ksef-hub-migrate \
+          gcloud run jobs update ksef-hub-migrate \
             --region "${GCP_REGION}" \
             --image "${IMAGE_NAME}:${{ github.sha }}" \
             --set-secrets "DATABASE_URL=database-url:latest,SECRET_KEY_BASE=secret-key-base:latest" \
             --service-account "ksef-hub-runner@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
-            --execute-now \
+            --args "eval,KsefHub.Release.migrate()" \
+            2>/dev/null || \
+          gcloud run jobs create ksef-hub-migrate \
+            --region "${GCP_REGION}" \
+            --image "${IMAGE_NAME}:${{ github.sha }}" \
+            --set-secrets "DATABASE_URL=database-url:latest,SECRET_KEY_BASE=secret-key-base:latest" \
+            --service-account "ksef-hub-runner@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
+            --args "eval,KsefHub.Release.migrate()"
+          
+          gcloud run jobs execute ksef-hub-migrate \
+            --region "${GCP_REGION}" \
             --wait \
-            --args "eval,KsefHub.Release.migrate()"
+            --args "eval,KsefHub.Release.migrate()"

This approach:

  1. Updates the existing job with the new image (or creates it if it doesn't exist)
  2. Then executes the job separately, ensuring it always runs with the latest image
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Run database migrations
run: |
gcloud run jobs execute ksef-hub-migrate \
--region "${GCP_REGION}" \
--wait \
--args "eval,KsefHub.Release.migrate()" \
2>/dev/null || \
gcloud run jobs create ksef-hub-migrate \
--region "${GCP_REGION}" \
--image "${IMAGE_NAME}:${{ github.sha }}" \
--set-secrets "DATABASE_URL=database-url:latest,SECRET_KEY_BASE=secret-key-base:latest" \
--service-account "ksef-hub-runner@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
--execute-now \
--wait \
--args "eval,KsefHub.Release.migrate()"
- name: Run database migrations
run: |
gcloud run jobs update ksef-hub-migrate \
--region "${GCP_REGION}" \
--image "${IMAGE_NAME}:${{ github.sha }}" \
--set-secrets "DATABASE_URL=database-url:latest,SECRET_KEY_BASE=secret-key-base:latest" \
--service-account "ksef-hub-runner@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
--args "eval,KsefHub.Release.migrate()" \
2>/dev/null || \
gcloud run jobs create ksef-hub-migrate \
--region "${GCP_REGION}" \
--image "${IMAGE_NAME}:${{ github.sha }}" \
--set-secrets "DATABASE_URL=database-url:latest,SECRET_KEY_BASE=secret-key-base:latest" \
--service-account "ksef-hub-runner@${GCP_PROJECT_ID}.iam.gserviceaccount.com" \
--args "eval,KsefHub.Release.migrate()"
gcloud run jobs execute ksef-hub-migrate \
--region "${GCP_REGION}" \
--wait \
--args "eval,KsefHub.Release.migrate()"
🤖 Prompt for AI Agents
In @.github/workflows/deploy.yml around lines 55 - 69, The migration step
currently executes an existing job which can use a stale image and hides stderr;
change it to first upsert/update the job with the new image and same flags (for
job name ksef-hub-migrate, use --image "${IMAGE_NAME}:${{ github.sha }}",
--set-secrets and --service-account) so the job definition is updated if it
exists (use gcloud run jobs update or create as appropriate), then run the job
separately with gcloud run jobs execute (no redirection of stderr like
2>/dev/null) to ensure the migration runs with the latest image and real errors
surface.

Comment on lines +172 to 174
@spec format_datetime(DateTime.t() | NaiveDateTime.t() | nil) :: String.t()
defp format_datetime(nil), do: "Never"
defp format_datetime(dt), do: Calendar.strftime(dt, "%Y-%m-%d %H:%M")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, check if the file exists and examine the relevant sections
find . -name "token_live.ex" -type f | head -5

Repository: appunite/appunite-ksef-ex

Length of output: 106


🏁 Script executed:

# Get the file size to see if it's reasonable to read
wc -l lib/ksef_hub_web/live/token_live.ex

Repository: appunite/appunite-ksef-ex

Length of output: 108


🏁 Script executed:

# Check the actual content around the mentioned lines and find the functions
cat -n lib/ksef_hub_web/live/token_live.ex | head -200

Repository: appunite/appunite-ksef-ex

Length of output: 7371


Add @doc and @spec annotations to all public functions.

The module has @moduledoc, and the private helper format_datetime/1 has @spec, but all public functions (mount/3, handle_event/3, and render/1) lack both @doc and @spec annotations. These are required per lib/ guidelines.

🧾 Example (apply similarly to other public functions)
+ `@doc` "Initializes the API token management LiveView."
+ `@spec` mount(map(), map(), Phoenix.LiveView.Socket.t()) :: {:ok, Phoenix.LiveView.Socket.t()}
  def mount(_params, _session, socket) do
🤖 Prompt for AI Agents
In `@lib/ksef_hub_web/live/token_live.ex` around lines 172 - 174, Add `@doc` and
`@spec` annotations for the module's public LiveView callbacks: annotate mount/3,
handle_event/3 and render/1. For each function add a short descriptive `@doc`
string and a `@spec`: e.g. mount(params :: map(), session :: map(), socket ::
Phoenix.LiveView.Socket.t()) :: {:ok, Phoenix.LiveView.Socket.t()} | {:ok,
Phoenix.LiveView.Socket.t(), keyword()}, handle_event(event :: String.t() |
atom(), params :: map(), socket :: Phoenix.LiveView.Socket.t()) :: {:noreply,
Phoenix.LiveView.Socket.t()} | {:reply, map(), Phoenix.LiveView.Socket.t()}, and
render(assigns :: map()) :: Phoenix.LiveView.Rendered.t(); place these
annotations immediately above the corresponding function definitions (mount/3,
handle_event/3, render/1).

Fixes credo cyclomatic complexity warning (was 10, max 9).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@lib/ksef_hub_web/live/token_live.ex`:
- Around line 159-170: The template currently iterates over `@tokens` and checks
`@tokens` == [] which must be changed to use LiveView streams to avoid large
memory use: in the LiveView module where you assign `@tokens` (e.g., mount/3 or
fetch_tokens in TokenLive.*) replace assigning a raw list with stream(:tokens,
tokens_list) using Phoenix.LiveView.stream/3 and also keep a separate assign
like :tokens_count = length(tokens_list) for empty-state checks; update the
template to iterate over `@streams.tokens` (e.g., :for={{token <-
`@streams.tokens`}}) and put phx-update="stream" on the container (tbody or
wrapper) so you can use stream_insert/stream_delete/stream_replace in callbacks
when tokens change instead of reassigning the whole list. Ensure code
references: the assign of `@tokens` (replace usages of `@tokens`), the stream/3
call, and the template iteration over `@streams.tokens` plus tokens_count for the
empty check.
🧹 Nitpick comments (1)
lib/ksef_hub_web/live/certificate_live.ex (1)

219-230: Consider extracting the status badge into a helper component.

The status badge styling (lines 221-224 and 227) duplicates the pattern used in the Active Certificate section (lines 151-153). For maintainability, consider extracting this into a small private function component.

♻️ Optional refactor to extract status badge
+  defp status_badge(assigns) do
+    ~H"""
+    <span
+      :if={`@active`}
+      class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium border bg-success/10 text-success border-success/20"
+    >
+      Active
+    </span>
+    <span
+      :if={!@active}
+      class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium border bg-base-200 text-base-content/60 border-base-300"
+    >
+      Inactive
+    </span>
+    """
+  end

Then use <.status_badge active={cred.is_active} /> in both locations.

Comment thread lib/ksef_hub_web/live/token_live.ex
- token_live: replace @tokens list with stream(:tokens) to reduce
  memory usage; use stream_insert for create/revoke updates
- certificate_live: extract duplicated status badge into active_badge/1

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/ksef_hub_web/live/certificate_live.ex (1)

205-230: ⚠️ Potential issue | 🟠 Major

Switch certificates table to LiveView streams.

@credentials is still rendered as a plain list in this LiveView, which conflicts with the project's streams requirement. Please convert the collection to a stream and replace the @credentials != [] check with a boolean assign since streams aren't enumerable.

💡 Suggested update (template + assigns)
-    <div :if={`@credentials` != []} class="mt-6">
+    <div :if={`@has_credentials`} class="mt-6">
       <h2 class="text-lg font-semibold mb-3">All Certificates</h2>
       <div class="overflow-x-auto">
-        <.table id="credentials" rows={`@credentials`} row_id={fn c -> "cred-#{c.id}" end}>
-          <:col :let={cred} label="NIP">{cred.nip}</:col>
-          <:col :let={cred} label="Subject">{cred.certificate_subject || "-"}</:col>
-          <:col :let={cred} label="Expires">
+        <.table id="credentials" rows={`@streams.credentials`} row_id={fn {id, _} -> id end}>
+          <:col :let={{_id, cred}} label="NIP">{cred.nip}</:col>
+          <:col :let={{_id, cred}} label="Subject">{cred.certificate_subject || "-"}</:col>
+          <:col :let={{_id, cred}} label="Expires">
             {if cred.certificate_expires_at,
               do: Calendar.strftime(cred.certificate_expires_at, "%Y-%m-%d"),
               else: "-"}
           </:col>
-          <:col :let={cred} label="Status">
+          <:col :let={{_id, cred}} label="Status">
             <.active_badge active={cred.is_active} />
           </:col>
-          <:action :let={cred}>
+          <:action :let={{_id, cred}}>
             <button
               :if={cred.is_active}
               phx-click="deactivate"
-  defp load_credentials(%{assigns: %{current_company: nil}} = socket) do
-    assign(socket, credentials: [], active_credential: nil)
-  end
+  defp load_credentials(%{assigns: %{current_company: nil}} = socket) do
+    socket
+    |> assign(has_credentials: false, active_credential: nil)
+    |> stream(:credentials, [], reset: true)
+  end
@@
-  defp load_credentials(%{assigns: %{current_company: company}} = socket) do
-    credentials = Credentials.list_credentials(company.id)
-    active = Credentials.get_active_credential(company.id)
-
-    assign(socket,
-      credentials: credentials,
-      active_credential: active
-    )
-  end
+  defp load_credentials(%{assigns: %{current_company: company}} = socket) do
+    credentials = Credentials.list_credentials(company.id)
+    active = Credentials.get_active_credential(company.id)
+
+    socket
+    |> assign(has_credentials: credentials != [], active_credential: active)
+    |> stream(:credentials, credentials, reset: true)
+  end
🧹 Nitpick comments (1)
lib/ksef_hub_web/live/certificate_live.ex (1)

236-252: Refactor active_badge/1 to use pattern-matched clauses instead of inline conditionals.

Function components should leverage pattern matching in function heads over conditional rendering in templates. The suggested refactor eliminates the :if directives and makes the component's intent explicit across separate clauses.

♻️ Suggested refactor
 `@spec` active_badge(map()) :: Phoenix.LiveView.Rendered.t()
-defp active_badge(assigns) do
-  ~H"""
-  <span
-    :if={`@active`}
-    class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium border bg-success/10 text-success border-success/20"
-  >
-    Active
-  </span>
-  <span
-    :if={!@active}
-    class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium border bg-base-200 text-base-content/60 border-base-300"
-  >
-    Inactive
-  </span>
-  """
-end
+defp active_badge(%{active: true} = assigns) do
+  ~H"""
+  <span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium border bg-success/10 text-success border-success/20">
+    Active
+  </span>
+  """
+end
+
+defp active_badge(%{active: active} = assigns) when active in [false, nil] do
+  ~H"""
+  <span class="inline-flex items-center px-2 py-0.5 rounded-md text-xs font-medium border bg-base-200 text-base-content/60 border-base-300">
+    Inactive
+  </span>
+  """
+end

…badge

- Replace @credentials list with stream(:credentials) and
  @has_credentials boolean for empty-state check
- Refactor active_badge/1 to use pattern matching in function heads

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@emilwojtaszek emilwojtaszek merged commit 17afce9 into main Feb 8, 2026
2 checks passed
@emilwojtaszek emilwojtaszek deleted the feat/cloud-run-deploy branch February 9, 2026 00:07
This was referenced Mar 12, 2026
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.

1 participant