diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index ed32a55..53c597d 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -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 @@ -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 @@ -68,10 +50,10 @@ 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 @@ -79,7 +61,7 @@ jobs: - 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/* @@ -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 @@ -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 }} @@ -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: | @@ -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 diff --git a/build.rs b/build.rs index 4b5171a..220becb 100644 --- a/build.rs +++ b/build.rs @@ -21,9 +21,6 @@ mod storage { async fn flag_exists(&self, name: &str) -> AppResult; } - pub struct LocalStorage; - pub struct AzureStorage; - pub fn create_storage_backend(_uri: &str) -> AppResult> { panic!("Storage backend should not be used in build script") } diff --git a/docker-compose.yaml b/docker-compose.yaml index 35ce45e..084a2e2 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -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 diff --git a/src/handlers/api/flags.rs b/src/handlers/api/flags.rs index c44a35f..0f2e6fc 100644 --- a/src/handlers/api/flags.rs +++ b/src/handlers/api/flags.rs @@ -46,6 +46,7 @@ impl jsonschema::Retrieve for LocalSchemaRetriever { /// Application state containing configuration #[derive(Clone)] pub struct AppState { + #[allow(dead_code)] pub config: Arc, pub schema: Arc, pub storage: Arc, diff --git a/src/main.rs b/src/main.rs index f0c633f..377b7e5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 diff --git a/ui/src/app/state/flag-file-store.state.ts b/ui/src/app/state/flag-file-store.state.ts index 70cd675..eccf1cb 100644 --- a/ui/src/app/state/flag-file-store.state.ts +++ b/ui/src/app/state/flag-file-store.state.ts @@ -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; } @@ -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; @@ -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;