Skip to content

Commit 3fcecd0

Browse files
authored
feat: test-isolation env var interpolation (#3977)
* add ci-setup file * use script file * ignore scripts * add PROJECT_ID * test name * have local testing package * change to es module * separate entrypoint * fix import path * fix import syntax * support variable interpolation * file renames * add unit tests * add test workflow for scripts * install before testing * fix install * add quotes to string * remove dlp changes * do not make auth configurable * add RUN_ID * simplify code * rename to camelCase * fix name * fix tests * do not ignore scripts directory * use project env var * ignore scripts directory * do not ignore workflows directory * use node 16 for healthcare/dicom * changes to the config files should trigger all tests * increased timeout for these tests * fix flaky test
1 parent 6398a9a commit 3fcecd0

File tree

13 files changed

+352
-95
lines changed

13 files changed

+352
-95
lines changed

.github/config/nodejs-dev.jsonc

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,20 +13,15 @@
1313
See the License for the specific language governing permissions and
1414
limitations under the License.
1515
*/
16+
1617
{
17-
"package-file": [
18-
"package.json"
19-
],
18+
"package-file": [ "package.json" ],
2019
"ci-setup-filename": "ci-setup.json",
2120
"ci-setup-defaults": {
2221
"node-version": 20,
2322
"timeout-minutes": 10,
24-
"project-id": "long-door-651",
25-
"workload-identity-provider": "projects/1046198160504/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider",
26-
"service-account": "[email protected]",
27-
"access-token-lifetime": "600s", // 10 minutes
28-
"env": {},
29-
"secrets": {}
23+
"env": { },
24+
"secrets": { }
3025
},
3126
"ignore": [
3227
".eslintignore",
@@ -38,12 +33,11 @@
3833
".github/auto-label.yaml",
3934
".github/blunderbuss.yaml",
4035
".github/cloud-samples-tools/",
41-
".github/config/",
4236
".github/flakybot.yaml",
4337
".github/header-checker-lint.yaml",
38+
".github/scripts/",
4439
".github/snippet-bot.yml",
4540
".github/trusted-contribution.yml",
46-
".github/workflows/",
4741
".gitignore",
4842
".kokoro/",
4943
".prettierignore",
@@ -200,4 +194,4 @@
200194
"tpu",
201195
"workflows/invoke-private-endpoint"
202196
]
203-
}
197+
}

.github/config/nodejs-prod.jsonc

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,6 @@
2020
"ci-setup-defaults": {
2121
"node-version": 20,
2222
"timeout-minutes": 10,
23-
"project-id": "long-door-651",
24-
"workload-identity-provider": "projects/1046198160504/locations/global/workloadIdentityPools/github-actions-pool/providers/github-actions-provider",
25-
"service-account": "[email protected]",
26-
"access-token-lifetime": "600s", // 10 minutes
2723
"env": { },
2824
"secrets": { }
2925
},
@@ -37,12 +33,11 @@
3733
".github/auto-label.yaml",
3834
".github/blunderbuss.yaml",
3935
".github/cloud-samples-tools/",
40-
".github/config/",
4136
".github/flakybot.yaml",
4237
".github/header-checker-lint.yaml",
38+
".github/scripts/",
4339
".github/snippet-bot.yml",
4440
".github/trusted-contribution.yml",
45-
".github/workflows/",
4641
".gitignore",
4742
".kokoro/",
4843
".prettierignore",

.github/scripts/cli/vars.js

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
Copyright 2025 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import fs from 'node:fs';
18+
import path from 'node:path';
19+
import setupVars from '../setup-vars.js';
20+
21+
const project_id = process.env.PROJECT_ID;
22+
if (!project_id) {
23+
console.error(
24+
'Please set the PROJECT_ID environment variable to your Google Cloud project.'
25+
);
26+
process.exit(1);
27+
}
28+
29+
const core = {
30+
exportVariable: (_key, _value) => null,
31+
};
32+
33+
const setupFile = process.argv[2];
34+
if (!setupFile) {
35+
console.error('Please provide the path to a setup file.');
36+
process.exit(1);
37+
}
38+
const data = fs.readFileSync(path.join('..', '..', setupFile), 'utf8');
39+
const setup = JSON.parse(data);
40+
41+
setupVars({project_id, core, setup});

