Skip to content

Commit 951af3f

Browse files
authored
feat(build): add build-time secret injection for launchdarkly (#157)
* feat(build): add build-time secret injection for launchdarkly - Add reusable workflow for AWS Secrets Manager integration - Update Dockerfile with BuildKit secret mounts for secure injection - Configure Angular define feature for build-time constant replacement - Implement OpenFeature with LaunchDarkly provider - Add feature flag service and provider with SSR support - Update all Docker build workflows to use reusable secret workflow - Add comprehensive documentation for build-time secrets pattern - Support local development with fallback values for ng serve LFXV2-750 Signed-off-by: Asitha de Silva <[email protected]> * refactor(feature-flags): remove duplicate sdk interfaces and update docs - Remove duplicate EvaluationContext and LaunchDarklyConfig interfaces from shared package (available from @openfeature/web-sdk) - Update feature flags documentation to show provideAppInitializer instead of deprecated APP_INITIALIZER pattern - Replace hardcoded LaunchDarkly client IDs with placeholders in documentation - Add preferred_username field to User interface for feature flag targeting - Improve type safety in feature-flag.service.ts by using User type LFXV2-750 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> * fix(build): correct --define syntax for angular cli - Fix Dockerfile to use --define=KEY=VALUE without -- separator - Update documentation with verified working syntax - Remove incorrect -- separator from all examples - Tested build completes successfully and value is injected The Angular CLI requires --define=KEY=VALUE format directly on yarn build commands without the -- separator. String values must be quoted within quotes: --define=KEY="'value'" Verified with test build that value is properly injected into bundle. LFXV2-750 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> * refactor(build): separate shared and ui builds to support --define - Build shared package separately before UI to avoid TypeScript compiler errors - Use yarn workspace commands for targeted builds - Update Dockerfile to build @lfx-one/shared then lfx-one-ui - Update documentation with workspace build approach - Change --define syntax to use space (matches Angular CLI docs) This resolves the issue where running build from root would fail because the shared package's TypeScript compiler doesn't understand --define flags. Building them separately allows each package to use appropriate build flags. LFXV2-750 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> * refactor(ci): use GITHUB_ENV for secret retrieval Remove get-launchdarkly-secrets.yml reusable workflow and implement direct AWS Secrets Manager retrieval in each job using GITHUB_ENV. This resolves the GitHub Actions issue where masked values cannot be used in workflow outputs. Changes: - Update docker-build-main.yml to retrieve secrets directly in job - Update docker-build-tag.yml to retrieve secrets directly in job - Update docker-build-pr.yml to retrieve secrets directly in job - Delete get-launchdarkly-secrets.yml reusable workflow file - Update build-time-secrets.md documentation to reflect new approach - LaunchDarkly client-side IDs are NOT masked (they're public values) LFXV2-750 Generated with [Claude Code](https://claude.ai/code) Signed-off-by: Asitha de Silva <[email protected]> --------- Signed-off-by: Asitha de Silva <[email protected]>
1 parent 8b2114e commit 951af3f

23 files changed

+2333
-36
lines changed

.github/workflows/docker-build-main.yml

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ on:
1111

1212
permissions:
1313
contents: read
14+
id-token: write
15+
packages: write
1416

1517
env:
1618
REGISTRY: ghcr.io
@@ -19,15 +21,33 @@ env:
1921
jobs:
2022
build-and-push:
2123
runs-on: ubuntu-latest
22-
permissions:
23-
contents: read
24-
packages: write
25-
id-token: write
2624

2725
steps:
2826
- name: Checkout repository
2927
uses: actions/checkout@v4
3028

29+
- name: OIDC Auth
30+
uses: aws-actions/configure-aws-credentials@v4
31+
with:
32+
audience: sts.amazonaws.com
33+
role-to-assume: arn:aws:iam::788942260905:role/github-actions-deploy
34+
aws-region: us-west-2
35+
36+
- name: Get LaunchDarkly client ID from AWS Secrets Manager
37+
uses: aws-actions/aws-secretsmanager-get-secrets@v2
38+
with:
39+
secret-ids: |
40+
LAUNCHDARKLY, /cloudops/managed-secrets/launchdarkly/lfx_one/ld_client_id
41+
42+
- name: Set LaunchDarkly client ID environment variable
43+
run: |
44+
LAUNCHDARKLY_CLIENT_ID=$(echo "$LAUNCHDARKLY" | jq -r '.ld_client_id // empty')
45+
if [ -z "$LAUNCHDARKLY_CLIENT_ID" ]; then
46+
echo "❌ LaunchDarkly client ID not found in AWS Secrets Manager"
47+
exit 1
48+
fi
49+
echo "LAUNCHDARKLY_CLIENT_ID=$LAUNCHDARKLY_CLIENT_ID" >> $GITHUB_ENV
50+
3151
- name: Set up Docker Buildx
3252
uses: docker/setup-buildx-action@v3
3353

@@ -68,3 +88,5 @@ jobs:
6888
cache-to: type=gha,mode=max
6989
build-args: |
7090
BUILD_ENV=development
91+
secret-envs: |
92+
LAUNCHDARKLY_CLIENT_ID=LAUNCHDARKLY_CLIENT_ID

.github/workflows/docker-build-pr.yml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ permissions:
1212
packages: write
1313
pull-requests: write
1414
issues: write
15+
id-token: write
1516

1617
env:
1718
REGISTRY: ghcr.io
@@ -27,10 +28,33 @@ jobs:
2728
github.event.pull_request.head.repo.fork == false &&
2829
contains(github.event.pull_request.labels.*.name, 'deploy-preview')
2930
runs-on: ubuntu-latest
31+
3032
steps:
3133
- name: Checkout repository
3234
uses: actions/checkout@v4
3335

36+
- name: OIDC Auth
37+
uses: aws-actions/configure-aws-credentials@v4
38+
with:
39+
audience: sts.amazonaws.com
40+
role-to-assume: arn:aws:iam::788942260905:role/github-actions-deploy
41+
aws-region: us-west-2
42+
43+
- name: Get LaunchDarkly client ID from AWS Secrets Manager
44+
uses: aws-actions/aws-secretsmanager-get-secrets@v2
45+
with:
46+
secret-ids: |
47+
LAUNCHDARKLY, /cloudops/managed-secrets/launchdarkly/lfx_one/ld_client_id
48+
49+
- name: Set LaunchDarkly client ID environment variable
50+
run: |
51+
LAUNCHDARKLY_CLIENT_ID=$(echo "$LAUNCHDARKLY" | jq -r '.ld_client_id // empty')
52+
if [ -z "$LAUNCHDARKLY_CLIENT_ID" ]; then
53+
echo "❌ LaunchDarkly client ID not found in AWS Secrets Manager"
54+
exit 1
55+
fi
56+
echo "LAUNCHDARKLY_CLIENT_ID=$LAUNCHDARKLY_CLIENT_ID" >> $GITHUB_ENV
57+
3458
- name: Set up Docker Buildx
3559
uses: docker/setup-buildx-action@v3
3660

@@ -72,6 +96,8 @@ jobs:
7296
cache-to: type=gha,mode=max
7397
build-args: |
7498
BUILD_ENV=development
99+
secret-envs: |
100+
LAUNCHDARKLY_CLIENT_ID=LAUNCHDARKLY_CLIENT_ID
75101
76102
- name: Comment deployment URL on PR
77103
uses: actions/github-script@v7

.github/workflows/docker-build-tag.yml

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ on:
1010

1111
permissions:
1212
contents: read
13+
id-token: write
14+
packages: write
1315

1416
env:
1517
REGISTRY: ghcr.io
@@ -20,10 +22,6 @@ env:
2022
jobs:
2123
build-and-push:
2224
runs-on: ubuntu-latest
23-
permissions:
24-
contents: read
25-
packages: write
26-
id-token: write
2725
outputs:
2826
app_version: ${{ steps.prepare.outputs.app_version }}
2927
chart_name: ${{ steps.prepare.outputs.chart_name }}
@@ -33,6 +31,28 @@ jobs:
3331
- name: Checkout repository
3432
uses: actions/checkout@v4
3533

34+
- name: OIDC Auth
35+
uses: aws-actions/configure-aws-credentials@v4
36+
with:
37+
audience: sts.amazonaws.com
38+
role-to-assume: arn:aws:iam::788942260905:role/github-actions-deploy
39+
aws-region: us-west-2
40+
41+
- name: Get LaunchDarkly client ID from AWS Secrets Manager
42+
uses: aws-actions/aws-secretsmanager-get-secrets@v2
43+
with:
44+
secret-ids: |
45+
LAUNCHDARKLY, /cloudops/managed-secrets/launchdarkly/lfx_one/ld_client_id
46+
47+
- name: Set LaunchDarkly client ID environment variable
48+
run: |
49+
LAUNCHDARKLY_CLIENT_ID=$(echo "$LAUNCHDARKLY" | jq -r '.ld_client_id // empty')
50+
if [ -z "$LAUNCHDARKLY_CLIENT_ID" ]; then
51+
echo "❌ LaunchDarkly client ID not found in AWS Secrets Manager"
52+
exit 1
53+
fi
54+
echo "LAUNCHDARKLY_CLIENT_ID=$LAUNCHDARKLY_CLIENT_ID" >> $GITHUB_ENV
55+
3656
- name: Prepare versions and chart name
3757
id: prepare
3858
run: |
@@ -86,6 +106,8 @@ jobs:
86106
cache-to: type=gha,mode=max
87107
build-args: |
88108
BUILD_ENV=production
109+
secret-envs: |
110+
LAUNCHDARKLY_CLIENT_ID=LAUNCHDARKLY_CLIENT_ID
89111
90112
release-helm-chart:
91113
needs: build-and-push

Dockerfile

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,13 @@ RUN yarn install --immutable
2424
# NOW copy source code (changes here won't invalidate the dependency layer)
2525
COPY . .
2626

27-
# Build the application with specified environment
28-
RUN yarn build:${BUILD_ENV}
27+
# Build shared package first (doesn't need --define flag)
28+
RUN yarn workspace @lfx-one/shared build:${BUILD_ENV}
29+
30+
# Build the Angular application with LaunchDarkly client ID from secret
31+
RUN --mount=type=secret,id=LAUNCHDARKLY_CLIENT_ID \
32+
LAUNCHDARKLY_CLIENT_ID=$(cat /run/secrets/LAUNCHDARKLY_CLIENT_ID) && \
33+
yarn workspace lfx-one-ui build:${BUILD_ENV} --define LAUNCHDARKLY_CLIENT_ID="'${LAUNCHDARKLY_CLIENT_ID}'"
2934

3035
# Expose port 4000
3136
EXPOSE 4000

apps/lfx-one/.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,12 @@ SNOWFLAKE_ROLE=your-read-role
6666
AI_PROXY_URL=https://litellm.tools.lfx.dev/chat/completions
6767
AI_API_KEY=your-litellm-ai-api-key
6868

69+
# LaunchDarkly Feature Flags Configuration
70+
# Client ID for LaunchDarkly feature flag service
71+
# This is injected at Docker build time via --build-arg
72+
# For local development, set this to your LaunchDarkly client-side ID
73+
LD_CLIENT_ID=your-launchdarkly-client-id
74+
6975
# E2E Test Configuration (Optional)
7076
# Test user credentials for automated testing
7177
TEST_USERNAME=your-test-username

apps/lfx-one/angular.json

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,10 @@
5353
"entry": "src/server/server.ts"
5454
},
5555
"allowedCommonJsDependencies": ["@linuxfoundation/lfx-ui-core"],
56-
"externalDependencies": ["snowflake-sdk"]
56+
"externalDependencies": ["snowflake-sdk"],
57+
"define": {
58+
"LAUNCHDARKLY_CLIENT_ID": "''"
59+
}
5760
},
5861
"configurations": {
5962
"production": {
@@ -75,7 +78,10 @@
7578
"replace": "src/environments/environment.ts",
7679
"with": "src/environments/environment.prod.ts"
7780
}
78-
]
81+
],
82+
"define": {
83+
"LAUNCHDARKLY_CLIENT_ID": "''"
84+
}
7985
},
8086
"staging": {
8187
"budgets": [
@@ -96,7 +102,10 @@
96102
"replace": "src/environments/environment.ts",
97103
"with": "src/environments/environment.staging.ts"
98104
}
99-
]
105+
],
106+
"define": {
107+
"LAUNCHDARKLY_CLIENT_ID": "''"
108+
}
100109
},
101110
"development": {
102111
"optimization": false,
@@ -107,12 +116,18 @@
107116
"replace": "src/environments/environment.ts",
108117
"with": "src/environments/environment.dev.ts"
109118
}
110-
]
119+
],
120+
"define": {
121+
"LAUNCHDARKLY_CLIENT_ID": "''"
122+
}
111123
},
112124
"local": {
113125
"optimization": false,
114126
"extractLicenses": false,
115-
"sourceMap": true
127+
"sourceMap": true,
128+
"define": {
129+
"LAUNCHDARKLY_CLIENT_ID": "''"
130+
}
116131
}
117132
},
118133
"defaultConfiguration": "production"

