@backstage-community/plugin-jenkins-common 0.12.0 → 0.14.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/CHANGELOG.md +12 -0
- package/dist/client/buildApi.cjs.js +34 -0
- package/dist/client/buildApi.cjs.js.map +1 -0
- package/dist/client/buildApi.esm.js +32 -0
- package/dist/client/buildApi.esm.js.map +1 -0
- package/dist/client/jobApi.cjs.js +180 -0
- package/dist/client/jobApi.cjs.js.map +1 -0
- package/dist/client/jobApi.esm.js +178 -0
- package/dist/client/jobApi.esm.js.map +1 -0
- package/dist/client/utils.cjs.js +39 -0
- package/dist/client/utils.cjs.js.map +1 -0
- package/dist/client/utils.esm.js +33 -0
- package/dist/client/utils.esm.js.map +1 -0
- package/dist/client.cjs.js +180 -0
- package/dist/client.cjs.js.map +1 -0
- package/dist/client.esm.js +174 -0
- package/dist/client.esm.js.map +1 -0
- package/dist/index.cjs.js +2 -0
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +116 -1
- package/dist/index.esm.js +1 -0
- package/dist/index.esm.js.map +1 -1
- package/package.json +6 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @backstage-community/plugin-jenkins-common
|
|
2
2
|
|
|
3
|
+
## 0.14.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- daa9057: Backstage version bump to v1.45.3
|
|
8
|
+
|
|
9
|
+
## 0.13.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- cac3437: Replace the deprecated `jenkins` NPM package with a built-in, light-weight client.
|
|
14
|
+
|
|
3
15
|
## 0.12.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function createBuildApi(deps) {
|
|
4
|
+
const { normalizeJobName, request } = deps;
|
|
5
|
+
return {
|
|
6
|
+
/**
|
|
7
|
+
* Retrieves a build's JSON representation from Jenkins.
|
|
8
|
+
*
|
|
9
|
+
* @param name - A build name (string or segments).
|
|
10
|
+
* @param buildNumber - The build number to retrieve.
|
|
11
|
+
* @returns A `JenkinsBuild` object with metadata about the specified build.
|
|
12
|
+
*/
|
|
13
|
+
get: async (name, buildNumber) => {
|
|
14
|
+
const jobPath = normalizeJobName(name);
|
|
15
|
+
return request(`${jobPath}/${buildNumber}/api/json`);
|
|
16
|
+
},
|
|
17
|
+
/**
|
|
18
|
+
* Retrieves a build's consoleText from Jenkins.
|
|
19
|
+
*
|
|
20
|
+
* @param name - A build name (string or segments).
|
|
21
|
+
* @param buildNumber - The build number to retrieve logs for.
|
|
22
|
+
* @returns The build's console output as plain text.
|
|
23
|
+
*/
|
|
24
|
+
getConsoleText: async (name, buildNumber) => {
|
|
25
|
+
const jobPath = normalizeJobName(name);
|
|
26
|
+
return request(`${jobPath}/${buildNumber}/consoleText`, {
|
|
27
|
+
rawText: true
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
exports.createBuildApi = createBuildApi;
|
|
34
|
+
//# sourceMappingURL=buildApi.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buildApi.cjs.js","sources":["../../src/client/buildApi.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { JenkinsBuild } from '../types';\n\nexport interface BuildDeps {\n normalizeJobName(string: string | string[] | undefined): string | undefined;\n request(\n path: string,\n opts?: {\n method?: string;\n query?: Record<string, string | number | undefined>;\n body?: any;\n rawText?: boolean;\n contentType?: string;\n },\n ): Promise<any>;\n}\n\n/**\n * Factory for creating a Jenkins Build API interface.\n *\n * Provides helpers for common Jenkins job operations such as:\n * - Fetching build details (`get`)\n * - Fetching build console output as plain text (`getConsoleText`)\n *\n * This function is intended to be used by higher-level clients (e.g., `Jenkins`)\n * and delegates low-level requests to the provided `request` dependency.\n *\n * @param deps - Dependency injection hooks for request handling and job name normalization.\n * @returns An object with methods for interacting with Jenkins builds.\n */\nexport function createBuildApi(deps: BuildDeps) {\n const { normalizeJobName, request } = deps;\n\n return {\n /**\n * Retrieves a build's JSON representation from Jenkins.\n *\n * @param name - A build name (string or segments).\n * @param buildNumber - The build number to retrieve.\n * @returns A `JenkinsBuild` object with metadata about the specified build.\n */\n get: async (\n name: string | string[],\n buildNumber: number | string,\n ): Promise<JenkinsBuild> => {\n const jobPath = normalizeJobName(name);\n return request(`${jobPath}/${buildNumber}/api/json`);\n },\n\n /**\n * Retrieves a build's consoleText from Jenkins.\n *\n * @param name - A build name (string or segments).\n * @param buildNumber - The build number to retrieve logs for.\n * @returns The build's console output as plain text.\n */\n getConsoleText: async (\n name: string | string[],\n buildNumber: number | string,\n ): Promise<string> => {\n const jobPath = normalizeJobName(name);\n return request(`${jobPath}/${buildNumber}/consoleText`, {\n rawText: true,\n }) as Promise<string>;\n },\n };\n}\n"],"names":[],"mappings":";;AA4CO,SAAS,eAAe,IAAiB,EAAA;AAC9C,EAAM,MAAA,EAAE,gBAAkB,EAAA,OAAA,EAAY,GAAA,IAAA;AAEtC,EAAO,OAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQL,GAAA,EAAK,OACH,IAAA,EACA,WAC0B,KAAA;AAC1B,MAAM,MAAA,OAAA,GAAU,iBAAiB,IAAI,CAAA;AACrC,MAAA,OAAO,OAAQ,CAAA,CAAA,EAAG,OAAO,CAAA,CAAA,EAAI,WAAW,CAAW,SAAA,CAAA,CAAA;AAAA,KACrD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASA,cAAA,EAAgB,OACd,IAAA,EACA,WACoB,KAAA;AACpB,MAAM,MAAA,OAAA,GAAU,iBAAiB,IAAI,CAAA;AACrC,MAAA,OAAO,OAAQ,CAAA,CAAA,EAAG,OAAO,CAAA,CAAA,EAAI,WAAW,CAAgB,YAAA,CAAA,EAAA;AAAA,QACtD,OAAS,EAAA;AAAA,OACV,CAAA;AAAA;AACH,GACF;AACF;;;;"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
function createBuildApi(deps) {
|
|
2
|
+
const { normalizeJobName, request } = deps;
|
|
3
|
+
return {
|
|
4
|
+
/**
|
|
5
|
+
* Retrieves a build's JSON representation from Jenkins.
|
|
6
|
+
*
|
|
7
|
+
* @param name - A build name (string or segments).
|
|
8
|
+
* @param buildNumber - The build number to retrieve.
|
|
9
|
+
* @returns A `JenkinsBuild` object with metadata about the specified build.
|
|
10
|
+
*/
|
|
11
|
+
get: async (name, buildNumber) => {
|
|
12
|
+
const jobPath = normalizeJobName(name);
|
|
13
|
+
return request(`${jobPath}/${buildNumber}/api/json`);
|
|
14
|
+
},
|
|
15
|
+
/**
|
|
16
|
+
* Retrieves a build's consoleText from Jenkins.
|
|
17
|
+
*
|
|
18
|
+
* @param name - A build name (string or segments).
|
|
19
|
+
* @param buildNumber - The build number to retrieve logs for.
|
|
20
|
+
* @returns The build's console output as plain text.
|
|
21
|
+
*/
|
|
22
|
+
getConsoleText: async (name, buildNumber) => {
|
|
23
|
+
const jobPath = normalizeJobName(name);
|
|
24
|
+
return request(`${jobPath}/${buildNumber}/consoleText`, {
|
|
25
|
+
rawText: true
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export { createBuildApi };
|
|
32
|
+
//# sourceMappingURL=buildApi.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buildApi.esm.js","sources":["../../src/client/buildApi.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { JenkinsBuild } from '../types';\n\nexport interface BuildDeps {\n normalizeJobName(string: string | string[] | undefined): string | undefined;\n request(\n path: string,\n opts?: {\n method?: string;\n query?: Record<string, string | number | undefined>;\n body?: any;\n rawText?: boolean;\n contentType?: string;\n },\n ): Promise<any>;\n}\n\n/**\n * Factory for creating a Jenkins Build API interface.\n *\n * Provides helpers for common Jenkins job operations such as:\n * - Fetching build details (`get`)\n * - Fetching build console output as plain text (`getConsoleText`)\n *\n * This function is intended to be used by higher-level clients (e.g., `Jenkins`)\n * and delegates low-level requests to the provided `request` dependency.\n *\n * @param deps - Dependency injection hooks for request handling and job name normalization.\n * @returns An object with methods for interacting with Jenkins builds.\n */\nexport function createBuildApi(deps: BuildDeps) {\n const { normalizeJobName, request } = deps;\n\n return {\n /**\n * Retrieves a build's JSON representation from Jenkins.\n *\n * @param name - A build name (string or segments).\n * @param buildNumber - The build number to retrieve.\n * @returns A `JenkinsBuild` object with metadata about the specified build.\n */\n get: async (\n name: string | string[],\n buildNumber: number | string,\n ): Promise<JenkinsBuild> => {\n const jobPath = normalizeJobName(name);\n return request(`${jobPath}/${buildNumber}/api/json`);\n },\n\n /**\n * Retrieves a build's consoleText from Jenkins.\n *\n * @param name - A build name (string or segments).\n * @param buildNumber - The build number to retrieve logs for.\n * @returns The build's console output as plain text.\n */\n getConsoleText: async (\n name: string | string[],\n buildNumber: number | string,\n ): Promise<string> => {\n const jobPath = normalizeJobName(name);\n return request(`${jobPath}/${buildNumber}/consoleText`, {\n rawText: true,\n }) as Promise<string>;\n },\n };\n}\n"],"names":[],"mappings":"AA4CO,SAAS,eAAe,IAAiB,EAAA;AAC9C,EAAM,MAAA,EAAE,gBAAkB,EAAA,OAAA,EAAY,GAAA,IAAA;AAEtC,EAAO,OAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAQL,GAAA,EAAK,OACH,IAAA,EACA,WAC0B,KAAA;AAC1B,MAAM,MAAA,OAAA,GAAU,iBAAiB,IAAI,CAAA;AACrC,MAAA,OAAO,OAAQ,CAAA,CAAA,EAAG,OAAO,CAAA,CAAA,EAAI,WAAW,CAAW,SAAA,CAAA,CAAA;AAAA,KACrD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IASA,cAAA,EAAgB,OACd,IAAA,EACA,WACoB,KAAA;AACpB,MAAM,MAAA,OAAA,GAAU,iBAAiB,IAAI,CAAA;AACrC,MAAA,OAAO,OAAQ,CAAA,CAAA,EAAG,OAAO,CAAA,CAAA,EAAI,WAAW,CAAgB,YAAA,CAAA,EAAA;AAAA,QACtD,OAAS,EAAA;AAAA,OACV,CAAA;AAAA;AACH,GACF;AACF;;;;"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function createJobApi(deps) {
|
|
4
|
+
const { normalizeJobName, request } = deps;
|
|
5
|
+
const paramsToSearchParams = (params) => {
|
|
6
|
+
if (!params) {
|
|
7
|
+
return new URLSearchParams();
|
|
8
|
+
}
|
|
9
|
+
if (params instanceof URLSearchParams) {
|
|
10
|
+
return params;
|
|
11
|
+
}
|
|
12
|
+
const result = new URLSearchParams();
|
|
13
|
+
for (const [k, v] of Object.entries(params)) {
|
|
14
|
+
if (v === void 0 || v === null) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
result.set(k, String(v));
|
|
18
|
+
}
|
|
19
|
+
return result;
|
|
20
|
+
};
|
|
21
|
+
const leafSegment = (name) => {
|
|
22
|
+
if (Array.isArray(name)) {
|
|
23
|
+
return name[name.length - 1];
|
|
24
|
+
}
|
|
25
|
+
const parts = name.split("/").filter(Boolean);
|
|
26
|
+
return parts[parts.length - 1] ?? "";
|
|
27
|
+
};
|
|
28
|
+
const parentSegments = (name) => {
|
|
29
|
+
if (!name) {
|
|
30
|
+
return [];
|
|
31
|
+
}
|
|
32
|
+
if (Array.isArray(name)) {
|
|
33
|
+
return name.slice(0, -1);
|
|
34
|
+
}
|
|
35
|
+
const parts = name.split("/").filter(Boolean);
|
|
36
|
+
if (parts.length > 1) {
|
|
37
|
+
return parts.slice(0, -1);
|
|
38
|
+
}
|
|
39
|
+
return [];
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
/**
|
|
43
|
+
* Retrieves a job’s JSON representation from Jenkins.
|
|
44
|
+
*
|
|
45
|
+
* @param input - A {@link JobGetOptions} object. `tree` and `depth`
|
|
46
|
+
* are forwarded to `/api/json` as query params.
|
|
47
|
+
* @returns The parsed job JSON.
|
|
48
|
+
*/
|
|
49
|
+
get: async (input) => {
|
|
50
|
+
const { name, tree, depth } = input;
|
|
51
|
+
const jobPath = normalizeJobName(name);
|
|
52
|
+
const query = {};
|
|
53
|
+
if (tree) {
|
|
54
|
+
query.tree = tree;
|
|
55
|
+
}
|
|
56
|
+
if (typeof depth === "number") {
|
|
57
|
+
query.depth = depth;
|
|
58
|
+
}
|
|
59
|
+
return request(`${jobPath}/api/json`, { query });
|
|
60
|
+
},
|
|
61
|
+
/**
|
|
62
|
+
* Retrieves only the builds portion of a job (server-side filtered via `tree`).
|
|
63
|
+
*
|
|
64
|
+
* @param name - The job name (string or array).
|
|
65
|
+
* @param tree - The Jenkins Remote API `tree` expression selecting build fields.
|
|
66
|
+
* Defaults to `builds[number,url,result,timestamp,id,queueId,displayName,duration]`
|
|
67
|
+
* @returns A JSON object containing the requested build fields.
|
|
68
|
+
*/
|
|
69
|
+
getBuilds: async (name, tree = "builds[number,url,result,timestamp,id,queueId,displayName,duration]") => {
|
|
70
|
+
const jobPath = normalizeJobName(name);
|
|
71
|
+
return request(`${jobPath}/api/json`, {
|
|
72
|
+
query: { tree }
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
/**
|
|
76
|
+
* Triggers a Jenkins job build.
|
|
77
|
+
*
|
|
78
|
+
* Uses `/build` or `/buildWithParameters` depending on whether parameters are provided.
|
|
79
|
+
* Automatically URL-encodes parameters and supports legacy options like `delay` and `token`.
|
|
80
|
+
*
|
|
81
|
+
* @param name - The job name (string or array form).
|
|
82
|
+
* @param opts - Optional build options (parameters, token, delay).
|
|
83
|
+
* @returns A promise that resolves when the build request is accepted.
|
|
84
|
+
*/
|
|
85
|
+
build: async (name, opts) => {
|
|
86
|
+
const { parameters, token, delay } = opts ?? {};
|
|
87
|
+
const jobPath = normalizeJobName(name);
|
|
88
|
+
const hasParams = parameters instanceof URLSearchParams ? parameters.toString().length > 0 : parameters && Object.keys(parameters).length > 0;
|
|
89
|
+
const endpoint = hasParams ? "buildWithParameters" : "build";
|
|
90
|
+
const query = {
|
|
91
|
+
...token ? { token } : {},
|
|
92
|
+
// Legacy client support: add delay option
|
|
93
|
+
...delay !== void 0 ? { delay } : {}
|
|
94
|
+
};
|
|
95
|
+
const body = hasParams ? paramsToSearchParams(parameters) : void 0;
|
|
96
|
+
return request(`${jobPath}/${endpoint}`, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
query,
|
|
99
|
+
body
|
|
100
|
+
});
|
|
101
|
+
},
|
|
102
|
+
/**
|
|
103
|
+
* Copies a job to a new name (optionally inside folders).
|
|
104
|
+
*
|
|
105
|
+
* **Important:** For the `from` argument, pass the *slashy* full name (e.g. `"a/b/src"`).
|
|
106
|
+
* Do **not** normalize it to `/job/...` form, Jenkins expects the raw slash-separated name.
|
|
107
|
+
* Only the *leaf* of the new job goes in the `?name=` query; parent folders are derived
|
|
108
|
+
* from `name` and embedded in the URL path.
|
|
109
|
+
*
|
|
110
|
+
* @param name - Target job name (string or segments). Parent parts become folders; leaf is the new job name.
|
|
111
|
+
* @param from - Source job’s slashy full name (e.g. `"folder/old"`).
|
|
112
|
+
*/
|
|
113
|
+
copy: async (name, from) => {
|
|
114
|
+
const segments = parentSegments(name);
|
|
115
|
+
const leaf = leafSegment(name);
|
|
116
|
+
const folderPath = segments.length ? segments.map(normalizeJobName).join("/") : "";
|
|
117
|
+
const url = folderPath ? `${folderPath}/createItem` : "createItem";
|
|
118
|
+
return request(url, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
query: {
|
|
121
|
+
name: leaf,
|
|
122
|
+
mode: "copy",
|
|
123
|
+
from
|
|
124
|
+
// Keep slashy!
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
},
|
|
128
|
+
/**
|
|
129
|
+
* Creates a new job from an XML configuration payload.
|
|
130
|
+
*
|
|
131
|
+
* Only the *leaf* job name is sent in `?name=`; any parent segments become
|
|
132
|
+
* folder parts embedded in the URL path.
|
|
133
|
+
*
|
|
134
|
+
* @param name - The destination job name (string or segments).
|
|
135
|
+
* @param xml - The Jenkins job config.xml content.
|
|
136
|
+
*/
|
|
137
|
+
create: async (name, xml) => {
|
|
138
|
+
const segments = parentSegments(name);
|
|
139
|
+
const leaf = leafSegment(name);
|
|
140
|
+
const folderPath = segments.length ? segments.map(normalizeJobName).join("/") : "";
|
|
141
|
+
const url = folderPath ? `${folderPath}/createItem` : "createItem";
|
|
142
|
+
return request(url, {
|
|
143
|
+
method: "POST",
|
|
144
|
+
query: { name: leaf },
|
|
145
|
+
body: xml,
|
|
146
|
+
contentType: "application/xml"
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
/**
|
|
150
|
+
* Permanently deletes a job.
|
|
151
|
+
*
|
|
152
|
+
* @param name - The job name (string or segments).
|
|
153
|
+
*/
|
|
154
|
+
destroy: async (name) => {
|
|
155
|
+
const jobPath = normalizeJobName(name);
|
|
156
|
+
return request(`${jobPath}/doDelete`, { method: "POST" });
|
|
157
|
+
},
|
|
158
|
+
/**
|
|
159
|
+
* Enables a disabled job.
|
|
160
|
+
*
|
|
161
|
+
* @param name - The job name (string or segments).
|
|
162
|
+
*/
|
|
163
|
+
enable: async (name) => {
|
|
164
|
+
const jobPath = normalizeJobName(name);
|
|
165
|
+
return request(`${jobPath}/enable`, { method: "POST" });
|
|
166
|
+
},
|
|
167
|
+
/**
|
|
168
|
+
* Disables a job (prevents builds from being scheduled).
|
|
169
|
+
*
|
|
170
|
+
* @param name - The job name (string or segments).
|
|
171
|
+
*/
|
|
172
|
+
disable: async (name) => {
|
|
173
|
+
const jobPath = normalizeJobName(name);
|
|
174
|
+
return request(`${jobPath}/disable`, { method: "POST" });
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
exports.createJobApi = createJobApi;
|
|
180
|
+
//# sourceMappingURL=jobApi.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jobApi.cjs.js","sources":["../../src/client/jobApi.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { JenkinsParams, JobBuildOptions, JobGetOptions } from './types';\n\nexport interface JobDeps {\n normalizeJobName(name: string | string[] | undefined): string | undefined;\n request(\n path: string,\n opts?: {\n method?: string;\n query?: Record<string, string | number | undefined>;\n body?: any;\n rawText?: boolean;\n contentType?: string;\n },\n ): Promise<any>;\n}\n\n/**\n * Factory for creating a Jenkins Job API interface.\n *\n * Provides helpers for common Jenkins job operations such as:\n * - Fetching job details (`get`)\n * - Triggering builds (`build`)\n * - Copying or creating jobs (`copy`, `create`)\n * - Managing job state (`enable`, `disable`, `destroy`)\n *\n * This function is intended to be used by higher-level clients (e.g., `Jenkins`)\n * and delegates low-level requests to the provided `request` dependency.\n *\n * @param deps - Dependency injection hooks for request handling and job name normalization.\n * @returns An object with methods for interacting with Jenkins jobs.\n */\nexport function createJobApi(deps: JobDeps) {\n const { normalizeJobName, request } = deps;\n\n // Helper utils\n\n /**\n * Takes in a {@link JenkinsParams} object and returns a {@link URLSearchParams} object.\n * - If the object passed is `undefined`, an empty {@link URLSearchParams} is returned.\n * - If the object passed is already a {@link URLSearchParams} it gets returned as is.\n *\n * @param params a {@link JenkinsParams} object.\n * @returns a {@link URLSearchParams} object\n */\n const paramsToSearchParams = (params?: JenkinsParams): URLSearchParams => {\n if (!params) {\n return new URLSearchParams();\n }\n if (params instanceof URLSearchParams) {\n return params;\n }\n\n const result = new URLSearchParams();\n for (const [k, v] of Object.entries(params)) {\n if (v === undefined || v === null) {\n continue;\n }\n result.set(k, String(v));\n }\n return result;\n };\n\n /**\n * Extracts the last path segment (the \"leaf\" job name) from a Jenkins job name.\n *\n * @param name - The job name, either as a slash-delimited string (e.g. \"folder/job\")\n * or as an array of segments (e.g. [\"folder\", \"job\"]).\n * @returns The last segment of the job name, or an empty string if none exists.\n *\n * @example\n * leafSegment(\"folder/job\"); // \"job\"\n * leafSegment([\"folder\", \"job\"]); // \"job\"\n * leafSegment(\"root\"); // \"root\"\n */\n const leafSegment = (name: string | string[]): string => {\n if (Array.isArray(name)) {\n return name[name.length - 1];\n }\n const parts = name.split('/').filter(Boolean);\n return parts[parts.length - 1] ?? '';\n };\n\n /**\n * Returns all parent path segments of a Jenkins job name, excluding the leaf.\n *\n * @param name - The job name, either as a slash-delimited string (e.g. \"a/b/c\")\n * or as an array of segments (e.g. [\"a\", \"b\", \"c\"]).\n * @returns An array of all parent segments, or an empty array if the job is at the root.\n *\n * @example\n * parentSegments(\"a/b/c\"); // [\"a\", \"b\"]\n * parentSegments([\"a\", \"b\", \"c\"]); // [\"a\", \"b\"]\n * parentSegments(\"job\"); // []\n */\n const parentSegments = (name: string | string[] | undefined): string[] => {\n if (!name) {\n return [];\n }\n\n // Return everything but leaf\n if (Array.isArray(name)) {\n return name.slice(0, -1);\n }\n\n const parts = name.split('/').filter(Boolean);\n if (parts.length > 1) {\n return parts.slice(0, -1);\n }\n\n return [];\n };\n\n return {\n /**\n * Retrieves a job’s JSON representation from Jenkins.\n *\n * @param input - A {@link JobGetOptions} object. `tree` and `depth`\n * are forwarded to `/api/json` as query params.\n * @returns The parsed job JSON.\n */\n get: async (input: JobGetOptions) => {\n const { name, tree, depth } = input;\n const jobPath = normalizeJobName(name);\n const query: Record<string, string | number> = {};\n if (tree) {\n query.tree = tree;\n }\n if (typeof depth === 'number') {\n query.depth = depth;\n }\n return request(`${jobPath}/api/json`, { query });\n },\n\n /**\n * Retrieves only the builds portion of a job (server-side filtered via `tree`).\n *\n * @param name - The job name (string or array).\n * @param tree - The Jenkins Remote API `tree` expression selecting build fields.\n * Defaults to `builds[number,url,result,timestamp,id,queueId,displayName,duration]`\n * @returns A JSON object containing the requested build fields.\n */\n getBuilds: async (\n name: string | string[],\n tree = 'builds[number,url,result,timestamp,id,queueId,displayName,duration]',\n ): Promise<unknown> => {\n const jobPath = normalizeJobName(name);\n return request(`${jobPath}/api/json`, {\n query: { tree },\n });\n },\n\n /**\n * Triggers a Jenkins job build.\n *\n * Uses `/build` or `/buildWithParameters` depending on whether parameters are provided.\n * Automatically URL-encodes parameters and supports legacy options like `delay` and `token`.\n *\n * @param name - The job name (string or array form).\n * @param opts - Optional build options (parameters, token, delay).\n * @returns A promise that resolves when the build request is accepted.\n */\n build: async (\n name: string | string[],\n opts?: JobBuildOptions,\n ): Promise<unknown> => {\n const { parameters, token, delay } = opts ?? {};\n\n const jobPath = normalizeJobName(name);\n\n // Check if we have search params\n // This will determine the endpoint used\n const hasParams =\n parameters instanceof URLSearchParams\n ? parameters.toString().length > 0\n : parameters && Object.keys(parameters).length > 0;\n\n const endpoint = hasParams ? 'buildWithParameters' : 'build';\n\n const query: Record<string, string> = {\n ...(token ? { token } : {}),\n // Legacy client support: add delay option\n ...(delay !== undefined ? { delay } : {}),\n };\n\n const body: URLSearchParams | undefined = hasParams\n ? paramsToSearchParams(parameters)\n : undefined;\n\n return request(`${jobPath}/${endpoint}`, {\n method: 'POST',\n query,\n body,\n });\n },\n\n /**\n * Copies a job to a new name (optionally inside folders).\n *\n * **Important:** For the `from` argument, pass the *slashy* full name (e.g. `\"a/b/src\"`).\n * Do **not** normalize it to `/job/...` form, Jenkins expects the raw slash-separated name.\n * Only the *leaf* of the new job goes in the `?name=` query; parent folders are derived\n * from `name` and embedded in the URL path.\n *\n * @param name - Target job name (string or segments). Parent parts become folders; leaf is the new job name.\n * @param from - Source job’s slashy full name (e.g. `\"folder/old\"`).\n */\n copy: async (name: string | string[], from: string): Promise<void> => {\n const segments = parentSegments(name);\n const leaf = leafSegment(name);\n const folderPath = segments.length\n ? segments.map(normalizeJobName).join('/')\n : '';\n\n const url = folderPath ? `${folderPath}/createItem` : 'createItem';\n return request(url, {\n method: 'POST',\n query: {\n name: leaf,\n mode: 'copy',\n from: from, // Keep slashy!\n },\n });\n },\n\n /**\n * Creates a new job from an XML configuration payload.\n *\n * Only the *leaf* job name is sent in `?name=`; any parent segments become\n * folder parts embedded in the URL path.\n *\n * @param name - The destination job name (string or segments).\n * @param xml - The Jenkins job config.xml content.\n */\n create: async (name: string | string[], xml: string): Promise<void> => {\n const segments = parentSegments(name);\n const leaf = leafSegment(name);\n const folderPath = segments.length\n ? segments.map(normalizeJobName).join('/')\n : '';\n\n const url = folderPath ? `${folderPath}/createItem` : 'createItem';\n\n return request(url, {\n method: 'POST',\n query: { name: leaf },\n body: xml,\n contentType: 'application/xml',\n });\n },\n\n /**\n * Permanently deletes a job.\n *\n * @param name - The job name (string or segments).\n */\n destroy: async (name: string | string[]): Promise<void> => {\n const jobPath = normalizeJobName(name);\n return request(`${jobPath}/doDelete`, { method: 'POST' });\n },\n\n /**\n * Enables a disabled job.\n *\n * @param name - The job name (string or segments).\n */\n enable: async (name: string | string[]): Promise<void> => {\n const jobPath = normalizeJobName(name);\n return request(`${jobPath}/enable`, { method: 'POST' });\n },\n\n /**\n * Disables a job (prevents builds from being scheduled).\n *\n * @param name - The job name (string or segments).\n */\n disable: async (name: string | string[]): Promise<void> => {\n const jobPath = normalizeJobName(name);\n return request(`${jobPath}/disable`, { method: 'POST' });\n },\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
function createJobApi(deps) {
|
|
2
|
+
const { normalizeJobName, request } = deps;
|
|
3
|
+
const paramsToSearchParams = (params) => {
|
|
4
|
+
if (!params) {
|
|
5
|
+
return new URLSearchParams();
|
|
6
|
+
}
|
|
7
|
+
if (params instanceof URLSearchParams) {
|
|
8
|
+
return params;
|
|
9
|
+
}
|
|
10
|
+
const result = new URLSearchParams();
|
|
11
|
+
for (const [k, v] of Object.entries(params)) {
|
|
12
|
+
if (v === void 0 || v === null) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
result.set(k, String(v));
|
|
16
|
+
}
|
|
17
|
+
return result;
|
|
18
|
+
};
|
|
19
|
+
const leafSegment = (name) => {
|
|
20
|
+
if (Array.isArray(name)) {
|
|
21
|
+
return name[name.length - 1];
|
|
22
|
+
}
|
|
23
|
+
const parts = name.split("/").filter(Boolean);
|
|
24
|
+
return parts[parts.length - 1] ?? "";
|
|
25
|
+
};
|
|
26
|
+
const parentSegments = (name) => {
|
|
27
|
+
if (!name) {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
if (Array.isArray(name)) {
|
|
31
|
+
return name.slice(0, -1);
|
|
32
|
+
}
|
|
33
|
+
const parts = name.split("/").filter(Boolean);
|
|
34
|
+
if (parts.length > 1) {
|
|
35
|
+
return parts.slice(0, -1);
|
|
36
|
+
}
|
|
37
|
+
return [];
|
|
38
|
+
};
|
|
39
|
+
return {
|
|
40
|
+
/**
|
|
41
|
+
* Retrieves a job’s JSON representation from Jenkins.
|
|
42
|
+
*
|
|
43
|
+
* @param input - A {@link JobGetOptions} object. `tree` and `depth`
|
|
44
|
+
* are forwarded to `/api/json` as query params.
|
|
45
|
+
* @returns The parsed job JSON.
|
|
46
|
+
*/
|
|
47
|
+
get: async (input) => {
|
|
48
|
+
const { name, tree, depth } = input;
|
|
49
|
+
const jobPath = normalizeJobName(name);
|
|
50
|
+
const query = {};
|
|
51
|
+
if (tree) {
|
|
52
|
+
query.tree = tree;
|
|
53
|
+
}
|
|
54
|
+
if (typeof depth === "number") {
|
|
55
|
+
query.depth = depth;
|
|
56
|
+
}
|
|
57
|
+
return request(`${jobPath}/api/json`, { query });
|
|
58
|
+
},
|
|
59
|
+
/**
|
|
60
|
+
* Retrieves only the builds portion of a job (server-side filtered via `tree`).
|
|
61
|
+
*
|
|
62
|
+
* @param name - The job name (string or array).
|
|
63
|
+
* @param tree - The Jenkins Remote API `tree` expression selecting build fields.
|
|
64
|
+
* Defaults to `builds[number,url,result,timestamp,id,queueId,displayName,duration]`
|
|
65
|
+
* @returns A JSON object containing the requested build fields.
|
|
66
|
+
*/
|
|
67
|
+
getBuilds: async (name, tree = "builds[number,url,result,timestamp,id,queueId,displayName,duration]") => {
|
|
68
|
+
const jobPath = normalizeJobName(name);
|
|
69
|
+
return request(`${jobPath}/api/json`, {
|
|
70
|
+
query: { tree }
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
/**
|
|
74
|
+
* Triggers a Jenkins job build.
|
|
75
|
+
*
|
|
76
|
+
* Uses `/build` or `/buildWithParameters` depending on whether parameters are provided.
|
|
77
|
+
* Automatically URL-encodes parameters and supports legacy options like `delay` and `token`.
|
|
78
|
+
*
|
|
79
|
+
* @param name - The job name (string or array form).
|
|
80
|
+
* @param opts - Optional build options (parameters, token, delay).
|
|
81
|
+
* @returns A promise that resolves when the build request is accepted.
|
|
82
|
+
*/
|
|
83
|
+
build: async (name, opts) => {
|
|
84
|
+
const { parameters, token, delay } = opts ?? {};
|
|
85
|
+
const jobPath = normalizeJobName(name);
|
|
86
|
+
const hasParams = parameters instanceof URLSearchParams ? parameters.toString().length > 0 : parameters && Object.keys(parameters).length > 0;
|
|
87
|
+
const endpoint = hasParams ? "buildWithParameters" : "build";
|
|
88
|
+
const query = {
|
|
89
|
+
...token ? { token } : {},
|
|
90
|
+
// Legacy client support: add delay option
|
|
91
|
+
...delay !== void 0 ? { delay } : {}
|
|
92
|
+
};
|
|
93
|
+
const body = hasParams ? paramsToSearchParams(parameters) : void 0;
|
|
94
|
+
return request(`${jobPath}/${endpoint}`, {
|
|
95
|
+
method: "POST",
|
|
96
|
+
query,
|
|
97
|
+
body
|
|
98
|
+
});
|
|
99
|
+
},
|
|
100
|
+
/**
|
|
101
|
+
* Copies a job to a new name (optionally inside folders).
|
|
102
|
+
*
|
|
103
|
+
* **Important:** For the `from` argument, pass the *slashy* full name (e.g. `"a/b/src"`).
|
|
104
|
+
* Do **not** normalize it to `/job/...` form, Jenkins expects the raw slash-separated name.
|
|
105
|
+
* Only the *leaf* of the new job goes in the `?name=` query; parent folders are derived
|
|
106
|
+
* from `name` and embedded in the URL path.
|
|
107
|
+
*
|
|
108
|
+
* @param name - Target job name (string or segments). Parent parts become folders; leaf is the new job name.
|
|
109
|
+
* @param from - Source job’s slashy full name (e.g. `"folder/old"`).
|
|
110
|
+
*/
|
|
111
|
+
copy: async (name, from) => {
|
|
112
|
+
const segments = parentSegments(name);
|
|
113
|
+
const leaf = leafSegment(name);
|
|
114
|
+
const folderPath = segments.length ? segments.map(normalizeJobName).join("/") : "";
|
|
115
|
+
const url = folderPath ? `${folderPath}/createItem` : "createItem";
|
|
116
|
+
return request(url, {
|
|
117
|
+
method: "POST",
|
|
118
|
+
query: {
|
|
119
|
+
name: leaf,
|
|
120
|
+
mode: "copy",
|
|
121
|
+
from
|
|
122
|
+
// Keep slashy!
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
},
|
|
126
|
+
/**
|
|
127
|
+
* Creates a new job from an XML configuration payload.
|
|
128
|
+
*
|
|
129
|
+
* Only the *leaf* job name is sent in `?name=`; any parent segments become
|
|
130
|
+
* folder parts embedded in the URL path.
|
|
131
|
+
*
|
|
132
|
+
* @param name - The destination job name (string or segments).
|
|
133
|
+
* @param xml - The Jenkins job config.xml content.
|
|
134
|
+
*/
|
|
135
|
+
create: async (name, xml) => {
|
|
136
|
+
const segments = parentSegments(name);
|
|
137
|
+
const leaf = leafSegment(name);
|
|
138
|
+
const folderPath = segments.length ? segments.map(normalizeJobName).join("/") : "";
|
|
139
|
+
const url = folderPath ? `${folderPath}/createItem` : "createItem";
|
|
140
|
+
return request(url, {
|
|
141
|
+
method: "POST",
|
|
142
|
+
query: { name: leaf },
|
|
143
|
+
body: xml,
|
|
144
|
+
contentType: "application/xml"
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
/**
|
|
148
|
+
* Permanently deletes a job.
|
|
149
|
+
*
|
|
150
|
+
* @param name - The job name (string or segments).
|
|
151
|
+
*/
|
|
152
|
+
destroy: async (name) => {
|
|
153
|
+
const jobPath = normalizeJobName(name);
|
|
154
|
+
return request(`${jobPath}/doDelete`, { method: "POST" });
|
|
155
|
+
},
|
|
156
|
+
/**
|
|
157
|
+
* Enables a disabled job.
|
|
158
|
+
*
|
|
159
|
+
* @param name - The job name (string or segments).
|
|
160
|
+
*/
|
|
161
|
+
enable: async (name) => {
|
|
162
|
+
const jobPath = normalizeJobName(name);
|
|
163
|
+
return request(`${jobPath}/enable`, { method: "POST" });
|
|
164
|
+
},
|
|
165
|
+
/**
|
|
166
|
+
* Disables a job (prevents builds from being scheduled).
|
|
167
|
+
*
|
|
168
|
+
* @param name - The job name (string or segments).
|
|
169
|
+
*/
|
|
170
|
+
disable: async (name) => {
|
|
171
|
+
const jobPath = normalizeJobName(name);
|
|
172
|
+
return request(`${jobPath}/disable`, { method: "POST" });
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export { createJobApi };
|
|
178
|
+
//# sourceMappingURL=jobApi.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jobApi.esm.js","sources":["../../src/client/jobApi.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { JenkinsParams, JobBuildOptions, JobGetOptions } from './types';\n\nexport interface JobDeps {\n normalizeJobName(name: string | string[] | undefined): string | undefined;\n request(\n path: string,\n opts?: {\n method?: string;\n query?: Record<string, string | number | undefined>;\n body?: any;\n rawText?: boolean;\n contentType?: string;\n },\n ): Promise<any>;\n}\n\n/**\n * Factory for creating a Jenkins Job API interface.\n *\n * Provides helpers for common Jenkins job operations such as:\n * - Fetching job details (`get`)\n * - Triggering builds (`build`)\n * - Copying or creating jobs (`copy`, `create`)\n * - Managing job state (`enable`, `disable`, `destroy`)\n *\n * This function is intended to be used by higher-level clients (e.g., `Jenkins`)\n * and delegates low-level requests to the provided `request` dependency.\n *\n * @param deps - Dependency injection hooks for request handling and job name normalization.\n * @returns An object with methods for interacting with Jenkins jobs.\n */\nexport function createJobApi(deps: JobDeps) {\n const { normalizeJobName, request } = deps;\n\n // Helper utils\n\n /**\n * Takes in a {@link JenkinsParams} object and returns a {@link URLSearchParams} object.\n * - If the object passed is `undefined`, an empty {@link URLSearchParams} is returned.\n * - If the object passed is already a {@link URLSearchParams} it gets returned as is.\n *\n * @param params a {@link JenkinsParams} object.\n * @returns a {@link URLSearchParams} object\n */\n const paramsToSearchParams = (params?: JenkinsParams): URLSearchParams => {\n if (!params) {\n return new URLSearchParams();\n }\n if (params instanceof URLSearchParams) {\n return params;\n }\n\n const result = new URLSearchParams();\n for (const [k, v] of Object.entries(params)) {\n if (v === undefined || v === null) {\n continue;\n }\n result.set(k, String(v));\n }\n return result;\n };\n\n /**\n * Extracts the last path segment (the \"leaf\" job name) from a Jenkins job name.\n *\n * @param name - The job name, either as a slash-delimited string (e.g. \"folder/job\")\n * or as an array of segments (e.g. [\"folder\", \"job\"]).\n * @returns The last segment of the job name, or an empty string if none exists.\n *\n * @example\n * leafSegment(\"folder/job\"); // \"job\"\n * leafSegment([\"folder\", \"job\"]); // \"job\"\n * leafSegment(\"root\"); // \"root\"\n */\n const leafSegment = (name: string | string[]): string => {\n if (Array.isArray(name)) {\n return name[name.length - 1];\n }\n const parts = name.split('/').filter(Boolean);\n return parts[parts.length - 1] ?? '';\n };\n\n /**\n * Returns all parent path segments of a Jenkins job name, excluding the leaf.\n *\n * @param name - The job name, either as a slash-delimited string (e.g. \"a/b/c\")\n * or as an array of segments (e.g. [\"a\", \"b\", \"c\"]).\n * @returns An array of all parent segments, or an empty array if the job is at the root.\n *\n * @example\n * parentSegments(\"a/b/c\"); // [\"a\", \"b\"]\n * parentSegments([\"a\", \"b\", \"c\"]); // [\"a\", \"b\"]\n * parentSegments(\"job\"); // []\n */\n const parentSegments = (name: string | string[] | undefined): string[] => {\n if (!name) {\n return [];\n }\n\n // Return everything but leaf\n if (Array.isArray(name)) {\n return name.slice(0, -1);\n }\n\n const parts = name.split('/').filter(Boolean);\n if (parts.length > 1) {\n return parts.slice(0, -1);\n }\n\n return [];\n };\n\n return {\n /**\n * Retrieves a job’s JSON representation from Jenkins.\n *\n * @param input - A {@link JobGetOptions} object. `tree` and `depth`\n * are forwarded to `/api/json` as query params.\n * @returns The parsed job JSON.\n */\n get: async (input: JobGetOptions) => {\n const { name, tree, depth } = input;\n const jobPath = normalizeJobName(name);\n const query: Record<string, string | number> = {};\n if (tree) {\n query.tree = tree;\n }\n if (typeof depth === 'number') {\n query.depth = depth;\n }\n return request(`${jobPath}/api/json`, { query });\n },\n\n /**\n * Retrieves only the builds portion of a job (server-side filtered via `tree`).\n *\n * @param name - The job name (string or array).\n * @param tree - The Jenkins Remote API `tree` expression selecting build fields.\n * Defaults to `builds[number,url,result,timestamp,id,queueId,displayName,duration]`\n * @returns A JSON object containing the requested build fields.\n */\n getBuilds: async (\n name: string | string[],\n tree = 'builds[number,url,result,timestamp,id,queueId,displayName,duration]',\n ): Promise<unknown> => {\n const jobPath = normalizeJobName(name);\n return request(`${jobPath}/api/json`, {\n query: { tree },\n });\n },\n\n /**\n * Triggers a Jenkins job build.\n *\n * Uses `/build` or `/buildWithParameters` depending on whether parameters are provided.\n * Automatically URL-encodes parameters and supports legacy options like `delay` and `token`.\n *\n * @param name - The job name (string or array form).\n * @param opts - Optional build options (parameters, token, delay).\n * @returns A promise that resolves when the build request is accepted.\n */\n build: async (\n name: string | string[],\n opts?: JobBuildOptions,\n ): Promise<unknown> => {\n const { parameters, token, delay } = opts ?? {};\n\n const jobPath = normalizeJobName(name);\n\n // Check if we have search params\n // This will determine the endpoint used\n const hasParams =\n parameters instanceof URLSearchParams\n ? parameters.toString().length > 0\n : parameters && Object.keys(parameters).length > 0;\n\n const endpoint = hasParams ? 'buildWithParameters' : 'build';\n\n const query: Record<string, string> = {\n ...(token ? { token } : {}),\n // Legacy client support: add delay option\n ...(delay !== undefined ? { delay } : {}),\n };\n\n const body: URLSearchParams | undefined = hasParams\n ? paramsToSearchParams(parameters)\n : undefined;\n\n return request(`${jobPath}/${endpoint}`, {\n method: 'POST',\n query,\n body,\n });\n },\n\n /**\n * Copies a job to a new name (optionally inside folders).\n *\n * **Important:** For the `from` argument, pass the *slashy* full name (e.g. `\"a/b/src\"`).\n * Do **not** normalize it to `/job/...` form, Jenkins expects the raw slash-separated name.\n * Only the *leaf* of the new job goes in the `?name=` query; parent folders are derived\n * from `name` and embedded in the URL path.\n *\n * @param name - Target job name (string or segments). Parent parts become folders; leaf is the new job name.\n * @param from - Source job’s slashy full name (e.g. `\"folder/old\"`).\n */\n copy: async (name: string | string[], from: string): Promise<void> => {\n const segments = parentSegments(name);\n const leaf = leafSegment(name);\n const folderPath = segments.length\n ? segments.map(normalizeJobName).join('/')\n : '';\n\n const url = folderPath ? `${folderPath}/createItem` : 'createItem';\n return request(url, {\n method: 'POST',\n query: {\n name: leaf,\n mode: 'copy',\n from: from, // Keep slashy!\n },\n });\n },\n\n /**\n * Creates a new job from an XML configuration payload.\n *\n * Only the *leaf* job name is sent in `?name=`; any parent segments become\n * folder parts embedded in the URL path.\n *\n * @param name - The destination job name (string or segments).\n * @param xml - The Jenkins job config.xml content.\n */\n create: async (name: string | string[], xml: string): Promise<void> => {\n const segments = parentSegments(name);\n const leaf = leafSegment(name);\n const folderPath = segments.length\n ? segments.map(normalizeJobName).join('/')\n : '';\n\n const url = folderPath ? `${folderPath}/createItem` : 'createItem';\n\n return request(url, {\n method: 'POST',\n query: { name: leaf },\n body: xml,\n contentType: 'application/xml',\n });\n },\n\n /**\n * Permanently deletes a job.\n *\n * @param name - The job name (string or segments).\n */\n destroy: async (name: string | string[]): Promise<void> => {\n const jobPath = normalizeJobName(name);\n return request(`${jobPath}/doDelete`, { method: 'POST' });\n },\n\n /**\n * Enables a disabled job.\n *\n * @param name - The job name (string or segments).\n */\n enable: async (name: string | string[]): Promise<void> => {\n const jobPath = normalizeJobName(name);\n return request(`${jobPath}/enable`, { method: 'POST' });\n },\n\n /**\n * Disables a job (prevents builds from being scheduled).\n *\n * @param name - The job name (string or segments).\n */\n disable: async (name: string | string[]): Promise<void> => {\n const jobPath = normalizeJobName(name);\n return request(`${jobPath}/disable`, { method: 'POST' });\n },\n };\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
function addQueryParams(u, q) {
|
|
4
|
+
const dup = new URL(u.toString());
|
|
5
|
+
for (const [k, v] of Object.entries(q)) {
|
|
6
|
+
if (v === void 0) {
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
dup.searchParams.set(k, String(v));
|
|
10
|
+
}
|
|
11
|
+
return dup;
|
|
12
|
+
}
|
|
13
|
+
function joinUrl(base, path) {
|
|
14
|
+
let dupBase = base;
|
|
15
|
+
if (!dupBase.endsWith("/")) {
|
|
16
|
+
dupBase += "/";
|
|
17
|
+
}
|
|
18
|
+
return dupBase + path;
|
|
19
|
+
}
|
|
20
|
+
function trimLeadingSlash(p) {
|
|
21
|
+
return p.startsWith("/") ? p.slice(1) : p;
|
|
22
|
+
}
|
|
23
|
+
function ensureTrailingSlash(u) {
|
|
24
|
+
return u.endsWith("/") ? u : `${u}/`;
|
|
25
|
+
}
|
|
26
|
+
async function safeExtractText(res) {
|
|
27
|
+
try {
|
|
28
|
+
return await res.text();
|
|
29
|
+
} catch {
|
|
30
|
+
return "<no response body>";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
exports.addQueryParams = addQueryParams;
|
|
35
|
+
exports.ensureTrailingSlash = ensureTrailingSlash;
|
|
36
|
+
exports.joinUrl = joinUrl;
|
|
37
|
+
exports.safeExtractText = safeExtractText;
|
|
38
|
+
exports.trimLeadingSlash = trimLeadingSlash;
|
|
39
|
+
//# sourceMappingURL=utils.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.cjs.js","sources":["../../src/client/utils.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { type Response } from 'node-fetch';\n\n/**\n * Copies the {@link URL} object passed, appends query params and returns the resulting {@link URL}.\n *\n * @param u The {@link URL} object that will be copied\n * @param q The {@link Record<string, string | number | undefined>} that stores query params\n * @returns The resulting {@link URL} with query params.\n */\nexport function addQueryParams(\n u: URL,\n q: Record<string, string | number | undefined>,\n): URL {\n // Create duplicate of URL, do not mutate original\n const dup = new URL(u.toString());\n for (const [k, v] of Object.entries(q)) {\n if (v === undefined) {\n continue;\n }\n dup.searchParams.set(k, String(v));\n }\n\n return dup;\n}\n\n/**\n * Joins the base URL string with the specified path.\n * Appends a `/` to the end of the `base` if it doesn't already have it.\n *\n * @param base The base URL string\n * @param path The path that appends to the base string\n * @returns A string of the full URL\n */\nexport function joinUrl(base: string, path: string): string {\n let dupBase = base;\n if (!dupBase.endsWith('/')) {\n dupBase += '/';\n }\n return dupBase + path;\n}\n\n/**\n * Utility function that removes the `/` from the start of a string if it exists.\n *\n * @param p The string to trim\n * @returns The string without the leading `/`\n */\nexport function trimLeadingSlash(p: string): string {\n return p.startsWith('/') ? p.slice(1) : p;\n}\n\n/**\n * Utility function that ensures that string ends with `/`,\n *\n * @param u The string to modify\n * @returns The resulting string, ending with `/`\n */\nexport function ensureTrailingSlash(u: string): string {\n return u.endsWith('/') ? u : `${u}/`;\n}\n\n/**\n * Utility function that safely extracts the text from a response.\n * If the operation results in an error a default string is returned instead.\n *\n * @param res The {@link Response} containing the `Body.text`\n * @returns The resulting `Body.text` value or a default string if the operation failed\n */\nexport async function safeExtractText(res: Response): Promise<string> {\n try {\n return await res.text();\n } catch {\n return '<no response body>';\n }\n}\n"],"names":[],"mappings":";;AAwBgB,SAAA,cAAA,CACd,GACA,CACK,EAAA;AAEL,EAAA,MAAM,GAAM,GAAA,IAAI,GAAI,CAAA,CAAA,CAAE,UAAU,CAAA;AAChC,EAAA,KAAA,MAAW,CAAC,CAAG,EAAA,CAAC,KAAK,MAAO,CAAA,OAAA,CAAQ,CAAC,CAAG,EAAA;AACtC,IAAA,IAAI,MAAM,KAAW,CAAA,EAAA;AACnB,MAAA;AAAA;AAEF,IAAA,GAAA,CAAI,YAAa,CAAA,GAAA,CAAI,CAAG,EAAA,MAAA,CAAO,CAAC,CAAC,CAAA;AAAA;AAGnC,EAAO,OAAA,GAAA;AACT;AAUgB,SAAA,OAAA,CAAQ,MAAc,IAAsB,EAAA;AAC1D,EAAA,IAAI,OAAU,GAAA,IAAA;AACd,EAAA,IAAI,CAAC,OAAA,CAAQ,QAAS,CAAA,GAAG,CAAG,EAAA;AAC1B,IAAW,OAAA,IAAA,GAAA;AAAA;AAEb,EAAA,OAAO,OAAU,GAAA,IAAA;AACnB;AAQO,SAAS,iBAAiB,CAAmB,EAAA;AAClD,EAAA,OAAO,EAAE,UAAW,CAAA,GAAG,IAAI,CAAE,CAAA,KAAA,CAAM,CAAC,CAAI,GAAA,CAAA;AAC1C;AAQO,SAAS,oBAAoB,CAAmB,EAAA;AACrD,EAAA,OAAO,EAAE,QAAS,CAAA,GAAG,CAAI,GAAA,CAAA,GAAI,GAAG,CAAC,CAAA,CAAA,CAAA;AACnC;AASA,eAAsB,gBAAgB,GAAgC,EAAA;AACpE,EAAI,IAAA;AACF,IAAO,OAAA,MAAM,IAAI,IAAK,EAAA;AAAA,GAChB,CAAA,MAAA;AACN,IAAO,OAAA,oBAAA;AAAA;AAEX;;;;;;;;"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
function addQueryParams(u, q) {
|
|
2
|
+
const dup = new URL(u.toString());
|
|
3
|
+
for (const [k, v] of Object.entries(q)) {
|
|
4
|
+
if (v === void 0) {
|
|
5
|
+
continue;
|
|
6
|
+
}
|
|
7
|
+
dup.searchParams.set(k, String(v));
|
|
8
|
+
}
|
|
9
|
+
return dup;
|
|
10
|
+
}
|
|
11
|
+
function joinUrl(base, path) {
|
|
12
|
+
let dupBase = base;
|
|
13
|
+
if (!dupBase.endsWith("/")) {
|
|
14
|
+
dupBase += "/";
|
|
15
|
+
}
|
|
16
|
+
return dupBase + path;
|
|
17
|
+
}
|
|
18
|
+
function trimLeadingSlash(p) {
|
|
19
|
+
return p.startsWith("/") ? p.slice(1) : p;
|
|
20
|
+
}
|
|
21
|
+
function ensureTrailingSlash(u) {
|
|
22
|
+
return u.endsWith("/") ? u : `${u}/`;
|
|
23
|
+
}
|
|
24
|
+
async function safeExtractText(res) {
|
|
25
|
+
try {
|
|
26
|
+
return await res.text();
|
|
27
|
+
} catch {
|
|
28
|
+
return "<no response body>";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { addQueryParams, ensureTrailingSlash, joinUrl, safeExtractText, trimLeadingSlash };
|
|
33
|
+
//# sourceMappingURL=utils.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.esm.js","sources":["../../src/client/utils.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { type Response } from 'node-fetch';\n\n/**\n * Copies the {@link URL} object passed, appends query params and returns the resulting {@link URL}.\n *\n * @param u The {@link URL} object that will be copied\n * @param q The {@link Record<string, string | number | undefined>} that stores query params\n * @returns The resulting {@link URL} with query params.\n */\nexport function addQueryParams(\n u: URL,\n q: Record<string, string | number | undefined>,\n): URL {\n // Create duplicate of URL, do not mutate original\n const dup = new URL(u.toString());\n for (const [k, v] of Object.entries(q)) {\n if (v === undefined) {\n continue;\n }\n dup.searchParams.set(k, String(v));\n }\n\n return dup;\n}\n\n/**\n * Joins the base URL string with the specified path.\n * Appends a `/` to the end of the `base` if it doesn't already have it.\n *\n * @param base The base URL string\n * @param path The path that appends to the base string\n * @returns A string of the full URL\n */\nexport function joinUrl(base: string, path: string): string {\n let dupBase = base;\n if (!dupBase.endsWith('/')) {\n dupBase += '/';\n }\n return dupBase + path;\n}\n\n/**\n * Utility function that removes the `/` from the start of a string if it exists.\n *\n * @param p The string to trim\n * @returns The string without the leading `/`\n */\nexport function trimLeadingSlash(p: string): string {\n return p.startsWith('/') ? p.slice(1) : p;\n}\n\n/**\n * Utility function that ensures that string ends with `/`,\n *\n * @param u The string to modify\n * @returns The resulting string, ending with `/`\n */\nexport function ensureTrailingSlash(u: string): string {\n return u.endsWith('/') ? u : `${u}/`;\n}\n\n/**\n * Utility function that safely extracts the text from a response.\n * If the operation results in an error a default string is returned instead.\n *\n * @param res The {@link Response} containing the `Body.text`\n * @returns The resulting `Body.text` value or a default string if the operation failed\n */\nexport async function safeExtractText(res: Response): Promise<string> {\n try {\n return await res.text();\n } catch {\n return '<no response body>';\n }\n}\n"],"names":[],"mappings":"AAwBgB,SAAA,cAAA,CACd,GACA,CACK,EAAA;AAEL,EAAA,MAAM,GAAM,GAAA,IAAI,GAAI,CAAA,CAAA,CAAE,UAAU,CAAA;AAChC,EAAA,KAAA,MAAW,CAAC,CAAG,EAAA,CAAC,KAAK,MAAO,CAAA,OAAA,CAAQ,CAAC,CAAG,EAAA;AACtC,IAAA,IAAI,MAAM,KAAW,CAAA,EAAA;AACnB,MAAA;AAAA;AAEF,IAAA,GAAA,CAAI,YAAa,CAAA,GAAA,CAAI,CAAG,EAAA,MAAA,CAAO,CAAC,CAAC,CAAA;AAAA;AAGnC,EAAO,OAAA,GAAA;AACT;AAUgB,SAAA,OAAA,CAAQ,MAAc,IAAsB,EAAA;AAC1D,EAAA,IAAI,OAAU,GAAA,IAAA;AACd,EAAA,IAAI,CAAC,OAAA,CAAQ,QAAS,CAAA,GAAG,CAAG,EAAA;AAC1B,IAAW,OAAA,IAAA,GAAA;AAAA;AAEb,EAAA,OAAO,OAAU,GAAA,IAAA;AACnB;AAQO,SAAS,iBAAiB,CAAmB,EAAA;AAClD,EAAA,OAAO,EAAE,UAAW,CAAA,GAAG,IAAI,CAAE,CAAA,KAAA,CAAM,CAAC,CAAI,GAAA,CAAA;AAC1C;AAQO,SAAS,oBAAoB,CAAmB,EAAA;AACrD,EAAA,OAAO,EAAE,QAAS,CAAA,GAAG,CAAI,GAAA,CAAA,GAAI,GAAG,CAAC,CAAA,CAAA,CAAA;AACnC;AASA,eAAsB,gBAAgB,GAAgC,EAAA;AACpE,EAAI,IAAA;AACF,IAAO,OAAA,MAAM,IAAI,IAAK,EAAA;AAAA,GAChB,CAAA,MAAA;AACN,IAAO,OAAA,oBAAA;AAAA;AAEX;;;;"}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var fetch = require('node-fetch');
|
|
4
|
+
var jobApi = require('./client/jobApi.cjs.js');
|
|
5
|
+
var buildApi = require('./client/buildApi.cjs.js');
|
|
6
|
+
var utils = require('./client/utils.cjs.js');
|
|
7
|
+
|
|
8
|
+
function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
|
|
9
|
+
|
|
10
|
+
var fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch);
|
|
11
|
+
|
|
12
|
+
class Jenkins {
|
|
13
|
+
crumbData;
|
|
14
|
+
opts;
|
|
15
|
+
constructor(opts) {
|
|
16
|
+
if (!opts.baseUrl) {
|
|
17
|
+
throw new Error("Jenkins: opts.baseUrl is required");
|
|
18
|
+
}
|
|
19
|
+
if (opts.crumbIssuer === void 0) {
|
|
20
|
+
opts.crumbIssuer = true;
|
|
21
|
+
}
|
|
22
|
+
const referer = utils.ensureTrailingSlash(opts.baseUrl);
|
|
23
|
+
opts.headers = { referer, ...opts.headers ?? {} };
|
|
24
|
+
this.opts = opts;
|
|
25
|
+
}
|
|
26
|
+
// Add APIs
|
|
27
|
+
job = jobApi.createJobApi({
|
|
28
|
+
normalizeJobName: (name) => this.normalizeJobName(name),
|
|
29
|
+
request: (path, opts) => this.request(path, opts)
|
|
30
|
+
});
|
|
31
|
+
build = buildApi.createBuildApi({
|
|
32
|
+
normalizeJobName: (name) => this.normalizeJobName(name),
|
|
33
|
+
request: (path, opts) => this.request(path, opts)
|
|
34
|
+
});
|
|
35
|
+
/**
|
|
36
|
+
* Retrieves and caches the Jenkins CSRF protection crumb.
|
|
37
|
+
*
|
|
38
|
+
* Jenkins uses a "crumb" (similar to a CSRF token) to protect write operations
|
|
39
|
+
* such as POST requests. This method handles retrieving that crumb based on
|
|
40
|
+
* the client's configuration.
|
|
41
|
+
*
|
|
42
|
+
* Behavior:
|
|
43
|
+
* - If `crumbIssuer` is not enabled in the client options, it returns `undefined`.
|
|
44
|
+
* - If a cached crumb already exists, it is returned immediately.
|
|
45
|
+
* - If `crumbIssuer` is a function, that function is called to obtain the crumb.
|
|
46
|
+
* - Otherwise, it performs a network request to
|
|
47
|
+
* `<baseUrl>/crumbIssuer/api/json` to fetch the crumb from Jenkins.
|
|
48
|
+
*
|
|
49
|
+
* The result is cached in `this.crumbData` for subsequent calls.
|
|
50
|
+
*
|
|
51
|
+
* @returns A `CrumbData` object containing the header name and value,
|
|
52
|
+
* or `undefined` if no crumb issuer is configured or the request fails.
|
|
53
|
+
* @throws Any network or parsing errors that occur during the crumb fetch.
|
|
54
|
+
*/
|
|
55
|
+
async getCrumb() {
|
|
56
|
+
const { crumbIssuer } = this.opts;
|
|
57
|
+
if (!crumbIssuer) {
|
|
58
|
+
return void 0;
|
|
59
|
+
}
|
|
60
|
+
if (this.crumbData) {
|
|
61
|
+
return this.crumbData;
|
|
62
|
+
}
|
|
63
|
+
if (typeof crumbIssuer === "function") {
|
|
64
|
+
this.crumbData = await crumbIssuer(this);
|
|
65
|
+
return this.crumbData;
|
|
66
|
+
}
|
|
67
|
+
const res = await this.fetchRaw(
|
|
68
|
+
`${utils.ensureTrailingSlash(this.opts.baseUrl)}crumbIssuer/api/json`
|
|
69
|
+
);
|
|
70
|
+
if (!res.ok) {
|
|
71
|
+
return void 0;
|
|
72
|
+
}
|
|
73
|
+
const data = await res.json();
|
|
74
|
+
this.crumbData = {
|
|
75
|
+
headerName: data.crumbRequestField,
|
|
76
|
+
headerValue: data.crumb
|
|
77
|
+
};
|
|
78
|
+
return this.crumbData;
|
|
79
|
+
}
|
|
80
|
+
async request(path, opts = {}) {
|
|
81
|
+
let url = new URL(
|
|
82
|
+
utils.joinUrl(utils.ensureTrailingSlash(this.opts.baseUrl), utils.trimLeadingSlash(path))
|
|
83
|
+
);
|
|
84
|
+
if (opts.query) {
|
|
85
|
+
url = utils.addQueryParams(url, opts.query);
|
|
86
|
+
}
|
|
87
|
+
const method = (opts?.method || (opts?.body ? "POST" : "GET")).toLocaleUpperCase("en-US");
|
|
88
|
+
const headers = {
|
|
89
|
+
...this.opts.headers ?? {}
|
|
90
|
+
};
|
|
91
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
92
|
+
const crumb = await this.getCrumb();
|
|
93
|
+
if (crumb) {
|
|
94
|
+
headers[crumb.headerName] = crumb.headerValue;
|
|
95
|
+
if (crumb.cookies?.length) {
|
|
96
|
+
const prior = typeof headers.cookie === "string" ? headers.cookie : "";
|
|
97
|
+
const extra = crumb.cookies.join("; ");
|
|
98
|
+
headers.cookie = prior ? `${prior}; ${extra}` : extra;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
let resolvedContentType;
|
|
103
|
+
if (opts?.contentType) {
|
|
104
|
+
resolvedContentType = opts?.contentType;
|
|
105
|
+
}
|
|
106
|
+
if (!resolvedContentType && opts?.body instanceof URLSearchParams) {
|
|
107
|
+
resolvedContentType = "application/x-www-form-urlencoded; charset=UTF-8";
|
|
108
|
+
}
|
|
109
|
+
if (resolvedContentType) {
|
|
110
|
+
headers["content-type"] = resolvedContentType;
|
|
111
|
+
}
|
|
112
|
+
const res = await this.fetchRaw(url.toString(), {
|
|
113
|
+
method,
|
|
114
|
+
headers,
|
|
115
|
+
body: opts?.body
|
|
116
|
+
});
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
const text = await utils.safeExtractText(res);
|
|
119
|
+
throw new Error(
|
|
120
|
+
`Jenkins API error ${res.status} ${method} ${url.toString()}: ${text}`
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
if (opts?.rawText) {
|
|
124
|
+
return res.text();
|
|
125
|
+
}
|
|
126
|
+
const contentType = (res.headers.get("content-type") || "").toLocaleLowerCase("en-US");
|
|
127
|
+
if (contentType.includes("application/json")) {
|
|
128
|
+
return res.json();
|
|
129
|
+
}
|
|
130
|
+
return res.text();
|
|
131
|
+
}
|
|
132
|
+
async fetchRaw(input, init) {
|
|
133
|
+
const flattened = {};
|
|
134
|
+
for (const [k, v] of Object.entries(init?.headers ?? {})) {
|
|
135
|
+
if (Array.isArray(v)) {
|
|
136
|
+
flattened[k] = v.join(", ");
|
|
137
|
+
} else if (v === void 0) {
|
|
138
|
+
continue;
|
|
139
|
+
} else {
|
|
140
|
+
flattened[k] = v;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return fetch__default.default(input, {
|
|
144
|
+
...init ?? {},
|
|
145
|
+
headers: flattened
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Normalizes a Jenkins job name into a fully qualified job path.
|
|
150
|
+
*
|
|
151
|
+
* Jenkins job URLs use a hierarchical format like:
|
|
152
|
+
* `/job/folder/job/subfolder/job/pipeline`
|
|
153
|
+
*
|
|
154
|
+
* This method takes a job name (either a string like `"folder/pipeline"`
|
|
155
|
+
* or an array like `["folder", "pipeline"]`) and converts it into the proper
|
|
156
|
+
* Jenkins API path format by inserting `job/` segments and URL-encoding
|
|
157
|
+
* each component.
|
|
158
|
+
*
|
|
159
|
+
* - If the input already contains `/job/` segments, it is returned as-is
|
|
160
|
+
* (after trimming any leading slash).
|
|
161
|
+
* - If the input is undefined, an error is thrown.
|
|
162
|
+
*
|
|
163
|
+
* @param name - The job name to normalize, either as a string or an array of path segments.
|
|
164
|
+
* @returns The normalized Jenkins job path (e.g. `"job/folder/job/pipeline"`).
|
|
165
|
+
* @throws If the name is undefined or empty.
|
|
166
|
+
*/
|
|
167
|
+
normalizeJobName(name) {
|
|
168
|
+
if (!name) {
|
|
169
|
+
throw new Error('Jenkins.normalizeJobName: "name" is required');
|
|
170
|
+
}
|
|
171
|
+
const parts = Array.isArray(name) ? name : name.split("/").filter(Boolean);
|
|
172
|
+
if (parts.join("/").includes("/job/")) {
|
|
173
|
+
return utils.trimLeadingSlash(Array.isArray(name) ? parts.join("/") : name);
|
|
174
|
+
}
|
|
175
|
+
return parts.map(encodeURIComponent).map((s) => `job/${s}`).join("/");
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
exports.Jenkins = Jenkins;
|
|
180
|
+
//# sourceMappingURL=client.cjs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.cjs.js","sources":["../src/client.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport fetch from 'node-fetch';\nimport { createJobApi } from './client/jobApi';\nimport { createBuildApi } from './client/buildApi';\nimport { CrumbData, CrumbDataHeaderValues, HeaderValue } from './client/types';\n\nimport {\n addQueryParams,\n joinUrl,\n trimLeadingSlash,\n ensureTrailingSlash,\n safeExtractText,\n} from './client/utils';\n\n/** @public */\nexport interface JenkinsClientOptions {\n baseUrl: string; // e.g. \"https://jenkins.example.com\"\n crumbIssuer?: boolean | ((client: any) => Promise<CrumbData>) | undefined;\n headers?: Record<string, HeaderValue>;\n promisify?: boolean; // For compatibility with old legacy API, not used.\n}\n\n/** @public */\nexport class Jenkins {\n private crumbData?: CrumbData;\n public readonly opts: JenkinsClientOptions;\n\n constructor(opts: JenkinsClientOptions) {\n if (!opts.baseUrl) {\n throw new Error('Jenkins: opts.baseUrl is required');\n }\n\n // Legacy client behavior: set crumbIssuer to true if unset.\n if (opts.crumbIssuer === undefined) {\n opts.crumbIssuer = true;\n }\n\n // Legacy client behavior: set default headers and Referer\n // The referer here is the baseUrl.\n const referer = ensureTrailingSlash(opts.baseUrl);\n opts.headers = { referer, ...(opts.headers ?? {}) };\n\n this.opts = opts;\n }\n\n // Add APIs\n job = createJobApi({\n normalizeJobName: name => this.normalizeJobName(name),\n request: (path, opts) => this.request(path, opts),\n });\n\n build = createBuildApi({\n normalizeJobName: name => this.normalizeJobName(name),\n request: (path, opts) => this.request(path, opts),\n });\n\n /**\n * Retrieves and caches the Jenkins CSRF protection crumb.\n *\n * Jenkins uses a \"crumb\" (similar to a CSRF token) to protect write operations\n * such as POST requests. This method handles retrieving that crumb based on\n * the client's configuration.\n *\n * Behavior:\n * - If `crumbIssuer` is not enabled in the client options, it returns `undefined`.\n * - If a cached crumb already exists, it is returned immediately.\n * - If `crumbIssuer` is a function, that function is called to obtain the crumb.\n * - Otherwise, it performs a network request to\n * `<baseUrl>/crumbIssuer/api/json` to fetch the crumb from Jenkins.\n *\n * The result is cached in `this.crumbData` for subsequent calls.\n *\n * @returns A `CrumbData` object containing the header name and value,\n * or `undefined` if no crumb issuer is configured or the request fails.\n * @throws Any network or parsing errors that occur during the crumb fetch.\n */\n private async getCrumb(): Promise<CrumbData | undefined> {\n const { crumbIssuer } = this.opts;\n\n if (!crumbIssuer) {\n return undefined;\n }\n\n if (this.crumbData) {\n return this.crumbData;\n }\n\n if (typeof crumbIssuer === 'function') {\n this.crumbData = await crumbIssuer(this);\n return this.crumbData;\n }\n\n // Fetch crumb from Jenkins\n const res = await this.fetchRaw(\n `${ensureTrailingSlash(this.opts.baseUrl)}crumbIssuer/api/json`,\n );\n if (!res.ok) {\n return undefined;\n }\n const data = (await res.json()) as CrumbDataHeaderValues;\n this.crumbData = {\n headerName: data.crumbRequestField,\n headerValue: data.crumb,\n };\n\n return this.crumbData;\n }\n\n private async request(\n path: string,\n opts: {\n method?: string;\n query?: Record<string, string | number | undefined>;\n body?: any;\n rawText?: boolean;\n contentType?: string;\n } = {},\n ): Promise<any> {\n let url = new URL(\n joinUrl(ensureTrailingSlash(this.opts.baseUrl), trimLeadingSlash(path)),\n );\n if (opts.query) {\n url = addQueryParams(url, opts.query);\n }\n\n const method = (\n opts?.method || (opts?.body ? 'POST' : 'GET')\n ).toLocaleUpperCase('en-US');\n const headers: Record<string, HeaderValue> = {\n ...(this.opts.headers ?? {}),\n };\n\n // Legacy client support: Add crumb if request is not read-only\n if (method !== 'GET' && method !== 'HEAD') {\n const crumb = await this.getCrumb();\n if (crumb) {\n headers[crumb.headerName] = crumb.headerValue;\n // If the crumb call told us to include some cookies, merge them into\n // the existing cookie header\n if (crumb.cookies?.length) {\n const prior =\n typeof headers.cookie === 'string' ? headers.cookie : '';\n const extra = crumb.cookies.join('; ');\n headers.cookie = prior ? `${prior}; ${extra}` : extra;\n }\n }\n }\n\n // Set Content-Type, default to undefined if not set\n // Check caller-specified content-type first\n let resolvedContentType: string | undefined;\n if (opts?.contentType) {\n resolvedContentType = opts?.contentType;\n }\n\n // URLSearchParams -> x-www-form-urlencoded\n if (!resolvedContentType && opts?.body instanceof URLSearchParams) {\n resolvedContentType = 'application/x-www-form-urlencoded; charset=UTF-8';\n }\n\n if (resolvedContentType) {\n headers['content-type'] = resolvedContentType;\n }\n\n const res = await this.fetchRaw(url.toString(), {\n method,\n headers,\n body: opts?.body,\n });\n\n if (!res.ok) {\n const text = await safeExtractText(res);\n throw new Error(\n `Jenkins API error ${res.status} ${method} ${url.toString()}: ${text}`,\n );\n }\n\n if (opts?.rawText) {\n return res.text();\n }\n\n const contentType = (\n res.headers.get('content-type') || ''\n ).toLocaleLowerCase('en-US');\n if (contentType.includes('application/json')) {\n return res.json();\n }\n\n return res.text();\n }\n\n private async fetchRaw(\n input: string,\n init?: {\n method?: string;\n headers?: Record<string, HeaderValue>;\n body?: any;\n },\n ) {\n // Flatten the values passed in \"headers\"\n const flattened: Record<string, string> = {};\n for (const [k, v] of Object.entries(init?.headers ?? {})) {\n if (Array.isArray(v)) {\n flattened[k] = v.join(', ');\n } else if (v === undefined) {\n continue;\n } else {\n flattened[k] = v;\n }\n }\n\n return fetch(input, {\n ...(init ?? {}),\n headers: flattened as any,\n });\n }\n\n /**\n * Normalizes a Jenkins job name into a fully qualified job path.\n *\n * Jenkins job URLs use a hierarchical format like:\n * `/job/folder/job/subfolder/job/pipeline`\n *\n * This method takes a job name (either a string like `\"folder/pipeline\"`\n * or an array like `[\"folder\", \"pipeline\"]`) and converts it into the proper\n * Jenkins API path format by inserting `job/` segments and URL-encoding\n * each component.\n *\n * - If the input already contains `/job/` segments, it is returned as-is\n * (after trimming any leading slash).\n * - If the input is undefined, an error is thrown.\n *\n * @param name - The job name to normalize, either as a string or an array of path segments.\n * @returns The normalized Jenkins job path (e.g. `\"job/folder/job/pipeline\"`).\n * @throws If the name is undefined or empty.\n */\n private normalizeJobName(\n name: string | string[] | undefined,\n ): string | undefined {\n if (!name) {\n throw new Error('Jenkins.normalizeJobName: \"name\" is required');\n }\n\n const parts = Array.isArray(name) ? name : name.split('/').filter(Boolean);\n if (parts.join('/').includes('/job/')) {\n return trimLeadingSlash(Array.isArray(name) ? parts.join('/') : name);\n }\n\n return parts\n .map(encodeURIComponent)\n .map(s => `job/${s}`)\n .join('/');\n }\n}\n"],"names":["ensureTrailingSlash","createJobApi","createBuildApi","joinUrl","trimLeadingSlash","addQueryParams","safeExtractText","fetch"],"mappings":"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"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import fetch from 'node-fetch';
|
|
2
|
+
import { createJobApi } from './client/jobApi.esm.js';
|
|
3
|
+
import { createBuildApi } from './client/buildApi.esm.js';
|
|
4
|
+
import { ensureTrailingSlash, joinUrl, trimLeadingSlash, addQueryParams, safeExtractText } from './client/utils.esm.js';
|
|
5
|
+
|
|
6
|
+
class Jenkins {
|
|
7
|
+
crumbData;
|
|
8
|
+
opts;
|
|
9
|
+
constructor(opts) {
|
|
10
|
+
if (!opts.baseUrl) {
|
|
11
|
+
throw new Error("Jenkins: opts.baseUrl is required");
|
|
12
|
+
}
|
|
13
|
+
if (opts.crumbIssuer === void 0) {
|
|
14
|
+
opts.crumbIssuer = true;
|
|
15
|
+
}
|
|
16
|
+
const referer = ensureTrailingSlash(opts.baseUrl);
|
|
17
|
+
opts.headers = { referer, ...opts.headers ?? {} };
|
|
18
|
+
this.opts = opts;
|
|
19
|
+
}
|
|
20
|
+
// Add APIs
|
|
21
|
+
job = createJobApi({
|
|
22
|
+
normalizeJobName: (name) => this.normalizeJobName(name),
|
|
23
|
+
request: (path, opts) => this.request(path, opts)
|
|
24
|
+
});
|
|
25
|
+
build = createBuildApi({
|
|
26
|
+
normalizeJobName: (name) => this.normalizeJobName(name),
|
|
27
|
+
request: (path, opts) => this.request(path, opts)
|
|
28
|
+
});
|
|
29
|
+
/**
|
|
30
|
+
* Retrieves and caches the Jenkins CSRF protection crumb.
|
|
31
|
+
*
|
|
32
|
+
* Jenkins uses a "crumb" (similar to a CSRF token) to protect write operations
|
|
33
|
+
* such as POST requests. This method handles retrieving that crumb based on
|
|
34
|
+
* the client's configuration.
|
|
35
|
+
*
|
|
36
|
+
* Behavior:
|
|
37
|
+
* - If `crumbIssuer` is not enabled in the client options, it returns `undefined`.
|
|
38
|
+
* - If a cached crumb already exists, it is returned immediately.
|
|
39
|
+
* - If `crumbIssuer` is a function, that function is called to obtain the crumb.
|
|
40
|
+
* - Otherwise, it performs a network request to
|
|
41
|
+
* `<baseUrl>/crumbIssuer/api/json` to fetch the crumb from Jenkins.
|
|
42
|
+
*
|
|
43
|
+
* The result is cached in `this.crumbData` for subsequent calls.
|
|
44
|
+
*
|
|
45
|
+
* @returns A `CrumbData` object containing the header name and value,
|
|
46
|
+
* or `undefined` if no crumb issuer is configured or the request fails.
|
|
47
|
+
* @throws Any network or parsing errors that occur during the crumb fetch.
|
|
48
|
+
*/
|
|
49
|
+
async getCrumb() {
|
|
50
|
+
const { crumbIssuer } = this.opts;
|
|
51
|
+
if (!crumbIssuer) {
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
if (this.crumbData) {
|
|
55
|
+
return this.crumbData;
|
|
56
|
+
}
|
|
57
|
+
if (typeof crumbIssuer === "function") {
|
|
58
|
+
this.crumbData = await crumbIssuer(this);
|
|
59
|
+
return this.crumbData;
|
|
60
|
+
}
|
|
61
|
+
const res = await this.fetchRaw(
|
|
62
|
+
`${ensureTrailingSlash(this.opts.baseUrl)}crumbIssuer/api/json`
|
|
63
|
+
);
|
|
64
|
+
if (!res.ok) {
|
|
65
|
+
return void 0;
|
|
66
|
+
}
|
|
67
|
+
const data = await res.json();
|
|
68
|
+
this.crumbData = {
|
|
69
|
+
headerName: data.crumbRequestField,
|
|
70
|
+
headerValue: data.crumb
|
|
71
|
+
};
|
|
72
|
+
return this.crumbData;
|
|
73
|
+
}
|
|
74
|
+
async request(path, opts = {}) {
|
|
75
|
+
let url = new URL(
|
|
76
|
+
joinUrl(ensureTrailingSlash(this.opts.baseUrl), trimLeadingSlash(path))
|
|
77
|
+
);
|
|
78
|
+
if (opts.query) {
|
|
79
|
+
url = addQueryParams(url, opts.query);
|
|
80
|
+
}
|
|
81
|
+
const method = (opts?.method || (opts?.body ? "POST" : "GET")).toLocaleUpperCase("en-US");
|
|
82
|
+
const headers = {
|
|
83
|
+
...this.opts.headers ?? {}
|
|
84
|
+
};
|
|
85
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
86
|
+
const crumb = await this.getCrumb();
|
|
87
|
+
if (crumb) {
|
|
88
|
+
headers[crumb.headerName] = crumb.headerValue;
|
|
89
|
+
if (crumb.cookies?.length) {
|
|
90
|
+
const prior = typeof headers.cookie === "string" ? headers.cookie : "";
|
|
91
|
+
const extra = crumb.cookies.join("; ");
|
|
92
|
+
headers.cookie = prior ? `${prior}; ${extra}` : extra;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
let resolvedContentType;
|
|
97
|
+
if (opts?.contentType) {
|
|
98
|
+
resolvedContentType = opts?.contentType;
|
|
99
|
+
}
|
|
100
|
+
if (!resolvedContentType && opts?.body instanceof URLSearchParams) {
|
|
101
|
+
resolvedContentType = "application/x-www-form-urlencoded; charset=UTF-8";
|
|
102
|
+
}
|
|
103
|
+
if (resolvedContentType) {
|
|
104
|
+
headers["content-type"] = resolvedContentType;
|
|
105
|
+
}
|
|
106
|
+
const res = await this.fetchRaw(url.toString(), {
|
|
107
|
+
method,
|
|
108
|
+
headers,
|
|
109
|
+
body: opts?.body
|
|
110
|
+
});
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
const text = await safeExtractText(res);
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Jenkins API error ${res.status} ${method} ${url.toString()}: ${text}`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
if (opts?.rawText) {
|
|
118
|
+
return res.text();
|
|
119
|
+
}
|
|
120
|
+
const contentType = (res.headers.get("content-type") || "").toLocaleLowerCase("en-US");
|
|
121
|
+
if (contentType.includes("application/json")) {
|
|
122
|
+
return res.json();
|
|
123
|
+
}
|
|
124
|
+
return res.text();
|
|
125
|
+
}
|
|
126
|
+
async fetchRaw(input, init) {
|
|
127
|
+
const flattened = {};
|
|
128
|
+
for (const [k, v] of Object.entries(init?.headers ?? {})) {
|
|
129
|
+
if (Array.isArray(v)) {
|
|
130
|
+
flattened[k] = v.join(", ");
|
|
131
|
+
} else if (v === void 0) {
|
|
132
|
+
continue;
|
|
133
|
+
} else {
|
|
134
|
+
flattened[k] = v;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return fetch(input, {
|
|
138
|
+
...init ?? {},
|
|
139
|
+
headers: flattened
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Normalizes a Jenkins job name into a fully qualified job path.
|
|
144
|
+
*
|
|
145
|
+
* Jenkins job URLs use a hierarchical format like:
|
|
146
|
+
* `/job/folder/job/subfolder/job/pipeline`
|
|
147
|
+
*
|
|
148
|
+
* This method takes a job name (either a string like `"folder/pipeline"`
|
|
149
|
+
* or an array like `["folder", "pipeline"]`) and converts it into the proper
|
|
150
|
+
* Jenkins API path format by inserting `job/` segments and URL-encoding
|
|
151
|
+
* each component.
|
|
152
|
+
*
|
|
153
|
+
* - If the input already contains `/job/` segments, it is returned as-is
|
|
154
|
+
* (after trimming any leading slash).
|
|
155
|
+
* - If the input is undefined, an error is thrown.
|
|
156
|
+
*
|
|
157
|
+
* @param name - The job name to normalize, either as a string or an array of path segments.
|
|
158
|
+
* @returns The normalized Jenkins job path (e.g. `"job/folder/job/pipeline"`).
|
|
159
|
+
* @throws If the name is undefined or empty.
|
|
160
|
+
*/
|
|
161
|
+
normalizeJobName(name) {
|
|
162
|
+
if (!name) {
|
|
163
|
+
throw new Error('Jenkins.normalizeJobName: "name" is required');
|
|
164
|
+
}
|
|
165
|
+
const parts = Array.isArray(name) ? name : name.split("/").filter(Boolean);
|
|
166
|
+
if (parts.join("/").includes("/job/")) {
|
|
167
|
+
return trimLeadingSlash(Array.isArray(name) ? parts.join("/") : name);
|
|
168
|
+
}
|
|
169
|
+
return parts.map(encodeURIComponent).map((s) => `job/${s}`).join("/");
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export { Jenkins };
|
|
174
|
+
//# sourceMappingURL=client.esm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.esm.js","sources":["../src/client.ts"],"sourcesContent":["/*\n * Copyright 2025 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport fetch from 'node-fetch';\nimport { createJobApi } from './client/jobApi';\nimport { createBuildApi } from './client/buildApi';\nimport { CrumbData, CrumbDataHeaderValues, HeaderValue } from './client/types';\n\nimport {\n addQueryParams,\n joinUrl,\n trimLeadingSlash,\n ensureTrailingSlash,\n safeExtractText,\n} from './client/utils';\n\n/** @public */\nexport interface JenkinsClientOptions {\n baseUrl: string; // e.g. \"https://jenkins.example.com\"\n crumbIssuer?: boolean | ((client: any) => Promise<CrumbData>) | undefined;\n headers?: Record<string, HeaderValue>;\n promisify?: boolean; // For compatibility with old legacy API, not used.\n}\n\n/** @public */\nexport class Jenkins {\n private crumbData?: CrumbData;\n public readonly opts: JenkinsClientOptions;\n\n constructor(opts: JenkinsClientOptions) {\n if (!opts.baseUrl) {\n throw new Error('Jenkins: opts.baseUrl is required');\n }\n\n // Legacy client behavior: set crumbIssuer to true if unset.\n if (opts.crumbIssuer === undefined) {\n opts.crumbIssuer = true;\n }\n\n // Legacy client behavior: set default headers and Referer\n // The referer here is the baseUrl.\n const referer = ensureTrailingSlash(opts.baseUrl);\n opts.headers = { referer, ...(opts.headers ?? {}) };\n\n this.opts = opts;\n }\n\n // Add APIs\n job = createJobApi({\n normalizeJobName: name => this.normalizeJobName(name),\n request: (path, opts) => this.request(path, opts),\n });\n\n build = createBuildApi({\n normalizeJobName: name => this.normalizeJobName(name),\n request: (path, opts) => this.request(path, opts),\n });\n\n /**\n * Retrieves and caches the Jenkins CSRF protection crumb.\n *\n * Jenkins uses a \"crumb\" (similar to a CSRF token) to protect write operations\n * such as POST requests. This method handles retrieving that crumb based on\n * the client's configuration.\n *\n * Behavior:\n * - If `crumbIssuer` is not enabled in the client options, it returns `undefined`.\n * - If a cached crumb already exists, it is returned immediately.\n * - If `crumbIssuer` is a function, that function is called to obtain the crumb.\n * - Otherwise, it performs a network request to\n * `<baseUrl>/crumbIssuer/api/json` to fetch the crumb from Jenkins.\n *\n * The result is cached in `this.crumbData` for subsequent calls.\n *\n * @returns A `CrumbData` object containing the header name and value,\n * or `undefined` if no crumb issuer is configured or the request fails.\n * @throws Any network or parsing errors that occur during the crumb fetch.\n */\n private async getCrumb(): Promise<CrumbData | undefined> {\n const { crumbIssuer } = this.opts;\n\n if (!crumbIssuer) {\n return undefined;\n }\n\n if (this.crumbData) {\n return this.crumbData;\n }\n\n if (typeof crumbIssuer === 'function') {\n this.crumbData = await crumbIssuer(this);\n return this.crumbData;\n }\n\n // Fetch crumb from Jenkins\n const res = await this.fetchRaw(\n `${ensureTrailingSlash(this.opts.baseUrl)}crumbIssuer/api/json`,\n );\n if (!res.ok) {\n return undefined;\n }\n const data = (await res.json()) as CrumbDataHeaderValues;\n this.crumbData = {\n headerName: data.crumbRequestField,\n headerValue: data.crumb,\n };\n\n return this.crumbData;\n }\n\n private async request(\n path: string,\n opts: {\n method?: string;\n query?: Record<string, string | number | undefined>;\n body?: any;\n rawText?: boolean;\n contentType?: string;\n } = {},\n ): Promise<any> {\n let url = new URL(\n joinUrl(ensureTrailingSlash(this.opts.baseUrl), trimLeadingSlash(path)),\n );\n if (opts.query) {\n url = addQueryParams(url, opts.query);\n }\n\n const method = (\n opts?.method || (opts?.body ? 'POST' : 'GET')\n ).toLocaleUpperCase('en-US');\n const headers: Record<string, HeaderValue> = {\n ...(this.opts.headers ?? {}),\n };\n\n // Legacy client support: Add crumb if request is not read-only\n if (method !== 'GET' && method !== 'HEAD') {\n const crumb = await this.getCrumb();\n if (crumb) {\n headers[crumb.headerName] = crumb.headerValue;\n // If the crumb call told us to include some cookies, merge them into\n // the existing cookie header\n if (crumb.cookies?.length) {\n const prior =\n typeof headers.cookie === 'string' ? headers.cookie : '';\n const extra = crumb.cookies.join('; ');\n headers.cookie = prior ? `${prior}; ${extra}` : extra;\n }\n }\n }\n\n // Set Content-Type, default to undefined if not set\n // Check caller-specified content-type first\n let resolvedContentType: string | undefined;\n if (opts?.contentType) {\n resolvedContentType = opts?.contentType;\n }\n\n // URLSearchParams -> x-www-form-urlencoded\n if (!resolvedContentType && opts?.body instanceof URLSearchParams) {\n resolvedContentType = 'application/x-www-form-urlencoded; charset=UTF-8';\n }\n\n if (resolvedContentType) {\n headers['content-type'] = resolvedContentType;\n }\n\n const res = await this.fetchRaw(url.toString(), {\n method,\n headers,\n body: opts?.body,\n });\n\n if (!res.ok) {\n const text = await safeExtractText(res);\n throw new Error(\n `Jenkins API error ${res.status} ${method} ${url.toString()}: ${text}`,\n );\n }\n\n if (opts?.rawText) {\n return res.text();\n }\n\n const contentType = (\n res.headers.get('content-type') || ''\n ).toLocaleLowerCase('en-US');\n if (contentType.includes('application/json')) {\n return res.json();\n }\n\n return res.text();\n }\n\n private async fetchRaw(\n input: string,\n init?: {\n method?: string;\n headers?: Record<string, HeaderValue>;\n body?: any;\n },\n ) {\n // Flatten the values passed in \"headers\"\n const flattened: Record<string, string> = {};\n for (const [k, v] of Object.entries(init?.headers ?? {})) {\n if (Array.isArray(v)) {\n flattened[k] = v.join(', ');\n } else if (v === undefined) {\n continue;\n } else {\n flattened[k] = v;\n }\n }\n\n return fetch(input, {\n ...(init ?? {}),\n headers: flattened as any,\n });\n }\n\n /**\n * Normalizes a Jenkins job name into a fully qualified job path.\n *\n * Jenkins job URLs use a hierarchical format like:\n * `/job/folder/job/subfolder/job/pipeline`\n *\n * This method takes a job name (either a string like `\"folder/pipeline\"`\n * or an array like `[\"folder\", \"pipeline\"]`) and converts it into the proper\n * Jenkins API path format by inserting `job/` segments and URL-encoding\n * each component.\n *\n * - If the input already contains `/job/` segments, it is returned as-is\n * (after trimming any leading slash).\n * - If the input is undefined, an error is thrown.\n *\n * @param name - The job name to normalize, either as a string or an array of path segments.\n * @returns The normalized Jenkins job path (e.g. `\"job/folder/job/pipeline\"`).\n * @throws If the name is undefined or empty.\n */\n private normalizeJobName(\n name: string | string[] | undefined,\n ): string | undefined {\n if (!name) {\n throw new Error('Jenkins.normalizeJobName: \"name\" is required');\n }\n\n const parts = Array.isArray(name) ? name : name.split('/').filter(Boolean);\n if (parts.join('/').includes('/job/')) {\n return trimLeadingSlash(Array.isArray(name) ? parts.join('/') : name);\n }\n\n return parts\n .map(encodeURIComponent)\n .map(s => `job/${s}`)\n .join('/');\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/index.cjs.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
var permissions = require('./permissions.cjs.js');
|
|
4
|
+
var client = require('./client.cjs.js');
|
|
4
5
|
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
exports.jenkinsExecutePermission = permissions.jenkinsExecutePermission;
|
|
8
9
|
exports.jenkinsPermissions = permissions.jenkinsPermissions;
|
|
10
|
+
exports.Jenkins = client.Jenkins;
|
|
9
11
|
//# sourceMappingURL=index.cjs.js.map
|
package/dist/index.cjs.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;"}
|
package/dist/index.d.ts
CHANGED
|
@@ -13,4 +13,119 @@ declare const jenkinsExecutePermission: _backstage_plugin_permission_common.Reso
|
|
|
13
13
|
*/
|
|
14
14
|
declare const jenkinsPermissions: _backstage_plugin_permission_common.ResourcePermission<"catalog-entity">[];
|
|
15
15
|
|
|
16
|
-
|
|
16
|
+
/** @public */
|
|
17
|
+
interface CommonBuild {
|
|
18
|
+
timestamp: number;
|
|
19
|
+
building: boolean;
|
|
20
|
+
duration: number;
|
|
21
|
+
result?: string;
|
|
22
|
+
fullDisplayName: string;
|
|
23
|
+
displayName: string;
|
|
24
|
+
url: string;
|
|
25
|
+
number: number;
|
|
26
|
+
}
|
|
27
|
+
/** @public */
|
|
28
|
+
interface JenkinsBuild extends CommonBuild {
|
|
29
|
+
actions: any;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @public */
|
|
33
|
+
interface CrumbData {
|
|
34
|
+
headerName: string;
|
|
35
|
+
headerValue: string;
|
|
36
|
+
cookies?: string[];
|
|
37
|
+
}
|
|
38
|
+
/** @public */
|
|
39
|
+
interface CrumbDataHeaderValues {
|
|
40
|
+
crumbRequestField: string;
|
|
41
|
+
crumb: string;
|
|
42
|
+
}
|
|
43
|
+
/** @public */
|
|
44
|
+
type HeaderValue = string | string[] | undefined;
|
|
45
|
+
/** @public */
|
|
46
|
+
type JenkinsParams = Record<string, unknown> | URLSearchParams | undefined;
|
|
47
|
+
/** @public */
|
|
48
|
+
interface JobBuildOptions {
|
|
49
|
+
parameters?: JenkinsParams;
|
|
50
|
+
token?: string;
|
|
51
|
+
delay?: string;
|
|
52
|
+
}
|
|
53
|
+
/** @public */
|
|
54
|
+
interface JobGetOptions {
|
|
55
|
+
name: string | string[];
|
|
56
|
+
tree?: string;
|
|
57
|
+
depth?: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** @public */
|
|
61
|
+
interface JenkinsClientOptions {
|
|
62
|
+
baseUrl: string;
|
|
63
|
+
crumbIssuer?: boolean | ((client: any) => Promise<CrumbData>) | undefined;
|
|
64
|
+
headers?: Record<string, HeaderValue>;
|
|
65
|
+
promisify?: boolean;
|
|
66
|
+
}
|
|
67
|
+
/** @public */
|
|
68
|
+
declare class Jenkins {
|
|
69
|
+
private crumbData?;
|
|
70
|
+
readonly opts: JenkinsClientOptions;
|
|
71
|
+
constructor(opts: JenkinsClientOptions);
|
|
72
|
+
job: {
|
|
73
|
+
get: (input: JobGetOptions) => Promise<any>;
|
|
74
|
+
getBuilds: (name: string | string[], tree?: string) => Promise<unknown>;
|
|
75
|
+
build: (name: string | string[], opts?: JobBuildOptions | undefined) => Promise<unknown>;
|
|
76
|
+
copy: (name: string | string[], from: string) => Promise<void>;
|
|
77
|
+
create: (name: string | string[], xml: string) => Promise<void>;
|
|
78
|
+
destroy: (name: string | string[]) => Promise<void>;
|
|
79
|
+
enable: (name: string | string[]) => Promise<void>;
|
|
80
|
+
disable: (name: string | string[]) => Promise<void>;
|
|
81
|
+
};
|
|
82
|
+
build: {
|
|
83
|
+
get: (name: string | string[], buildNumber: string | number) => Promise<JenkinsBuild>;
|
|
84
|
+
getConsoleText: (name: string | string[], buildNumber: string | number) => Promise<string>;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Retrieves and caches the Jenkins CSRF protection crumb.
|
|
88
|
+
*
|
|
89
|
+
* Jenkins uses a "crumb" (similar to a CSRF token) to protect write operations
|
|
90
|
+
* such as POST requests. This method handles retrieving that crumb based on
|
|
91
|
+
* the client's configuration.
|
|
92
|
+
*
|
|
93
|
+
* Behavior:
|
|
94
|
+
* - If `crumbIssuer` is not enabled in the client options, it returns `undefined`.
|
|
95
|
+
* - If a cached crumb already exists, it is returned immediately.
|
|
96
|
+
* - If `crumbIssuer` is a function, that function is called to obtain the crumb.
|
|
97
|
+
* - Otherwise, it performs a network request to
|
|
98
|
+
* `<baseUrl>/crumbIssuer/api/json` to fetch the crumb from Jenkins.
|
|
99
|
+
*
|
|
100
|
+
* The result is cached in `this.crumbData` for subsequent calls.
|
|
101
|
+
*
|
|
102
|
+
* @returns A `CrumbData` object containing the header name and value,
|
|
103
|
+
* or `undefined` if no crumb issuer is configured or the request fails.
|
|
104
|
+
* @throws Any network or parsing errors that occur during the crumb fetch.
|
|
105
|
+
*/
|
|
106
|
+
private getCrumb;
|
|
107
|
+
private request;
|
|
108
|
+
private fetchRaw;
|
|
109
|
+
/**
|
|
110
|
+
* Normalizes a Jenkins job name into a fully qualified job path.
|
|
111
|
+
*
|
|
112
|
+
* Jenkins job URLs use a hierarchical format like:
|
|
113
|
+
* `/job/folder/job/subfolder/job/pipeline`
|
|
114
|
+
*
|
|
115
|
+
* This method takes a job name (either a string like `"folder/pipeline"`
|
|
116
|
+
* or an array like `["folder", "pipeline"]`) and converts it into the proper
|
|
117
|
+
* Jenkins API path format by inserting `job/` segments and URL-encoding
|
|
118
|
+
* each component.
|
|
119
|
+
*
|
|
120
|
+
* - If the input already contains `/job/` segments, it is returned as-is
|
|
121
|
+
* (after trimming any leading slash).
|
|
122
|
+
* - If the input is undefined, an error is thrown.
|
|
123
|
+
*
|
|
124
|
+
* @param name - The job name to normalize, either as a string or an array of path segments.
|
|
125
|
+
* @returns The normalized Jenkins job path (e.g. `"job/folder/job/pipeline"`).
|
|
126
|
+
* @throws If the name is undefined or empty.
|
|
127
|
+
*/
|
|
128
|
+
private normalizeJobName;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export { type CommonBuild, type CrumbData, type CrumbDataHeaderValues, type HeaderValue, Jenkins, type JenkinsBuild, type JenkinsClientOptions, type JenkinsParams, type JobBuildOptions, type JobGetOptions, jenkinsExecutePermission, jenkinsPermissions };
|
package/dist/index.esm.js
CHANGED
package/dist/index.esm.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"index.esm.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@backstage-community/plugin-jenkins-common",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.0",
|
|
4
4
|
"backstage": {
|
|
5
5
|
"role": "common-library",
|
|
6
6
|
"pluginId": "jenkins",
|
|
@@ -38,11 +38,13 @@
|
|
|
38
38
|
"test": "backstage-cli package test"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@backstage/plugin-catalog-common": "^1.1.
|
|
42
|
-
"@backstage/plugin-permission-common": "^0.9.
|
|
41
|
+
"@backstage/plugin-catalog-common": "^1.1.7",
|
|
42
|
+
"@backstage/plugin-permission-common": "^0.9.3",
|
|
43
|
+
"form-data": "^4.0.4",
|
|
44
|
+
"node-fetch": "^2.6.7"
|
|
43
45
|
},
|
|
44
46
|
"devDependencies": {
|
|
45
|
-
"@backstage/cli": "^0.34.
|
|
47
|
+
"@backstage/cli": "^0.34.5"
|
|
46
48
|
},
|
|
47
49
|
"typesVersions": {
|
|
48
50
|
"*": {
|