Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 15 additions & 104 deletions .github/workflows/docker-image.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,31 +18,13 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v7

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Extract OpenAPI schema
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
target: artifacts
outputs: type=local,dest=./artifacts
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Upload OpenAPI schema
uses: actions/upload-artifact@v4
with:
name: openapi-schema
path: artifacts/openapi.json
if-no-files-found: error
retention-days: 1
uses: docker/setup-buildx-action@v4

- name: Build image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
Expand All @@ -53,7 +35,7 @@ jobs:
cache-to: type=gha,mode=max

- name: Upload image artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: flagd-ui-ci-image
path: /tmp/flagd-ui-ci.tar
Expand All @@ -68,18 +50,18 @@ jobs:
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

- name: Download Docker image artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: flagd-ui-ci-image
path: /tmp

- name: Load Docker image
run: docker load --input /tmp/flagd-ui-ci.tar

- uses: actions/setup-node@v4
- uses: actions/setup-node@v7
with:
node-version: lts/*

Expand All @@ -95,91 +77,23 @@ jobs:
env:
FLAGD_UI_IMAGE: flagd-ui:ci

- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30

- name: Upload docs screenshots
if: ${{ always() }}
uses: actions/upload-artifact@v4
with:
name: docs-screenshots
path: docs/assets/images/
retention-days: 1

docs-deploy:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: playwright-test
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
permissions:
contents: read
pages: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Configure GitHub Pages
uses: actions/configure-pages@v5

- name: Setup uv
uses: astral-sh/setup-uv@v5

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
cache-dependency-path: ui/package-lock.json

- name: Install UI dependencies
run: npm ci
working-directory: ui

- name: Download OpenAPI schema
uses: actions/download-artifact@v4
with:
name: openapi-schema
path: ui/

- name: Download docs screenshots
uses: actions/download-artifact@v4
with:
name: docs-screenshots
path: docs/assets/images/

- name: Build docs
run: uvx zensical build --clean

- name: Build UI demo for docs
run: npm run build-demo
working-directory: ui

- name: Upload Pages artifact
uses: actions/upload-pages-artifact@v4
with:
path: site

- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

docker-publish:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
runs-on: ubuntu-24.04-arm
permissions:
contents: read
packages: write

steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v7
with:
fetch-depth: 0

Expand All @@ -202,14 +116,11 @@ jobs:
echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
echo "minor=$MINOR" >> "$GITHUB_OUTPUT"

- name: Set up QEMU
uses: docker/setup-qemu-action@v3

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4

- name: Login to GHCR
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
Expand All @@ -221,7 +132,7 @@ jobs:

- name: Generate Docker metadata
id: meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: ${{ steps.image.outputs.name }}
tags: |
Expand All @@ -232,12 +143,12 @@ jobs:
type=raw,value=${{ steps.version.outputs.major }}

- name: Build and push image
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
push: true
platforms: linux/amd64,linux/arm64
platforms: linux/arm64
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
Expand Down
3 changes: 0 additions & 3 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,6 @@ mod storage {
async fn flag_exists(&self, name: &str) -> AppResult<bool>;
}

pub struct LocalStorage;
pub struct AzureStorage;

pub fn create_storage_backend(_uri: &str) -> AppResult<Arc<dyn StorageBackend>> {
panic!("Storage backend should not be used in build script")
}
Expand Down
32 changes: 0 additions & 32 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,35 +20,3 @@ services:
- '8016:8016'
- '8014:8014'

azurite:
image: local-azurite:latest
build:
context: services/azurite
ports:
- 10000:10000
- 10001:10001
- 10002:10002
volumes:
- ./services/azurite/azurite-store:/data
- ./services/azurite/azurite-logs:/logs

azurite-init:
image: mcr.microsoft.com/azure-cli:2.81.0
depends_on:
- azurite
volumes:
- ./services/azurite/init.sh:/workdir/init.sh:ro
- ./flags/demo.flagd.json:/workdir/demo.flagd.json
working_dir: /workdir
command: ['sh', '/workdir/init.sh']
# auth:
# image: local-keycloak:latest
# build: ./services/authentication
# environment:
# - KEYCLOAK_ADMIN=admin
# - KEYCLOAK_ADMIN_PASSWORD=admin
# ports:
# - 8080:8080
# command:
# - start-dev
# - --import-realm
1 change: 1 addition & 0 deletions src/handlers/api/flags.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ impl jsonschema::Retrieve for LocalSchemaRetriever {
/// Application state containing configuration
#[derive(Clone)]
pub struct AppState {
#[allow(dead_code)]
pub config: Arc<ServerConfig>,
pub schema: Arc<jsonschema::Validator>,
pub storage: Arc<dyn StorageBackend>,
Expand Down
33 changes: 32 additions & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,41 @@ async fn main() {

tracing::info!("Server listening on {}", addr);

// Start the server
// Start the server with graceful shutdown on SIGTERM/SIGINT
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await
.expect("Server failed to start");

tracing::info!("Server shut down gracefully");
}

async fn shutdown_signal() {
use tokio::signal;

let ctrl_c = async {
signal::ctrl_c()
.await
.expect("failed to install Ctrl+C handler");
};

#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install SIGTERM handler")
.recv()
.await;
};

#[cfg(not(unix))]
let terminate = std::future::pending::<()>();

tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}

tracing::info!("Shutdown signal received, starting graceful shutdown");
}

/// Create the Axum router with all routes and middleware
Expand Down
23 changes: 14 additions & 9 deletions ui/src/app/state/flag-file-store.state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,14 @@ export class FlagFileStore implements NgxsOnInit {
return;
}

await this.syncRemoteBackend(ctx, backend);
// Only sync files the user has explicitly edited (isDirty), then
// import the server's current state. Previously this synced ALL
// cached files (including stale localStorage state) before importing,
// which overwrote external edits on page load.
const hasDirtyFiles = backend.files.some((f) => f.isDirty);
if (hasDirtyFiles) {
await this.syncRemoteBackend(ctx, backend);
}
await this.importRemoteBackend(ctx, backend);
return;
}
Expand All @@ -360,11 +367,15 @@ export class FlagFileStore implements NgxsOnInit {
const api = new FlagsService(this.httpClient, backend.uri);

try {
const dirtyFiles = backend.files.filter((f) => f.isDirty);
if (dirtyFiles.length === 0) {
return;
}

const listResponse = await firstValueFrom(api.listFlags());
const remoteFileNames = new Set(listResponse?.files ?? []);
const localFileNames = new Set(backend.files.map((file) => file.name));

for (const file of backend.files) {
for (const file of dirtyFiles) {
const parsed = this.parseFlagFileContent(file.content);
if (!parsed) {
continue;
Expand All @@ -383,12 +394,6 @@ export class FlagFileStore implements NgxsOnInit {
}
}

for (const remoteFileName of remoteFileNames) {
if (!localFileNames.has(remoteFileName)) {
await firstValueFrom(api.deleteFlag(remoteFileName));
}
}

this.markBackendFilesSynced(ctx, 'remote', backend.uri);
} catch {
return;
Expand Down
Loading