@solana-epic/github-action 0.1.0-beta.1 → 0.1.0-beta.2
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 +157 -0
- package/package.json +27 -4
- package/src/github.ts +0 -81
- package/src/index.ts +0 -66
- package/src/report.ts +0 -143
- package/test/report.test.mjs +0 -107
package/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# EPIC
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<b>Upgrade Intelligence for Solana Programs</b>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://www.npmjs.com/package/@solana-epic/cli"><img src="https://img.shields.io/npm/v/@solana-epic/cli.svg?style=flat-square&color=blue" alt="npm version" /></a>
|
|
9
|
+
<a href="LICENSE"><img src="https://img.shields.io/github/license/solana-epic/epic.svg?style=flat-square" alt="license" /></a>
|
|
10
|
+
<a href="https://github.com/solana-epic/epic/releases"><img src="https://img.shields.io/github/v/release/solana-epic/epic.svg?style=flat-square&color=orange" alt="GitHub release" /></a>
|
|
11
|
+
<a href="https://github.com/solana-epic/epic/actions"><img src="https://img.shields.io/github/actions/workflow/status/solana-epic/epic/test.yml?branch=main&style=flat-square" alt="GitHub Actions status" /></a>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
EPIC is the deployment readiness and upgrade intelligence infrastructure for Solana programs. Positioned between git push and mainnet, EPIC evaluates state layout evolution, ABI compatibility, and security regressions to answer a simple question before you deploy:
|
|
17
|
+
|
|
18
|
+
**"Can this upgrade safely reach mainnet?"**
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Why EPIC Exists
|
|
23
|
+
|
|
24
|
+
Standard developer tooling tells you if your code compiles. Security scanners tell you if a codebase has known vulnerabilities. **Neither tells you if the transition between your old deployment and your new code will break state on mainnet.**
|
|
25
|
+
|
|
26
|
+
Every Solana program upgrade is a high-risk migration. A minor type shift, field reordering, or missing state reload can corrupt deserialization layouts, lock user accounts, or introduce severe security regressions.
|
|
27
|
+
|
|
28
|
+
EPIC catches these upgrade compatibility issues and regressions in local development and on every pull request.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## What EPIC Does
|
|
33
|
+
|
|
34
|
+
### 1. Upgrade Compatibility (`epic check`)
|
|
35
|
+
Compare two program versions to verify layout compatibility and prevent state corruption.
|
|
36
|
+
```
|
|
37
|
+
$ epic check ./old-program ./new-program
|
|
38
|
+
|
|
39
|
+
🔍 Comparing Program Layouts...
|
|
40
|
+
[CRITICAL] Layout size decrease detected on struct Position: 56 bytes -> 48 bytes.
|
|
41
|
+
Account shrinkage can lead to mainnet deserialization failures.
|
|
42
|
+
Consider using realloc or adding padding fields to preserve layout sizing.
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### 2. State Layout Analysis (`epic analyze`)
|
|
46
|
+
Track account layout evolution, serialized sizes, and memory offsets to manage state scaling.
|
|
47
|
+
```
|
|
48
|
+
$ epic analyze .
|
|
49
|
+
|
|
50
|
+
🔍 Analyzing State Account Layouts...
|
|
51
|
+
STATE ACCOUNTS:
|
|
52
|
+
├── Vault (49 bytes) [program::lib] [Static]
|
|
53
|
+
└── Position (56 bytes) [program::lib] [Static]
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 3. Upgrade Safety Verification (`epic audit`)
|
|
57
|
+
Verify that modifications to instruction state rules and safety invariants do not introduce security regressions.
|
|
58
|
+
```
|
|
59
|
+
$ epic audit .
|
|
60
|
+
|
|
61
|
+
🔍 Verifying Invariant Safety...
|
|
62
|
+
[CRITICAL] EPIC-SEC-003: Missing Post-CPI Account Reload
|
|
63
|
+
Affected File: programs/vault/src/lib.rs:42
|
|
64
|
+
Context: State mutation of Vault account following CPI invocation
|
|
65
|
+
Recommendation: Reload local state cache (e.g. run vault.reload()?) after CPI.
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
|
|
72
|
+
Install the CLI wrapper:
|
|
73
|
+
```bash
|
|
74
|
+
npm install -g @solana-epic/cli
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Verify your installation:
|
|
78
|
+
```bash
|
|
79
|
+
epic rules
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Quick Start
|
|
85
|
+
|
|
86
|
+
### 1. Check Upgrade Compatibility
|
|
87
|
+
Compare your current working directory against a previous release or program folder:
|
|
88
|
+
```bash
|
|
89
|
+
epic check ./old_release_dir ./new_release_dir
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 2. Run Layout Invariant Verification
|
|
93
|
+
Audit your codebase for security regressions before committing:
|
|
94
|
+
```bash
|
|
95
|
+
epic audit .
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### 3. Integrate with CI/CD
|
|
99
|
+
Incorporate upgrade checks directly into your pull requests. EPIC supports standard SARIF outputs for GitHub Actions integration:
|
|
100
|
+
```yaml
|
|
101
|
+
- name: Run EPIC Upgrade Checks
|
|
102
|
+
run: npx @solana-epic/cli audit . -f sarif
|
|
103
|
+
|
|
104
|
+
- name: Upload Safety Report
|
|
105
|
+
uses: github/code-scanning-upload-aurora@v2
|
|
106
|
+
with:
|
|
107
|
+
sarif_file: sarif.json
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Safety Invariant Rules
|
|
113
|
+
|
|
114
|
+
EPIC parses Rust source code directly to ensure upgrade changes do not break safety invariants:
|
|
115
|
+
|
|
116
|
+
| Rule ID | Name | Severity | Description |
|
|
117
|
+
| :--- | :--- | :--- | :--- |
|
|
118
|
+
| **EPIC-SEC-001** | Owner Validation | Critical | Ensures mutable account write paths are guarded by ownership checks (`account.owner == program_id`). |
|
|
119
|
+
| **EPIC-SEC-002** | Signer Validation | Critical | Verifies privileged mutations check signer authority. |
|
|
120
|
+
| **EPIC-SEC-003** | Missing Post-CPI Reload | Critical | Flags reads/writes on stale deserialized state cached before a mutating CPI. |
|
|
121
|
+
| **EPIC-SEC-004** | PDA Seed Collision Risk | High | Identifies adjacent variable-length seeds lacking delimiters that could cause derivation collision. |
|
|
122
|
+
| **EPIC-SEC-005** | Arbitrary CPI Targets | Critical | Flags CPIs targeting dynamic program IDs without validations. |
|
|
123
|
+
|
|
124
|
+
To inspect a rule's criteria in detail, run:
|
|
125
|
+
```bash
|
|
126
|
+
epic explain EPIC-SEC-001
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## Architecture Overview
|
|
132
|
+
|
|
133
|
+
EPIC constructs control-flow representations of program ASTs and diffs state schemas across the following unified pipeline:
|
|
134
|
+
```
|
|
135
|
+
Source Code ➔ Rust AST Parser ➔ Type Registry ➔ CFG Builder ➔ SSA Engine ➔ Dominance Tree ➔ GuardFacts IR ➔ Rules Analyzer
|
|
136
|
+
```
|
|
137
|
+
For a deep dive into the compiler and engine architecture, see [docs/architecture.md](docs/architecture.md).
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Roadmap
|
|
142
|
+
|
|
143
|
+
* **IDL-based layout drift verification**: Track compatibility profiles directly via published IDLs.
|
|
144
|
+
* **Editor LSP integration**: Real-time IDE diagnostics for layout drift and offset alignment.
|
|
145
|
+
* **Migration assistance**: Automatically generate Anchor state migration wrappers.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Contributing
|
|
150
|
+
|
|
151
|
+
We welcome contributions to EPIC! See [CONTRIBUTING.md](CONTRIBUTING.md) for local development setup, package structure, and submission guidelines.
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## License
|
|
156
|
+
|
|
157
|
+
EPIC is open-source developer tooling licensed under the **MIT License**. See [LICENSE](LICENSE) for details.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@solana-epic/github-action",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.2",
|
|
4
4
|
"description": "Solana EPIC Upgrade Guard GitHub Action",
|
|
5
5
|
"private": false,
|
|
6
6
|
"publishConfig": {
|
|
@@ -16,11 +16,34 @@
|
|
|
16
16
|
"dependencies": {
|
|
17
17
|
"@actions/core": "^1.10.1",
|
|
18
18
|
"@actions/github": "^6.0.0",
|
|
19
|
-
"@solana-epic/diff-engine": "^0.1.0-beta.
|
|
20
|
-
"@solana-epic/parser": "^0.1.0-beta.
|
|
19
|
+
"@solana-epic/diff-engine": "^0.1.0-beta.2",
|
|
20
|
+
"@solana-epic/parser": "^0.1.0-beta.2"
|
|
21
21
|
},
|
|
22
22
|
"devDependencies": {
|
|
23
23
|
"esbuild": "^0.20.1",
|
|
24
24
|
"typescript": "^5.3.3"
|
|
25
|
-
}
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"homepage": "https://github.com/solana-epic/epic#readme",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/solana-epic/epic.git"
|
|
31
|
+
},
|
|
32
|
+
"bugs": {
|
|
33
|
+
"url": "https://github.com/solana-epic/epic/issues"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"solana",
|
|
37
|
+
"anchor",
|
|
38
|
+
"security",
|
|
39
|
+
"static-analysis",
|
|
40
|
+
"audit",
|
|
41
|
+
"upgrade-safety",
|
|
42
|
+
"rust"
|
|
43
|
+
],
|
|
44
|
+
"author": "Solana EPIC Team",
|
|
45
|
+
"files": [
|
|
46
|
+
"dist",
|
|
47
|
+
"action.yml"
|
|
48
|
+
]
|
|
26
49
|
}
|
package/src/github.ts
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import * as github from "@actions/github";
|
|
2
|
-
import * as core from "@actions/core";
|
|
3
|
-
|
|
4
|
-
export async function upsertPRComment(token: string, reportMarkdown: string): Promise<void> {
|
|
5
|
-
const context = github.context;
|
|
6
|
-
|
|
7
|
-
if (!context.payload.pull_request) {
|
|
8
|
-
core.info("Not a pull request event. Skipping comment posting.");
|
|
9
|
-
return;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
const prNumber = context.payload.pull_request.number;
|
|
13
|
-
const owner = context.repo.owner;
|
|
14
|
-
const repo = context.repo.repo;
|
|
15
|
-
|
|
16
|
-
const octokit = github.getOctokit(token);
|
|
17
|
-
|
|
18
|
-
const commentHeader = "<!-- epic-upgrade-guard-comment -->";
|
|
19
|
-
const bodyWithHeader = `${commentHeader}\n${reportMarkdown}`;
|
|
20
|
-
|
|
21
|
-
core.info(`Searching for existing EPIC comments on PR #${prNumber}...`);
|
|
22
|
-
|
|
23
|
-
const { data: comments } = await octokit.rest.issues.listComments({
|
|
24
|
-
owner,
|
|
25
|
-
repo,
|
|
26
|
-
issue_number: prNumber,
|
|
27
|
-
per_page: 100
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const existingComment = comments.find((comment) => comment.body?.includes(commentHeader));
|
|
31
|
-
|
|
32
|
-
if (existingComment) {
|
|
33
|
-
core.info(`Found existing comment (ID: ${existingComment.id}). Updating it to avoid comment spam...`);
|
|
34
|
-
await octokit.rest.issues.updateComment({
|
|
35
|
-
owner,
|
|
36
|
-
repo,
|
|
37
|
-
comment_id: existingComment.id,
|
|
38
|
-
body: bodyWithHeader
|
|
39
|
-
});
|
|
40
|
-
} else {
|
|
41
|
-
core.info(`No existing comment found. Creating a new one...`);
|
|
42
|
-
await octokit.rest.issues.createComment({
|
|
43
|
-
owner,
|
|
44
|
-
repo,
|
|
45
|
-
issue_number: prNumber,
|
|
46
|
-
body: bodyWithHeader
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export async function checkIfConfigChanged(token: string): Promise<boolean> {
|
|
52
|
-
const context = github.context;
|
|
53
|
-
|
|
54
|
-
if (!context.payload.pull_request) {
|
|
55
|
-
core.info("Not a pull request context. Skipping config change audit.");
|
|
56
|
-
return false;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const prNumber = context.payload.pull_request.number;
|
|
60
|
-
const owner = context.repo.owner;
|
|
61
|
-
const repo = context.repo.repo;
|
|
62
|
-
const octokit = github.getOctokit(token);
|
|
63
|
-
|
|
64
|
-
try {
|
|
65
|
-
core.info(`Auditing PR #${prNumber} files for modifications to epic.toml...`);
|
|
66
|
-
const { data: files } = await octokit.rest.pulls.listFiles({
|
|
67
|
-
owner,
|
|
68
|
-
repo,
|
|
69
|
-
pull_number: prNumber,
|
|
70
|
-
per_page: 100
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
const isModified = files.some(file => file.filename.endsWith("epic.toml"));
|
|
74
|
-
core.info(`Config modification audit: ${isModified ? "MODIFIED" : "UNMODIFIED"}`);
|
|
75
|
-
return isModified;
|
|
76
|
-
} catch (error) {
|
|
77
|
-
core.warning(`Failed to list pull request files from GitHub API: ${error}`);
|
|
78
|
-
return false;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
package/src/index.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import * as core from "@actions/core";
|
|
2
|
-
import { compareAnchorPrograms } from "@solana-epic/diff-engine";
|
|
3
|
-
import { config } from "@solana-epic/parser";
|
|
4
|
-
import { upsertPRComment, checkIfConfigChanged } from "./github.js";
|
|
5
|
-
import { generateCompactMarkdownReport } from "./report.js";
|
|
6
|
-
|
|
7
|
-
async function run(): Promise<void> {
|
|
8
|
-
try {
|
|
9
|
-
const githubToken = core.getInput("github_token", { required: true });
|
|
10
|
-
const oldPath = core.getInput("old_path", { required: true });
|
|
11
|
-
const newPath = core.getInput("new_path", { required: true });
|
|
12
|
-
const configPath = core.getInput("config_path") || undefined;
|
|
13
|
-
|
|
14
|
-
core.info(`Running EPIC Upgrade Guard compare:`);
|
|
15
|
-
core.info(`Old path: ${oldPath}`);
|
|
16
|
-
core.info(`New path: ${newPath}`);
|
|
17
|
-
if (configPath) {
|
|
18
|
-
core.info(`Custom config path: ${configPath}`);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// Load epic.toml configuration
|
|
22
|
-
let epicConfig: config.ResolvedEpicConfig;
|
|
23
|
-
try {
|
|
24
|
-
epicConfig = config.loadEpicConfig(configPath);
|
|
25
|
-
core.info(`Loaded epic.toml settings. Fail severity threshold: ${epicConfig.failOnSeverity}`);
|
|
26
|
-
} catch (err: any) {
|
|
27
|
-
core.setFailed(`Failed to validate epic.toml configuration: ${err.message}`);
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Check if configuration changed in the pull request
|
|
32
|
-
const configChanged = await checkIfConfigChanged(githubToken);
|
|
33
|
-
|
|
34
|
-
// Run layout verification comparison
|
|
35
|
-
const report = await compareAnchorPrograms(oldPath, newPath, epicConfig);
|
|
36
|
-
|
|
37
|
-
const findingsCount = report.findings.length;
|
|
38
|
-
core.setOutput("severity", report.severity);
|
|
39
|
-
core.setOutput("findings_count", findingsCount.toString());
|
|
40
|
-
|
|
41
|
-
core.info(`Diff completed. Severity: ${report.severity}, Findings: ${findingsCount}`);
|
|
42
|
-
|
|
43
|
-
// Generate compact markdown report
|
|
44
|
-
const markdownReport = generateCompactMarkdownReport(report, epicConfig, configChanged);
|
|
45
|
-
|
|
46
|
-
// Upsert the comment on GitHub Pull Request
|
|
47
|
-
await upsertPRComment(githubToken, markdownReport);
|
|
48
|
-
|
|
49
|
-
// Gate verification
|
|
50
|
-
const severityLevels = ["SAFE", "MINOR", "MAJOR", "CRITICAL"];
|
|
51
|
-
const thresholdIndex = severityLevels.indexOf(epicConfig.failOnSeverity);
|
|
52
|
-
const reportSeverityIndex = severityLevels.indexOf(report.severity);
|
|
53
|
-
|
|
54
|
-
if (thresholdIndex !== -1 && reportSeverityIndex !== -1 && reportSeverityIndex >= thresholdIndex) {
|
|
55
|
-
core.setFailed(`EPIC Guard Blocked: Upgrade severity is ${report.severity} (threshold: ${epicConfig.failOnSeverity}).`);
|
|
56
|
-
} else {
|
|
57
|
-
core.info("EPIC Guard approved upgrade.");
|
|
58
|
-
}
|
|
59
|
-
} catch (error) {
|
|
60
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
-
core.setFailed(`EPIC Upgrade Guard failed: ${message}`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
run();
|
|
66
|
-
|
package/src/report.ts
DELETED
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
import type { DiffReport } from "@solana-epic/diff-engine";
|
|
2
|
-
import type { config } from "@solana-epic/parser";
|
|
3
|
-
import { createUpgradeIntelligenceItem } from "@solana-epic/diff-engine";
|
|
4
|
-
|
|
5
|
-
export function generateCompactMarkdownReport(
|
|
6
|
-
report: DiffReport,
|
|
7
|
-
cfg: config.ResolvedEpicConfig,
|
|
8
|
-
configChanged: boolean
|
|
9
|
-
): string {
|
|
10
|
-
const lines: string[] = [];
|
|
11
|
-
|
|
12
|
-
// Determine Overall Status
|
|
13
|
-
const failSeverity = cfg.failOnSeverity;
|
|
14
|
-
const severityOrder = ["SAFE", "MINOR", "MAJOR", "CRITICAL"];
|
|
15
|
-
const thresholdIndex = severityOrder.indexOf(failSeverity);
|
|
16
|
-
const reportSeverityIndex = severityOrder.indexOf(report.severity);
|
|
17
|
-
const blocked = thresholdIndex !== -1 && reportSeverityIndex !== -1 && reportSeverityIndex >= thresholdIndex;
|
|
18
|
-
|
|
19
|
-
const hasOverrides = report.findings.some(f => {
|
|
20
|
-
const original = f.kind === "FIELD_ADDED" ? "MAJOR" : "CRITICAL";
|
|
21
|
-
return f.severity !== original;
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
// 1. Status Banner
|
|
25
|
-
if (blocked) {
|
|
26
|
-
lines.push("## 🔴 EPIC Guard: UPGRADE BLOCKED");
|
|
27
|
-
lines.push("");
|
|
28
|
-
lines.push(`Upgrade checks failed because layout changes exceed the **${failSeverity}** threshold.`);
|
|
29
|
-
} else if (hasOverrides) {
|
|
30
|
-
lines.push("## 🟡 EPIC Guard: APPROVED WITH OVERRIDES");
|
|
31
|
-
lines.push("");
|
|
32
|
-
lines.push("Upgrade checks passed with muted warnings. Custom overrides are active in `epic.toml`.");
|
|
33
|
-
} else {
|
|
34
|
-
lines.push("## 🟢 EPIC Guard: APPROVED");
|
|
35
|
-
lines.push("");
|
|
36
|
-
lines.push("Upgrade checks approved. No layout compatibility risks detected.");
|
|
37
|
-
}
|
|
38
|
-
lines.push("");
|
|
39
|
-
|
|
40
|
-
// 2. Config Change Warning
|
|
41
|
-
if (configChanged) {
|
|
42
|
-
lines.push("> [!WARNING]");
|
|
43
|
-
lines.push("> **UPGRADE CONFIGURATION GATE MODIFIED**");
|
|
44
|
-
lines.push("> This Pull Request contains changes to `epic.toml` configuration rules.");
|
|
45
|
-
lines.push("> Signers must audit the modifications below to ensure safety limits are not bypassed.");
|
|
46
|
-
lines.push("");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// 3. Summary Table
|
|
50
|
-
lines.push("### 📊 Upgrade Summary");
|
|
51
|
-
lines.push("");
|
|
52
|
-
lines.push("| Program | Account | Finding | Final Severity | Overridden? |");
|
|
53
|
-
lines.push("| :--- | :--- | :--- | :--- | :--- |");
|
|
54
|
-
|
|
55
|
-
if (report.findings.length === 0) {
|
|
56
|
-
lines.push("| *N/A* | *All Accounts* | *No structural changes* | `SAFE` | No |");
|
|
57
|
-
} else {
|
|
58
|
-
for (const f of report.findings) {
|
|
59
|
-
const original = f.kind === "FIELD_ADDED" ? "MAJOR" : "CRITICAL";
|
|
60
|
-
const isOverridden = f.severity !== original;
|
|
61
|
-
lines.push(`| \`marginfi\` | \`${f.account}\` | \`${f.kind}\` | \`${f.severity}\` | ${isOverridden ? "✅ Yes" : "No"} |`);
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
lines.push("");
|
|
65
|
-
|
|
66
|
-
// 4. Detailed Findings
|
|
67
|
-
if (report.findings.length > 0) {
|
|
68
|
-
lines.push("### 🔍 Layout Findings");
|
|
69
|
-
lines.push("");
|
|
70
|
-
for (const f of report.findings) {
|
|
71
|
-
const intel = createUpgradeIntelligenceItem(f);
|
|
72
|
-
lines.push(`#### Struct \`${f.account}\` — **${f.kind}**`);
|
|
73
|
-
lines.push("");
|
|
74
|
-
lines.push(`* **Finding Type**: ${f.kind}`);
|
|
75
|
-
if (f.field) {
|
|
76
|
-
lines.push(`* **Field**: \`${f.field.name}\` (${f.field.oldType || "new"} ──► ${f.field.newType || "removed"})`);
|
|
77
|
-
}
|
|
78
|
-
lines.push(`* **Size Impact**: \`${f.oldSize}B\` ──► \`${f.newSize}B\` (${f.newSize - f.oldSize >= 0 ? "+" : ""}${f.newSize - f.oldSize} bytes)`);
|
|
79
|
-
lines.push(`* **Risk Class**: ${intel.riskCategory}`);
|
|
80
|
-
lines.push(`* **Severity**: \`${f.severity}\``);
|
|
81
|
-
lines.push("");
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// 5. Applied Overrides Section
|
|
86
|
-
const appliedOverrides: Array<{ account: string; finding: string; field?: string; shift: string; note: string }> = [];
|
|
87
|
-
for (const f of report.findings) {
|
|
88
|
-
const original = f.kind === "FIELD_ADDED" ? "MAJOR" : "CRITICAL";
|
|
89
|
-
if (f.severity !== original) {
|
|
90
|
-
// Find override note
|
|
91
|
-
let note = "No note provided.";
|
|
92
|
-
for (const [name, program] of cfg.programs.entries()) {
|
|
93
|
-
const match = program.overrides.find(o => {
|
|
94
|
-
const accountMatch = o.account.toLowerCase() === f.account.toLowerCase();
|
|
95
|
-
const findingMatch = o.finding.toUpperCase() === f.kind.toUpperCase();
|
|
96
|
-
const fieldMatch = f.field && o.field && o.field.toLowerCase() === f.field.name.toLowerCase();
|
|
97
|
-
return accountMatch && findingMatch && (fieldMatch || !o.field);
|
|
98
|
-
});
|
|
99
|
-
if (match) {
|
|
100
|
-
note = match.note;
|
|
101
|
-
break;
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
appliedOverrides.push({
|
|
105
|
-
account: f.account,
|
|
106
|
-
finding: f.kind,
|
|
107
|
-
field: f.field?.name,
|
|
108
|
-
shift: `\`${original}\` ──► \`${f.severity}\``,
|
|
109
|
-
note
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
if (appliedOverrides.length > 0) {
|
|
115
|
-
lines.push("### 🔑 Applied Layout Overrides");
|
|
116
|
-
lines.push("");
|
|
117
|
-
lines.push("| Struct | Finding | Field | Severity Shift | Note / Safety Justification |");
|
|
118
|
-
lines.push("| :--- | :--- | :--- | :--- | :--- |");
|
|
119
|
-
for (const o of appliedOverrides) {
|
|
120
|
-
lines.push(`| \`${o.account}\` | \`${o.finding}\` | \`${o.field || "global"}\` | ${o.shift} | ${o.note} |`);
|
|
121
|
-
}
|
|
122
|
-
lines.push("");
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// 6. Recommended Actions
|
|
126
|
-
lines.push("### 💡 Recommended Actions");
|
|
127
|
-
lines.push("");
|
|
128
|
-
if (report.findings.length === 0) {
|
|
129
|
-
lines.push("* No action required. Layout upgrades are safe to proceed.");
|
|
130
|
-
} else {
|
|
131
|
-
const uniqueRecommendations = new Set<string>();
|
|
132
|
-
for (const f of report.findings) {
|
|
133
|
-
const intel = createUpgradeIntelligenceItem(f);
|
|
134
|
-
uniqueRecommendations.add(intel.recommendation);
|
|
135
|
-
}
|
|
136
|
-
for (const rec of uniqueRecommendations) {
|
|
137
|
-
lines.push(`* ${rec}`);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
lines.push("");
|
|
141
|
-
|
|
142
|
-
return lines.join("\n");
|
|
143
|
-
}
|
package/test/report.test.mjs
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
import assert from "node:assert/strict";
|
|
2
|
-
import { test } from "node:test";
|
|
3
|
-
import { generateCompactMarkdownReport } from "../dist/report.js";
|
|
4
|
-
|
|
5
|
-
// Helper to construct a mock config
|
|
6
|
-
const createMockConfig = (failOnSeverity = "MAJOR") => ({
|
|
7
|
-
compareMode: "ast",
|
|
8
|
-
failOnSeverity,
|
|
9
|
-
excludePaths: [],
|
|
10
|
-
enforcePadding: false,
|
|
11
|
-
programs: new Map([
|
|
12
|
-
["marginfi", {
|
|
13
|
-
name: "marginfi",
|
|
14
|
-
absolutePath: "/workspace/programs/marginfi",
|
|
15
|
-
programId: "MFv2...",
|
|
16
|
-
overrides: [
|
|
17
|
-
{
|
|
18
|
-
account: "Bank",
|
|
19
|
-
finding: "PADDING_REPURPOSE",
|
|
20
|
-
field: "reserved",
|
|
21
|
-
action: "allow",
|
|
22
|
-
note: "Replaced padding safely.",
|
|
23
|
-
used: true
|
|
24
|
-
}
|
|
25
|
-
]
|
|
26
|
-
}]
|
|
27
|
-
])
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("report: generates approved status banner when clean", () => {
|
|
31
|
-
const report = {
|
|
32
|
-
oldProgramPath: "/old",
|
|
33
|
-
newProgramPath: "/new",
|
|
34
|
-
severity: "SAFE",
|
|
35
|
-
findings: []
|
|
36
|
-
};
|
|
37
|
-
const config = createMockConfig();
|
|
38
|
-
|
|
39
|
-
const md = generateCompactMarkdownReport(report, config, false);
|
|
40
|
-
assert.match(md, /🟢 EPIC Guard: APPROVED/);
|
|
41
|
-
assert.match(md, /Upgrade checks approved/);
|
|
42
|
-
assert.match(md, /No structural changes/);
|
|
43
|
-
assert.match(md, /No action required/);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("report: generates overrides active banner when warnings are muted", () => {
|
|
47
|
-
const report = {
|
|
48
|
-
oldProgramPath: "/old",
|
|
49
|
-
newProgramPath: "/new",
|
|
50
|
-
severity: "SAFE",
|
|
51
|
-
findings: [
|
|
52
|
-
{
|
|
53
|
-
severity: "SAFE", // Overridden from CRITICAL
|
|
54
|
-
account: "Bank",
|
|
55
|
-
kind: "PADDING_REPURPOSE",
|
|
56
|
-
field: { name: "reserved" },
|
|
57
|
-
oldSize: 100,
|
|
58
|
-
newSize: 100
|
|
59
|
-
}
|
|
60
|
-
]
|
|
61
|
-
};
|
|
62
|
-
const config = createMockConfig();
|
|
63
|
-
|
|
64
|
-
const md = generateCompactMarkdownReport(report, config, false);
|
|
65
|
-
assert.match(md, /🟡 EPIC Guard: APPROVED WITH OVERRIDES/);
|
|
66
|
-
assert.match(md, /Applied Layout Overrides/);
|
|
67
|
-
assert.match(md, /Replaced padding safely/);
|
|
68
|
-
assert.match(md, /`CRITICAL` ──► `SAFE`/);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("report: generates blocked status banner when threshold is exceeded", () => {
|
|
72
|
-
const report = {
|
|
73
|
-
oldProgramPath: "/old",
|
|
74
|
-
newProgramPath: "/new",
|
|
75
|
-
severity: "MAJOR",
|
|
76
|
-
findings: [
|
|
77
|
-
{
|
|
78
|
-
severity: "MAJOR", // Not overridden
|
|
79
|
-
account: "Bank",
|
|
80
|
-
kind: "FIELD_ADDED",
|
|
81
|
-
field: { name: "maker_rebate" },
|
|
82
|
-
oldSize: 100,
|
|
83
|
-
newSize: 108
|
|
84
|
-
}
|
|
85
|
-
]
|
|
86
|
-
};
|
|
87
|
-
const config = createMockConfig("MAJOR"); // Will block since report severity is MAJOR
|
|
88
|
-
|
|
89
|
-
const md = generateCompactMarkdownReport(report, config, false);
|
|
90
|
-
assert.match(md, /🔴 EPIC Guard: UPGRADE BLOCKED/);
|
|
91
|
-
assert.match(md, /Upgrade checks failed because layout changes exceed/);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test("report: injects warning banner when epic.toml changed", () => {
|
|
95
|
-
const report = {
|
|
96
|
-
oldProgramPath: "/old",
|
|
97
|
-
newProgramPath: "/new",
|
|
98
|
-
severity: "SAFE",
|
|
99
|
-
findings: []
|
|
100
|
-
};
|
|
101
|
-
const config = createMockConfig();
|
|
102
|
-
|
|
103
|
-
const md = generateCompactMarkdownReport(report, config, true); // configChanged = true
|
|
104
|
-
assert.match(md, /🟢 EPIC Guard: APPROVED/);
|
|
105
|
-
assert.match(md, /UPGRADE CONFIGURATION GATE MODIFIED/);
|
|
106
|
-
assert.match(md, /This Pull Request contains changes to `epic.toml`/);
|
|
107
|
-
});
|