Skip to content

Commit b228929

Browse files
authored
Merge pull request kubernetes-sigs#4368 from illume/agentplug
headlamp-plugin: Add AGENTS.md to created plugins and bundle examples
2 parents f75905b + 233364a commit b228929

File tree

8 files changed

+591
-6
lines changed

8 files changed

+591
-6
lines changed

plugins/headlamp-plugin/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,6 @@ node_modules
22
kinvolk-headlamp-plugin-*.tgz
33
kinvolk-headlamp-plugin-*.tar.gz
44
lib
5+
examples
6+
official-plugins
7+
.temp-official-plugins

plugins/headlamp-plugin/bin/headlamp-plugin.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -952,6 +952,7 @@ function upgrade(packageFolder, skipPackageUpdates, headlampPluginVersion) {
952952
path.join('.vscode', 'settings.json'),
953953
path.join('.vscode', 'tasks.json'),
954954
'tsconfig.json',
955+
'AGENTS.md',
955956
];
956957
const templateFolder = path.resolve(__dirname, '..', 'template');
957958

@@ -967,9 +968,14 @@ function upgrade(packageFolder, skipPackageUpdates, headlampPluginVersion) {
967968
fs.copyFileSync(from, to);
968969
}
969970
// Add file if it is different
970-
if (fs.readFileSync(from, 'utf8') !== fs.readFileSync(to, 'utf8')) {
971-
console.log(`Updating file: "${to}"`);
972-
fs.copyFileSync(from, to);
971+
if (fs.existsSync(to)) {
972+
const fromContent = fs.readFileSync(from, 'utf8');
973+
const toContent = fs.readFileSync(to, 'utf8');
974+
975+
if (fromContent !== toContent) {
976+
console.log(`Updating file: "${to}"`);
977+
fs.writeFileSync(to, fromContent);
978+
}
973979
}
974980
});
975981
}