.github/scripts/nodejs-test.sh

Lines changed: 0 additions & 36 deletions
This file was deleted.

.github/scripts/package.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"name": "aritest",
3+
"version": "1.0.0",
4+
"type": "module",
5+
"license": "Apache-2.0",
6+
"private": true,
7+
"scripts": {
8+
"vars": "node cli/vars.js",
9+
"test": "mocha -p -j 2 **/*.test.js"
10+
},
11+
"devDependencies": {
12+
"mocha": "^11.1.0"
13+
}
14+
}

.github/scripts/setup-vars.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
Copyright 2025 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
export default function setupVars({projectId, core, setup}, runId = null) {
18+
// Define automatic variables plus custom variables.
19+
const vars = {
20+
PROJECT_ID: projectId,
21+
RUN_ID: runId || uniqueId(),
22+
...(setup.env || {}),
23+
};
24+
25+
// Apply variable interpolation.
26+
const env = Object.fromEntries(
27+
Object.keys(vars).map(key => [key, substituteVars(vars[key], vars)])
28+
);
29+
30+
// Export environment variables.
31+
console.log('env:');
32+
for (const key in env) {
33+
const value = env[key];
34+
console.log(` ${key}: ${value}`);
35+
core.exportVariable(key, value);
36+
}
37+
38+
// Show exported secrets, for logging purposes.
39+
// TODO: We might want to fetch the secrets here and export them directly.
40+
// https://cloud.google.com/secret-manager/docs/create-secret-quickstart#secretmanager-quickstart-nodejs
41+
console.log('secrets:');
42+
for (const key in setup.secrets || {}) {
43+
// This is the Google Cloud Secret Manager secret ID.
44+
// NOT the secret value, so it's ok to show.
45+
console.log(` ${key}: ${setup.secrets[key]}`);
46+
}
47+
48+
// Return env and secrets to use for further steps.
49+
return {
50+
env: env,
51+
// Transform secrets into the format needed for the GHA secret manager step.
52+
secrets: Object.keys(setup.secrets || {})
53+
.map(key => `${key}:${setup.secrets[key]}`)
54+
.join('\n'),
55+
};
56+
}
57+
58+
export function substituteVars(value, env) {
59+
for (const key in env) {
60+
let re = new RegExp(`\\$(${key}\\b|\\{\\s*${key}\\s*\\})`, 'g');
61+
value = value.replaceAll(re, env[key]);
62+
}
63+
return value;
64+
}
65+
66+
export function uniqueId(length = 6) {
67+
const min = 2 ** 32;
68+
const max = 2 ** 64;
69+
return Math.floor(Math.random() * max + min)
70+
.toString(36)
71+
.slice(0, length);
72+
}