apps/lfx-one/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,15 @@
4040
"@fullcalendar/timegrid": "^6.1.19",
4141
"@lfx-one/shared": "workspace:*",
4242
"@linuxfoundation/lfx-ui-core": "^0.0.20",
43+
"@openfeature/core": "^1.9.1",
44+
"@openfeature/launchdarkly-client-provider": "^0.3.3",
45+
"@openfeature/web-sdk": "^1.7.1",
4346
"@primeng/themes": "^19.1.4",
4447
"compression": "^1.8.1",
4548
"dotenv": "^17.2.1",
4649
"express": "^4.18.2",
4750
"express-openid-connect": "^2.19.2",
51+
"launchdarkly-js-client-sdk": "^3.9.0",
4852
"nats": "^2.29.3",
4953
"ngx-cookie-service-ssr": "^19.1.2",
5054
"pino-http": "^10.5.0",

apps/lfx-one/src/app/app.component.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { RouterOutlet } from '@angular/router';
77
import { AuthContext } from '@lfx-one/shared/interfaces';
88
import { ToastModule } from 'primeng/toast';
99

10+
import { FeatureFlagService } from './shared/services/feature-flag.service';
1011
import { SegmentService } from './shared/services/segment.service';
1112
import { UserService } from './shared/services/user.service';
1213

