@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.
@@ -1,189 +0,0 @@
1
- import { readFileSync } from "node:fs";
2
-
3
- import yaml from "yaml";
4
-
5
- import { loadRenovatePolicy, type RenovatePolicy } from "./renovate-guardrails.js";
6
-
7
- export type PackageCategory = "high_touch" | "low_risk_tooling" | "runtime" | "unlisted";
8
-
9
- export type SensitivePathRule = {
10
- patterns: string[];
11
- risk_class: string;
12
- };
13
-
14
- export type ConsumerRepoFacts = {
15
- workspace_roots?: string[];
16
- sensitive_paths?: string[];
17
- sensitive_path_rules?: SensitivePathRule[];
18
- analytics_paths?: string[];
19
- auth_paths?: string[];
20
- renovate_branch_prefix?: string;
21
- };
22
-
23
- export type ConsumerPackageFacts = {
24
- high_touch?: string[];
25
- low_risk_tooling?: string[];
26
- };
27
-
28
- export type LockfileThresholdFacts = {
29
- line_delta_limit_default: number;
30
- line_delta_limit_lockfile_maintenance: number;
31
- pr_file_count_single_package_max: number;
32
- };
33
-
34
- export type PrCiCheckFacts = {
35
- workflow?: string;
36
- job?: string;
37
- };
38
-
39
- export type ConsumerPolicyFacts = RenovatePolicy & {
40
- packages?: ConsumerPackageFacts;
41
- repo?: ConsumerRepoFacts;
42
- };
43
-
44
- export type ClassifyPackageInput = {
45
- name: string;
46
- isDevDependency: boolean;
47
- };
48
-
49
- function normalizePath(value: string): string {
50
- return value.replace(/\\/g, "/");
51
- }
52
-
53
- export function packageMatchesPattern(name: string, pattern: string): boolean {
54
- const normalizedPattern = pattern.trim();
55
- if (normalizedPattern.endsWith("/*")) {
56
- const prefix = normalizedPattern.slice(0, -1);
57
- return name.startsWith(prefix);
58
- }
59
- if (normalizedPattern.includes("*")) {
60
- const escaped = normalizedPattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
61
- return new RegExp(`^${escaped}$`).test(name);
62
- }
63
- return name === normalizedPattern;
64
- }
65
-
66
- export function packageMatchesAnyPattern(name: string, patterns: string[] | undefined): boolean {
67
- if (!patterns || patterns.length === 0) {
68
- return false;
69
- }
70
- return patterns.some((pattern) => packageMatchesPattern(name, pattern));
71
- }
72
-
73
- export function loadConsumerPolicyFacts(path: string): ConsumerPolicyFacts {
74
- loadRenovatePolicy(path);
75
- const raw = readFileSync(path, "utf8");
76
- return yaml.parse(raw) as ConsumerPolicyFacts;
77
- }
78
-
79
- export function classifyPackage(
80
- input: ClassifyPackageInput,
81
- policy: ConsumerPolicyFacts
82
- ): PackageCategory {
83
- const { name, isDevDependency } = input;
84
- const packages = policy.packages ?? {};
85
-
86
- if (packageMatchesAnyPattern(name, packages.high_touch)) {
87
- return "high_touch";
88
- }
89
-
90
- if (packageMatchesAnyPattern(name, packages.low_risk_tooling)) {
91
- return "low_risk_tooling";
92
- }
93
-
94
- if (!isDevDependency) {
95
- return "runtime";
96
- }
97
-
98
- return "unlisted";
99
- }
100
-
101
- export function riskClassForPackageCategory(category: PackageCategory): string {
102
- switch (category) {
103
- case "high_touch":
104
- return "high_touch_tooling";
105
- case "low_risk_tooling":
106
- return "low_risk_tooling_patch";
107
- case "runtime":
108
- return "runtime_dependency";
109
- case "unlisted":
110
- return "unlisted_package";
111
- }
112
- }
113
-
114
- function pathMatchesPattern(file: string, pattern: string): boolean {
115
- const normalizedFile = normalizePath(file);
116
- const normalizedPattern = normalizePath(pattern.trim());
117
-
118
- if (normalizedPattern.endsWith("/**")) {
119
- const prefix = normalizedPattern.slice(0, -3);
120
- return normalizedFile === prefix || normalizedFile.startsWith(`${prefix}/`);
121
- }
122
-
123
- if (normalizedPattern.includes("*")) {
124
- const escaped = normalizedPattern
125
- .replace(/[.+?^${}()|[\]\\]/g, "\\$&")
126
- .replace(/\*\*/g, ".*")
127
- .replace(/\*/g, "[^/]*");
128
- return new RegExp(`^${escaped}$`).test(normalizedFile);
129
- }
130
-
131
- return normalizedFile === normalizedPattern;
132
- }
133
-
134
- export function resolveSensitivePathRiskClass(
135
- file: string,
136
- policy: ConsumerPolicyFacts
137
- ): string | null {
138
- const repo = policy.repo;
139
- if (!repo) {
140
- return null;
141
- }
142
-
143
- for (const rule of repo.sensitive_path_rules ?? []) {
144
- if (rule.patterns.some((pattern) => pathMatchesPattern(file, pattern))) {
145
- return rule.risk_class;
146
- }
147
- }
148
-
149
- if (repo.analytics_paths?.some((pattern) => pathMatchesPattern(file, pattern))) {
150
- return "analytics_or_telemetry";
151
- }
152
-
153
- if (repo.auth_paths?.some((pattern) => pathMatchesPattern(file, pattern))) {
154
- return "auth_or_security";
155
- }
156
-
157
- if (repo.sensitive_paths?.some((pattern) => pathMatchesPattern(file, pattern))) {
158
- return "sensitive_path_change";
159
- }
160
-
161
- return null;
162
- }
163
-
164
- export function getLockfileThresholds(policy: ConsumerPolicyFacts): LockfileThresholdFacts {
165
- const thresholds = (
166
- policy.checks?.lockfile_within_threshold as { thresholds?: Partial<LockfileThresholdFacts> }
167
- )?.thresholds;
168
-
169
- return {
170
- line_delta_limit_default: thresholds?.line_delta_limit_default ?? 800,
171
- line_delta_limit_lockfile_maintenance: thresholds?.line_delta_limit_lockfile_maintenance ?? 2000,
172
- pr_file_count_single_package_max: thresholds?.pr_file_count_single_package_max ?? 30,
173
- };
174
- }
175
-
176
- export function getPrCiCheck(policy: ConsumerPolicyFacts): PrCiCheckFacts {
177
- const check = policy.checks?.pr_ci_green as { workflow?: string; job?: string } | undefined;
178
- return {
179
- workflow: check?.workflow,
180
- job: check?.job,
181
- };
182
- }
183
-
184
- export function lineDeltaLimit(lockfileMaintenance: boolean, policy: ConsumerPolicyFacts): number {
185
- const thresholds = getLockfileThresholds(policy);
186
- return lockfileMaintenance
187
- ? thresholds.line_delta_limit_lockfile_maintenance
188
- : thresholds.line_delta_limit_default;
189
- }
@@ -1,210 +0,0 @@
1
- #!/usr/bin/env node
2
- import { execFile } from "node:child_process";
3
- import { realpathSync } from "node:fs";
4
- import { promisify } from "node:util";
5
- import { fileURLToPath } from "node:url";
6
-
7
- import {
8
- normalizeGhCheckRunsForExpectedHead,
9
- normalizeGhCommitStatusesForExpectedHead,
10
- runRenovateBabysit,
11
- type CiObservation,
12
- type GhCheckRunsResponse,
13
- type GhCommitStatusResponse,
14
- type MergeObservation,
15
- type RenovateBabysitConfig,
16
- } from "./lib/renovate-freshness-poll.js";
17
-
18
- const execFileAsync = promisify(execFile);
19
-
20
- type CliOptions = {
21
- repo: string;
22
- pr: string;
23
- expectedHeadSha: string;
24
- config: RenovateBabysitConfig;
25
- };
26
-
27
- type GhPrViewResponse = {
28
- headRefOid?: string;
29
- baseRefOid?: string;
30
- mergeStateStatus?: string;
31
- };
32
-
33
- function printUsage(): void {
34
- console.error(`Usage: npm exec -- tsx scripts/renovate-freshness-poll.ts --repo <owner/repo> --pr <number> --expected-head <sha>
35
-
36
- Runs the Renovate post-update babysit helper and prints one JSON result.
37
-
38
- Required:
39
- --repo <owner/repo>
40
- --pr <number>
41
- --expected-head <sha>
42
-
43
- Debug-only overrides (do not use from renovate-classifier skill prose):
44
- --debug-poll-interval-ms <ms>
45
- --debug-ci-budget-ms <ms>
46
- --debug-unknown-max <count>
47
- `);
48
- }
49
-
50
- function readFlagValue(args: string[], index: number, flag: string): string {
51
- const value = args[index + 1];
52
- if (value === undefined || value.startsWith("--")) {
53
- throw new Error(`${flag} requires a value`);
54
- }
55
- return value;
56
- }
57
-
58
- function parsePositiveInteger(value: string, flag: string): number {
59
- const parsed = Number(value);
60
- if (!Number.isInteger(parsed) || parsed <= 0) {
61
- throw new Error(`${flag} must be a positive integer`);
62
- }
63
- return parsed;
64
- }
65
-
66
- export function parseRenovateFreshnessPollArgs(args: string[]): CliOptions {
67
- const options: Partial<CliOptions> & { config: RenovateBabysitConfig } = { config: {} };
68
-
69
- for (let index = 0; index < args.length; index += 1) {
70
- const arg = args[index];
71
- switch (arg) {
72
- case "--repo":
73
- options.repo = readFlagValue(args, index, arg);
74
- index += 1;
75
- break;
76
- case "--pr":
77
- options.pr = readFlagValue(args, index, arg);
78
- index += 1;
79
- break;
80
- case "--expected-head":
81
- options.expectedHeadSha = readFlagValue(args, index, arg);
82
- index += 1;
83
- break;
84
- case "--debug-poll-interval-ms":
85
- options.config.pollIntervalMs = parsePositiveInteger(readFlagValue(args, index, arg), arg);
86
- index += 1;
87
- break;
88
- case "--debug-ci-budget-ms":
89
- options.config.ciBudgetMs = parsePositiveInteger(readFlagValue(args, index, arg), arg);
90
- index += 1;
91
- break;
92
- case "--debug-unknown-max":
93
- options.config.unknownMax = parsePositiveInteger(readFlagValue(args, index, arg), arg);
94
- index += 1;
95
- break;
96
- case "--help":
97
- case "-h":
98
- printUsage();
99
- process.exit(0);
100
- default:
101
- throw new Error(`unknown argument: ${arg}`);
102
- }
103
- }
104
-
105
- if (!options.repo || !/^[^/\s]+\/[^/\s]+$/.test(options.repo)) {
106
- throw new Error("--repo must be in owner/repo form");
107
- }
108
- if (!options.pr || !/^\d+$/.test(options.pr)) {
109
- throw new Error("--pr must be a PR number");
110
- }
111
- if (!options.expectedHeadSha || options.expectedHeadSha.trim() === "") {
112
- throw new Error("--expected-head is required");
113
- }
114
-
115
- return {
116
- repo: options.repo,
117
- pr: options.pr,
118
- expectedHeadSha: options.expectedHeadSha,
119
- config: options.config,
120
- };
121
- }
122
-
123
- async function ghJson<T>(args: string[]): Promise<T> {
124
- const { stdout } = await execFileAsync("gh", args, { maxBuffer: 10 * 1024 * 1024 });
125
- return JSON.parse(stdout) as T;
126
- }
127
-
128
- async function fetchMergeState(options: CliOptions): Promise<MergeObservation> {
129
- const response = await ghJson<GhPrViewResponse>([
130
- "pr",
131
- "view",
132
- options.pr,
133
- "--repo",
134
- options.repo,
135
- "--json",
136
- "headRefOid,baseRefOid,mergeStateStatus",
137
- ]);
138
-
139
- if (!response.mergeStateStatus) {
140
- throw new Error("gh pr view did not return mergeStateStatus");
141
- }
142
-
143
- return {
144
- mergeState: response.mergeStateStatus,
145
- headSha: response.headRefOid ?? "",
146
- baseSha: response.baseRefOid,
147
- };
148
- }
149
-
150
- async function fetchCiState(options: CliOptions, expectedHeadSha: string): Promise<CiObservation> {
151
- const checkRuns = await ghJson<GhCheckRunsResponse>([
152
- "api",
153
- `repos/${options.repo}/commits/${expectedHeadSha}/check-runs`,
154
- ]);
155
- const normalizedCheckRuns = normalizeGhCheckRunsForExpectedHead(checkRuns, expectedHeadSha);
156
- if (normalizedCheckRuns.ciState !== "not_found") {
157
- return normalizedCheckRuns;
158
- }
159
-
160
- const statuses = await ghJson<GhCommitStatusResponse>([
161
- "api",
162
- `repos/${options.repo}/commits/${expectedHeadSha}/status`,
163
- ]);
164
- return normalizeGhCommitStatusesForExpectedHead(statuses, expectedHeadSha);
165
- }
166
-
167
- export async function runRenovateFreshnessPollCli(args: string[]): Promise<void> {
168
- let options: CliOptions;
169
- try {
170
- options = parseRenovateFreshnessPollArgs(args);
171
- } catch (error) {
172
- printUsage();
173
- console.error(error instanceof Error ? error.message : String(error));
174
- process.exit(2);
175
- }
176
-
177
- const result = await runRenovateBabysit(
178
- { expectedHeadSha: options.expectedHeadSha, config: options.config },
179
- {
180
- now: () => Date.now(),
181
- sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
182
- fetchMergeState: () => fetchMergeState(options),
183
- fetchCiState: (expectedHeadSha) => fetchCiState(options, expectedHeadSha),
184
- }
185
- );
186
-
187
- console.log(JSON.stringify(result, null, 2));
188
- }
189
-
190
- function isDirectRun(): boolean {
191
- const entry = process.argv[1];
192
- if (entry === undefined) {
193
- return false;
194
- }
195
- const modulePath = fileURLToPath(import.meta.url);
196
- try {
197
- return realpathSync(modulePath) === realpathSync(entry);
198
- } catch {
199
- return modulePath === entry;
200
- }
201
- }
202
-
203
- const isDirectRunEntry = isDirectRun();
204
-
205
- if (isDirectRunEntry) {
206
- runRenovateFreshnessPollCli(process.argv.slice(2)).catch((error: unknown) => {
207
- console.error(error instanceof Error ? error.message : String(error));
208
- process.exit(1);
209
- });
210
- }