flagshark 1.0.1 โ 1.1.0
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/cli.js +14 -3790
- package/package.json +12 -35
- package/LICENSE +0 -21
- package/README.md +0 -175
- package/action/index.ts +0 -316
- package/dist/action.cjs +0 -31832
- package/dist/action.js +0 -31742
package/package.json
CHANGED
|
@@ -1,56 +1,33 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "flagshark",
|
|
3
|
-
"version": "1.0
|
|
4
|
-
"description": "Find stale feature flags in your codebase",
|
|
3
|
+
"version": "1.1.0",
|
|
5
4
|
"type": "module",
|
|
5
|
+
"description": "Find stale feature flags in your codebase",
|
|
6
6
|
"license": "MIT",
|
|
7
|
+
"homepage": "https://flagshark.com",
|
|
7
8
|
"repository": {
|
|
8
9
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/FlagShark/flagshark.git"
|
|
10
|
-
|
|
11
|
-
"homepage": "https://flagshark.com",
|
|
12
|
-
"keywords": [
|
|
13
|
-
"feature-flags",
|
|
14
|
-
"launchdarkly",
|
|
15
|
-
"unleash",
|
|
16
|
-
"flipt",
|
|
17
|
-
"split",
|
|
18
|
-
"posthog",
|
|
19
|
-
"cleanup",
|
|
20
|
-
"stale-flags",
|
|
21
|
-
"technical-debt",
|
|
22
|
-
"github-action"
|
|
23
|
-
],
|
|
24
|
-
"bin": {
|
|
25
|
-
"flagshark": "./bin/flagshark.mjs"
|
|
10
|
+
"url": "https://github.com/FlagShark/flagshark.git",
|
|
11
|
+
"directory": "packages/cli"
|
|
26
12
|
},
|
|
13
|
+
"keywords": ["feature-flags", "stale-flags", "cleanup", "technical-debt"],
|
|
14
|
+
"bin": { "flagshark": "./bin/flagshark.mjs" },
|
|
27
15
|
"main": "./dist/cli.js",
|
|
28
|
-
"files": [
|
|
29
|
-
"dist/",
|
|
30
|
-
"bin/",
|
|
31
|
-
"action/"
|
|
32
|
-
],
|
|
16
|
+
"files": ["dist/", "bin/"],
|
|
33
17
|
"scripts": {
|
|
34
|
-
"build": "
|
|
35
|
-
"build:cli": "esbuild src/cli.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/cli.js --external:zod",
|
|
36
|
-
"build:action": "esbuild action/index.ts --bundle --platform=node --target=node18 --format=cjs --outfile=dist/action.cjs",
|
|
18
|
+
"build": "esbuild src/cli.ts --bundle --platform=node --target=node18 --format=esm --outfile=dist/cli.js --external:zod --external:@flagshark/core",
|
|
37
19
|
"test": "vitest run",
|
|
38
|
-
"
|
|
39
|
-
"prepublishOnly": "npm run build && npm test"
|
|
20
|
+
"typecheck": "tsc --noEmit"
|
|
40
21
|
},
|
|
41
22
|
"dependencies": {
|
|
42
|
-
"
|
|
23
|
+
"@flagshark/core": "workspace:*",
|
|
43
24
|
"zod": "^3.23.0"
|
|
44
25
|
},
|
|
45
26
|
"devDependencies": {
|
|
46
|
-
"@actions/core": "^1.10.0",
|
|
47
|
-
"@actions/github": "^6.0.0",
|
|
48
27
|
"@types/node": "^22.0.0",
|
|
49
28
|
"esbuild": "^0.24.0",
|
|
50
29
|
"typescript": "^5.7.0",
|
|
51
30
|
"vitest": "^3.0.0"
|
|
52
31
|
},
|
|
53
|
-
"engines": {
|
|
54
|
-
"node": ">=18.0.0"
|
|
55
|
-
}
|
|
32
|
+
"engines": { "node": ">=18.0.0" }
|
|
56
33
|
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 FlagShark
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
package/README.md
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
# FlagShark
|
|
2
|
-
|
|
3
|
-
Find stale feature flags in your codebase. CLI tool + GitHub Action.
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
npx flagshark scan
|
|
7
|
-
```
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
๐ฆ FlagShark v1.0.0
|
|
11
|
-
|
|
12
|
-
Scanned 156 files across 4 languages
|
|
13
|
-
Detected providers: LaunchDarkly (JS SDK), Unleash (Go SDK)
|
|
14
|
-
Found 23 feature flags, 7 stale
|
|
15
|
-
|
|
16
|
-
Stale flags:
|
|
17
|
-
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
18
|
-
โ Flag โ File โ Added โ Signal โ
|
|
19
|
-
โโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค
|
|
20
|
-
โ CHECKOUT_V2 โ src/checkout.ts:47 โ 14 months ago โ Age > 6 months โ
|
|
21
|
-
โ NEW_NAV โ src/layout.tsx:12 โ 8 months ago โ Age > 6 months, Single file โ
|
|
22
|
-
โ BETA_SEARCH โ src/search.ts:91 โ 11 months ago โ Single file reference โ
|
|
23
|
-
โโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
24
|
-
|
|
25
|
-
Flag Health Score: 70/100 (7/23 flags are stale)
|
|
26
|
-
```
|
|
27
|
-
|
|
28
|
-
## Install
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
# Run without installing
|
|
32
|
-
npx flagshark scan
|
|
33
|
-
|
|
34
|
-
# Or install globally
|
|
35
|
-
npm install -g flagshark
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
## CLI Usage
|
|
39
|
-
|
|
40
|
-
```bash
|
|
41
|
-
# Scan current directory
|
|
42
|
-
flagshark scan
|
|
43
|
-
|
|
44
|
-
# JSON output (for piping to other tools)
|
|
45
|
-
flagshark scan --json
|
|
46
|
-
|
|
47
|
-
# Only scan files changed since a git ref
|
|
48
|
-
flagshark scan --diff HEAD~1
|
|
49
|
-
flagshark scan --diff main
|
|
50
|
-
|
|
51
|
-
# Custom staleness threshold (default: 6 months)
|
|
52
|
-
flagshark scan --threshold 3
|
|
53
|
-
|
|
54
|
-
# Show all stale flags (default shows top 10)
|
|
55
|
-
flagshark scan --verbose
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
### Exit codes
|
|
59
|
-
|
|
60
|
-
| Code | Meaning |
|
|
61
|
-
|------|---------|
|
|
62
|
-
| 0 | No stale flags found |
|
|
63
|
-
| 1 | Stale flags detected |
|
|
64
|
-
| 2 | Runtime error |
|
|
65
|
-
|
|
66
|
-
## GitHub Action
|
|
67
|
-
|
|
68
|
-
Add to your workflow:
|
|
69
|
-
|
|
70
|
-
```yaml
|
|
71
|
-
name: FlagShark
|
|
72
|
-
on: [pull_request]
|
|
73
|
-
|
|
74
|
-
permissions:
|
|
75
|
-
contents: read
|
|
76
|
-
pull-requests: write
|
|
77
|
-
|
|
78
|
-
jobs:
|
|
79
|
-
flagshark:
|
|
80
|
-
runs-on: ubuntu-latest
|
|
81
|
-
steps:
|
|
82
|
-
- uses: actions/checkout@v4
|
|
83
|
-
with:
|
|
84
|
-
fetch-depth: 0 # Required for git blame age detection
|
|
85
|
-
- uses: FlagShark/flagshark@v1
|
|
86
|
-
env:
|
|
87
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
### Action Inputs
|
|
91
|
-
|
|
92
|
-
| Input | Default | Description |
|
|
93
|
-
|-------|---------|-------------|
|
|
94
|
-
| `scan` | `changed` | `changed` (PR files only) or `full` (entire repo) |
|
|
95
|
-
| `threshold` | `6` | Staleness threshold in months |
|
|
96
|
-
| `fail-threshold` | `0` | Health score below which the check fails (0 = never fail) |
|
|
97
|
-
|
|
98
|
-
### Scan Modes
|
|
99
|
-
|
|
100
|
-
**`scan: changed`** (default) scans only files modified in the PR. Fast, focused on what you're changing.
|
|
101
|
-
|
|
102
|
-
**`scan: full`** scans the entire repository. Shows your full flag health score and finds stale flags everywhere, not just in changed files. Great for seeing the big picture:
|
|
103
|
-
|
|
104
|
-
```yaml
|
|
105
|
-
- uses: FlagShark/flagshark@v1
|
|
106
|
-
with:
|
|
107
|
-
scan: full
|
|
108
|
-
env:
|
|
109
|
-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
110
|
-
```
|
|
111
|
-
|
|
112
|
-
### What the Action does
|
|
113
|
-
|
|
114
|
-
On every PR, FlagShark comments with a table of stale flags:
|
|
115
|
-
|
|
116
|
-
> ### ๐ฆ FlagShark found 3 stale flags
|
|
117
|
-
>
|
|
118
|
-
> | Flag | File | Added | Signal |
|
|
119
|
-
> |------|------|-------|--------|
|
|
120
|
-
> | `CHECKOUT_V2` | src/checkout.ts:47 | 14 months ago | Age > 6 months |
|
|
121
|
-
> | `NEW_NAV` | src/layout.tsx:12 | 8 months ago | Single file |
|
|
122
|
-
>
|
|
123
|
-
> **Flag Health:** 70/100
|
|
124
|
-
|
|
125
|
-
It also sets a GitHub status check that can optionally block merge if health drops below a threshold.
|
|
126
|
-
|
|
127
|
-
## Supported Languages
|
|
128
|
-
|
|
129
|
-
FlagShark detects feature flags across 13 languages:
|
|
130
|
-
|
|
131
|
-
| Language | Extensions |
|
|
132
|
-
|----------|-----------|
|
|
133
|
-
| TypeScript/JavaScript | .ts, .tsx, .js, .jsx, .mjs, .cjs |
|
|
134
|
-
| Go | .go |
|
|
135
|
-
| Python | .py |
|
|
136
|
-
| Java | .java |
|
|
137
|
-
| Kotlin | .kt |
|
|
138
|
-
| Swift | .swift |
|
|
139
|
-
| Ruby | .rb |
|
|
140
|
-
| C# | .cs |
|
|
141
|
-
| PHP | .php |
|
|
142
|
-
| Rust | .rs |
|
|
143
|
-
| C/C++ | .c, .cpp, .h, .hpp |
|
|
144
|
-
| Objective-C | .m |
|
|
145
|
-
|
|
146
|
-
## Supported Providers
|
|
147
|
-
|
|
148
|
-
Auto-detected from imports (no configuration needed):
|
|
149
|
-
|
|
150
|
-
- LaunchDarkly
|
|
151
|
-
- Unleash
|
|
152
|
-
- Flipt
|
|
153
|
-
- Split.io
|
|
154
|
-
- PostHog
|
|
155
|
-
- Flagsmith
|
|
156
|
-
- ConfigCat
|
|
157
|
-
- Statsig
|
|
158
|
-
- GrowthBook
|
|
159
|
-
- DevCycle
|
|
160
|
-
- Eppo
|
|
161
|
-
- Optimizely
|
|
162
|
-
- Custom flag implementations
|
|
163
|
-
|
|
164
|
-
## How Staleness Works
|
|
165
|
-
|
|
166
|
-
A flag is marked stale if **any** of these signals fires:
|
|
167
|
-
|
|
168
|
-
1. **Age:** `git blame` shows the flag reference was added more than 6 months ago (configurable with `--threshold`)
|
|
169
|
-
2. **Single file:** The flag name appears in only one file across the entire repo, suggesting a completed rollout
|
|
170
|
-
|
|
171
|
-
FlagShark only checks files that actually import a flag SDK. A function called `isEnabled()` in a file that doesn't import LaunchDarkly/Unleash/etc. won't be flagged. This prevents false positives.
|
|
172
|
-
|
|
173
|
-
## License
|
|
174
|
-
|
|
175
|
-
MIT
|
package/action/index.ts
DELETED
|
@@ -1,316 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* GitHub Action entry point for FlagShark.
|
|
3
|
-
*
|
|
4
|
-
* Scans a repo for stale feature flags, posts a rich PR comment,
|
|
5
|
-
* writes a GitHub Actions job summary, and sets a status check.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
|
9
|
-
import { join, extname } from 'node:path'
|
|
10
|
-
|
|
11
|
-
import * as core from '@actions/core'
|
|
12
|
-
import * as github from '@actions/github'
|
|
13
|
-
|
|
14
|
-
import { createDefaultRegistry } from '../src/detection/index.js'
|
|
15
|
-
import { PolyglotAnalyzer } from '../src/detection/polyglot-analyzer.js'
|
|
16
|
-
import { analyzeStaleness } from '../src/staleness.js'
|
|
17
|
-
|
|
18
|
-
import type { FeatureFlag } from '../src/detection/feature-flag.js'
|
|
19
|
-
import type { StaleFlag } from '../src/staleness.js'
|
|
20
|
-
|
|
21
|
-
const COMMENT_MARKER = '<!-- flagshark-action -->'
|
|
22
|
-
const SKIP_DIRS = new Set([
|
|
23
|
-
'node_modules', 'vendor', '.git', 'dist', 'build',
|
|
24
|
-
'coverage', '__pycache__', '.next', '.turbo',
|
|
25
|
-
])
|
|
26
|
-
|
|
27
|
-
// Logger that serializes objects properly instead of [object Object]
|
|
28
|
-
const logger = {
|
|
29
|
-
debug: (...args: unknown[]) => core.debug(formatLogArgs(args)),
|
|
30
|
-
info: (...args: unknown[]) => core.info(formatLogArgs(args)),
|
|
31
|
-
warn: (...args: unknown[]) => core.warning(formatLogArgs(args)),
|
|
32
|
-
error: (...args: unknown[]) => core.error(formatLogArgs(args)),
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function formatLogArgs(args: unknown[]): string {
|
|
36
|
-
return args.map(a =>
|
|
37
|
-
typeof a === 'object' && a !== null ? JSON.stringify(a, null, 2) : String(a)
|
|
38
|
-
).join(' ')
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async function run(): Promise<void> {
|
|
42
|
-
const startTime = Date.now()
|
|
43
|
-
|
|
44
|
-
try {
|
|
45
|
-
const scanMode = core.getInput('scan') || 'changed'
|
|
46
|
-
const threshold = parseInt(core.getInput('threshold') || '6', 10)
|
|
47
|
-
const failThreshold = parseInt(core.getInput('fail-threshold') || '0', 10)
|
|
48
|
-
|
|
49
|
-
const registry = createDefaultRegistry()
|
|
50
|
-
const supportedExts = new Set(registry.getSupportedExtensions())
|
|
51
|
-
const analyzer = new PolyglotAnalyzer(registry, logger)
|
|
52
|
-
|
|
53
|
-
// Determine files to scan
|
|
54
|
-
let filePaths: string[]
|
|
55
|
-
|
|
56
|
-
if (scanMode === 'changed' && github.context.payload.pull_request) {
|
|
57
|
-
const token = process.env.GITHUB_TOKEN || core.getInput('token')
|
|
58
|
-
if (!token) {
|
|
59
|
-
core.setFailed('GITHUB_TOKEN is required for changed-file scanning')
|
|
60
|
-
return
|
|
61
|
-
}
|
|
62
|
-
const octokit = github.getOctokit(token)
|
|
63
|
-
const { data: prFiles } = await octokit.rest.pulls.listFiles({
|
|
64
|
-
...github.context.repo,
|
|
65
|
-
pull_number: github.context.payload.pull_request.number,
|
|
66
|
-
per_page: 100,
|
|
67
|
-
})
|
|
68
|
-
filePaths = prFiles
|
|
69
|
-
.filter((f) => f.status !== 'removed')
|
|
70
|
-
.map((f) => f.filename)
|
|
71
|
-
.filter((f) => supportedExts.has(extname(f)))
|
|
72
|
-
} else {
|
|
73
|
-
filePaths = walkDir('.', supportedExts)
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Read file contents
|
|
77
|
-
const files = new Map<string, string>()
|
|
78
|
-
for (const fp of filePaths) {
|
|
79
|
-
try {
|
|
80
|
-
const stat = statSync(fp)
|
|
81
|
-
if (stat.size > 5 * 1024 * 1024) continue
|
|
82
|
-
files.set(fp, readFileSync(fp, 'utf-8'))
|
|
83
|
-
} catch {
|
|
84
|
-
// Skip unreadable files
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
core.info(`Scanning ${files.size} files...`)
|
|
89
|
-
|
|
90
|
-
// Run detection
|
|
91
|
-
const result = await analyzer.analyzeFiles(files)
|
|
92
|
-
const totalFlags = result.totalFlags.size
|
|
93
|
-
|
|
94
|
-
// Collect language stats
|
|
95
|
-
const langStats: Record<string, number> = {}
|
|
96
|
-
for (const [lang, count] of result.languages) {
|
|
97
|
-
langStats[lang] = count
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// Collect detected providers
|
|
101
|
-
const allFlags: FeatureFlag[] = []
|
|
102
|
-
for (const flags of result.totalFlags.values()) {
|
|
103
|
-
allFlags.push(...(flags as FeatureFlag[]))
|
|
104
|
-
}
|
|
105
|
-
const providers = [...new Set(
|
|
106
|
-
allFlags.map(f => f.provider).filter((p): p is string => p !== null && p !== undefined && p !== '')
|
|
107
|
-
)]
|
|
108
|
-
|
|
109
|
-
core.info(`Detection complete: ${totalFlags} unique flags across ${Object.keys(langStats).length} languages`)
|
|
110
|
-
|
|
111
|
-
// Run staleness analysis
|
|
112
|
-
const staleFlags = await analyzeStaleness(result.totalFlags as Map<string, FeatureFlag[]>, {
|
|
113
|
-
thresholdMonths: threshold,
|
|
114
|
-
repoRoot: process.cwd(),
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
const uniqueStaleNames = new Set(staleFlags.map((f) => f.name)).size
|
|
118
|
-
const healthScore =
|
|
119
|
-
totalFlags > 0 ? Math.round(((totalFlags - uniqueStaleNames) / totalFlags) * 100) : 100
|
|
120
|
-
const scanDuration = Date.now() - startTime
|
|
121
|
-
|
|
122
|
-
// Pretty log output
|
|
123
|
-
core.info('')
|
|
124
|
-
core.info('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ')
|
|
125
|
-
core.info('โ ๐ฆ FlagShark Scan Results โ')
|
|
126
|
-
core.info('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโค')
|
|
127
|
-
core.info(`โ Files scanned: ${String(files.size).padStart(6)} โ`)
|
|
128
|
-
core.info(`โ Languages: ${String(Object.keys(langStats).length).padStart(6)} โ`)
|
|
129
|
-
core.info(`โ Flags detected: ${String(totalFlags).padStart(6)} โ`)
|
|
130
|
-
core.info(`โ Stale flags: ${String(uniqueStaleNames).padStart(6)} โ`)
|
|
131
|
-
core.info(`โ Health score: ${String(healthScore).padStart(3)}/100 โ`)
|
|
132
|
-
core.info(`โ Scan time: ${String(scanDuration).padStart(5)}ms โ`)
|
|
133
|
-
core.info('โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ')
|
|
134
|
-
core.info('')
|
|
135
|
-
|
|
136
|
-
if (providers.length > 0) {
|
|
137
|
-
core.info(`Detected providers: ${providers.slice(0, 8).join(', ')}${providers.length > 8 ? ` (+${providers.length - 8} more)` : ''}`)
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
// Set outputs
|
|
141
|
-
core.setOutput('health-score', healthScore.toString())
|
|
142
|
-
core.setOutput('stale-count', uniqueStaleNames.toString())
|
|
143
|
-
core.setOutput('total-count', totalFlags.toString())
|
|
144
|
-
|
|
145
|
-
// Post PR comment
|
|
146
|
-
if (github.context.payload.pull_request && totalFlags > 0) {
|
|
147
|
-
const token = process.env.GITHUB_TOKEN || core.getInput('token')
|
|
148
|
-
if (token) {
|
|
149
|
-
await postComment(token, staleFlags, totalFlags, healthScore, scanMode, langStats, providers, scanDuration)
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Set status check
|
|
154
|
-
if (failThreshold > 0 && healthScore < failThreshold) {
|
|
155
|
-
core.setFailed(
|
|
156
|
-
`Flag health score ${healthScore}/100 is below threshold ${failThreshold}/100. ` +
|
|
157
|
-
`${uniqueStaleNames} stale flags found.`,
|
|
158
|
-
)
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// Job summary (visible in Actions UI under "Summary" tab)
|
|
162
|
-
const healthEmoji = healthScore >= 90 ? '๐ข' : healthScore >= 70 ? '๐ก' : healthScore >= 40 ? '๐ ' : '๐ด'
|
|
163
|
-
|
|
164
|
-
core.summary.addHeading('๐ฆ FlagShark Scan Results', 2)
|
|
165
|
-
core.summary.addRaw(`\n${healthEmoji} **Health Score: ${healthScore}/100**\n\n`)
|
|
166
|
-
core.summary.addTable([
|
|
167
|
-
[{ data: 'Metric', header: true }, { data: 'Value', header: true }],
|
|
168
|
-
['Files scanned', files.size.toString()],
|
|
169
|
-
['Languages', Object.keys(langStats).join(', ') || 'none'],
|
|
170
|
-
['Total flags', totalFlags.toString()],
|
|
171
|
-
['Stale flags', uniqueStaleNames.toString()],
|
|
172
|
-
['Scan mode', scanMode],
|
|
173
|
-
['Scan time', `${scanDuration}ms`],
|
|
174
|
-
])
|
|
175
|
-
|
|
176
|
-
if (providers.length > 0) {
|
|
177
|
-
core.summary.addRaw(`\n**Detected providers:** ${providers.join(', ')}\n`)
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (uniqueStaleNames > 0) {
|
|
181
|
-
core.summary.addRaw('\n### Top stale flags\n\n')
|
|
182
|
-
core.summary.addTable([
|
|
183
|
-
[{ data: 'Flag', header: true }, { data: 'File', header: true }, { data: 'Age', header: true }, { data: 'Signal', header: true }],
|
|
184
|
-
...staleFlags.slice(0, 15).map(f => [
|
|
185
|
-
`\`${f.name}\``,
|
|
186
|
-
`${f.filePath}:${f.lineNumber}`,
|
|
187
|
-
f.age || 'unknown',
|
|
188
|
-
f.signals.map(s => s.description).join(', '),
|
|
189
|
-
]),
|
|
190
|
-
])
|
|
191
|
-
if (staleFlags.length > 15) {
|
|
192
|
-
core.summary.addRaw(`\n*... and ${staleFlags.length - 15} more stale flags*\n`)
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
core.summary.addRaw('\n---\n')
|
|
197
|
-
core.summary.addRaw('*Powered by [FlagShark](https://github.com/FlagShark/flagshark) โ find stale feature flags before they cause incidents*\n')
|
|
198
|
-
core.summary.addRaw('\n[Automate flag cleanup](https://flagshark.com) ยท [Open source CLI](https://github.com/FlagShark/flagshark) ยท [Report an issue](https://github.com/FlagShark/flagshark/issues)\n')
|
|
199
|
-
|
|
200
|
-
await core.summary.write()
|
|
201
|
-
|
|
202
|
-
} catch (error) {
|
|
203
|
-
if (error instanceof Error) {
|
|
204
|
-
core.setFailed(error.message)
|
|
205
|
-
} else {
|
|
206
|
-
core.setFailed('An unexpected error occurred')
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
async function postComment(
|
|
212
|
-
token: string,
|
|
213
|
-
staleFlags: StaleFlag[],
|
|
214
|
-
totalFlags: number,
|
|
215
|
-
healthScore: number,
|
|
216
|
-
scanMode: string,
|
|
217
|
-
langStats: Record<string, number>,
|
|
218
|
-
providers: string[],
|
|
219
|
-
scanDuration: number,
|
|
220
|
-
): Promise<void> {
|
|
221
|
-
const octokit = github.getOctokit(token)
|
|
222
|
-
const { owner, repo } = github.context.repo
|
|
223
|
-
const prNumber = github.context.payload.pull_request!.number
|
|
224
|
-
|
|
225
|
-
const uniqueStaleCount = new Set(staleFlags.map((f) => f.name)).size
|
|
226
|
-
const modeLabel = scanMode === 'full' ? 'Full repo scan' : 'Changed files only'
|
|
227
|
-
const healthEmoji = healthScore >= 90 ? '๐ข' : healthScore >= 70 ? '๐ก' : healthScore >= 40 ? '๐ ' : '๐ด'
|
|
228
|
-
const langList = Object.entries(langStats).map(([l, c]) => `${l} (${c})`).join(', ')
|
|
229
|
-
const providerList = providers.length > 0
|
|
230
|
-
? providers.slice(0, 5).join(', ') + (providers.length > 5 ? ` +${providers.length - 5} more` : '')
|
|
231
|
-
: 'none detected'
|
|
232
|
-
|
|
233
|
-
let body = `${COMMENT_MARKER}\n`
|
|
234
|
-
|
|
235
|
-
// Header
|
|
236
|
-
if (uniqueStaleCount === 0) {
|
|
237
|
-
body += `## ๐ฆ FlagShark โ All flags healthy\n\n`
|
|
238
|
-
} else {
|
|
239
|
-
body += `## ๐ฆ FlagShark โ ${uniqueStaleCount} stale flag${uniqueStaleCount !== 1 ? 's' : ''} found\n\n`
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// Health score badge
|
|
243
|
-
body += `${healthEmoji} **Health Score: ${healthScore}/100**\n\n`
|
|
244
|
-
|
|
245
|
-
// Stats row
|
|
246
|
-
body += `| Metric | Value |\n`
|
|
247
|
-
body += `|--------|-------|\n`
|
|
248
|
-
body += `| Flags detected | ${totalFlags} |\n`
|
|
249
|
-
body += `| Stale flags | ${uniqueStaleCount} |\n`
|
|
250
|
-
body += `| Languages | ${langList} |\n`
|
|
251
|
-
body += `| Providers | ${providerList} |\n`
|
|
252
|
-
body += `| Scan mode | ${modeLabel} |\n`
|
|
253
|
-
body += `| Scan time | ${scanDuration}ms |\n\n`
|
|
254
|
-
|
|
255
|
-
// Stale flags table
|
|
256
|
-
if (uniqueStaleCount > 0) {
|
|
257
|
-
body += `<details${uniqueStaleCount <= 5 ? ' open' : ''}>\n`
|
|
258
|
-
body += `<summary><strong>Stale flags (${uniqueStaleCount})</strong></summary>\n\n`
|
|
259
|
-
body += '| Flag | File | Age | Why it looks stale |\n'
|
|
260
|
-
body += '|------|------|-----|--------------------|\n'
|
|
261
|
-
|
|
262
|
-
const displayFlags = staleFlags.slice(0, 20)
|
|
263
|
-
for (const flag of displayFlags) {
|
|
264
|
-
const signals = flag.signals.map(s => s.description).join(', ')
|
|
265
|
-
const shortPath = flag.filePath.replace(/^\.\//, '')
|
|
266
|
-
body += `| \`${flag.name}\` | \`${shortPath}:${flag.lineNumber}\` | ${flag.age || 'unknown'} | ${signals} |\n`
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
if (staleFlags.length > 20) {
|
|
270
|
-
body += `\n*... and ${staleFlags.length - 20} more. Run \`npx flagshark scan --verbose\` locally for the full list.*\n`
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
body += '\n</details>\n\n'
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// Footer with links
|
|
277
|
-
body += '---\n'
|
|
278
|
-
body += `*[FlagShark](https://github.com/FlagShark/flagshark) finds stale feature flags before they cause incidents*\n\n`
|
|
279
|
-
body += `[Automate flag cleanup](https://flagshark.com) ยท `
|
|
280
|
-
body += `[Install CLI](https://www.npmjs.com/package/flagshark) ยท `
|
|
281
|
-
body += `[Open source](https://github.com/FlagShark/flagshark)\n`
|
|
282
|
-
|
|
283
|
-
// Find existing comment to update
|
|
284
|
-
const { data: comments } = await octokit.rest.issues.listComments({
|
|
285
|
-
owner, repo, issue_number: prNumber, per_page: 100,
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
const existing = comments.find((c) => c.body?.includes(COMMENT_MARKER))
|
|
289
|
-
|
|
290
|
-
if (existing) {
|
|
291
|
-
await octokit.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body })
|
|
292
|
-
core.info('Updated existing FlagShark comment')
|
|
293
|
-
} else {
|
|
294
|
-
await octokit.rest.issues.createComment({ owner, repo, issue_number: prNumber, body })
|
|
295
|
-
core.info('Posted new FlagShark comment')
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function walkDir(dir: string, supportedExts: Set<string>): string[] {
|
|
300
|
-
const results: string[] = []
|
|
301
|
-
try {
|
|
302
|
-
const entries = readdirSync(dir, { withFileTypes: true })
|
|
303
|
-
for (const entry of entries) {
|
|
304
|
-
if (SKIP_DIRS.has(entry.name) || entry.name.startsWith('.')) continue
|
|
305
|
-
const fullPath = join(dir, entry.name)
|
|
306
|
-
if (entry.isDirectory()) {
|
|
307
|
-
results.push(...walkDir(fullPath, supportedExts))
|
|
308
|
-
} else if (entry.isFile() && supportedExts.has(extname(entry.name))) {
|
|
309
|
-
results.push(fullPath)
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
} catch { /* skip unreadable dirs */ }
|
|
313
|
-
return results
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
run()
|