@codyswann/lisa 4.27.7 → 4.27.8
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/dist/core/lisa-owned-hash-ledger.d.ts.map +1 -1
- package/dist/core/lisa-owned-hash-ledger.js +98 -0
- package/dist/core/lisa-owned-hash-ledger.js.map +1 -1
- package/dist/core/nightly-e2e-guard-behavior-certificate.js +2 -2
- package/dist/core/project-config-nightly-e2e.d.ts +21 -0
- package/dist/core/project-config-nightly-e2e.d.ts.map +1 -0
- package/dist/core/project-config-nightly-e2e.js +43 -0
- package/dist/core/project-config-nightly-e2e.js.map +1 -0
- package/dist/core/project-config.d.ts +4 -0
- package/dist/core/project-config.d.ts.map +1 -1
- package/dist/core/project-config.js +3 -0
- package/dist/core/project-config.js.map +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +52 -10
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/expo/create-only/.github/nightly-e2e-policy.json +16 -0
- package/expo/create-only/.github/workflows/nightly-e2e-health.yml +33 -8
- package/expo/create-only/.github/workflows/nightly-e2e-tracking.yml +30 -0
- package/package.json +3 -3
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-exploratory-qa/SKILL.md +10 -6
- package/plugins/lisa/.codex-plugin/skills/lisa-intake/SKILL.md +18 -13
- package/plugins/lisa/.codex-plugin/skills/lisa-learnings-audit/SKILL.md +13 -7
- package/plugins/lisa/.codex-plugin/skills/lisa-linear-build-intake/SKILL.md +3 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-monitor/SKILL.md +10 -6
- package/plugins/lisa/.codex-plugin/skills/lisa-project-ideation/SKILL.md +10 -6
- package/plugins/lisa/.codex-plugin/skills/lisa-repair-intake/SKILL.md +10 -6
- package/plugins/lisa/skills/lisa-exploratory-qa/SKILL.md +10 -6
- package/plugins/lisa/skills/lisa-intake/SKILL.md +18 -13
- package/plugins/lisa/skills/lisa-learnings-audit/SKILL.md +13 -7
- package/plugins/lisa/skills/lisa-linear-build-intake/SKILL.md +3 -1
- package/plugins/lisa/skills/lisa-monitor/SKILL.md +10 -6
- package/plugins/lisa/skills/lisa-project-ideation/SKILL.md +10 -6
- package/plugins/lisa/skills/lisa-repair-intake/SKILL.md +10 -6
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-exploratory-qa/SKILL.md +10 -6
- package/plugins/lisa-agy/skills/lisa-intake/SKILL.md +18 -13
- package/plugins/lisa-agy/skills/lisa-learnings-audit/SKILL.md +13 -7
- package/plugins/lisa-agy/skills/lisa-linear-build-intake/SKILL.md +3 -1
- package/plugins/lisa-agy/skills/lisa-monitor/SKILL.md +10 -6
- package/plugins/lisa-agy/skills/lisa-project-ideation/SKILL.md +10 -6
- package/plugins/lisa-agy/skills/lisa-repair-intake/SKILL.md +10 -6
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/skills/lisa-exploratory-qa/SKILL.md +10 -6
- package/plugins/lisa-copilot/skills/lisa-intake/SKILL.md +18 -13
- package/plugins/lisa-copilot/skills/lisa-learnings-audit/SKILL.md +13 -7
- package/plugins/lisa-copilot/skills/lisa-linear-build-intake/SKILL.md +3 -1
- package/plugins/lisa-copilot/skills/lisa-monitor/SKILL.md +10 -6
- package/plugins/lisa-copilot/skills/lisa-project-ideation/SKILL.md +10 -6
- package/plugins/lisa-copilot/skills/lisa-repair-intake/SKILL.md +10 -6
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/skills/lisa-exploratory-qa/SKILL.md +10 -6
- package/plugins/lisa-cursor/skills/lisa-intake/SKILL.md +18 -13
- package/plugins/lisa-cursor/skills/lisa-learnings-audit/SKILL.md +13 -7
- package/plugins/lisa-cursor/skills/lisa-linear-build-intake/SKILL.md +3 -1
- package/plugins/lisa-cursor/skills/lisa-monitor/SKILL.md +10 -6
- package/plugins/lisa-cursor/skills/lisa-project-ideation/SKILL.md +10 -6
- package/plugins/lisa-cursor/skills/lisa-repair-intake/SKILL.md +10 -6
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/skills/lisa-exploratory-qa/SKILL.md +10 -6
- package/plugins/src/base/skills/lisa-intake/SKILL.md +18 -13
- package/plugins/src/base/skills/lisa-learnings-audit/SKILL.md +13 -7
- package/plugins/src/base/skills/lisa-linear-build-intake/SKILL.md +3 -1
- package/plugins/src/base/skills/lisa-monitor/SKILL.md +10 -6
- package/plugins/src/base/skills/lisa-project-ideation/SKILL.md +10 -6
- package/plugins/src/base/skills/lisa-repair-intake/SKILL.md +10 -6
- package/scripts/two-channel-couplings.json +70 -5
- package/typescript/copy-overwrite/.prettierignore +7 -0
- package/typescript/copy-overwrite/scripts/nightly-e2e-provider-action.mjs +70 -0
- package/typescript/copy-overwrite/scripts/nightly-e2e-provider-github-jira.mjs +296 -0
- package/typescript/copy-overwrite/scripts/nightly-e2e-provider-linear-sentry.mjs +283 -0
- package/typescript/copy-overwrite/scripts/nightly-e2e-provider-support.mjs +55 -0
- package/typescript/copy-overwrite/scripts/nightly-e2e-tracking-config.mjs +149 -0
- package/typescript/copy-overwrite/scripts/reconcile-nightly-e2e-tracking-cli.mjs +56 -0
- package/typescript/copy-overwrite/scripts/reconcile-nightly-e2e-tracking.mjs +208 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Public configuration and provider dispatch for nightly-E2E tracking.
|
|
6
|
+
*
|
|
7
|
+
* @module scripts/nightly-e2e-tracking-config
|
|
8
|
+
*/
|
|
9
|
+
export const MAX_TRACKING_DIAGNOSTIC = 4096;
|
|
10
|
+
export const TRACKING_DESTINATIONS = Object.freeze([
|
|
11
|
+
"github",
|
|
12
|
+
"sentry",
|
|
13
|
+
"jira",
|
|
14
|
+
"linear",
|
|
15
|
+
"none",
|
|
16
|
+
]);
|
|
17
|
+
const PROVIDER_WORKFLOWS = Object.freeze({
|
|
18
|
+
github: ".github/workflows/create-github-issue-on-failure.yml",
|
|
19
|
+
sentry: ".github/workflows/create-sentry-issue-on-failure.yml",
|
|
20
|
+
jira: ".github/workflows/create-jira-issue-on-failure.yml",
|
|
21
|
+
linear: ".github/workflows/create-linear-issue-on-failure.yml",
|
|
22
|
+
});
|
|
23
|
+
const PROVIDER_SECRETS = Object.freeze({
|
|
24
|
+
github: ["PAT"],
|
|
25
|
+
sentry: ["SENTRY_AUTH_TOKEN"],
|
|
26
|
+
jira: ["JIRA_API_TOKEN"],
|
|
27
|
+
linear: ["LINEAR_API_KEY"],
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/** Return whether a value is a plain object. */
|
|
31
|
+
function isRecord(value) {
|
|
32
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Throw a stable diagnostic which contains no rejected value. */
|
|
36
|
+
export function refuseTracking(message) {
|
|
37
|
+
throw new Error(message.slice(0, MAX_TRACKING_DIAGNOSTIC));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Read one non-empty string from a selected provider block. */
|
|
41
|
+
function requiredString(record, key, pathName) {
|
|
42
|
+
const prefix =
|
|
43
|
+
"nightlyE2E.tracking.destination supports " +
|
|
44
|
+
"github, sentry, jira, linear, none";
|
|
45
|
+
if (!isRecord(record)) {
|
|
46
|
+
refuseTracking(`${prefix}; ${pathName} must be an object`);
|
|
47
|
+
}
|
|
48
|
+
const value = record[key];
|
|
49
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
50
|
+
refuseTracking(`${prefix}; ${pathName}.${key} must be a non-empty string`);
|
|
51
|
+
}
|
|
52
|
+
return value;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Resolve a selected provider's existing project configuration. */
|
|
56
|
+
function resolveProvider(config, destination) {
|
|
57
|
+
if (destination === "github") {
|
|
58
|
+
return Object.freeze({
|
|
59
|
+
org: requiredString(config.github, "org", "github"),
|
|
60
|
+
repo: requiredString(config.github, "repo", "github"),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
if (destination === "sentry") {
|
|
64
|
+
return Object.freeze({
|
|
65
|
+
org: requiredString(config.sentry, "org", "sentry"),
|
|
66
|
+
project: requiredString(config.sentry, "project", "sentry"),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
if (destination === "jira") {
|
|
70
|
+
return Object.freeze({
|
|
71
|
+
jira: Object.freeze({
|
|
72
|
+
...config.jira,
|
|
73
|
+
project: requiredString(config.jira, "project", "jira"),
|
|
74
|
+
}),
|
|
75
|
+
atlassian: Object.freeze({
|
|
76
|
+
...config.atlassian,
|
|
77
|
+
site: requiredString(config.atlassian, "site", "atlassian"),
|
|
78
|
+
}),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
return Object.freeze({
|
|
82
|
+
...config.linear,
|
|
83
|
+
teamKey: requiredString(config.linear, "teamKey", "linear"),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Resolve the public destination and reuse its existing provider block. */
|
|
88
|
+
export function resolveNightlyTrackingConfig(config) {
|
|
89
|
+
if (!isRecord(config)) refuseTracking("project config must be an object");
|
|
90
|
+
if (!("nightlyE2E" in config)) {
|
|
91
|
+
return Object.freeze({ destination: "none", provider: null });
|
|
92
|
+
}
|
|
93
|
+
if (!isRecord(config.nightlyE2E)) {
|
|
94
|
+
refuseTracking("nightlyE2E must be an object");
|
|
95
|
+
}
|
|
96
|
+
if (!("tracking" in config.nightlyE2E)) {
|
|
97
|
+
return Object.freeze({ destination: "none", provider: null });
|
|
98
|
+
}
|
|
99
|
+
if (!isRecord(config.nightlyE2E.tracking)) {
|
|
100
|
+
refuseTracking("nightlyE2E.tracking must be an object");
|
|
101
|
+
}
|
|
102
|
+
const destination =
|
|
103
|
+
"destination" in config.nightlyE2E.tracking
|
|
104
|
+
? config.nightlyE2E.tracking.destination
|
|
105
|
+
: "none";
|
|
106
|
+
if (!TRACKING_DESTINATIONS.includes(destination)) {
|
|
107
|
+
refuseTracking(
|
|
108
|
+
"nightlyE2E.tracking.destination must be one of " +
|
|
109
|
+
"github, sentry, jira, linear, none"
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
return Object.freeze({
|
|
113
|
+
destination,
|
|
114
|
+
provider:
|
|
115
|
+
destination === "none" ? null : resolveProvider(config, destination),
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Build the exact handoff consumed by the selected provider workflow. */
|
|
120
|
+
export function buildProviderDispatch(settings, plan) {
|
|
121
|
+
const { destination, provider } = settings;
|
|
122
|
+
if (destination === "none" || !provider) {
|
|
123
|
+
refuseTracking("requested destination none has no provider dispatch");
|
|
124
|
+
}
|
|
125
|
+
const inputs = {
|
|
126
|
+
workflow_name: "Nightly E2E condition",
|
|
127
|
+
tracking_action: plan.action,
|
|
128
|
+
tracking_marker: plan.marker,
|
|
129
|
+
tracking_title: plan.title ?? "",
|
|
130
|
+
tracking_body: plan.body ?? "",
|
|
131
|
+
tracking_id: plan.trackerId ?? "",
|
|
132
|
+
};
|
|
133
|
+
if (destination === "sentry") {
|
|
134
|
+
inputs.SENTRY_ORG = provider.org;
|
|
135
|
+
inputs.SENTRY_PROJECT = provider.project;
|
|
136
|
+
} else if (destination === "jira") {
|
|
137
|
+
inputs.JIRA_BASE_URL = `https://${provider.atlassian.site}`;
|
|
138
|
+
inputs.JIRA_USER_EMAIL = provider.atlassian.email ?? "";
|
|
139
|
+
inputs.JIRA_PROJECT_KEY = provider.jira.project;
|
|
140
|
+
} else if (destination === "linear") {
|
|
141
|
+
inputs.team_key = provider.teamKey;
|
|
142
|
+
}
|
|
143
|
+
return Object.freeze({
|
|
144
|
+
destination,
|
|
145
|
+
workflow: PROVIDER_WORKFLOWS[destination],
|
|
146
|
+
inputs: Object.freeze(inputs),
|
|
147
|
+
secrets: Object.freeze([...PROVIDER_SECRETS[destination]]),
|
|
148
|
+
});
|
|
149
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
2
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GitHub Actions output adapter for the nightly-E2E tracking planner.
|
|
6
|
+
*
|
|
7
|
+
* @module scripts/reconcile-nightly-e2e-tracking-cli
|
|
8
|
+
*/
|
|
9
|
+
import * as fs from "node:fs";
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
|
|
12
|
+
/** Append one GitHub Actions output without command injection. */
|
|
13
|
+
function appendOutput(name, value) {
|
|
14
|
+
const file = process.env.GITHUB_OUTPUT;
|
|
15
|
+
if (!file) throw new Error("GITHUB_OUTPUT is required in --plan mode");
|
|
16
|
+
let delimiter = `LISA_${name}_${randomUUID()}`;
|
|
17
|
+
while (String(value).includes(delimiter)) {
|
|
18
|
+
delimiter = `LISA_${name}_${randomUUID()}`;
|
|
19
|
+
}
|
|
20
|
+
fs.appendFileSync(file, `${name}<<${delimiter}\n${value}\n${delimiter}\n`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve config/findings into provider-job outputs.
|
|
25
|
+
*
|
|
26
|
+
* @param input - Pure config and plan functions
|
|
27
|
+
*/
|
|
28
|
+
export function runTrackingPlan(input) {
|
|
29
|
+
const configPath = process.env.LISA_CONFIG_PATH ?? ".lisa.config.json";
|
|
30
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
31
|
+
const findings = JSON.parse(process.env.NIGHTLY_E2E_FINDINGS ?? "[]");
|
|
32
|
+
const settings = input.resolveConfig(config);
|
|
33
|
+
const plan = input.plan(findings, []);
|
|
34
|
+
let destination = settings.destination;
|
|
35
|
+
let action = plan.action === "refresh" ? "create" : plan.action;
|
|
36
|
+
if (settings.destination === "none") {
|
|
37
|
+
action = "none";
|
|
38
|
+
} else if (plan.action === "none" && plan.reason === "both_suites_green") {
|
|
39
|
+
action = "close";
|
|
40
|
+
} else if (plan.action === "none") {
|
|
41
|
+
destination = "none";
|
|
42
|
+
}
|
|
43
|
+
appendOutput("destination", destination);
|
|
44
|
+
appendOutput("tracking_action", action);
|
|
45
|
+
appendOutput("tracking_marker", plan.marker);
|
|
46
|
+
appendOutput("tracking_title", plan.title ?? "");
|
|
47
|
+
appendOutput("tracking_body", plan.body ?? "");
|
|
48
|
+
appendOutput("tracking_id", plan.trackerId ?? "");
|
|
49
|
+
appendOutput("provider_json", JSON.stringify(settings.provider ?? {}));
|
|
50
|
+
appendOutput("tracking_findings", JSON.stringify(findings));
|
|
51
|
+
if (settings.destination === "none") {
|
|
52
|
+
console.log("tracking_disabled");
|
|
53
|
+
const summary = process.env.GITHUB_STEP_SUMMARY;
|
|
54
|
+
if (summary) fs.appendFileSync(summary, "tracking_disabled\n");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// This file is managed by Lisa and IS replaced on each `lisa` run.
|
|
3
|
+
// Do not edit directly — durable changes belong upstream in Lisa.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve and reconcile one nightly-E2E condition across provider adapters.
|
|
7
|
+
*
|
|
8
|
+
* @module scripts/reconcile-nightly-e2e-tracking
|
|
9
|
+
*/
|
|
10
|
+
import * as path from "node:path";
|
|
11
|
+
import { pathToFileURL } from "node:url";
|
|
12
|
+
import {
|
|
13
|
+
MAX_TRACKING_DIAGNOSTIC,
|
|
14
|
+
TRACKING_DESTINATIONS,
|
|
15
|
+
buildProviderDispatch,
|
|
16
|
+
refuseTracking as refuse,
|
|
17
|
+
resolveNightlyTrackingConfig,
|
|
18
|
+
} from "./nightly-e2e-tracking-config.mjs";
|
|
19
|
+
import { runTrackingPlan } from "./reconcile-nightly-e2e-tracking-cli.mjs";
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
TRACKING_DESTINATIONS,
|
|
23
|
+
buildProviderDispatch,
|
|
24
|
+
resolveNightlyTrackingConfig,
|
|
25
|
+
};
|
|
26
|
+
export const TRACKED_SUITE_LABELS = Object.freeze([
|
|
27
|
+
"🎭 Playwright Web E2E",
|
|
28
|
+
"📱 Maestro Native E2E",
|
|
29
|
+
]);
|
|
30
|
+
export const CONDITION_MARKER = "<!-- lisa_nightly_e2e_condition:v1 -->";
|
|
31
|
+
|
|
32
|
+
/** Validate exact suite and tracker authority before deriving any action. */
|
|
33
|
+
function validateAuthority(findings, trackers) {
|
|
34
|
+
const exactSuiteMessage =
|
|
35
|
+
"findings must contain exactly Playwright Web E2E and Maestro Native E2E";
|
|
36
|
+
if (!Array.isArray(findings) || findings.length !== 2) {
|
|
37
|
+
refuse(exactSuiteMessage);
|
|
38
|
+
}
|
|
39
|
+
const labels = findings.map(finding => finding?.label).sort();
|
|
40
|
+
const expected = [...TRACKED_SUITE_LABELS].sort();
|
|
41
|
+
if (JSON.stringify(labels) !== JSON.stringify(expected)) {
|
|
42
|
+
refuse(exactSuiteMessage);
|
|
43
|
+
}
|
|
44
|
+
if (!Array.isArray(trackers)) refuse("tracker list must be an array");
|
|
45
|
+
if (trackers.some(tracker => tracker?.marker !== CONDITION_MARKER)) {
|
|
46
|
+
refuse("tracker adapter returned a record outside marker authority");
|
|
47
|
+
}
|
|
48
|
+
if (trackers.length > 1) refuse("multiple matching condition trackers found");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Produce one destination-neutral create, refresh, close, or no-op plan. */
|
|
52
|
+
export function planCombinedTracking(findings, trackers) {
|
|
53
|
+
validateAuthority(findings, trackers);
|
|
54
|
+
const incomplete = findings.some(
|
|
55
|
+
finding => !finding.complete || finding.state === "unknown"
|
|
56
|
+
);
|
|
57
|
+
if (incomplete) {
|
|
58
|
+
return Object.freeze({
|
|
59
|
+
action: "none",
|
|
60
|
+
marker: CONDITION_MARKER,
|
|
61
|
+
trackerId: null,
|
|
62
|
+
title: null,
|
|
63
|
+
body: null,
|
|
64
|
+
pin: null,
|
|
65
|
+
reason: "incomplete_evidence",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
const existing = trackers[0] ?? null;
|
|
69
|
+
const red = findings.some(finding => finding.state === "fail");
|
|
70
|
+
if (!red && !existing) {
|
|
71
|
+
return Object.freeze({
|
|
72
|
+
action: "none",
|
|
73
|
+
marker: CONDITION_MARKER,
|
|
74
|
+
trackerId: null,
|
|
75
|
+
title: null,
|
|
76
|
+
body: null,
|
|
77
|
+
pin: null,
|
|
78
|
+
reason: "both_suites_green",
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
if (!red) {
|
|
82
|
+
return Object.freeze({
|
|
83
|
+
action: "close",
|
|
84
|
+
marker: CONDITION_MARKER,
|
|
85
|
+
trackerId: existing.id,
|
|
86
|
+
title: null,
|
|
87
|
+
body: null,
|
|
88
|
+
pin: false,
|
|
89
|
+
reason: "both_suites_green",
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const status = findings.map(
|
|
93
|
+
finding =>
|
|
94
|
+
`- **${finding.label}**: ${finding.state} ([run](${finding.runUrl}))`
|
|
95
|
+
);
|
|
96
|
+
return Object.freeze({
|
|
97
|
+
action: existing ? "refresh" : "create",
|
|
98
|
+
marker: CONDITION_MARKER,
|
|
99
|
+
trackerId: existing?.id ?? null,
|
|
100
|
+
title: "Nightly E2E condition is red",
|
|
101
|
+
body: `${CONDITION_MARKER}\n## Nightly E2E condition\n\n${status.join("\n")}`,
|
|
102
|
+
pin: true,
|
|
103
|
+
reason: "at_least_one_suite_red",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Convert a provider exception into a bounded, secret-free classification. */
|
|
108
|
+
function providerFailure(destination, action, error) {
|
|
109
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
110
|
+
const status =
|
|
111
|
+
raw.match(/\bHTTP\s+\d{3}\b/iu)?.[0] ??
|
|
112
|
+
raw.match(/\breadback\b.*\bauthority\b/iu)?.[0] ??
|
|
113
|
+
(raw.includes("success false") ? "success false" : undefined) ??
|
|
114
|
+
(raw.includes("refused") ? "refused" : "provider error");
|
|
115
|
+
return new Error(
|
|
116
|
+
`Requested ${destination} destination ${action} failed: ${status}`.slice(
|
|
117
|
+
0,
|
|
118
|
+
MAX_TRACKING_DIAGNOSTIC
|
|
119
|
+
)
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Invoke one adapter action and wrap its errors at the trust boundary. */
|
|
124
|
+
async function providerCall(destination, action, invoke) {
|
|
125
|
+
try {
|
|
126
|
+
return await invoke();
|
|
127
|
+
} catch (error) {
|
|
128
|
+
throw providerFailure(destination, action, error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Reconcile the combined condition through exactly one selected adapter. */
|
|
133
|
+
export async function reconcileCombinedTracking(input) {
|
|
134
|
+
const settings = resolveNightlyTrackingConfig(input.config);
|
|
135
|
+
if (settings.destination === "none") {
|
|
136
|
+
return Object.freeze({
|
|
137
|
+
destination: "none",
|
|
138
|
+
action: "skipped",
|
|
139
|
+
trackerId: null,
|
|
140
|
+
reason: "tracking_disabled",
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const destination = settings.destination;
|
|
144
|
+
const adapters = input.adapters ?? {};
|
|
145
|
+
const adapter = adapters[destination];
|
|
146
|
+
if (!adapter) refuse(`${destination} adapter is unavailable`);
|
|
147
|
+
const trackers = await providerCall(destination, "list", () =>
|
|
148
|
+
adapter.list(CONDITION_MARKER)
|
|
149
|
+
);
|
|
150
|
+
const plan = planCombinedTracking(input.findings, trackers);
|
|
151
|
+
if (plan.action === "none") {
|
|
152
|
+
return Object.freeze({
|
|
153
|
+
destination,
|
|
154
|
+
action: "none",
|
|
155
|
+
trackerId: null,
|
|
156
|
+
reason: plan.reason,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (plan.action === "create") {
|
|
160
|
+
const tracker = await providerCall(destination, "create", () =>
|
|
161
|
+
adapter.create(plan)
|
|
162
|
+
);
|
|
163
|
+
if (adapter.pin) {
|
|
164
|
+
await providerCall(destination, "pin", () => adapter.pin(tracker.id));
|
|
165
|
+
}
|
|
166
|
+
return Object.freeze({
|
|
167
|
+
destination,
|
|
168
|
+
action: "create",
|
|
169
|
+
trackerId: tracker.id,
|
|
170
|
+
reason: plan.reason,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (plan.action === "refresh") {
|
|
174
|
+
const tracker = await providerCall(destination, "refresh", () =>
|
|
175
|
+
adapter.refresh(plan.trackerId, plan)
|
|
176
|
+
);
|
|
177
|
+
if (adapter.pin) {
|
|
178
|
+
await providerCall(destination, "pin", () => adapter.pin(tracker.id));
|
|
179
|
+
}
|
|
180
|
+
return Object.freeze({
|
|
181
|
+
destination,
|
|
182
|
+
action: "refresh",
|
|
183
|
+
trackerId: tracker.id,
|
|
184
|
+
reason: plan.reason,
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (adapter.unpin) {
|
|
188
|
+
await providerCall(destination, "unpin", () =>
|
|
189
|
+
adapter.unpin(plan.trackerId)
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
await providerCall(destination, "close", () => adapter.close(plan.trackerId));
|
|
193
|
+
return Object.freeze({
|
|
194
|
+
destination,
|
|
195
|
+
action: "close",
|
|
196
|
+
trackerId: plan.trackerId,
|
|
197
|
+
reason: plan.reason,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
|
202
|
+
if (invokedPath && pathToFileURL(invokedPath).href === import.meta.url) {
|
|
203
|
+
if (!process.argv.includes("--plan")) refuse("expected --plan");
|
|
204
|
+
runTrackingPlan({
|
|
205
|
+
resolveConfig: resolveNightlyTrackingConfig,
|
|
206
|
+
plan: planCombinedTracking,
|
|
207
|
+
});
|
|
208
|
+
}
|