@@ -19,6 +20,7 @@ import { UserService } from './shared/services/user.service';
1920
export class AppComponent {
2021
private readonly userService = inject(UserService);
2122
private readonly segmentService = inject(SegmentService);
23+
private readonly featureFlagService = inject(FeatureFlagService);
2224

2325
public auth: AuthContext | undefined;
2426
public transferState = inject(TransferState);
@@ -52,6 +54,11 @@ export class AppComponent {
5254

5355
// Identify user with Segment tracking (pass entire Auth0 user object)
5456
this.segmentService.identifyUser(this.auth.user);
57+
58+
// Initialize feature flags with user context
59+
this.featureFlagService.initialize(this.auth.user).catch((error) => {
60+
console.error('Failed to initialize feature flags:', error);
61+
});
5562
}
5663
}
5764
}

apps/lfx-one/src/app/app.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { providePrimeNG } from 'primeng/config';
1515
import { DialogService } from 'primeng/dynamicdialog';
1616

1717
import { routes } from './app.routes';
18+
import { provideFeatureFlags } from './shared/providers/feature-flag.provider';
1819
import { CustomPreloadingStrategy } from './shared/strategies/custom-preloading.strategy';
1920

2021
const customPreset = definePreset(Aura, {
@@ -45,6 +46,7 @@ export const appConfig: ApplicationConfig = {
4546
},
4647
},
4748
}),
49+
provideFeatureFlags(),
4850
ConfirmationService,
4951
DialogService,
5052
MessageService,