.github/scripts/setup-vars.test.js

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
/*
2+
Copyright 2025 Google LLC
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
https://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import {deepStrictEqual} from 'assert';
18+
import setupVars from './setup-vars.js';
19+
import {substituteVars, uniqueId} from './setup-vars.js';
20+
21+
const projectId = 'my-test-project';
22+
const core = {
23+
exportVariable: (_key, _value) => null,
24+
};
25+
26+
const autovars = {PROJECT_ID: projectId, RUN_ID: 'run-id'};
27+
28+
describe('setupVars', () => {
29+
describe('env', () => {
30+
it('empty', () => {
31+
const setup = {};
32+
const vars = setupVars({projectId, core, setup}, 'run-id');
33+
const expected = autovars;
34+
deepStrictEqual(vars.env, expected);
35+
});
36+
37+
it('zero vars', () => {
38+
const setup = {env: {}};
39+
const vars = setupVars({projectId, core, setup}, 'run-id');
40+
const expected = autovars;
41+
deepStrictEqual(vars.env, expected);
42+
});
43+
44+
it('one var', () => {
45+
const setup = {env: {A: 'x'}};
46+
const vars = setupVars({projectId, core, setup}, 'run-id');
47+
const expected = {...autovars, A: 'x'};
48+
deepStrictEqual(vars.env, expected);
49+
});
50+
51+
it('three vars', () => {
52+
const setup = {env: {A: 'x', B: 'y', C: 'z'}};
53+
const vars = setupVars({projectId, core, setup}, 'run-id');
54+
const expected = {...autovars, A: 'x', B: 'y', C: 'z'};
55+
deepStrictEqual(vars.env, expected);
56+
});
57+
58+
it('should override automatic variables', () => {
59+
const setup = {env: {PROJECT_ID: 'custom-value'}};
60+
const vars = setupVars({projectId, core, setup}, 'run-id');
61+
const expected = {PROJECT_ID: 'custom-value', RUN_ID: 'run-id'};
62+
deepStrictEqual(vars.env, expected);
63+
});
64+
65+
it('should interpolate variables', () => {
66+
const setup = {env: {A: 'x', B: 'y', C: '$A/${B}'}};
67+
const vars = setupVars({projectId, core, setup}, 'run-id');
68+
const expected = {...autovars, A: 'x', B: 'y', C: 'x/y'};
69+
deepStrictEqual(vars.env, expected);
70+
});
71+
72+
it('should not interpolate secrets', () => {
73+
const setup = {
74+
env: {C: '$x/$y'},
75+
secrets: {A: 'x', B: 'y'},
76+
};
77+
const vars = setupVars({projectId, core, setup}, 'run-id');
78+
const expected = {...autovars, C: '$x/$y'};
79+
deepStrictEqual(vars.env, expected);
80+
});
81+
});
82+
83+
describe('secrets', () => {
84+
it('zero secrets', () => {
85+
const setup = {secrets: {}};
86+
const vars = setupVars({projectId, core, setup}, 'run-id');
87+
deepStrictEqual(vars.secrets, '');
88+
});
89+
90+
it('one secret', () => {
91+
const setup = {secrets: {A: 'x'}};
92+
const vars = setupVars({projectId, core, setup}, 'run-id');
93+
const expected = 'A:x';
94+
deepStrictEqual(vars.secrets, expected);
95+
});
96+
97+
it('three secrets', () => {
98+
const setup = {secrets: {A: 'x', B: 'y', C: 'z'}};
99+
const vars = setupVars({projectId, core, setup}, 'run-id');
100+
const expected = 'A:x\nB:y\nC:z';
101+
deepStrictEqual(vars.secrets, expected);
102+
});
103+
104+
it('should not interpolate variables', () => {
105+
const setup = {
106+
env: {A: 'x', B: 'y'},
107+
secrets: {C: '$A/$B'},
108+
};
109+
const vars = setupVars({projectId, core, setup}, 'run-id');
110+
const expected = 'C:$A/$B';
111+
deepStrictEqual(vars.secrets, expected);
112+
});
113+
114+
it('should not interpolate secrets', () => {
115+
const setup = {secrets: {A: 'x', B: 'y', C: '$A/$B'}};
116+
const vars = setupVars({projectId, core, setup}, 'run-id');
117+
const expected = 'A:x\nB:y\nC:$A/$B';
118+
deepStrictEqual(vars.secrets, expected);
119+
});
120+
});
121+
});
122+
123+
describe('substituteVars', () => {
124+
it('should interpolate $VAR', () => {
125+
const got = substituteVars('$A-$B', {A: 'x', B: 'y'});
126+
const expected = 'x-y';
127+
deepStrictEqual(got, expected);
128+
});
129+
130+
it('should interpolate ${VAR}', () => {
131+
const got = substituteVars('${A}-${B}', {A: 'x', B: 'y'});
132+
const expected = 'x-y';
133+
deepStrictEqual(got, expected);
134+
});
135+
136+
it('should interpolate ${ VAR }', () => {
137+
const got = substituteVars('${ A }-${ \tB\t }', {A: 'x', B: 'y'});
138+
const expected = 'x-y';
139+
deepStrictEqual(got, expected);
140+
});
141+
142+
it('should not interpolate on non-word boundary', () => {
143+
const got = substituteVars('$Ab', {A: 'x'});
144+
const expected = '$Ab';
145+
deepStrictEqual(got, expected);
146+
});
147+
});
148+
149+
describe('uniqueId', () => {
150+
it('should match length', () => {
151+
const n = 6;
152+
deepStrictEqual(uniqueId(n).length, n);
153+
});
154+
});

0 commit comments

Comments
 (0)