diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml
index 1c622dbcd9..ee2790a2ba 100644
--- a/.github/workflows/copilot-setup-steps.yml
+++ b/.github/workflows/copilot-setup-steps.yml
@@ -17,9 +17,9 @@ jobs:
permissions:
contents: read
steps:
- - uses: actions/checkout@v5
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
- name: Setup Bun
- uses: oven-sh/setup-bun@v2
+ uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.8
diff --git a/.github/workflows/dockerize-profiles.yml b/.github/workflows/dockerize-profiles.yml
index aafa95f324..cc26e115e9 100644
--- a/.github/workflows/dockerize-profiles.yml
+++ b/.github/workflows/dockerize-profiles.yml
@@ -14,20 +14,20 @@ jobs:
steps:
- name: Checkout the repo
- uses: actions/checkout@v2
+ uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2.8.0
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2
+ uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v2
+ uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0
- name: Log in to Docker Hub
- uses: docker/login-action@v3
+ uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
- uses: docker/metadata-action@v5
+ uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: appwrite/console-profiles
tags: |
@@ -40,7 +40,7 @@ jobs:
- name: Build and push Docker image
id: push
- uses: docker/build-push-action@v6
+ uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
push: true
diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml
index 924f4c52cf..a5ac621fa7 100644
--- a/.github/workflows/e2e.yml
+++ b/.github/workflows/e2e.yml
@@ -11,9 +11,9 @@ jobs:
e2e:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Setup Bun
- uses: oven-sh/setup-bun@v2
+ uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.8
@@ -21,7 +21,7 @@ jobs:
run: bun install --frozen-lockfile
- name: Cache Playwright browsers
- uses: actions/cache@v4
+ uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
id: playwright-cache
with:
path: ~/.cache/ms-playwright
@@ -34,7 +34,7 @@ jobs:
- name: E2E Tests
run: bun test:e2e
- - uses: actions/upload-artifact@v4
+ - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: ${{ !cancelled() }}
with:
name: playwright-report
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 4bb77fbcbc..31bf8a1985 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -9,19 +9,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
- uses: actions/checkout@v2
+ uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2.8.0
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2
+ uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v2
+ uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0
- name: Log in to Docker Hub
- uses: docker/login-action@v3
+ uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
- uses: docker/metadata-action@v5
+ uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: appwrite/console-cloud
tags: |
@@ -30,7 +30,7 @@ jobs:
type=semver,pattern={{major}}
- name: Build and push Docker image
id: push
- uses: docker/build-push-action@v6
+ uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
push: true
@@ -51,19 +51,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
- uses: actions/checkout@v2
+ uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2.8.0
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2
+ uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v2
+ uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0
- name: Log in to Docker Hub
- uses: docker/login-action@v3
+ uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
- uses: docker/metadata-action@v5
+ uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: appwrite/console-cloud-stage
tags: |
@@ -72,7 +72,7 @@ jobs:
type=semver,pattern={{major}}
- name: Build and push Docker image
id: push
- uses: docker/build-push-action@v6
+ uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
push: true
@@ -91,13 +91,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
- uses: actions/checkout@v2
+ uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2.8.0
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2
+ uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v2
+ uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0
- name: Log in to Docker Hub
- uses: docker/login-action@v3
+ uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
@@ -112,7 +112,7 @@ jobs:
type=semver,pattern={{major}}
- name: Build and push Docker image
id: push
- uses: docker/build-push-action@v6
+ uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
push: true
@@ -130,19 +130,19 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout the repo
- uses: actions/checkout@v2
+ uses: actions/checkout@0717577d45739eb3c851188b29f50ed6c0b2194e # v2.8.0
- name: Set up QEMU
- uses: docker/setup-qemu-action@v2
+ uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0
- name: Set up Docker Buildx
- uses: docker/setup-buildx-action@v2
+ uses: docker/setup-buildx-action@885d1462b80bc1c1c7f0b00334ad271f09369c55 # v2.10.0
- name: Log in to Docker Hub
- uses: docker/login-action@v3
+ uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
- uses: docker/metadata-action@v5
+ uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
with:
images: appwrite/console-cloud-no-regions
tags: |
@@ -151,7 +151,7 @@ jobs:
type=semver,pattern={{major}}
- name: Build and push Docker image
id: push
- uses: docker/build-push-action@v6
+ uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
with:
context: .
push: true
diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml
index d018dbf919..f950a650f9 100644
--- a/.github/workflows/stale.yml
+++ b/.github/workflows/stale.yml
@@ -9,7 +9,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/stale@v9
+ - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9.1.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
stale-issue-message: "This issue has been labeled as a 'question', indicating that it requires additional information from the requestor. It has been inactive for 7 days. If no further activity occurs, this issue will be closed in 14 days."
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index fa21cfc861..c94731c418 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -14,9 +14,9 @@ jobs:
build:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Setup Bun
- uses: oven-sh/setup-bun@v2
+ uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: 1.3.8
diff --git a/src/lib/helpers/timeConversion.test.ts b/src/lib/helpers/timeConversion.test.ts
index 951de79f4c..37026df400 100644
--- a/src/lib/helpers/timeConversion.test.ts
+++ b/src/lib/helpers/timeConversion.test.ts
@@ -1,5 +1,6 @@
import {
calculateTime,
+ formatReplicationLag,
timeToSeconds,
timeToMinutes,
secsToUnit
@@ -169,3 +170,25 @@ describe('Seconds to unit', () => {
});
});
});
+
+describe('Format replication lag', () => {
+ it('renders an unknown lag as a dash', () => {
+ expect(formatReplicationLag(null)).toBe('-');
+ expect(formatReplicationLag(undefined)).toBe('-');
+ });
+
+ it('renders zero as a bound rather than an exact reading', () => {
+ // MySQL and MongoDB report whole seconds, so their 0 covers everything
+ // up to a second. Printing it as 0s claimed a precision no engine gives.
+ expect(formatReplicationLag(0)).toBe('< 1s');
+ });
+
+ it('renders a sub-second lag in milliseconds', () => {
+ expect(formatReplicationLag(0.184)).toBe('184ms');
+ });
+
+ it('renders a lag of a second or more in the largest fitting unit', () => {
+ expect(formatReplicationLag(7)).toBe('7s');
+ expect(formatReplicationLag(90)).toBe('1m');
+ });
+});
diff --git a/src/lib/helpers/timeConversion.ts b/src/lib/helpers/timeConversion.ts
index 7cf8de1667..bec4690298 100644
--- a/src/lib/helpers/timeConversion.ts
+++ b/src/lib/helpers/timeConversion.ts
@@ -31,6 +31,27 @@ export function calculateTime(time: number) {
}
}
+/**
+ * Formats a replication lag reading for display.
+ *
+ * A zero renders as `< 1s` rather than `0s` because it does not mean the same
+ * thing on every engine: MySQL reports `Seconds_Behind_Source` in whole seconds
+ * and MongoDB derives its lag from the oplog timestamp's second granularity, so
+ * neither can distinguish "caught up" from "900ms behind". Only PostgreSQL
+ * reports a true zero, and the reading carries no engine to tell them apart.
+ *
+ * @export
+ * @param {number | null | undefined} lagSeconds - The lag in seconds, or null when the backend took no reading.
+ * @returns {string} The formatted lag, or `-` when the lag is unknown.
+ */
+export function formatReplicationLag(lagSeconds: number | null | undefined): string {
+ if (typeof lagSeconds !== 'number' || Number.isNaN(lagSeconds)) {
+ return '-';
+ }
+
+ return lagSeconds <= 0 ? '< 1s' : calculateTime(lagSeconds);
+}
+
type Unit = 'ms' | 's' | 'm' | 'h' | 'd' | 'M' | 'y';
export function timeToSeconds(time: number, unit: Unit) {
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateCrossRegion.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateCrossRegion.svelte
index 4b88ab9857..26134078cb 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateCrossRegion.svelte
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateCrossRegion.svelte
@@ -11,6 +11,7 @@
import type { Models } from '@appwrite.io/console';
import { Badge, Layout } from '@appwrite.io/pink-svelte';
import { toLocaleDateTime } from '$lib/helpers/date';
+ import { formatReplicationLag } from '$lib/helpers/timeConversion';
let {
database
@@ -35,7 +36,7 @@
primaryRegion: string;
standbyRegion: string;
standbyStatus: 'healthy' | 'degraded' | 'unhealthy' | 'provisioning';
- lagSeconds: number;
+ lagSeconds: number | null;
lastSyncedAt: string;
};
@@ -209,7 +210,8 @@
• Standby: {crossRegionStatus.standbyRegion}
- Lag: {crossRegionStatus.lagSeconds}s • Last synced: {toLocaleDateTime(
+ Lag: {formatReplicationLag(crossRegionStatus.lagSeconds)} •
+ Last synced: {toLocaleDateTime(
crossRegionStatus.lastSyncedAt
)}
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateHAStatus.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateHAStatus.svelte
index fd5d4c410a..effb2dc424 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateHAStatus.svelte
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateHAStatus.svelte
@@ -4,6 +4,7 @@
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
+ import { formatReplicationLag } from '$lib/helpers/timeConversion';
import { Button, Form, InputSwitch, InputNumber, InputSelect } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
@@ -196,7 +197,7 @@
type={getHealthType(replica.status)}
content={replica.status} />
- Lag: {replica.lagSeconds}s
+ Lag: {formatReplicationLag(replica.lagSeconds)}
diff --git a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateReadReplicas.svelte b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateReadReplicas.svelte
index d335aba742..ba258eb50a 100644
--- a/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateReadReplicas.svelte
+++ b/src/routes/(console)/project-[region]-[project]/databases/database-[database]/settings/updateReadReplicas.svelte
@@ -4,6 +4,7 @@
import { Submit, trackError, trackEvent } from '$lib/actions/analytics';
import { CardGrid, Modal } from '$lib/components';
import { Dependencies } from '$lib/constants';
+ import { formatReplicationLag } from '$lib/helpers/timeConversion';
import { Button, Form, InputSelect, InputCheckbox } from '$lib/elements/forms';
import { addNotification } from '$lib/stores/notifications';
import { sdk } from '$lib/stores/sdk';
@@ -176,7 +177,9 @@
{replica.sourceRegion} → {replica.targetRegion}
- • Lag: {replica.lagSeconds}s • {replica.hostname}
+ • Lag: {formatReplicationLag(
+ replica.lagSeconds
+ )} • {replica.hostname}