apps/lfx-one/src/app/layouts/main-layout/main-layout.component.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<div class="flex min-h-screen">
55
<!-- Sidebar - Desktop -->
66
<div class="hidden lg:block w-64 flex-shrink-0 fixed top-0 left-0">
7-
<lfx-sidebar [items]="sidebarItems" [footerItems]="sidebarFooterItems" [showProjectSelector]="true"></lfx-sidebar>
7+
<lfx-sidebar [items]="sidebarItems()" [footerItems]="sidebarFooterItems" [showProjectSelector]="true"></lfx-sidebar>
88
</div>
99

1010
<!-- Sidebar - Mobile Overlay -->
@@ -23,7 +23,7 @@ <h2 class="text-lg font-semibold text-gray-900">Menu</h2>
2323
</button>
2424
</div>
2525
<div class="overflow-y-auto h-[calc(100vh-4rem)]">
26-
<lfx-sidebar [items]="sidebarItems" [footerItems]="sidebarFooterItems" [showProjectSelector]="true"></lfx-sidebar>
26+
<lfx-sidebar [items]="sidebarItems()" [footerItems]="sidebarFooterItems" [showProjectSelector]="true"></lfx-sidebar>
2727
</div>
2828
</div>
2929
</div>

0 commit comments

Comments
 (0)