Skip to content

Commit a80eb73

Browse files
authored
feat(repo): migrate and update archive script from BCP (#2739)
Signed-off-by: Hope Hadfield <hhadfiel@redhat.com>
1 parent 23e5d0c commit a80eb73

3 files changed

Lines changed: 366 additions & 3 deletions

File tree

ARCHIVED_WORKSPACES.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ When a workspace or plugin is archived:
1414

1515
## Archived Items
1616

17-
| Workspace | Package | Reason | Source |
18-
| ------------------------ | ------------------------------------------------------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
19-
| openshift-image-registry | `@red-hat-developer-hub/backstage-plugin-openshift-image-registry` | No longer maintained | [@red-hat-developer-hub/backstage-plugin-openshift-image-registry@1.18.0](https://github.com/redhat-developer/rhdh-plugins/tree/%40red-hat-developer-hub/backstage-plugin-openshift-image-registry%401.18.0/workspaces/openshift-image-registry) |
17+
| Workspace | Package | Reason | Source |
18+
| ------------------------ | ------------------------------------------------------------------ | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
19+
| openshift-image-registry | `@red-hat-developer-hub/backstage-plugin-openshift-image-registry` | No longer maintained | [@red-hat-developer-hub/backstage-plugin-openshift-image-registry@1.18.0](https://github.com/redhat-developer/rhdh-plugins/tree/%40red-hat-developer-hub%2Fbackstage-plugin-openshift-image-registry%401.18.0/workspaces/openshift-image-registry) |

CONTRIBUTING.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ The `redhat-developer/rhdh-plugins` repository is designed as a collaborative sp
3232
- [Dependency Updates](#dependency-updates)
3333
- [Security Fixes](#security-fixes)
3434
- [Opt-in to Knip Reports Check](#opt-in-to-knip-reports-check)
35+
- [Archiving a plugin or workspace](#archiving-a-plugin-or-workspace)
36+
- [When to archive](#when-to-archive)
37+
- [Steps](#steps)
3538

3639
## License
3740

@@ -355,3 +358,48 @@ This repository uses [Renovate](https://docs.renovatebot.com/) to automatically
355358
Plugin owners can opt in to Knip reports check in CI by creating a `bcp.json` file in the root of their workspace (`workspaces/${WORKSPACE}/bcp.json`) and adding `{ "knip-reports": true }`. This ensures that knip reports in your workspace stay up to date.
356359
357360
[Knip](https://knip.dev/) is a tool that helps with clean-up and maintenance by identifying unused dependencies within workspaces. Regularly reviewing and addressing these reports can significantly improve code quality and reduce bloat.
361+
362+
## Archiving a plugin or workspace
363+
364+
When a plugin is no longer maintained, archive it rather than leaving stale code in the default branch. The archive script records the last published version in `.github/archived-plugins.json`, documents it in `ARCHIVED_WORKSPACES.md`, and strips repository configuration that referenced the workspace when you archive an entire workspace. You then remove the workspace or plugin tree in the same PR (or a follow-up). After the PR merges, npm deprecation runs via `.github/workflows/deprecate-archived-plugins.yml`.
365+
366+
### When to archive
367+
368+
Consider archiving when the plugin is unmaintained, has no owner, has unfixable security issues, has been superseded, or cannot be updated for current Backstage versions.
369+
370+
### Steps
371+
372+
1. From the repository root, run the archive script (use a branch based on current `main`):
373+
374+
```bash
375+
# Entire workspace (default reason: "No longer maintained")
376+
node scripts/archive.js <workspace-name>
377+
378+
# Entire workspace with a custom reason
379+
node scripts/archive.js <workspace-name> "Custom reason"
380+
381+
# Single plugin under workspaces/<workspace>/plugins/ (by plugin directory name or package suffix)
382+
node scripts/archive.js <workspace-name> <plugin-dir-or-package-suffix> "Custom reason"
383+
```
384+
385+
The script only considers packages under the `@red-hat-developer-hub` scope. It updates:
386+
- `.github/archived-plugins.json`
387+
- `ARCHIVED_WORKSPACES.md`
388+
- `.github/CODEOWNERS` (full workspace only — removes the `/workspaces/<name>` line)
389+
- `.github/renovate.json` and `.github/renovate-presets/workspace/rhdh-<workspace>-presets.json` (full workspace only, when present)
390+
391+
It does **not** delete source directories; do that in the next step.
392+
393+
2. Dry-run npm deprecation (optional):
394+
395+
```bash
396+
./scripts/ci/deprecate-archived-plugins.sh --dry-run
397+
```
398+
399+
3. Delete the workspace or plugin directory from the repository (`workspaces/<name>` for a full workspace, or `workspaces/<workspace>/plugins/<plugin>` when archiving specific plugins). Update any documentation that referenced the removed code.
400+
401+
4. Run `yarn install` at the repository root if your tooling reports workspace or lockfile issues.
402+
403+
5. Open a pull request including the script output and the deletion. Changes to `.github/archived-plugins.json` are sensitive because they drive automated npm deprecation after merge; ensure the usual repository reviewers (see `.github/CODEOWNERS`) approve the PR.
404+
405+
6. After the PR merges, confirm the **Deprecate Archived Plugins** workflow ran successfully for the updated `.github/archived-plugins.json`.

scripts/archive.js

Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
#!/usr/bin/env node
2+
/*
3+
* Copyright Red Hat, Inc.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
import fs from 'node:fs/promises';
19+
import path from 'node:path';
20+
import { format as prettierFormat } from 'prettier';
21+
import { fileURLToPath } from 'node:url';
22+
import { parseArgs } from 'node:util';
23+
24+
const __filename = fileURLToPath(import.meta.url);
25+
const __dirname = path.dirname(__filename);
26+
27+
const REPO_SLUG = 'redhat-developer/rhdh-plugins';
28+
const NPM_SCOPE = '@red-hat-developer-hub/';
29+
30+
const ROOT = path.join(__dirname, '..');
31+
const ARCHIVED_FILE = path.join(ROOT, '.github', 'archived-plugins.json');
32+
const ARCHIVED_WORKSPACES_FILE = path.join(ROOT, 'ARCHIVED_WORKSPACES.md');
33+
const CODEOWNERS = path.join(ROOT, '.github', 'CODEOWNERS');
34+
const RENOVATE_JSON = path.join(ROOT, '.github', 'renovate.json');
35+
const RENOVATE_PRESETS_DIR = path.join(
36+
ROOT,
37+
'.github',
38+
'renovate-presets',
39+
'workspace',
40+
);
41+
42+
function printUsage() {
43+
console.log(`Usage:
44+
node scripts/archive.js <workspace> [plugin-dir-or-package-suffix] ["reason"]
45+
46+
Examples:
47+
node scripts/archive.js my-workspace
48+
node scripts/archive.js my-workspace "Superseded by another plugin"
49+
node scripts/archive.js my-workspace bulk-import "No longer maintained"
50+
node scripts/archive.js my-workspace backstage-plugin-bulk-import "No longer maintained"
51+
52+
The second argument is treated as a custom reason if it contains spaces.
53+
Otherwise it selects a single plugin under workspaces/<workspace>/plugins/
54+
(by directory name or @red-hat-developer-hub package name suffix).
55+
`);
56+
}
57+
58+
function treeUrlForGitTag(gitTag, workspace) {
59+
const encodedRef = encodeURIComponent(gitTag);
60+
const encodedWorkspace = encodeURIComponent(workspace);
61+
return `https://github.com/${REPO_SLUG}/tree/${encodedRef}/workspaces/${encodedWorkspace}`;
62+
}
63+
64+
function matchesPluginTarget(pluginDir, packageName, target) {
65+
if (!target) {
66+
return true;
67+
}
68+
if (!packageName.startsWith(NPM_SCOPE)) {
69+
return false;
70+
}
71+
const suffix = packageName.slice(NPM_SCOPE.length);
72+
if (pluginDir === target) {
73+
return true;
74+
}
75+
if (suffix === target) {
76+
return true;
77+
}
78+
if (suffix === `backstage-plugin-${target}`) {
79+
return true;
80+
}
81+
if (
82+
suffix.startsWith('backstage-plugin-') &&
83+
suffix.slice('backstage-plugin-'.length) === target
84+
) {
85+
return true;
86+
}
87+
return false;
88+
}
89+
90+
async function appendToArchivedWorkspacesMd(entries) {
91+
console.log('Updating ARCHIVED_WORKSPACES.md...');
92+
93+
const tableRows = entries.map(entry => {
94+
const { workspace, pluginName, reason, gitTag } = entry;
95+
const pkg = `\`${pluginName}\``;
96+
const why = reason || 'No longer maintained';
97+
const sourceLink = `[${gitTag}](${treeUrlForGitTag(gitTag, workspace)})`;
98+
return `| ${workspace} | ${pkg} | ${why} | ${sourceLink} |`;
99+
});
100+
101+
const block = `${tableRows.join('\n')}\n`;
102+
await fs.appendFile(ARCHIVED_WORKSPACES_FILE, block);
103+
104+
console.log(`Added ${entries.length} row(s) to ARCHIVED_WORKSPACES.md`);
105+
}
106+
107+
async function removeWorkspaceFromCodeowners(workspace) {
108+
console.log(`Removing /workspaces/${workspace} from CODEOWNERS...`);
109+
const content = await fs.readFile(CODEOWNERS, 'utf8');
110+
const lines = content.split('\n');
111+
const specialChars = new RegExp(String.raw`[.*+?^\${}()|[\]\\]`, 'g');
112+
const escaped = workspace.replaceAll(specialChars, '\\$&');
113+
const workspacePattern = new RegExp(String.raw`^/workspaces/${escaped}\s`);
114+
const filteredLines = lines.filter(line => !workspacePattern.test(line));
115+
await fs.writeFile(CODEOWNERS, filteredLines.join('\n'));
116+
console.log('Updated CODEOWNERS');
117+
}
118+
119+
async function removeWorkspaceFromRenovateJson(workspace) {
120+
const presetRef = `github>${REPO_SLUG}//.github/renovate-presets/workspace/rhdh-${workspace}-presets`;
121+
const content = await fs.readFile(RENOVATE_JSON, 'utf8');
122+
const data = JSON.parse(content);
123+
const rule = data.packageRules?.find(
124+
r =>
125+
r.description === 'all RHDH Plugins workspaces' &&
126+
Array.isArray(r.extends),
127+
);
128+
if (!rule) {
129+
console.log(
130+
'Could not find "all RHDH Plugins workspaces" packageRules entry in renovate.json (skipping).',
131+
);
132+
return;
133+
}
134+
const beforeLen = rule.extends.length;
135+
rule.extends = rule.extends.filter(entry => entry !== presetRef);
136+
if (rule.extends.length === beforeLen) {
137+
console.log(
138+
`No Renovate preset reference found for workspace "${workspace}" (skipping renovate.json).`,
139+
);
140+
return;
141+
}
142+
const raw = JSON.stringify(data, null, 2);
143+
const formatted = await prettierFormat(raw, {
144+
filepath: RENOVATE_JSON,
145+
parser: 'json',
146+
});
147+
await fs.writeFile(
148+
RENOVATE_JSON,
149+
formatted.endsWith('\n') ? formatted : `${formatted}\n`,
150+
'utf8',
151+
);
152+
console.log('Updated renovate.json');
153+
}
154+
155+
async function removeRenovatePresetFile(workspace) {
156+
const presetPath = path.join(
157+
RENOVATE_PRESETS_DIR,
158+
`rhdh-${workspace}-presets.json`,
159+
);
160+
try {
161+
await fs.unlink(presetPath);
162+
console.log(`Removed ${path.relative(ROOT, presetPath)}`);
163+
} catch (e) {
164+
if (e.code !== 'ENOENT') {
165+
throw e;
166+
}
167+
console.log(
168+
`No preset file at ${path.relative(ROOT, presetPath)} (skipped).`,
169+
);
170+
}
171+
}
172+
173+
async function getPackagesFromWorkspace(workspace, targetPlugin = null) {
174+
const plugins = [];
175+
const pluginsDir = path.join(ROOT, 'workspaces', workspace, 'plugins');
176+
177+
let pluginDirs;
178+
try {
179+
pluginDirs = await fs.readdir(pluginsDir);
180+
} catch (e) {
181+
if (e.code === 'ENOENT') {
182+
return plugins;
183+
}
184+
throw e;
185+
}
186+
187+
for (const pluginDir of pluginDirs) {
188+
const pluginPath = path.join(pluginsDir, pluginDir);
189+
const stat = await fs.stat(pluginPath);
190+
191+
if (!stat.isDirectory()) {
192+
continue;
193+
}
194+
195+
const packageJsonPath = path.join(pluginPath, 'package.json');
196+
let packageData;
197+
try {
198+
packageData = JSON.parse(await fs.readFile(packageJsonPath, 'utf8'));
199+
} catch {
200+
continue;
201+
}
202+
203+
if (!packageData.name?.startsWith(NPM_SCOPE)) {
204+
continue;
205+
}
206+
207+
if (!matchesPluginTarget(pluginDir, packageData.name, targetPlugin)) {
208+
continue;
209+
}
210+
211+
plugins.push({
212+
name: packageData.name,
213+
version: packageData.version,
214+
workspace,
215+
plugin: pluginDir,
216+
});
217+
}
218+
219+
return plugins;
220+
}
221+
222+
async function addArchivedEntry(entries) {
223+
const content = await fs.readFile(ARCHIVED_FILE, 'utf8');
224+
const archivedData = JSON.parse(content);
225+
226+
for (const entry of entries) {
227+
console.log(`Adding archived entry for ${entry.pluginName}`);
228+
archivedData.archived.push(entry);
229+
}
230+
231+
await fs.writeFile(ARCHIVED_FILE, JSON.stringify(archivedData, null, 2));
232+
console.log(`Updated ${path.relative(ROOT, ARCHIVED_FILE)}`);
233+
}
234+
235+
async function main() {
236+
const { values, positionals } = parseArgs({
237+
args: process.argv.slice(2),
238+
options: {
239+
help: { type: 'boolean', short: 'h' },
240+
},
241+
allowPositionals: true,
242+
});
243+
244+
if (values.help) {
245+
printUsage();
246+
return;
247+
}
248+
249+
const workspace = positionals[0];
250+
if (!workspace) {
251+
printUsage();
252+
process.exit(1);
253+
}
254+
255+
let plugin = positionals[1];
256+
let reason = positionals[2] || 'No longer maintained';
257+
258+
if (plugin && plugin.includes(' ')) {
259+
reason = plugin;
260+
plugin = null;
261+
}
262+
263+
console.log(`Archiving in workspace ${workspace}...`);
264+
console.log(`Reason: ${reason}`);
265+
266+
const packages = await getPackagesFromWorkspace(workspace, plugin);
267+
268+
if (packages.length === 0) {
269+
console.log(
270+
'No matching @red-hat-developer-hub packages found to archive.',
271+
);
272+
process.exit(1);
273+
}
274+
275+
const fullWorkspace = !plugin;
276+
277+
const entries = packages.map(pkg => ({
278+
pluginName: pkg.name,
279+
version: pkg.version,
280+
workspace: pkg.workspace,
281+
plugin: pkg.plugin,
282+
gitTag: `${pkg.name}@${pkg.version}`,
283+
reason,
284+
archivedDate: new Date().toISOString().split('T')[0],
285+
}));
286+
287+
await addArchivedEntry(entries);
288+
await appendToArchivedWorkspacesMd(entries);
289+
290+
if (fullWorkspace) {
291+
console.log(
292+
'\nFull workspace archival: updating CODEOWNERS and Renovate configuration...',
293+
);
294+
await removeWorkspaceFromCodeowners(workspace);
295+
await removeRenovatePresetFile(workspace);
296+
await removeWorkspaceFromRenovateJson(workspace);
297+
}
298+
299+
console.log(
300+
`\nSuccessfully recorded ${entries.length} package(s) as archived:`,
301+
);
302+
entries.forEach(entry => {
303+
console.log(
304+
` - ${entry.pluginName} (${entry.workspace}/${entry.plugin}) — tag ${entry.gitTag}`,
305+
);
306+
});
307+
console.log(
308+
'\nNext: delete the workspace or plugin directory from the repository (and update any related documentation), then open a PR. After merge, confirm npm deprecation via the Deprecate Archived Plugins workflow.',
309+
);
310+
}
311+
312+
main().catch(error => {
313+
console.error('Error:', error.message);
314+
process.exit(1);
315+
});

0 commit comments

Comments
 (0)