@multipliers-dev/renovate-workflow 0.3.0 → 0.3.1
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/README.md +7 -18
- package/package.json +4 -13
- package/scripts/lib/agent-plugins-spec.ts +0 -150
- package/scripts/lib/bootstrap-tarball.ts +0 -230
- package/scripts/lib/derive-stop-causes.ts +0 -121
- package/scripts/lib/renovate-freshness-poll.ts +0 -624
- package/scripts/lib/renovate-guardrails.ts +0 -636
- package/scripts/lib/renovate-investigation-eligibility.ts +0 -65
- package/scripts/lib/renovate-policy-facts.ts +0 -189
- package/scripts/renovate-freshness-poll.ts +0 -210
package/README.md
CHANGED
|
@@ -67,22 +67,19 @@ Success is `queue_empty` or a classification packet. The classifier **never merg
|
|
|
67
67
|
|
|
68
68
|
Required for `/renovate-loop --babysit` and the freshness poll CLI. Not required to try the classifier.
|
|
69
69
|
|
|
70
|
-
Add
|
|
70
|
+
Add the npm devDependency and script in your consumer `package.json`:
|
|
71
71
|
|
|
72
72
|
```json
|
|
73
73
|
{
|
|
74
74
|
"devDependencies": {
|
|
75
|
-
"
|
|
76
|
-
"tsx": "^4.23.12"
|
|
75
|
+
"@multipliers-dev/renovate-workflow": "^0.3.0"
|
|
77
76
|
},
|
|
78
77
|
"scripts": {
|
|
79
|
-
"renovate:freshness-poll": "
|
|
78
|
+
"renovate:freshness-poll": "renovate-workflow freshness-poll"
|
|
80
79
|
}
|
|
81
80
|
}
|
|
82
81
|
```
|
|
83
82
|
|
|
84
|
-
The `scripts/` tree in the installable package is a **temporary legacy surface** for git consumers running `tsx` on TypeScript sources. The stable npm API is the compiled `renovate-workflow` CLI (`renovate-workflow freshness-poll`); see [`.cursor/plans/2026-10-07-npm-package-distribution.plan.md`](.cursor/plans/2026-10-07-npm-package-distribution.plan.md).
|
|
85
|
-
|
|
86
83
|
See [examples/adopt-stub/package.json](examples/adopt-stub/package.json) and [docs/adopt.md](docs/adopt.md).
|
|
87
84
|
|
|
88
85
|
---
|
|
@@ -117,22 +114,14 @@ Also:
|
|
|
117
114
|
|
|
118
115
|
## Publishing (maintainers)
|
|
119
116
|
|
|
120
|
-
This repository uses a **two-step release**
|
|
117
|
+
This repository uses a **two-step release**. **Merging to `main` never publishes.**
|
|
121
118
|
|
|
122
119
|
| Step | Who | What |
|
|
123
120
|
| --- | --- | --- |
|
|
124
|
-
| **A — Version bump** | Reviewed PR | Bump aligned manifests (`package.json`, `plugin.json`, `.cursor-plugin/plugin.json`)
|
|
125
|
-
| **B — Publish** | Maintainer |
|
|
126
|
-
|
|
127
|
-
**Before first release:**
|
|
128
|
-
|
|
129
|
-
1. `version-bump-0.3.0` merged on `main` (scoped name, `0.3.0`, not private).
|
|
130
|
-
2. **npm CLI preflight** — `sh scripts/npm-stage-cli-preflight.sh` (staged publishing requires npm **≥ 11.15.0**; Node version alone does not guarantee `npm stage` exists).
|
|
131
|
-
3. **One-time registry bootstrap** — validate `0.3.0` in the checkout (`npm ci`, `npm test`, `npm run typecheck`, `npm run build`, `npm pack`), then `npm run bootstrap:tarball` to build/preflight a **disposable `0.0.1` tarball** (npm pack in an isolated temp dir — not manual `tar -czf`; canonical checkout stays at `0.3.0`), then `npm stage publish` that tarball. **Do not** stage `0.3.0` and leave it pending. See [docs/versioning.md § Trusted Publishing bootstrap](docs/versioning.md#trusted-publishing-bootstrap).
|
|
132
|
-
4. Configure npm **Trusted Publisher** for workflow `release.yml` (`multipliers-dev/renovate-workflow`). See [docs/versioning.md § npm Trusted Publisher settings](docs/versioning.md#npm-trusted-publisher-settings).
|
|
133
|
-
5. Dispatch **Release** within **48 hours** of creating the Trusted Publisher configuration; after successful OIDC `0.3.0`, reject the pending bootstrap stage with `npm stage reject <stage-id>` (2FA).
|
|
121
|
+
| **A — Version bump** | Reviewed PR | Bump aligned manifests (`package.json`, `plugin.json`, `.cursor-plugin/plugin.json`) to the same `X.Y.Z`. |
|
|
122
|
+
| **B — Publish** | Maintainer | On `main`, manually dispatch [`.github/workflows/release.yml`](.github/workflows/release.yml) with confirmation `publish X.Y.Z`. |
|
|
134
123
|
|
|
135
|
-
Publishing uses **
|
|
124
|
+
Publishing uses npm **Trusted Publishing** (GitHub Actions OIDC). **Do not** create or store an `NPM_TOKEN` / `NODE_AUTH_TOKEN` for releases. Provenance is automatic for this public package from this public repo.
|
|
136
125
|
|
|
137
126
|
The release workflow reads **name and version from the checked-out `package.json` only** (`npm publish --access public` with no CLI coordinate override). It does not bump versions or commit to `main`. npm and plugin always share version *V* at the same commit. Only one release dispatch runs at a time (`concurrency: release`).
|
|
138
127
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@multipliers-dev/renovate-workflow",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -15,20 +15,11 @@
|
|
|
15
15
|
"renovate-workflow": "./dist/cli.js"
|
|
16
16
|
},
|
|
17
17
|
"engines": {
|
|
18
|
-
"node": ">=22.
|
|
18
|
+
"node": ">=22.23.3"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"dist",
|
|
22
|
-
"README.md"
|
|
23
|
-
"scripts/renovate-freshness-poll.ts",
|
|
24
|
-
"scripts/lib",
|
|
25
|
-
"!scripts/**/*.test.ts",
|
|
26
|
-
"!scripts/fixtures",
|
|
27
|
-
"!scripts/cli.ts",
|
|
28
|
-
"!scripts/check-agent-plugins-spec-drift.ts",
|
|
29
|
-
"!scripts/tsconfig.json",
|
|
30
|
-
"!scripts/tsconfig.build.json",
|
|
31
|
-
"!scripts/*.sh"
|
|
22
|
+
"README.md"
|
|
32
23
|
],
|
|
33
24
|
"scripts": {
|
|
34
25
|
"dev": "tsx watch src/index.ts",
|
|
@@ -51,6 +42,6 @@
|
|
|
51
42
|
"husky": "^9.1.7",
|
|
52
43
|
"tsx": "^4.23.15",
|
|
53
44
|
"typescript": "^7.0.2",
|
|
54
|
-
"vitest": "^5.0.
|
|
45
|
+
"vitest": "^5.0.5"
|
|
55
46
|
}
|
|
56
47
|
}
|
|
@@ -1,150 +0,0 @@
|
|
|
1
|
-
export const AGENT_PLUGINS_SCHEMA_BASE = "https://agent-plugins.org/schemas";
|
|
2
|
-
export const PUBLISHED_SPEC_MD_URL = "https://agent-plugins.org/specification.md";
|
|
3
|
-
|
|
4
|
-
const DECLARED_SCHEMA_PATTERN =
|
|
5
|
-
/^https:\/\/agent-plugins\.org\/schemas\/(\d+\.\d+\.\d+)\/plugin\.schema\.json$/;
|
|
6
|
-
|
|
7
|
-
const PUBLISHED_SPEC_VERSION_PATTERN = /^\*\*Spec Version:\s*(\d+\.\d+\.\d+)\s*\*\*$/m;
|
|
8
|
-
const PUBLISHED_STATUS_PATTERN = /^\*\*Status:\s*Published\s*\*\*$/m;
|
|
9
|
-
|
|
10
|
-
export type SpecVersionRelationship = "behind" | "current" | "ahead";
|
|
11
|
-
|
|
12
|
-
export type SpecRelationshipEvaluation = {
|
|
13
|
-
declaredVersion: string;
|
|
14
|
-
latestPublishedVersion: string;
|
|
15
|
-
relationship: SpecVersionRelationship;
|
|
16
|
-
driftDetected: boolean;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
export type PublishedSignalConfirmation = {
|
|
20
|
-
markdownVersion: string;
|
|
21
|
-
schemaVersion: string;
|
|
22
|
-
schemaHttpStatus: number;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
export function buildPublishedSchemaUrl(version: string): string {
|
|
26
|
-
return `${AGENT_PLUGINS_SCHEMA_BASE}/${version}/plugin.schema.json`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function parseAgentPluginsSchemaUrl(schemaUrl: string): string {
|
|
30
|
-
const match = DECLARED_SCHEMA_PATTERN.exec(schemaUrl);
|
|
31
|
-
if (!match) {
|
|
32
|
-
throw new Error(
|
|
33
|
-
`malformed Agent Plugins schema URL: expected https://agent-plugins.org/schemas/X.Y.Z/plugin.schema.json`,
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
return match[1]!;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function parseDeclaredSpecVersion($schema: string): string {
|
|
40
|
-
try {
|
|
41
|
-
return parseAgentPluginsSchemaUrl($schema);
|
|
42
|
-
} catch {
|
|
43
|
-
throw new Error(
|
|
44
|
-
`malformed $schema URL: expected https://agent-plugins.org/schemas/X.Y.Z/plugin.schema.json`,
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function parsePublishedSchemaDocumentVersion(schemaDocument: string): string {
|
|
50
|
-
let parsed: unknown;
|
|
51
|
-
try {
|
|
52
|
-
parsed = JSON.parse(schemaDocument) as unknown;
|
|
53
|
-
} catch {
|
|
54
|
-
throw new Error("published schema document is not valid JSON");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (typeof parsed !== "object" || parsed === null || !("$id" in parsed)) {
|
|
58
|
-
throw new Error("published schema document is missing $id");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const schemaId = (parsed as { $id: unknown }).$id;
|
|
62
|
-
if (typeof schemaId !== "string") {
|
|
63
|
-
throw new Error("published schema document has invalid $id");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return parseAgentPluginsSchemaUrl(schemaId);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function parsePublishedSpecVersion(specMarkdown: string): string {
|
|
70
|
-
if (!PUBLISHED_STATUS_PATTERN.test(specMarkdown)) {
|
|
71
|
-
throw new Error("upstream specification.md is missing Status: Published");
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const versionMatch = PUBLISHED_SPEC_VERSION_PATTERN.exec(specMarkdown);
|
|
75
|
-
if (!versionMatch) {
|
|
76
|
-
throw new Error("upstream specification.md is missing Spec Version");
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return versionMatch[1]!;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function compareSemver(a: string, b: string): -1 | 0 | 1 {
|
|
83
|
-
const aParts = a.split(".").map((part) => Number(part));
|
|
84
|
-
const bParts = b.split(".").map((part) => Number(part));
|
|
85
|
-
|
|
86
|
-
if (aParts.length !== 3 || bParts.length !== 3) {
|
|
87
|
-
throw new Error(`invalid semver: ${a} or ${b}`);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
for (let index = 0; index < 3; index += 1) {
|
|
91
|
-
const aPart = aParts[index]!;
|
|
92
|
-
const bPart = bParts[index]!;
|
|
93
|
-
|
|
94
|
-
if (!Number.isInteger(aPart) || !Number.isInteger(bPart)) {
|
|
95
|
-
throw new Error(`invalid semver: ${a} or ${b}`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (aPart < bPart) {
|
|
99
|
-
return -1;
|
|
100
|
-
}
|
|
101
|
-
if (aPart > bPart) {
|
|
102
|
-
return 1;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return 0;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function confirmPublishedUpstreamVersion(
|
|
110
|
-
signals: PublishedSignalConfirmation,
|
|
111
|
-
): string {
|
|
112
|
-
const { markdownVersion, schemaVersion, schemaHttpStatus } = signals;
|
|
113
|
-
|
|
114
|
-
if (schemaHttpStatus !== 200) {
|
|
115
|
-
throw new Error(
|
|
116
|
-
`published schema URL returned HTTP ${schemaHttpStatus} for version ${schemaVersion}`,
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (markdownVersion !== schemaVersion) {
|
|
121
|
-
throw new Error(
|
|
122
|
-
`published signal mismatch: specification.md reports ${markdownVersion} but published schema document $id reports ${schemaVersion}`,
|
|
123
|
-
);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return markdownVersion;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export function evaluateSpecRelationship(options: {
|
|
130
|
-
declared: string;
|
|
131
|
-
published: string;
|
|
132
|
-
}): SpecRelationshipEvaluation {
|
|
133
|
-
const comparison = compareSemver(options.declared, options.published);
|
|
134
|
-
|
|
135
|
-
let relationship: SpecVersionRelationship;
|
|
136
|
-
if (comparison < 0) {
|
|
137
|
-
relationship = "behind";
|
|
138
|
-
} else if (comparison > 0) {
|
|
139
|
-
relationship = "ahead";
|
|
140
|
-
} else {
|
|
141
|
-
relationship = "current";
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
return {
|
|
145
|
-
declaredVersion: options.declared,
|
|
146
|
-
latestPublishedVersion: options.published,
|
|
147
|
-
relationship,
|
|
148
|
-
driftDetected: relationship === "behind",
|
|
149
|
-
};
|
|
150
|
-
}
|
|
@@ -1,230 +0,0 @@
|
|
|
1
|
-
import { execSync } from "node:child_process";
|
|
2
|
-
import {
|
|
3
|
-
copyFileSync,
|
|
4
|
-
cpSync,
|
|
5
|
-
existsSync,
|
|
6
|
-
mkdirSync,
|
|
7
|
-
mkdtempSync,
|
|
8
|
-
readFileSync,
|
|
9
|
-
rmSync,
|
|
10
|
-
writeFileSync,
|
|
11
|
-
} from "node:fs";
|
|
12
|
-
import { tmpdir } from "node:os";
|
|
13
|
-
import { basename, dirname, join, resolve } from "node:path";
|
|
14
|
-
|
|
15
|
-
export const BOOTSTRAP_VERSION = "0.0.1";
|
|
16
|
-
|
|
17
|
-
const FORBIDDEN_ENTRY_PATTERNS: Array<{ label: string; test: (entry: string) => boolean }> = [
|
|
18
|
-
{
|
|
19
|
-
label: "directory entry package/ (trailing slash)",
|
|
20
|
-
test: (entry) => entry === "package/" || entry === "package",
|
|
21
|
-
},
|
|
22
|
-
{
|
|
23
|
-
label: "AppleDouble (._*)",
|
|
24
|
-
test: (entry) => /(^|\/)\._[^/]+/.test(entry),
|
|
25
|
-
},
|
|
26
|
-
{
|
|
27
|
-
label: ".git",
|
|
28
|
-
test: (entry) => entry.includes(".git"),
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
label: "test files",
|
|
32
|
-
test: (entry) => entry.endsWith(".test.ts") || entry.endsWith(".test.js"),
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
label: "fixtures",
|
|
36
|
-
test: (entry) => entry.includes("/fixtures/"),
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
label: "credentials",
|
|
40
|
-
test: (entry) =>
|
|
41
|
-
/(^|\/)\.env($|\.)/.test(entry) ||
|
|
42
|
-
entry.includes("credentials.json") ||
|
|
43
|
-
entry.includes("id_rsa"),
|
|
44
|
-
},
|
|
45
|
-
];
|
|
46
|
-
|
|
47
|
-
export function listTarballEntries(tarballPath: string): string[] {
|
|
48
|
-
const listing = execSync(`tar -tzf "${tarballPath}"`, { encoding: "utf8" });
|
|
49
|
-
return listing
|
|
50
|
-
.trim()
|
|
51
|
-
.split("\n")
|
|
52
|
-
.filter(Boolean);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function readPackedManifest(tarballPath: string): { name: string; version: string } {
|
|
56
|
-
const raw = execSync(`tar -xOf "${tarballPath}" package/package.json`, { encoding: "utf8" });
|
|
57
|
-
return JSON.parse(raw) as { name: string; version: string };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export function parsePackTarballName(packOutput: string): string {
|
|
61
|
-
const tarballName = packOutput
|
|
62
|
-
.split(/\r?\n/)
|
|
63
|
-
.map((line) => line.trim())
|
|
64
|
-
.filter((line) => line.endsWith(".tgz"))
|
|
65
|
-
.at(-1);
|
|
66
|
-
if (!tarballName) {
|
|
67
|
-
throw new Error(`npm pack did not report a .tgz filename:\n${packOutput}`);
|
|
68
|
-
}
|
|
69
|
-
return tarballName;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
export function assertBootstrapTarballEntries(entries: string[]): void {
|
|
73
|
-
const violations: string[] = [];
|
|
74
|
-
|
|
75
|
-
for (const entry of entries) {
|
|
76
|
-
for (const pattern of FORBIDDEN_ENTRY_PATTERNS) {
|
|
77
|
-
if (pattern.test(entry)) {
|
|
78
|
-
violations.push(`${entry} (${pattern.label})`);
|
|
79
|
-
break;
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (violations.length > 0) {
|
|
85
|
-
throw new Error(
|
|
86
|
-
`bootstrap tarball failed preflight:\n${violations.map((v) => ` - ${v}`).join("\n")}`
|
|
87
|
-
);
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
const normalized = entries.map((entry) => entry.replace(/^package\//, ""));
|
|
91
|
-
if (!normalized.some((entry) => entry.startsWith("dist/"))) {
|
|
92
|
-
throw new Error("bootstrap tarball missing dist/ publish surface");
|
|
93
|
-
}
|
|
94
|
-
if (!normalized.includes("package.json")) {
|
|
95
|
-
throw new Error("bootstrap tarball missing package/package.json");
|
|
96
|
-
}
|
|
97
|
-
if (!normalized.includes("README.md")) {
|
|
98
|
-
throw new Error("bootstrap tarball missing package/README.md");
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
export type BootstrapTarballResult = {
|
|
103
|
-
tarballPath: string;
|
|
104
|
-
entries: string[];
|
|
105
|
-
manifest: { name: string; version: string };
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
/** Lifecycle hooks that reference dev-only paths excluded from the packed `files` list. */
|
|
109
|
-
const ISOLATED_PACK_SCRIPT_KEYS = ["prepare", "prepack"] as const;
|
|
110
|
-
|
|
111
|
-
function stripIsolatedPackLifecycleScripts(packageDir: string): void {
|
|
112
|
-
const packageJsonPath = join(packageDir, "package.json");
|
|
113
|
-
const manifest = JSON.parse(readFileSync(packageJsonPath, "utf8")) as {
|
|
114
|
-
scripts?: Record<string, string>;
|
|
115
|
-
};
|
|
116
|
-
if (!manifest.scripts) {
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
for (const key of ISOLATED_PACK_SCRIPT_KEYS) {
|
|
120
|
-
delete manifest.scripts[key];
|
|
121
|
-
}
|
|
122
|
-
writeFileSync(packageJsonPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function packDirectoryToDestination(packageDir: string, packDestination: string): string {
|
|
126
|
-
mkdirSync(packDestination, { recursive: true });
|
|
127
|
-
const packOutput = execSync(
|
|
128
|
-
`npm pack --ignore-scripts --loglevel error --pack-destination "${packDestination}"`,
|
|
129
|
-
{
|
|
130
|
-
cwd: packageDir,
|
|
131
|
-
encoding: "utf8",
|
|
132
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
133
|
-
}
|
|
134
|
-
);
|
|
135
|
-
const packedName = parsePackTarballName(packOutput);
|
|
136
|
-
return join(packDestination, packedName);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Build a disposable bootstrap tarball from an already-validated `npm pack` artifact.
|
|
141
|
-
*
|
|
142
|
-
* Uses `npm pack --ignore-scripts` in an isolated temp directory so the tarball
|
|
143
|
-
* matches libnpmpack layout (file entries only under package/..., no bare package/
|
|
144
|
-
* directory entry). Manual `tar -czf` is rejected by the npm registry (E415 invalid
|
|
145
|
-
* path: package/) because BSD tar emits a trailing-slash directory entry.
|
|
146
|
-
*/
|
|
147
|
-
export function createBootstrapTarballFromValidatedPack(
|
|
148
|
-
validatedTarballPath: string,
|
|
149
|
-
options: { outputDir?: string; keepWorkdirs?: boolean } = {}
|
|
150
|
-
): BootstrapTarballResult {
|
|
151
|
-
const absoluteValidated = resolve(validatedTarballPath);
|
|
152
|
-
if (!existsSync(absoluteValidated)) {
|
|
153
|
-
throw new Error(`validated pack tarball not found: ${absoluteValidated}`);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const workRoot = mkdtempSync(join(tmpdir(), "renovate-workflow-bootstrap-work-"));
|
|
157
|
-
const extractDir = join(workRoot, "extract");
|
|
158
|
-
const isolatedPackageDir = join(workRoot, "isolated-package");
|
|
159
|
-
// Default: durable artifact dir outside workRoot so the returned .tgz survives work cleanup.
|
|
160
|
-
const artifactDir = options.outputDir
|
|
161
|
-
? resolve(options.outputDir)
|
|
162
|
-
: mkdtempSync(join(tmpdir(), "renovate-workflow-bootstrap-artifact-"));
|
|
163
|
-
|
|
164
|
-
try {
|
|
165
|
-
mkdirSync(extractDir, { recursive: true });
|
|
166
|
-
cpSync(absoluteValidated, join(workRoot, "validated.tgz"));
|
|
167
|
-
execSync(`tar -xzf "${join(workRoot, "validated.tgz")}" -C "${extractDir}"`, {
|
|
168
|
-
stdio: "pipe",
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
const extractedPackageDir = join(extractDir, "package");
|
|
172
|
-
if (!existsSync(join(extractedPackageDir, "package.json"))) {
|
|
173
|
-
throw new Error("validated pack tarball missing package/package.json");
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
cpSync(extractedPackageDir, isolatedPackageDir, { recursive: true });
|
|
177
|
-
execSync(`npm pkg set version=${BOOTSTRAP_VERSION}`, {
|
|
178
|
-
cwd: isolatedPackageDir,
|
|
179
|
-
stdio: "pipe",
|
|
180
|
-
});
|
|
181
|
-
// Packed artifact omits scripts/*.sh; npm pack may still run `prepare` unless removed.
|
|
182
|
-
stripIsolatedPackLifecycleScripts(isolatedPackageDir);
|
|
183
|
-
|
|
184
|
-
const packDir = join(workRoot, "packed");
|
|
185
|
-
const packedPath = packDirectoryToDestination(isolatedPackageDir, packDir);
|
|
186
|
-
const packedName = basename(packedPath);
|
|
187
|
-
const finalPath = join(artifactDir, packedName);
|
|
188
|
-
|
|
189
|
-
copyFileSync(packedPath, finalPath);
|
|
190
|
-
|
|
191
|
-
const entries = listTarballEntries(finalPath);
|
|
192
|
-
assertBootstrapTarballEntries(entries);
|
|
193
|
-
const manifest = readPackedManifest(finalPath);
|
|
194
|
-
|
|
195
|
-
if (manifest.version !== BOOTSTRAP_VERSION) {
|
|
196
|
-
throw new Error(
|
|
197
|
-
`bootstrap tarball version must be ${BOOTSTRAP_VERSION}, got ${manifest.version}`
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return { tarballPath: finalPath, entries, manifest };
|
|
202
|
-
} finally {
|
|
203
|
-
if (!options.keepWorkdirs) {
|
|
204
|
-
rmSync(workRoot, { recursive: true, force: true });
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
export function packValidatedReleaseArtifact(repoRoot: string): string {
|
|
210
|
-
const packDir = mkdtempSync(join(tmpdir(), "renovate-workflow-validated-pack-"));
|
|
211
|
-
return packDirectoryToDestination(repoRoot, packDir);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export function removePackedArtifactTree(tarballPath: string): void {
|
|
215
|
-
rmSync(dirname(tarballPath), { recursive: true, force: true });
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
/** Remove the temp directory that owns a default-path bootstrap tarball. */
|
|
219
|
-
export function removeBootstrapArtifactTree(tarballPath: string): void {
|
|
220
|
-
rmSync(dirname(tarballPath), { recursive: true, force: true });
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
export function assertCheckoutVersion(repoRoot: string, expectedVersion: string): void {
|
|
224
|
-
const manifest = JSON.parse(
|
|
225
|
-
readFileSync(join(repoRoot, "package.json"), "utf8")
|
|
226
|
-
) as { version: string };
|
|
227
|
-
if (manifest.version !== expectedVersion) {
|
|
228
|
-
throw new Error(`Expected package version ${expectedVersion}, got ${manifest.version}`);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Single derivation table for classifier emission and guardrails evaluation.
|
|
3
|
-
* Keys must match the reference table in the renovate investigation lane plan.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
export const TRIGGER_WATCH_KEYS = [
|
|
7
|
-
"implementation_changes_required",
|
|
8
|
-
"lockfile_threshold_exceeded",
|
|
9
|
-
"ci_failure_unexplained",
|
|
10
|
-
"runtime_behavior_affected",
|
|
11
|
-
] as const;
|
|
12
|
-
|
|
13
|
-
export type TriggerWatchKey = (typeof TRIGGER_WATCH_KEYS)[number];
|
|
14
|
-
|
|
15
|
-
export const KNOWN_STOP_CAUSES = [
|
|
16
|
-
"decision_human_required",
|
|
17
|
-
"stop_flag_expected_human_required",
|
|
18
|
-
"triggered_implementation_changes_required",
|
|
19
|
-
"triggered_lockfile_threshold_exceeded",
|
|
20
|
-
"triggered_ci_failure_unexplained",
|
|
21
|
-
"triggered_runtime_behavior_affected",
|
|
22
|
-
"triggered_runtime_behavior_affected_sole",
|
|
23
|
-
"triggered_human_required_unmapped",
|
|
24
|
-
"decision_defer",
|
|
25
|
-
] as const;
|
|
26
|
-
|
|
27
|
-
export type StopCauseKey = (typeof KNOWN_STOP_CAUSES)[number];
|
|
28
|
-
|
|
29
|
-
const KNOWN_STOP_CAUSE_SET = new Set<string>(KNOWN_STOP_CAUSES);
|
|
30
|
-
const TRIGGER_WATCH_KEY_SET = new Set<string>(TRIGGER_WATCH_KEYS);
|
|
31
|
-
|
|
32
|
-
export type DeriveStopCausesInput = {
|
|
33
|
-
stop?: boolean;
|
|
34
|
-
classification?: {
|
|
35
|
-
decision?: string;
|
|
36
|
-
};
|
|
37
|
-
triggered_human_required?: string[];
|
|
38
|
-
stop_causes?: string[];
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
export function isKnownStopCause(value: string): value is StopCauseKey {
|
|
42
|
-
return KNOWN_STOP_CAUSE_SET.has(value);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function deriveTriggerStopCauses(triggered: string[]): StopCauseKey[] {
|
|
46
|
-
const causes: StopCauseKey[] = [];
|
|
47
|
-
|
|
48
|
-
if (triggered.some((key) => !TRIGGER_WATCH_KEY_SET.has(key))) {
|
|
49
|
-
causes.push("triggered_human_required_unmapped");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
if (triggered.includes("implementation_changes_required")) {
|
|
53
|
-
causes.push("triggered_implementation_changes_required");
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (triggered.includes("lockfile_threshold_exceeded")) {
|
|
57
|
-
causes.push("triggered_lockfile_threshold_exceeded");
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
if (triggered.includes("ci_failure_unexplained")) {
|
|
61
|
-
causes.push("triggered_ci_failure_unexplained");
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (triggered.includes("runtime_behavior_affected")) {
|
|
65
|
-
if (triggered.length === 1) {
|
|
66
|
-
causes.push("triggered_runtime_behavior_affected_sole");
|
|
67
|
-
} else {
|
|
68
|
-
causes.push("triggered_runtime_behavior_affected");
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return causes;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export function deriveStopCauses(packet: DeriveStopCausesInput): StopCauseKey[] {
|
|
76
|
-
const causes: StopCauseKey[] = [];
|
|
77
|
-
const decision = packet.classification?.decision;
|
|
78
|
-
const triggered = packet.triggered_human_required ?? [];
|
|
79
|
-
|
|
80
|
-
if (decision === "defer") {
|
|
81
|
-
causes.push("decision_defer");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (decision === "human_required") {
|
|
85
|
-
causes.push("decision_human_required");
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (packet.stop === true && decision === "human_required") {
|
|
89
|
-
causes.push("stop_flag_expected_human_required");
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
causes.push(...deriveTriggerStopCauses(triggered));
|
|
93
|
-
|
|
94
|
-
return causes;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function stopCausesMatchDerivation(packet: DeriveStopCausesInput): boolean {
|
|
98
|
-
const expectedCauses = deriveStopCauses(packet);
|
|
99
|
-
const packetCauses = packet.stop_causes ?? [];
|
|
100
|
-
|
|
101
|
-
if (packetCauses.length !== expectedCauses.length) {
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const sortedPacketCauses = [...packetCauses].sort();
|
|
106
|
-
const sortedExpectedCauses = [...expectedCauses].sort();
|
|
107
|
-
|
|
108
|
-
return sortedPacketCauses.every((cause, index) => cause === sortedExpectedCauses[index]);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export function validateStopCausesDerivation(packet: DeriveStopCausesInput): string | null {
|
|
112
|
-
if (packet.stop !== true) {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
if (!stopCausesMatchDerivation(packet)) {
|
|
117
|
-
return "packet stop_causes do not match deriveStopCauses derivation";
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return null;
|
|
121
|
-
}
|