@grafana/create-plugin 7.4.0-canary.2630.25780630776.0 → 7.4.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/codemods/migrations/migrations.js +6 -0
- package/dist/codemods/migrations/scripts/008-bundle-stats-permissions.js +23 -0
- package/dist/commands/generate.command.js +1 -1
- package/dist/utils/utils.packageManager.js +2 -10
- package/package.json +2 -2
- package/src/codemods/migrations/migrations.ts +7 -0
- package/src/codemods/migrations/scripts/008-bundle-stats-permissions.test.ts +231 -0
- package/src/codemods/migrations/scripts/008-bundle-stats-permissions.ts +29 -0
- package/src/commands/generate.command.ts +2 -1
- package/src/utils/utils.packageManager.ts +2 -11
- package/templates/common/npmrc +12 -1
- package/templates/github/workflows/bundle-stats.yml +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
# v7.4.0 (Wed May 13 2026)
|
|
2
|
+
|
|
3
|
+
#### 🚀 Enhancement
|
|
4
|
+
|
|
5
|
+
- feat: harden bundle-stats workflow permissions [#2627](https://github.com/grafana/plugin-tools/pull/2627) ([@jackw](https://github.com/jackw))
|
|
6
|
+
|
|
7
|
+
#### Authors: 1
|
|
8
|
+
|
|
9
|
+
- Jack Westbrook ([@jackw](https://github.com/jackw))
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
1
13
|
# v7.3.1 (Wed May 06 2026)
|
|
2
14
|
|
|
3
15
|
#### 🐛 Bug Fix
|
|
@@ -42,6 +42,12 @@ var defaultMigrations = [
|
|
|
42
42
|
version: "6.1.13",
|
|
43
43
|
description: "Add setupTests.d.ts for @testing-library/jest-dom types and remove @types/testing-library__jest-dom npm package.",
|
|
44
44
|
scriptPath: import.meta.resolve("./scripts/007-remove-testing-library-types.js")
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "008-bundle-stats-permissions",
|
|
48
|
+
version: "7.3.2",
|
|
49
|
+
description: "Harden bundle-stats/bundle-size workflow permissions: contents permission was set to write but only read access is required; restricted to read for least-privilege.",
|
|
50
|
+
scriptPath: import.meta.resolve("./scripts/008-bundle-stats-permissions.js")
|
|
45
51
|
}
|
|
46
52
|
// Do not use LEGACY_UPDATE_CUTOFF_VERSION for new migrations. It is only used above to force migrations to run
|
|
47
53
|
// for those written before the switch to updates as migrations.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { parseDocument, stringify } from 'yaml';
|
|
2
|
+
|
|
3
|
+
const workflowPaths = ["./.github/workflows/bundle-stats.yml", "./.github/workflows/bundle-size.yml"];
|
|
4
|
+
async function migrate(context) {
|
|
5
|
+
for (const workflowPath of workflowPaths) {
|
|
6
|
+
if (!context.doesFileExist(workflowPath)) {
|
|
7
|
+
continue;
|
|
8
|
+
}
|
|
9
|
+
const workflowContent = context.getFile(workflowPath);
|
|
10
|
+
if (!workflowContent) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
const workflowDoc = parseDocument(workflowContent);
|
|
14
|
+
if (workflowDoc.getIn(["permissions", "contents"]) !== "write") {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
workflowDoc.setIn(["permissions", "contents"], "read");
|
|
18
|
+
context.updateFile(workflowPath, stringify(workflowDoc));
|
|
19
|
+
}
|
|
20
|
+
return context;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export { migrate as default };
|
|
@@ -93,7 +93,7 @@ function getActionsForTemplateFolder({
|
|
|
93
93
|
templateData
|
|
94
94
|
}) {
|
|
95
95
|
let files = glob.sync(`${folderPath}/**`, { dot: true });
|
|
96
|
-
if (templateData.packageManagerName !== "
|
|
96
|
+
if (templateData.packageManagerName !== "pnpm") {
|
|
97
97
|
files = files.filter((file) => path.basename(file) !== "npmrc");
|
|
98
98
|
}
|
|
99
99
|
files = files.filter((file) => {
|
|
@@ -17,17 +17,9 @@ async function configureYarn(cwd, packageManagerVersion) {
|
|
|
17
17
|
shell: true,
|
|
18
18
|
cwd
|
|
19
19
|
});
|
|
20
|
-
|
|
21
|
-
shell: true,
|
|
22
|
-
cwd
|
|
23
|
-
});
|
|
24
|
-
return "Configured Yarn Berry to use node_modules and disabled script execution during installation.";
|
|
20
|
+
return "Configured Yarn Berry to use node_modules (PnP is not supported)";
|
|
25
21
|
}
|
|
26
|
-
|
|
27
|
-
shell: true,
|
|
28
|
-
cwd
|
|
29
|
-
});
|
|
30
|
-
return "Configured Yarn to ignore scripts during installation.";
|
|
22
|
+
return "";
|
|
31
23
|
} catch (error) {
|
|
32
24
|
throw new Error(
|
|
33
25
|
"There was an error configuring Yarn. Please run `yarn set version stable && yarn config set nodeLinker node-modules` in your plugin directory."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@grafana/create-plugin",
|
|
3
|
-
"version": "7.4.0
|
|
3
|
+
"version": "7.4.0",
|
|
4
4
|
"repository": {
|
|
5
5
|
"directory": "packages/create-plugin",
|
|
6
6
|
"url": "https://github.com/grafana/plugin-tools"
|
|
@@ -55,5 +55,5 @@
|
|
|
55
55
|
"engines": {
|
|
56
56
|
"node": ">=20"
|
|
57
57
|
},
|
|
58
|
-
"gitHead": "
|
|
58
|
+
"gitHead": "d9190314df0893842f12e33bbdfb7d39c3a704cc"
|
|
59
59
|
}
|
|
@@ -50,6 +50,13 @@ export default [
|
|
|
50
50
|
'Add setupTests.d.ts for @testing-library/jest-dom types and remove @types/testing-library__jest-dom npm package.',
|
|
51
51
|
scriptPath: import.meta.resolve('./scripts/007-remove-testing-library-types.js'),
|
|
52
52
|
},
|
|
53
|
+
{
|
|
54
|
+
name: '008-bundle-stats-permissions',
|
|
55
|
+
version: '7.3.2',
|
|
56
|
+
description:
|
|
57
|
+
'Harden bundle-stats/bundle-size workflow permissions: contents permission was set to write but only read access is required; restricted to read for least-privilege.',
|
|
58
|
+
scriptPath: import.meta.resolve('./scripts/008-bundle-stats-permissions.js'),
|
|
59
|
+
},
|
|
53
60
|
// Do not use LEGACY_UPDATE_CUTOFF_VERSION for new migrations. It is only used above to force migrations to run
|
|
54
61
|
// for those written before the switch to updates as migrations.
|
|
55
62
|
] satisfies Migration[];
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { Context } from '../../context.js';
|
|
3
|
+
import migrate from './008-bundle-stats-permissions.js';
|
|
4
|
+
import { parse } from 'yaml';
|
|
5
|
+
|
|
6
|
+
const workflowPath = './.github/workflows/bundle-stats.yml';
|
|
7
|
+
const legacyWorkflowPath = './.github/workflows/bundle-size.yml';
|
|
8
|
+
|
|
9
|
+
describe('008-bundle-stats-permissions', () => {
|
|
10
|
+
it('should not modify anything if workflow file does not exist', async () => {
|
|
11
|
+
const context = new Context('/virtual');
|
|
12
|
+
|
|
13
|
+
await migrate(context);
|
|
14
|
+
|
|
15
|
+
expect(context.listChanges()).toEqual({});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('should not modify anything if workflow file is empty', async () => {
|
|
19
|
+
const context = new Context('/virtual');
|
|
20
|
+
context.addFile(workflowPath, '');
|
|
21
|
+
const initialChanges = context.listChanges();
|
|
22
|
+
|
|
23
|
+
await migrate(context);
|
|
24
|
+
|
|
25
|
+
expect(context.listChanges()).toEqual(initialChanges);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should not modify anything if permissions block is missing', async () => {
|
|
29
|
+
const context = new Context('/virtual');
|
|
30
|
+
context.addFile(
|
|
31
|
+
workflowPath,
|
|
32
|
+
`name: Bundle Stats
|
|
33
|
+
on: [pull_request]
|
|
34
|
+
jobs:
|
|
35
|
+
compare:
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v6
|
|
39
|
+
`
|
|
40
|
+
);
|
|
41
|
+
const initialChanges = context.listChanges();
|
|
42
|
+
|
|
43
|
+
await migrate(context);
|
|
44
|
+
|
|
45
|
+
expect(context.listChanges()).toEqual(initialChanges);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should not modify anything if permissions.contents key is missing', async () => {
|
|
49
|
+
const context = new Context('/virtual');
|
|
50
|
+
context.addFile(
|
|
51
|
+
workflowPath,
|
|
52
|
+
`name: Bundle Stats
|
|
53
|
+
on: [pull_request]
|
|
54
|
+
permissions:
|
|
55
|
+
pull-requests: write
|
|
56
|
+
actions: read
|
|
57
|
+
jobs:
|
|
58
|
+
compare:
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
steps:
|
|
61
|
+
- uses: actions/checkout@v6
|
|
62
|
+
`
|
|
63
|
+
);
|
|
64
|
+
const initialChanges = context.listChanges();
|
|
65
|
+
|
|
66
|
+
await migrate(context);
|
|
67
|
+
|
|
68
|
+
expect(context.listChanges()).toEqual(initialChanges);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should not modify anything if permissions.contents is already read', async () => {
|
|
72
|
+
const context = new Context('/virtual');
|
|
73
|
+
const original = `name: Bundle Stats
|
|
74
|
+
on: [pull_request]
|
|
75
|
+
permissions:
|
|
76
|
+
contents: read
|
|
77
|
+
pull-requests: write
|
|
78
|
+
actions: read
|
|
79
|
+
jobs:
|
|
80
|
+
compare:
|
|
81
|
+
runs-on: ubuntu-latest
|
|
82
|
+
steps:
|
|
83
|
+
- uses: actions/checkout@v6
|
|
84
|
+
`;
|
|
85
|
+
context.addFile(workflowPath, original);
|
|
86
|
+
|
|
87
|
+
await migrate(context);
|
|
88
|
+
|
|
89
|
+
expect(context.getFile(workflowPath)).toBe(original);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should not modify anything if permissions.contents is none', async () => {
|
|
93
|
+
const context = new Context('/virtual');
|
|
94
|
+
const original = `name: Bundle Stats
|
|
95
|
+
on: [pull_request]
|
|
96
|
+
permissions:
|
|
97
|
+
contents: none
|
|
98
|
+
pull-requests: write
|
|
99
|
+
actions: read
|
|
100
|
+
jobs:
|
|
101
|
+
compare:
|
|
102
|
+
runs-on: ubuntu-latest
|
|
103
|
+
steps:
|
|
104
|
+
- uses: actions/checkout@v6
|
|
105
|
+
`;
|
|
106
|
+
context.addFile(workflowPath, original);
|
|
107
|
+
|
|
108
|
+
await migrate(context);
|
|
109
|
+
|
|
110
|
+
expect(context.getFile(workflowPath)).toBe(original);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should update permissions.contents from write to read', async () => {
|
|
114
|
+
const context = new Context('/virtual');
|
|
115
|
+
context.addFile(
|
|
116
|
+
workflowPath,
|
|
117
|
+
`name: Bundle Stats
|
|
118
|
+
on: [pull_request]
|
|
119
|
+
permissions:
|
|
120
|
+
contents: write
|
|
121
|
+
pull-requests: write
|
|
122
|
+
actions: read
|
|
123
|
+
jobs:
|
|
124
|
+
compare:
|
|
125
|
+
runs-on: ubuntu-latest
|
|
126
|
+
steps:
|
|
127
|
+
- uses: actions/checkout@v6
|
|
128
|
+
`
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
await migrate(context);
|
|
132
|
+
|
|
133
|
+
const migrated = parse(context.getFile(workflowPath) || '');
|
|
134
|
+
expect(migrated.permissions.contents).toBe('read');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should preserve other permissions when updating contents', async () => {
|
|
138
|
+
const context = new Context('/virtual');
|
|
139
|
+
context.addFile(
|
|
140
|
+
workflowPath,
|
|
141
|
+
`name: Bundle Stats
|
|
142
|
+
on: [pull_request]
|
|
143
|
+
permissions:
|
|
144
|
+
contents: write
|
|
145
|
+
pull-requests: write
|
|
146
|
+
actions: read
|
|
147
|
+
jobs:
|
|
148
|
+
compare:
|
|
149
|
+
runs-on: ubuntu-latest
|
|
150
|
+
steps:
|
|
151
|
+
- uses: actions/checkout@v6
|
|
152
|
+
`
|
|
153
|
+
);
|
|
154
|
+
|
|
155
|
+
await migrate(context);
|
|
156
|
+
|
|
157
|
+
const migrated = parse(context.getFile(workflowPath) || '');
|
|
158
|
+
expect(migrated.permissions).toEqual({
|
|
159
|
+
contents: 'read',
|
|
160
|
+
'pull-requests': 'write',
|
|
161
|
+
actions: 'read',
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should update permissions.contents in the legacy bundle-size.yml workflow', async () => {
|
|
166
|
+
const context = new Context('/virtual');
|
|
167
|
+
context.addFile(
|
|
168
|
+
legacyWorkflowPath,
|
|
169
|
+
`name: Bundle Stats
|
|
170
|
+
on: [pull_request]
|
|
171
|
+
permissions:
|
|
172
|
+
contents: write
|
|
173
|
+
pull-requests: write
|
|
174
|
+
actions: read
|
|
175
|
+
jobs:
|
|
176
|
+
compare:
|
|
177
|
+
runs-on: ubuntu-latest
|
|
178
|
+
steps:
|
|
179
|
+
- uses: actions/checkout@v6
|
|
180
|
+
`
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
await migrate(context);
|
|
184
|
+
|
|
185
|
+
const migrated = parse(context.getFile(legacyWorkflowPath) || '');
|
|
186
|
+
expect(migrated.permissions.contents).toBe('read');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should update both workflow filenames if both exist', async () => {
|
|
190
|
+
const context = new Context('/virtual');
|
|
191
|
+
const original = `name: Bundle Stats
|
|
192
|
+
on: [pull_request]
|
|
193
|
+
permissions:
|
|
194
|
+
contents: write
|
|
195
|
+
pull-requests: write
|
|
196
|
+
jobs:
|
|
197
|
+
compare:
|
|
198
|
+
runs-on: ubuntu-latest
|
|
199
|
+
steps:
|
|
200
|
+
- uses: actions/checkout@v6
|
|
201
|
+
`;
|
|
202
|
+
context.addFile(workflowPath, original);
|
|
203
|
+
context.addFile(legacyWorkflowPath, original);
|
|
204
|
+
|
|
205
|
+
await migrate(context);
|
|
206
|
+
|
|
207
|
+
expect(parse(context.getFile(workflowPath) || '').permissions.contents).toBe('read');
|
|
208
|
+
expect(parse(context.getFile(legacyWorkflowPath) || '').permissions.contents).toBe('read');
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
it('should be idempotent', async () => {
|
|
212
|
+
const context = new Context('/virtual');
|
|
213
|
+
context.addFile(
|
|
214
|
+
workflowPath,
|
|
215
|
+
`name: Bundle Stats
|
|
216
|
+
on: [pull_request]
|
|
217
|
+
permissions:
|
|
218
|
+
contents: write
|
|
219
|
+
pull-requests: write
|
|
220
|
+
actions: read
|
|
221
|
+
jobs:
|
|
222
|
+
compare:
|
|
223
|
+
runs-on: ubuntu-latest
|
|
224
|
+
steps:
|
|
225
|
+
- uses: actions/checkout@v6
|
|
226
|
+
`
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
await expect(migrate).toBeIdempotent(context);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type Context } from '../../context.js';
|
|
2
|
+
import { parseDocument, stringify } from 'yaml';
|
|
3
|
+
|
|
4
|
+
const workflowPaths = ['./.github/workflows/bundle-stats.yml', './.github/workflows/bundle-size.yml'];
|
|
5
|
+
|
|
6
|
+
export default async function migrate(context: Context) {
|
|
7
|
+
for (const workflowPath of workflowPaths) {
|
|
8
|
+
if (!context.doesFileExist(workflowPath)) {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const workflowContent = context.getFile(workflowPath);
|
|
13
|
+
|
|
14
|
+
if (!workflowContent) {
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const workflowDoc = parseDocument(workflowContent);
|
|
19
|
+
|
|
20
|
+
if (workflowDoc.getIn(['permissions', 'contents']) !== 'write') {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
workflowDoc.setIn(['permissions', 'contents'], 'read');
|
|
25
|
+
context.updateFile(workflowPath, stringify(workflowDoc));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return context;
|
|
29
|
+
}
|
|
@@ -135,7 +135,8 @@ function getActionsForTemplateFolder({
|
|
|
135
135
|
}) {
|
|
136
136
|
let files = glob.sync(`${folderPath}/**`, { dot: true });
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
// The npmrc file is only useful for `pnpm` settings. We can remove it for other package managers.
|
|
139
|
+
if (templateData.packageManagerName !== 'pnpm') {
|
|
139
140
|
files = files.filter((file) => path.basename(file) !== 'npmrc');
|
|
140
141
|
}
|
|
141
142
|
|
|
@@ -24,18 +24,9 @@ export async function configureYarn(cwd: string, packageManagerVersion: string)
|
|
|
24
24
|
shell: true,
|
|
25
25
|
cwd,
|
|
26
26
|
});
|
|
27
|
-
|
|
28
|
-
shell: true,
|
|
29
|
-
cwd,
|
|
30
|
-
});
|
|
31
|
-
return 'Configured Yarn Berry to use node_modules and disabled script execution during installation.';
|
|
27
|
+
return 'Configured Yarn Berry to use node_modules (PnP is not supported)';
|
|
32
28
|
}
|
|
33
|
-
|
|
34
|
-
shell: true,
|
|
35
|
-
cwd,
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
return 'Configured Yarn to ignore scripts during installation.';
|
|
29
|
+
return '';
|
|
39
30
|
} catch (error) {
|
|
40
31
|
throw new Error(
|
|
41
32
|
'There was an error configuring Yarn. Please run `yarn set version stable && yarn config set nodeLinker node-modules` in your plugin directory.'
|
package/templates/common/npmrc
CHANGED
|
@@ -1 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
# This file is required for PNPM
|
|
2
|
+
|
|
3
|
+
# PNPM 8 changed the default resolution mode to "lowest-direct" which is not how we expect resolutions to work
|
|
4
|
+
resolution-mode="highest"
|
|
5
|
+
|
|
6
|
+
# Make sure the default patterns are still included (https://pnpm.io/npmrc#public-hoist-pattern)
|
|
7
|
+
public-hoist-pattern[]="*eslint*"
|
|
8
|
+
public-hoist-pattern[]="*prettier*"
|
|
9
|
+
|
|
10
|
+
# Hoist all types packages to the root for better TS support
|
|
11
|
+
public-hoist-pattern[]="@types/*"
|
|
12
|
+
public-hoist-pattern[]="*terser-webpack-plugin*"
|