plugins/headlamp-plugin/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@
77
"bin": "bin/headlamp-plugin.js",
88
"scripts": {
99
"prepack": "(node -e \"if (! require('fs').existsSync('./lib/components')) {process.exit(1)} \" || (echo 'lib dir is empty. Remember to run `npm run build` before packing' && exit 1))",
10-
"build": "npx shx rm -rf lib types lib/assets lib/components lib/helpers lib/i18n lib/lib lib/plugin lib/redux lib/resources && npx shx cp -r ../../frontend/src/assets ../../frontend/src/components ../../frontend/src/helpers ../../frontend/src/stateless ../../frontend/src/i18n ../../frontend/src/lib ../../frontend/src/plugin ../../frontend/src/redux ../../frontend/src/resources src/ && tsc --build ./tsconfig.json && npx shx cp -r src/additional.d.ts lib/ && npx shx cp -r ../../frontend/src/assets lib/ && npx shx cp -r ../../frontend/src/resources lib/ && npx shx cp -r ../../frontend/src/i18n/locales lib/i18n && node scripts/copy-static-assets.js",
10+
"build": "npm run prepare-bundle && npx shx rm -rf lib types lib/assets lib/components lib/helpers lib/i18n lib/lib lib/plugin lib/redux lib/resources && npx shx cp -r ../../frontend/src/assets ../../frontend/src/components ../../frontend/src/helpers ../../frontend/src/stateless ../../frontend/src/i18n ../../frontend/src/lib ../../frontend/src/plugin ../../frontend/src/redux ../../frontend/src/resources src/ && tsc --build ./tsconfig.json && npx shx cp -r src/additional.d.ts lib/ && npx shx cp -r ../../frontend/src/assets lib/ && npx shx cp -r ../../frontend/src/resources lib/ && npx shx cp -r ../../frontend/src/i18n/locales lib/i18n && node scripts/copy-static-assets.js",
11+
"bundle-examples": "node scripts/bundle-examples.js",
12+
"fetch-official-plugins": "node scripts/fetch-official-plugins.js",
13+
"prepare-bundle": "npm run bundle-examples && npm run fetch-official-plugins",
1114
"update-dependencies": "node dependencies-sync.js update && npm install && node scripts/copy-package-lock.js",
1215
"check-dependencies": "node dependencies-sync.js check",
1316
"copy-package-lock": "node scripts/copy-package-lock.js"
@@ -132,6 +135,8 @@
132135
"lib",
133136
"types",
134137
".storybook",
138+
"examples",
139+
"official-plugins",
135140
"plugin-management/plugin-management.js",
136141
"plugin-management/multi-plugin-management.js"
137142
],
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
#!/usr/bin/env node
2+
3+
/*
4+
* Copyright 2025 The Kubernetes Authors
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
/**
20+
* This script copies example plugins from plugins/examples into
21+
* examples/ directory for bundling with headlamp-plugin package.
22+
* These plugins are referenced in AGENTS.md and help agents write good plugins.
23+
*/
24+
25+
const fs = require('fs-extra');
26+
const path = require('path');
27+
const { getLocalGitHash, storeHash, shouldSkipBasedOnHash } = require('./git-hash-utils');
28+
29+
const scriptDir = __dirname;
30+
const pluginDir = path.resolve(scriptDir, '..');
31+
const examplesSourceDir = path.resolve(pluginDir, '..', 'examples');
32+
const examplesDestDir = path.resolve(pluginDir, 'examples');
33+
const hashFile = path.resolve(examplesDestDir, '.git-hash');
34+
35+
console.log('Bundling example plugins...');
36+
console.log(`Source: ${examplesSourceDir}`);
37+
console.log(`Destination: ${examplesDestDir}`);
38+
39+
// Get the current git hash
40+
const currentHash = getLocalGitHash('plugins/examples', path.resolve(pluginDir, '..', '..'));
41+
42+
// Check if we can skip bundling
43+
if (shouldSkipBasedOnHash(examplesDestDir, hashFile, currentHash)) {
44+
console.log('Example plugins are already up to date (git hash matches)');
45+
console.log('Skipping bundle...');
46+
process.exit(0);
47+
}
48+
49+
// Remove existing examples directory if it exists
50+
if (fs.existsSync(examplesDestDir)) {
51+
console.log('Removing existing examples directory...');
52+
fs.rmSync(examplesDestDir, { recursive: true });
53+
}
54+
55+
// Create examples directory
56+
fs.mkdirSync(examplesDestDir, { recursive: true });
57+
58+
// Get list of example plugins
59+
const examplePlugins = fs
60+
.readdirSync(examplesSourceDir, { withFileTypes: true })
61+
.filter(dirent => dirent.isDirectory())
62+
.map(dirent => dirent.name);
63+
64+
console.log(`Found ${examplePlugins.length} example plugins to bundle`);
65+
66+
// Copy each example plugin
67+
examplePlugins.forEach(pluginName => {
68+
const sourcePath = path.join(examplesSourceDir, pluginName);
69+
const destPath = path.join(examplesDestDir, pluginName);
70+
71+
console.log(`Copying ${pluginName}...`);
72+
73+
// Copy the plugin directory
74+
fs.copySync(sourcePath, destPath, {
75+
filter: src => {
76+
// Skip node_modules, dist, and other build artifacts
77+
const relativePath = path.relative(sourcePath, src);
78+
if (relativePath.includes('node_modules')) return false;
79+
if (relativePath.includes('dist')) return false;
80+
if (relativePath.includes('.eslintcache')) return false;
81+
if (relativePath.includes('storybook-static')) return false;
82+
if (relativePath.includes('package-lock.json')) return false;
83+
return true;
84+
},
85+
});
86+
});
87+
88+
// Store the git hash if available
89+
if (currentHash) {
90+
storeHash(hashFile, currentHash);
91+
console.log(`Successfully bundled ${examplePlugins.length} example plugins to examples/`);
92+
console.log(`Git hash: ${currentHash}`);
93+
} else {
94+
console.log(`Successfully bundled ${examplePlugins.length} example plugins to examples/`);
95+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
#!/usr/bin/env node
2+
3+
/*
4+
* Copyright 2025 The Kubernetes Authors
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
/**
20+
* This script fetches official plugins from https://github.com/headlamp-k8s/plugins/
21+
* and bundles them into official-plugins/ directory for inclusion with headlamp-plugin package.
22+
* These plugins are referenced in AGENTS.md and help agents write good plugins.
23+
*/
24+
25+
const fs = require('fs-extra');
26+
const path = require('path');
27+
const { execSync } = require('child_process');
28+
const { getRemoteGitHash, storeHash, shouldSkipBasedOnHash } = require('./git-hash-utils');
29+
30+
const scriptDir = __dirname;
31+
const pluginDir = path.resolve(scriptDir, '..');
32+
const officialPluginsDir = path.resolve(pluginDir, 'official-plugins');
33+
const hashFile = path.resolve(officialPluginsDir, '.git-hash');
34+
const officialPluginsRepo = 'https://github.com/headlamp-k8s/plugins.git';
35+
36+
console.log('Fetching official plugins...');
37+
38+
// Get the current remote hash
39+
const remoteHash = getRemoteGitHash(officialPluginsRepo);
40+
41+
// Check if we can skip fetching
42+
if (shouldSkipBasedOnHash(officialPluginsDir, hashFile, remoteHash)) {
43+
console.log('Official plugins are already up to date (git hash matches)');
44+
console.log('Skipping fetch...');
45+
process.exit(0);
46+
}
47+
48+
console.log('Fetching latest official plugins from GitHub...');
49+
50+
// Remove existing directory if it exists
51+
if (fs.existsSync(officialPluginsDir)) {
52+
console.log('Removing existing official-plugins directory...');
53+
fs.rmSync(officialPluginsDir, { recursive: true });
54+
}
55+
56+
// Create official-plugins directory
57+
fs.mkdirSync(officialPluginsDir, { recursive: true });
58+
59+
// Clone the repository into a temporary directory
60+
const tempDir = path.resolve(pluginDir, '.temp-official-plugins');
61+
if (fs.existsSync(tempDir)) {
62+
fs.rmSync(tempDir, { recursive: true });
63+
}
64+
65+
try {
66+
console.log('Cloning official plugins repository...');
67+
execSync(`git clone --depth 1 ${officialPluginsRepo} ${tempDir}`, {
68+
stdio: 'inherit',
69+
});
70+
71+
// Get the current commit hash
72+
const currentHash = execSync('git rev-parse HEAD', {
73+
cwd: tempDir,
74+
encoding: 'utf8',
75+
}).trim();
76+
77+
// Get list of plugin directories (skip .git and README.md)
78+
const pluginDirs = fs
79+
.readdirSync(tempDir, { withFileTypes: true })
80+
.filter(
81+
dirent =>
82+
dirent.isDirectory() && dirent.name !== '.git' && !dirent.name.startsWith('.')
83+
)
84+
.map(dirent => dirent.name);
85+
86+
console.log(`Found ${pluginDirs.length} official plugins`);
87+
88+
// Copy each plugin directory
89+
pluginDirs.forEach(pluginName => {
90+
const sourcePath = path.join(tempDir, pluginName);
91+
const destPath = path.join(officialPluginsDir, pluginName);
92+
93+
console.log(`Copying ${pluginName}...`);
94+
95+
// Copy the plugin directory
96+
fs.copySync(sourcePath, destPath, {
97+
filter: src => {
98+
// Skip node_modules, dist, and other build artifacts
99+
const relativePath = path.relative(sourcePath, src);
100+
if (relativePath.includes('node_modules')) return false;
101+
if (relativePath.includes('dist')) return false;
102+
if (relativePath.includes('.git')) return false;
103+
if (relativePath.includes('.eslintcache')) return false;
104+
if (relativePath.includes('storybook-static')) return false;
105+
if (relativePath.includes('package-lock.json')) return false;
106+
return true;
107+
},
108+
});
109+
});
110+
111+
// Store the git hash
112+
storeHash(hashFile, currentHash);
113+
114+
console.log(
115+
`Successfully fetched ${pluginDirs.length} official plugins to official-plugins/`
116+
);
117+
console.log(`Git hash: ${currentHash}`);
118+
} catch (error) {
119+
console.error('Failed to fetch official plugins:', error.message);
120+
process.exit(1);
121+
} finally {
122+
// Clean up temporary directory
123+
if (fs.existsSync(tempDir)) {
124+
console.log('Cleaning up temporary directory...');
125+
fs.rmSync(tempDir, { recursive: true });
126+
}
127+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
#!/usr/bin/env node
2+
3+
/*
4+
* Copyright 2025 The Kubernetes Authors
5+
*
6+
* Licensed under the Apache License, Version 2.0 (the "License");
7+
* you may not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing, software
13+
* distributed under the License is distributed on an "AS IS" BASIS,
14+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
* See the License for the specific language governing permissions and
16+
* limitations under the License.
17+
*/
18+
19+
/**
20+
* Shared utilities for git hash tracking used by bundling scripts.
21+
* These hashing functions make development faster by meaning the plugins
22+
* are not copied again and again if they don't change.
23+
*/
24+
25+
const fs = require('fs-extra');
26+
const { execSync } = require('child_process');
27+
28+
/**
29+
* Get the stored hash from a hash file
30+
* @param {string} hashFile - Path to the hash file
31+
* @returns {string|null} - The stored hash or null if file doesn't exist
32+
*/
33+
function getStoredHash(hashFile) {
34+
if (fs.existsSync(hashFile)) {
35+
return fs.readFileSync(hashFile, 'utf8').trim();
36+
}
37+
return null;
38+
}
39+
40+
/**
41+
* Store a hash to a hash file
42+
* @param {string} hashFile - Path to the hash file
43+
* @param {string} hash - The hash to store
44+
*/
45+
function storeHash(hashFile, hash) {
46+
fs.writeFileSync(hashFile, hash);
47+
}
48+
49+
/**
50+
* Get the git hash of a local directory path
51+
* @param {string} dirPath - The directory path relative to git root (e.g., 'plugins/examples')
52+
* @param {string} cwd - The working directory to run git command from
53+
* @returns {string|null} - The git hash or null if git is not available
54+
*/
55+
function getLocalGitHash(dirPath, cwd) {
56+
try {
57+
const output = execSync(`git log -1 --format=%H -- ${dirPath}`, {
58+
cwd: cwd,
59+
encoding: 'utf8',
60+
});
61+
return output.trim();
62+
} catch (error) {
63+
console.log('Git not available or not in a git repository');
64+
return null;
65+
}
66+
}
67+
68+
/**
69+
* Get the remote git hash from a repository
70+
* @param {string} repoUrl - The repository URL
71+
* @returns {string|null} - The remote hash or null if failed
72+
*/
73+
function getRemoteGitHash(repoUrl) {
74+
try {
75+
const output = execSync(`git ls-remote ${repoUrl} HEAD`, {
76+
encoding: 'utf8',
77+
});
78+
return output.split('\t')[0].trim();
79+
} catch (error) {
80+
console.error('Failed to get remote hash:', error.message);
81+
return null;
82+
}
83+
}
84+
85+
/**
86+
* Check if a directory should be skipped based on hash comparison
87+
* @param {string} targetDir - The target directory to check
88+
* @param {string} hashFile - Path to the hash file
89+
* @param {string} currentHash - The current hash to compare against
90+
* @returns {boolean} - True if should skip, false otherwise
91+
*/
92+
function shouldSkipBasedOnHash(targetDir, hashFile, currentHash) {
93+
// Check if directory exists and has content
94+
if (!fs.existsSync(targetDir)) {
95+
return false;
96+
}
97+
98+
const entries = fs.readdirSync(targetDir).filter(e => e !== '.git-hash');
99+
if (entries.length === 0) {
100+
return false;
101+
}
102+
103+
// Check if hashes match
104+
const storedHash = getStoredHash(hashFile);
105+
106+
if (!currentHash || !storedHash) {
107+
return false;
108+
}
109+
110+
return currentHash === storedHash;
111+
}
112+
113+
module.exports = {
114+
getStoredHash,
115+
storeHash,
116+
getLocalGitHash,
117+
getRemoteGitHash,
118+
shouldSkipBasedOnHash,
119+
};

0 commit comments

Comments
 (0)