Skip to content

Commit 2b164d8

Browse files
authored
Merge pull request #22 from Azure/aprilk/label-support
Add feature flag label support
2 parents 45ebfcd + cb46516 commit 2b164d8

File tree

12 files changed

+128
-47
lines changed

12 files changed

+128
-47
lines changed

.github/workflows/ci.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,3 +95,12 @@ jobs:
9595
!TestData/feature-flags3.json
9696
app-configuration-endpoint: ${{ vars.APP_CONFIGURATION_ENDPOINT }}
9797
strict: true
98+
99+
- name: Test Local Action with label
100+
id: test-action-label
101+
uses: ./
102+
with:
103+
path: TestData/*.json
104+
app-configuration-endpoint: ${{ vars.APP_CONFIGURATION_ENDPOINT }}
105+
strict: true
106+
label: test label

README.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,16 @@ path: |
3636
!/path/to/feature-flags-ignore.json
3737
```
3838
39-
- `app-configuration-endpoint` - An Azure app configuration endpoint eg..
39+
- `app-configuration-endpoint` - An Azure app configuration endpoint. E.g.
4040
`https://<app-configuration-name>.azconfig.io`
41-
- `strict` - If strict, the sync operation deletes feature flags not found in
42-
the config file. Choices: true, false.
43-
- `operation` - [optional] Possible values: validate or deploy - deploy by
44-
default. validate: only validates the configuration file. deploy: deploys the
45-
feature flags to Azure App Configuration deploy: Updates the Azure App
46-
configuration
41+
- `label` - [optional] Azure App Configuration label to apply to the feature
42+
flags. If not specficed, the default is no label.
43+
- `operation` - [optional] Possible values: `validate` or `deploy` - `deploy` by
44+
default. `validate`: only validates the configuration file. `deploy`: deploys
45+
the feature flags to Azure App Configuration
46+
- `strict` - Choices: `true` or `false`. If strict, the operation deletes
47+
feature flags not found in the configuration file. Required when operation is
48+
`deploy`.
4749

4850
### Example workflow
4951

__tests__/feature-flag-client.test.ts

Lines changed: 56 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,13 @@ describe('Feature Flag Client', () => {
2727

2828
it('should list feature flags', async () => {
2929
const appConfigEndpoint = 'https://example.com'
30+
const label = 'test-label'
3031
const getMock = jest.spyOn(axios, 'get').mockResolvedValue({
3132
status: 200,
3233
data: featureListResponse
3334
})
3435

35-
const results = await listFeatureFlags(appConfigEndpoint)
36+
const results = await listFeatureFlags(appConfigEndpoint, label)
3637
expect(results.items.length).toEqual(1)
3738
expect(results.items[0].key).toEqual(
3839
'.appconfig.featureflag/featureFlagId1'
@@ -41,27 +42,70 @@ describe('Feature Flag Client', () => {
4142

4243
it('should throw error when list api fails', async () => {
4344
const appConfigEndpoint = 'https://example.com'
45+
const label = 'test-label'
4446

4547
const getMock = jest.spyOn(axios, 'get').mockResolvedValue({
4648
status: 500,
4749
statusText: 'Internal Server Error'
4850
})
4951

50-
await expect(listFeatureFlags(appConfigEndpoint)).rejects.toThrow(ApiError)
52+
await expect(listFeatureFlags(appConfigEndpoint, label)).rejects.toThrow(
53+
ApiError
54+
)
5155
})
5256

53-
it('create or update feature flag', async () => {
57+
it('can create or update feature flag', async () => {
5458
const appConfigEndpoint = 'https://example.com'
5559
const featureFlagId = 'featureFlagId1'
60+
const label = ''
5661
const value = getDummyFeatureFlagItem(featureFlagId)
5762

5863
const getMock = jest.spyOn(axios, 'put').mockResolvedValue({
5964
status: 200
6065
})
6166

62-
await createOrUpdateFeatureFlag(appConfigEndpoint, featureFlagId, value)
67+
await createOrUpdateFeatureFlag(
68+
appConfigEndpoint,
69+
featureFlagId,
70+
value,
71+
label
72+
)
73+
expect(getMock).toBeCalledWith(
74+
`${appConfigEndpoint}/kv/.appconfig.featureflag%2FfeatureFlagId1?api-version=2023-11-01&label=`,
75+
{
76+
content_type:
77+
'application/vnd.microsoft.appconfig.ff+json;charset=utf-8',
78+
value: JSON.stringify(value)
79+
},
80+
{
81+
headers: {
82+
Accept: '*/*',
83+
Authorization: 'Bearer token',
84+
'Content-Type': 'application/vnd.microsoft.appconfig.kv+json'
85+
}
86+
}
87+
)
88+
})
89+
90+
it('can create or update feature flag with label', async () => {
91+
const appConfigEndpoint = 'https://example.com'
92+
const featureFlagId = 'featureFlagId1'
93+
// Use a label with special characters to test URL encoding
94+
const label = 'test label/with special&chars'
95+
const value = getDummyFeatureFlagItem(featureFlagId)
96+
97+
const getMock = jest.spyOn(axios, 'put').mockResolvedValue({
98+
status: 200
99+
})
100+
101+
await createOrUpdateFeatureFlag(
102+
appConfigEndpoint,
103+
featureFlagId,
104+
value,
105+
label
106+
)
63107
expect(getMock).toBeCalledWith(
64-
`${appConfigEndpoint}/kv/.appconfig.featureflag%2FfeatureFlagId1?api-version=2023-11-01`,
108+
`${appConfigEndpoint}/kv/.appconfig.featureflag%2FfeatureFlagId1?api-version=2023-11-01&label=test%20label%2Fwith%20special%26chars`,
65109
{
66110
content_type:
67111
'application/vnd.microsoft.appconfig.ff+json;charset=utf-8',
@@ -80,6 +124,7 @@ describe('Feature Flag Client', () => {
80124
it('should throw error when create or update feature flag fails', async () => {
81125
const appConfigEndpoint = 'https://example.com'
82126
const featureFlagId = 'featureFlagId1'
127+
const label = 'test-label'
83128
const value = getDummyFeatureFlagItem(featureFlagId)
84129

85130
const getMock = jest.spyOn(axios, 'put').mockResolvedValue({
@@ -88,21 +133,22 @@ describe('Feature Flag Client', () => {
88133
})
89134

90135
await expect(
91-
createOrUpdateFeatureFlag(appConfigEndpoint, featureFlagId, value)
136+
createOrUpdateFeatureFlag(appConfigEndpoint, featureFlagId, value, label)
92137
).rejects.toThrow(ApiError)
93138
})
94139

95140
it('should delete feature flag', async () => {
96141
const appConfigEndpoint = 'https://example.com'
97142
const featureFlagId = 'featureFlagId1'
143+
const label = 'test label/with special&chars'
98144

99145
const getMock = jest.spyOn(axios, 'delete').mockResolvedValue({
100146
status: 200
101147
})
102148

103-
await deleteFeatureFlag(appConfigEndpoint, featureFlagId)
149+
await deleteFeatureFlag(appConfigEndpoint, featureFlagId, label)
104150
expect(getMock).toBeCalledWith(
105-
`${appConfigEndpoint}/kv/.appconfig.featureflag%2FfeatureFlagId1?api-version=2023-11-01`,
151+
`${appConfigEndpoint}/kv/.appconfig.featureflag%2FfeatureFlagId1?api-version=2023-11-01&label=test%20label%2Fwith%20special%26chars`,
106152
{
107153
headers: {
108154
Accept: '*/*',
@@ -116,13 +162,14 @@ describe('Feature Flag Client', () => {
116162
it('should throw error when delete feature flag fails', async () => {
117163
const appConfigEndpoint = 'https://example.com'
118164
const featureFlagId = 'featureFlagId1'
165+
const label = 'test-label'
119166

120167
const getMock = jest.spyOn(axios, 'delete').mockResolvedValue({
121168
status: 500
122169
})
123170

124171
await expect(
125-
deleteFeatureFlag(appConfigEndpoint, featureFlagId)
172+
deleteFeatureFlag(appConfigEndpoint, featureFlagId, label)
126173
).rejects.toThrow(ApiError)
127174
})
128175

__tests__/input.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ describe('getActionInput', () => {
2525
configFile: 'configFile',
2626
strictSync: true,
2727
appConfigEndpoint: 'https://example.com',
28-
operation: 'deploy'
28+
operation: 'deploy',
29+
label: ''
2930
})
3031
})
3132

@@ -38,7 +39,8 @@ describe('getActionInput', () => {
3839
configFile: 'configFile',
3940
strictSync: false, // doesn't matter in validate mode
4041
appConfigEndpoint: '', // doesn't matter in validate mode
41-
operation: 'validate'
42+
operation: 'validate',
43+
label: ''
4244
})
4345
})
4446

__tests__/update-feature-flags.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,8 @@ describe('updateFeatureFlags', () => {
5353
expect(createOrUpdateFeatureFlag).toHaveBeenCalledWith(
5454
input.appConfigEndpoint,
5555
'featureFlagId2',
56-
configs[1]
56+
configs[1],
57+
'test-label'
5758
)
5859
expect(infoMock).toHaveBeenCalledWith('Updated 1 feature flags')
5960
})
@@ -81,7 +82,8 @@ describe('updateFeatureFlags', () => {
8182
expect(createOrUpdateFeatureFlag).toHaveBeenCalledWith(
8283
input.appConfigEndpoint,
8384
'featureFlagId2',
84-
configs[1]
85+
configs[1],
86+
'test-label'
8587
)
8688
expect(infoMock).toHaveBeenCalledWith('Updated 1 feature flags')
8789
expect(infoMock).toHaveBeenCalledWith(
@@ -90,7 +92,8 @@ describe('updateFeatureFlags', () => {
9092
expect(deleteFeatureFlag).toHaveBeenCalledTimes(1)
9193
expect(deleteFeatureFlag).toHaveBeenCalledWith(
9294
input.appConfigEndpoint,
93-
'featureFlagId3'
95+
'featureFlagId3',
96+
'test-label'
9497
)
9598
})
9699

@@ -99,7 +102,8 @@ describe('updateFeatureFlags', () => {
99102
configFile: 'configFile',
100103
strictSync: false,
101104
appConfigEndpoint: 'https://example.com',
102-
operation: 'deploy'
105+
operation: 'deploy',
106+
label: 'test-label'
103107
}
104108
}
105109

action.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,21 @@ inputs:
1616
required: true
1717

1818
app-configuration-endpoint:
19-
description: 'Destination endpoint for the Azure App Configuration store'
19+
description: 'Destination endpoint for the Azure App Configuration store.'
2020
required: false
2121

22+
label:
23+
description:
24+
'Azure App Configuration label to apply to the feature flags. If not
25+
specficed, the default is no label.'
26+
required: false
27+
default: None
28+
2229
operation: # Validate the configuration file only
2330
description:
2431
'Possible values: validate or deploy - deploy by default. validate: only
2532
validates the configuration file. deploy: deploys the feature flags to
26-
Azure App Configuration'
33+
Azure App Configuration.'
2734
required: false
2835
default: 'deploy'
2936

dist/index.js

Lines changed: 13 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/feature-flag-client.ts

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,11 @@ import { FeatureFlag } from './models/feature-flag.models'
1313
const apiVersion = '2023-11-01'
1414

1515
export const listFeatureFlags = async (
16-
appConfigEndpoint: string
16+
appConfigEndpoint: string,
17+
label: string
1718
): Promise<FeatureListResponse> => {
1819
const response = await axios.get<FeatureListResponse>(
19-
`${appConfigEndpoint}/kv?key=.appconfig.featureflag*&api-version=${apiVersion}`,
20+
`${appConfigEndpoint}/kv?key=.appconfig.featureflag*&api-version=${apiVersion}&label=${encodeURIComponent(label)}`,
2021
{ headers: await getHeaders(appConfigEndpoint) }
2122
)
2223

@@ -32,14 +33,15 @@ export const listFeatureFlags = async (
3233
export const createOrUpdateFeatureFlag = async (
3334
appConfigEndpoint: string,
3435
featureFlagId: string,
35-
value: FeatureFlag
36+
value: FeatureFlag,
37+
label: string
3638
): Promise<void> => {
3739
const payload: FeatureFlagResponse = {
3840
content_type: 'application/vnd.microsoft.appconfig.ff+json;charset=utf-8',
3941
value: JSON.stringify(value)
4042
}
4143
const response = await axios.put(
42-
`${appConfigEndpoint}/kv/${getAppConfigKey(featureFlagId)}?api-version=${apiVersion}`,
44+
`${appConfigEndpoint}/kv/${getAppConfigKey(encodeURIComponent(featureFlagId))}?api-version=${apiVersion}&label=${encodeURIComponent(label)}`,
4345
payload,
4446
{ headers: await getHeaders(appConfigEndpoint) }
4547
)
@@ -53,10 +55,11 @@ export const createOrUpdateFeatureFlag = async (
5355

5456
export const deleteFeatureFlag = async (
5557
appConfigEndpoint: string,
56-
featureFlagId: string
58+
featureFlagId: string,
59+
label: string
5760
): Promise<void> => {
5861
const response = await axios.delete(
59-
`${appConfigEndpoint}/kv/${getAppConfigKey(featureFlagId)}?api-version=${apiVersion}`,
62+
`${appConfigEndpoint}/kv/${getAppConfigKey(featureFlagId)}?api-version=${apiVersion}&label=${encodeURIComponent(label)}`,
6063
{ headers: await getHeaders(appConfigEndpoint) }
6164
)
6265

src/input.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@ export function getActionInput(): Input {
1212
configFile: getRequiredInputString('path'),
1313
strictSync: false, // In validate mode, strict sync is not required
1414
appConfigEndpoint: '', // In validate mode, app config endpoint is not required
15-
operation: operation
15+
operation: operation,
16+
label: getNonRequiredInputString('label') || ''
1617
}
1718
}
1819
return {
1920
configFile: getRequiredInputString('path'),
2021
strictSync: getRequiredBooleanInput('strict'),
2122
appConfigEndpoint: getAppConfigEndpoint(),
22-
operation: getOperationType()
23+
operation: getOperationType(),
24+
label: getNonRequiredInputString('label') || ''
2325
}
2426
}
2527

0 commit comments

Comments
 (0)