azdo-release-env 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/npm-publish.yml +33 -0
- package/LICENSE +21 -0
- package/README.md +66 -0
- package/package.json +39 -0
- package/src/index.js +458 -0
- package/tsconfig.json +21 -0
- package/types/index.d.ts +39 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
|
|
3
|
+
|
|
4
|
+
name: Node.js Package
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
release:
|
|
8
|
+
types: [created]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-node@v4
|
|
16
|
+
with:
|
|
17
|
+
node-version: 20
|
|
18
|
+
- run: npm ci
|
|
19
|
+
- run: npm test
|
|
20
|
+
|
|
21
|
+
publish-npm:
|
|
22
|
+
needs: build
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- uses: actions/setup-node@v4
|
|
27
|
+
with:
|
|
28
|
+
node-version: 20
|
|
29
|
+
registry-url: https://registry.npmjs.org/
|
|
30
|
+
- run: npm ci
|
|
31
|
+
- run: npm publish
|
|
32
|
+
env:
|
|
33
|
+
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joseph Cano
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# azdo-release-env
|
|
2
|
+
|
|
3
|
+
Interactive CLI that extracts environment variables from a selected Azure DevOps **Release** pipeline environment and exports them to `./.env` or `./env.json`.
|
|
4
|
+
|
|
5
|
+
## Disclaimer
|
|
6
|
+
|
|
7
|
+
This is a personal project and is not officially supported by Microsoft or Azure DevOps. This CLI requires the Azure CLI and Azure DevOps extension to be installed and authenticated. It reads the release definition and environment variables via the Azure DevOps REST API. It does not store or transmit any data outside of your local machine. Use at your own risk.
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- Node.js
|
|
12
|
+
- Azure CLI installed (`az`)
|
|
13
|
+
- https://learn.microsoft.com/cli/azure/install-azure-cli
|
|
14
|
+
- Azure DevOps extension installed
|
|
15
|
+
- `az extension add --name azure-devops`
|
|
16
|
+
- Authenticated to Azure DevOps
|
|
17
|
+
- `az devops login`
|
|
18
|
+
- Azure DevOps defaults (`organization` and `project`)
|
|
19
|
+
- If missing, the CLI will prompt and set them via `az devops configure -d ...`.
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
(Optional) Make the command available on your machine:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm link
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Usage
|
|
34
|
+
|
|
35
|
+
Run interactively:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm start
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Or (after `npm link`):
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
azdo-release-env
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The CLI will prompt you to:
|
|
48
|
+
|
|
49
|
+
- Select a release pipeline definition
|
|
50
|
+
- Choose an export format (`.env` or JSON)
|
|
51
|
+
- Confirm writing to the current directory
|
|
52
|
+
|
|
53
|
+
### Output
|
|
54
|
+
|
|
55
|
+
- `.env` export writes `./.env`
|
|
56
|
+
- `JSON` export writes `./env.json`
|
|
57
|
+
|
|
58
|
+
## Notes
|
|
59
|
+
|
|
60
|
+
- The CLI prompts you to select an environment from the release definition.
|
|
61
|
+
- Variable groups are currently not included (only explicit environment `variables`).
|
|
62
|
+
|
|
63
|
+
## Troubleshooting
|
|
64
|
+
|
|
65
|
+
- If `az` is not installed, install Azure CLI first.
|
|
66
|
+
- If you see an authentication error, run `az devops login`.
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "azdo-release-env",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI tool for exporting Azure DevOps release environment variables",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"azdo-release-env": "src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"types": "types/index.d.ts",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/index.js",
|
|
12
|
+
"test": "echo \"No tests yet\" && exit 0"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"azure-devops",
|
|
16
|
+
"release",
|
|
17
|
+
"environment",
|
|
18
|
+
"variables",
|
|
19
|
+
"cli"
|
|
20
|
+
],
|
|
21
|
+
"author": "Joseph Cano",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/Jcanotorr06/azdo-release-env"
|
|
26
|
+
},
|
|
27
|
+
"type": "module",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@inquirer/prompts": "^8.3.0",
|
|
30
|
+
"@types/node": "^25.3.3",
|
|
31
|
+
"chalk": "^5.6.2",
|
|
32
|
+
"commander": "^14.0.3",
|
|
33
|
+
"execa": "^9.6.1",
|
|
34
|
+
"typescript": "^5.9.3"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=20"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import * as inquirer from '@inquirer/prompts';
|
|
5
|
+
import { execa } from 'execa';
|
|
6
|
+
import fs from 'node:fs/promises';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} AZReleaseVariable
|
|
12
|
+
* @property {string} value Variable value.
|
|
13
|
+
* @property {boolean} isSecret Whether the variable is marked as secret.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* @typedef {Object} AZEnvironment
|
|
17
|
+
* @property {string} name Environment name.
|
|
18
|
+
* @property {number} id Environment id.
|
|
19
|
+
* @property {Record<string, AZReleaseVariable>} variables Environment variables.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} AZReleaseDefinition
|
|
23
|
+
* @property {string} name Release definition name.
|
|
24
|
+
* @property {number} id Release definition id.
|
|
25
|
+
* @property {AZEnvironment[]} environments List of environments in the release definition.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Creates a set of CLI style helpers.
|
|
30
|
+
*
|
|
31
|
+
* @param {import('chalk').ChalkInstance} chalkInstance Chalk instance used for styling.
|
|
32
|
+
* @returns {{ error: (message: string) => string }} Style helper functions.
|
|
33
|
+
*/
|
|
34
|
+
const createCliStyles = (chalkInstance) => ({
|
|
35
|
+
error: (message) => chalkInstance.redBright(message),
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const CLI_STYLES = createCliStyles(chalk);
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Formats an error for display in the CLI.
|
|
42
|
+
*
|
|
43
|
+
* @param {{ error: (message: string) => string }} styles Style helpers.
|
|
44
|
+
* @param {unknown} err Error-like value.
|
|
45
|
+
* @returns {string} Formatted error string.
|
|
46
|
+
*/
|
|
47
|
+
const formatCliError = (styles, err) => styles.error(err?.message || err);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Detects whether an error indicates the Azure CLI is not installed.
|
|
51
|
+
*
|
|
52
|
+
* @param {any} err Error thrown from executing `az`.
|
|
53
|
+
* @returns {boolean} True if `az` appears missing.
|
|
54
|
+
*/
|
|
55
|
+
function isAzNotInstalledError(err) {
|
|
56
|
+
// Windows: spawn az ENOENT, POSIX: ENOENT
|
|
57
|
+
return err?.code === 'ENOENT' || /spawn\s+az\s+enoent/i.test(err?.message || '');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Detects whether an error indicates Azure DevOps CLI authentication failure.
|
|
62
|
+
*
|
|
63
|
+
* @param {any} err Error thrown from executing `az devops`.
|
|
64
|
+
* @returns {boolean} True if the error appears auth-related.
|
|
65
|
+
*/
|
|
66
|
+
function isAzDevopsAuthError(err) {
|
|
67
|
+
const msg = `${err?.stderr || ''}\n${err?.message || ''}`.toLowerCase();
|
|
68
|
+
// Common messages:
|
|
69
|
+
// - "Please run 'az devops login'"
|
|
70
|
+
// - "TF400813: The user ... is not authorized"
|
|
71
|
+
// - "Authentication failed"
|
|
72
|
+
return (
|
|
73
|
+
msg.includes('az devops login') ||
|
|
74
|
+
msg.includes('authentication failed') ||
|
|
75
|
+
msg.includes('not authorized') ||
|
|
76
|
+
msg.includes('authorization') ||
|
|
77
|
+
msg.includes('tf400813')
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Wraps an Azure CLI execution error with a friendlier message.
|
|
83
|
+
*
|
|
84
|
+
* @param {string[]} args Azure CLI args (excluding the `az` binary).
|
|
85
|
+
* @param {any} err Error thrown from executing `az`.
|
|
86
|
+
* @returns {Error} Wrapped error.
|
|
87
|
+
*/
|
|
88
|
+
function wrapAzError(args, err) {
|
|
89
|
+
if (isAzNotInstalledError(err)) {
|
|
90
|
+
const e = new Error(
|
|
91
|
+
"Azure CLI (az) was not found. Install it first: https://learn.microsoft.com/cli/azure/install-azure-cli"
|
|
92
|
+
);
|
|
93
|
+
e.cause = err;
|
|
94
|
+
return e;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (isAzDevopsAuthError(err)) {
|
|
98
|
+
const e = new Error(
|
|
99
|
+
"Azure DevOps CLI authentication is missing/expired. Run: az devops login"
|
|
100
|
+
);
|
|
101
|
+
e.cause = err;
|
|
102
|
+
return e;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const details = err?.stderr || err?.shortMessage || err?.message || String(err);
|
|
106
|
+
const wrapped = new Error(`Azure CLI command failed: az ${args.join(' ')}\n${details}`);
|
|
107
|
+
wrapped.cause = err;
|
|
108
|
+
return wrapped;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Executes an Azure CLI command and parses its JSON output.
|
|
113
|
+
*
|
|
114
|
+
* @param {string[]} args Azure CLI args (excluding the `az` binary).
|
|
115
|
+
* @param {{ cwd?: string } | undefined} [options] Execution options.
|
|
116
|
+
* @returns {Promise<any>} Parsed JSON output.
|
|
117
|
+
*/
|
|
118
|
+
async function azJson(args, { cwd } = {}) {
|
|
119
|
+
try {
|
|
120
|
+
const finalArgs = [...args, '--output', 'json'];
|
|
121
|
+
const { stdout } = await execa('az', finalArgs, { cwd });
|
|
122
|
+
return JSON.parse(stdout || 'null');
|
|
123
|
+
} catch (err) {
|
|
124
|
+
throw wrapAzError(args, err);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Executes an Azure CLI command and returns its stdout.
|
|
130
|
+
*
|
|
131
|
+
* @param {string[]} args Azure CLI args (excluding the `az` binary).
|
|
132
|
+
* @param {{ cwd?: string } | undefined} [options] Execution options.
|
|
133
|
+
* @returns {Promise<string>} Command stdout.
|
|
134
|
+
*/
|
|
135
|
+
async function azText(args, { cwd } = {}) {
|
|
136
|
+
try {
|
|
137
|
+
const { stdout } = await execa('az', args, { cwd });
|
|
138
|
+
return stdout;
|
|
139
|
+
} catch (err) {
|
|
140
|
+
throw wrapAzError(args, err);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Prompts the user to choose an environment from a release definition.
|
|
146
|
+
*
|
|
147
|
+
* @param {AZReleaseDefinition} definition Azure DevOps release definition object.
|
|
148
|
+
* @param {{ definitionId?: number | string } | undefined} [options] Options for error context.
|
|
149
|
+
* @returns {Promise<AZEnvironment>} Selected environment object.
|
|
150
|
+
*/
|
|
151
|
+
async function promptForEnvironment(definition, { definitionId } = {}) {
|
|
152
|
+
const envs = definition?.environments;
|
|
153
|
+
if (!Array.isArray(envs) || envs.length === 0) {
|
|
154
|
+
throw new Error(
|
|
155
|
+
`No environments found in release definition${definitionId ? ` id ${definitionId}` : ''}.`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const envIdOrIndex = await inquirer.select({
|
|
160
|
+
message: 'Select an environment:',
|
|
161
|
+
pageSize: 20,
|
|
162
|
+
// Keep API order (no sorting)
|
|
163
|
+
choices: envs.map((e, idx) => {
|
|
164
|
+
const name = (e?.name && String(e.name).trim()) || '(unnamed environment)';
|
|
165
|
+
const id = e?.id;
|
|
166
|
+
const label = id !== undefined ? `${name} (id: ${id})` : name;
|
|
167
|
+
const value = id !== undefined ? id : idx;
|
|
168
|
+
return { name: label, value };
|
|
169
|
+
}),
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const selectedById = envs.find((e) => e?.id === envIdOrIndex);
|
|
173
|
+
if (selectedById) return selectedById;
|
|
174
|
+
|
|
175
|
+
if (Number.isInteger(envIdOrIndex) && envIdOrIndex >= 0 && envIdOrIndex < envs.length) {
|
|
176
|
+
return envs[envIdOrIndex];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
throw new Error('Selected environment could not be resolved.');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Converts a variable map into `.env` file content.
|
|
184
|
+
*
|
|
185
|
+
* @param {Record<string, { value: string } | string | number | boolean | null | undefined>} vars Variables to serialize.
|
|
186
|
+
* @returns {string} Dotenv formatted content.
|
|
187
|
+
*/
|
|
188
|
+
function toDotenv(vars) {
|
|
189
|
+
// vars: Record<string, { value: string } | string>
|
|
190
|
+
// We will output KEY=VALUE, quoting when necessary.
|
|
191
|
+
const lines = [];
|
|
192
|
+
for (const [key, raw] of Object.entries(vars)) {
|
|
193
|
+
const value = raw && typeof raw === 'object' && 'value' in raw ? raw.value : raw;
|
|
194
|
+
|
|
195
|
+
if (value === null || value === undefined) {
|
|
196
|
+
lines.push(`${key}=`);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const str = String(value);
|
|
201
|
+
|
|
202
|
+
// Quote if contains spaces, #, quotes, or newlines.
|
|
203
|
+
if (/[\s#"'\n\r]/.test(str)) {
|
|
204
|
+
// Use double quotes; escape backslashes, double quotes, and newlines.
|
|
205
|
+
const escaped = str
|
|
206
|
+
.replace(/\\/g, '\\\\')
|
|
207
|
+
.replace(/"/g, '\\"')
|
|
208
|
+
.replace(/\r/g, '\\r')
|
|
209
|
+
.replace(/\n/g, '\\n');
|
|
210
|
+
lines.push(`${key}="${escaped}"`);
|
|
211
|
+
} else {
|
|
212
|
+
lines.push(`${key}=${str}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return lines.join('\n') + (lines.length ? '\n' : '');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Simplifies Azure release variables to a plain key/value object.
|
|
220
|
+
*
|
|
221
|
+
* @param {Record<string, { value: string } | string | number | boolean | null | undefined>} vars Azure release variables.
|
|
222
|
+
* @returns {Record<string, any>} Simplified key/value object.
|
|
223
|
+
*/
|
|
224
|
+
function simplifyVars(vars) {
|
|
225
|
+
// Azure release variables shape: { KEY: { value, isSecret, ... } }
|
|
226
|
+
// We'll omit secrets? For now, include value if present.
|
|
227
|
+
const out = {};
|
|
228
|
+
for (const [key, v] of Object.entries(vars || {})) {
|
|
229
|
+
if (v && typeof v === 'object' && 'value' in v) out[key] = v.value;
|
|
230
|
+
else out[key] = v;
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Ensures the Azure CLI and the azure-devops extension are available.
|
|
237
|
+
*
|
|
238
|
+
* @param {void} _ Unused.
|
|
239
|
+
* @returns {Promise<void>} Resolves when checks pass.
|
|
240
|
+
*/
|
|
241
|
+
async function ensureAzAvailable() {
|
|
242
|
+
// Dedicated check so we can surface a clearer message.
|
|
243
|
+
try {
|
|
244
|
+
await azText(['--version']);
|
|
245
|
+
} catch (err) {
|
|
246
|
+
throw err;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Also validate azure-devops extension is available.
|
|
250
|
+
// This will also catch auth issues in some setups, but mostly ensures az devops exists.
|
|
251
|
+
await azText(['devops', '-h']);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Reads Azure DevOps defaults from `az devops configure -l`.
|
|
256
|
+
*
|
|
257
|
+
* @param {void} _ Unused.
|
|
258
|
+
* @returns {Promise<{ organization?: string, project?: string }>} Current defaults.
|
|
259
|
+
*/
|
|
260
|
+
async function getDevopsDefaults() {
|
|
261
|
+
// Returns { organization, project } possibly undefined.
|
|
262
|
+
// az devops configure -l output example (text):
|
|
263
|
+
// organization=https://dev.azure.com/foo
|
|
264
|
+
// project=bar
|
|
265
|
+
const out = await azText(['devops', 'configure', '-l']);
|
|
266
|
+
const defaults = {};
|
|
267
|
+
for (const line of String(out || '').split(/\r?\n/)) {
|
|
268
|
+
const m = line.match(/^\s*([^=]+)=(.*)\s*$/);
|
|
269
|
+
if (!m) continue;
|
|
270
|
+
const key = m[1].trim();
|
|
271
|
+
const val = m[2].trim();
|
|
272
|
+
if (key === 'organization') defaults.organization = val;
|
|
273
|
+
if (key === 'project') defaults.project = val;
|
|
274
|
+
}
|
|
275
|
+
return defaults;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Prompts for missing Azure DevOps defaults and persists them.
|
|
280
|
+
*
|
|
281
|
+
* @param {void} _ Unused.
|
|
282
|
+
* @returns {Promise<{ organization?: string, project?: string }>} Resolved defaults.
|
|
283
|
+
*/
|
|
284
|
+
async function promptAndSetDevopsDefaultsIfMissing() {
|
|
285
|
+
const defaults = await getDevopsDefaults();
|
|
286
|
+
|
|
287
|
+
const questions = [];
|
|
288
|
+
if (!defaults.organization) {
|
|
289
|
+
questions.push({
|
|
290
|
+
required: true,
|
|
291
|
+
message: 'Azure DevOps organization URL (e.g., https://dev.azure.com/your-org):',
|
|
292
|
+
validate: (v) => (String(v || '').trim() ? true : 'Organization is required'),
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
if (!defaults.project) {
|
|
296
|
+
questions.push({
|
|
297
|
+
required: true,
|
|
298
|
+
message: 'Azure DevOps project name:',
|
|
299
|
+
validate: (v) => (String(v || '').trim() ? true : 'Project is required'),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (questions.length === 0) return defaults;
|
|
304
|
+
|
|
305
|
+
questions.forEach(async (q) => {
|
|
306
|
+
const anser = await inquirer.input(q);
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
const newDefaults = {
|
|
310
|
+
organization: defaults.organization || answers.organization,
|
|
311
|
+
project: defaults.project || answers.project,
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
// Persist defaults for subsequent az devops commands.
|
|
315
|
+
await azText([
|
|
316
|
+
'devops',
|
|
317
|
+
'configure',
|
|
318
|
+
'-d',
|
|
319
|
+
`organization=${newDefaults.organization}`,
|
|
320
|
+
`project=${newDefaults.project}`,
|
|
321
|
+
]);
|
|
322
|
+
|
|
323
|
+
return newDefaults;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Lists Azure DevOps release definitions for the configured project.
|
|
328
|
+
*
|
|
329
|
+
* @param {void} _ Unused.
|
|
330
|
+
* @returns {Promise<Array<{ id: number, name: string }>>} Release definitions.
|
|
331
|
+
*/
|
|
332
|
+
async function listReleaseDefinitions() {
|
|
333
|
+
// Query trims output to id/name.
|
|
334
|
+
return azJson([
|
|
335
|
+
'pipelines',
|
|
336
|
+
'release',
|
|
337
|
+
'definition',
|
|
338
|
+
'list',
|
|
339
|
+
'--query',
|
|
340
|
+
"[].{id:id,name:name}",
|
|
341
|
+
]);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Fetches a single release definition by id.
|
|
346
|
+
*
|
|
347
|
+
* @param {number | string} id Release definition id.
|
|
348
|
+
* @returns {Promise<AZReleaseDefinition>} Release definition.
|
|
349
|
+
*/
|
|
350
|
+
async function showReleaseDefinition(id) {
|
|
351
|
+
return azJson(['pipelines', 'release', 'definition', 'show', '--id', String(id)]);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Runs the interactive CLI flow.
|
|
358
|
+
*
|
|
359
|
+
* @param {void} _ Unused.
|
|
360
|
+
* @returns {Promise<void>} Resolves when flow completes.
|
|
361
|
+
*/
|
|
362
|
+
async function runInteractive() {
|
|
363
|
+
await ensureAzAvailable();
|
|
364
|
+
await promptAndSetDevopsDefaultsIfMissing();
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* List release definitions
|
|
368
|
+
* @type {Array<{id: number, name: string}>}
|
|
369
|
+
*/
|
|
370
|
+
const defs = await listReleaseDefinitions();
|
|
371
|
+
if (!defs || defs.length === 0) {
|
|
372
|
+
throw new Error('No release definitions found in this Azure DevOps project.');
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const defId = await inquirer.select({
|
|
376
|
+
message: 'Select a release pipeline definition:',
|
|
377
|
+
choices: defs
|
|
378
|
+
.slice()
|
|
379
|
+
.sort((a, b) => String(a.name).localeCompare(String(b.name)))
|
|
380
|
+
.map((d) => ({
|
|
381
|
+
name: `${d.name} (id: ${d.id})`,
|
|
382
|
+
value: d.id,
|
|
383
|
+
})),
|
|
384
|
+
pageSize: 20,
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
const definition = await showReleaseDefinition(defId);
|
|
388
|
+
const selectedEnv = await promptForEnvironment(definition, { definitionId: defId });
|
|
389
|
+
|
|
390
|
+
const envVars = selectedEnv.variables || {};
|
|
391
|
+
const simplified = simplifyVars(envVars);
|
|
392
|
+
|
|
393
|
+
const keys = Object.keys(simplified);
|
|
394
|
+
if (keys.length === 0) {
|
|
395
|
+
const envName = selectedEnv?.name ? `"${selectedEnv.name}" ` : '';
|
|
396
|
+
throw new Error(`Selected environment ${envName}has no variables in definition id ${defId}.`);
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const format = await inquirer.select({
|
|
400
|
+
message: 'Export format:',
|
|
401
|
+
choices: [
|
|
402
|
+
{ name: '.env', value: 'env' },
|
|
403
|
+
{ name: 'JSON', value: 'json' },
|
|
404
|
+
],
|
|
405
|
+
default: 'env',
|
|
406
|
+
})
|
|
407
|
+
|
|
408
|
+
const cwd = process.cwd();
|
|
409
|
+
const outPath =
|
|
410
|
+
format === 'env' ? path.join(cwd, '.env') : path.join(cwd, 'env.json');
|
|
411
|
+
|
|
412
|
+
const confirmWrite = await inquirer.confirm({
|
|
413
|
+
message: `Write ${keys.length} variables to ${path.basename(outPath)} in current directory?`,
|
|
414
|
+
default: true,
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
if (!confirmWrite) return;
|
|
418
|
+
|
|
419
|
+
if (format === 'env') {
|
|
420
|
+
await fs.writeFile(outPath, toDotenv(simplified), 'utf8');
|
|
421
|
+
} else {
|
|
422
|
+
await fs.writeFile(outPath, JSON.stringify(simplified, null, 2) + '\n', 'utf8');
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// eslint-disable-next-line no-console
|
|
426
|
+
console.log(`Wrote ${keys.length} variables to ${outPath}`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* CLI entrypoint.
|
|
431
|
+
*
|
|
432
|
+
* @param {void} _ Unused.
|
|
433
|
+
* @returns {Promise<void>} Resolves when the CLI finishes parsing.
|
|
434
|
+
*/
|
|
435
|
+
async function main() {
|
|
436
|
+
const program = new Command();
|
|
437
|
+
|
|
438
|
+
program
|
|
439
|
+
.name('azdo-release-env')
|
|
440
|
+
.description(
|
|
441
|
+
'Extract DEV/Development environment variables from Azure DevOps release pipelines'
|
|
442
|
+
)
|
|
443
|
+
.version('1.0.0');
|
|
444
|
+
|
|
445
|
+
program.action(async () => {
|
|
446
|
+
try {
|
|
447
|
+
await runInteractive();
|
|
448
|
+
} catch (err) {
|
|
449
|
+
// eslint-disable-next-line no-console
|
|
450
|
+
console.error(formatCliError(CLI_STYLES, err));
|
|
451
|
+
process.exitCode = 1;
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
await program.parseAsync(process.argv);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
main();
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
// Change this to match your project
|
|
3
|
+
"include": ["src/**/*"],
|
|
4
|
+
"compilerOptions": {
|
|
5
|
+
// Tells TypeScript to read JS files, as
|
|
6
|
+
// normally they are ignored as source files
|
|
7
|
+
"allowJs": true,
|
|
8
|
+
// Generate d.ts files
|
|
9
|
+
"declaration": true,
|
|
10
|
+
// This compiler run should
|
|
11
|
+
// only output d.ts files
|
|
12
|
+
"emitDeclarationOnly": true,
|
|
13
|
+
// Types should go into this directory.
|
|
14
|
+
// Removing this would place the .d.ts files
|
|
15
|
+
// next to the .js files
|
|
16
|
+
"outDir": "dist",
|
|
17
|
+
// go to js file when using IDE functions like
|
|
18
|
+
// "Go to Definition" in VSCode
|
|
19
|
+
"declarationMap": true,
|
|
20
|
+
},
|
|
21
|
+
}
|
package/types/index.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export type AZReleaseVariable = {
|
|
3
|
+
/**
|
|
4
|
+
* Variable value.
|
|
5
|
+
*/
|
|
6
|
+
value: string;
|
|
7
|
+
/**
|
|
8
|
+
* Whether the variable is marked as secret.
|
|
9
|
+
*/
|
|
10
|
+
isSecret: boolean;
|
|
11
|
+
};
|
|
12
|
+
export type AZEnvironment = {
|
|
13
|
+
/**
|
|
14
|
+
* Environment name.
|
|
15
|
+
*/
|
|
16
|
+
name: string;
|
|
17
|
+
/**
|
|
18
|
+
* Environment id.
|
|
19
|
+
*/
|
|
20
|
+
id: number;
|
|
21
|
+
/**
|
|
22
|
+
* Environment variables.
|
|
23
|
+
*/
|
|
24
|
+
variables: Record<string, AZReleaseVariable>;
|
|
25
|
+
};
|
|
26
|
+
export type AZReleaseDefinition = {
|
|
27
|
+
/**
|
|
28
|
+
* Release definition name.
|
|
29
|
+
*/
|
|
30
|
+
name: string;
|
|
31
|
+
/**
|
|
32
|
+
* Release definition id.
|
|
33
|
+
*/
|
|
34
|
+
id: number;
|
|
35
|
+
/**
|
|
36
|
+
* List of environments in the release definition.
|
|
37
|
+
*/
|
|
38
|
+
environments: AZEnvironment[];
|
|
39
|
+
};
|