@cleartrip/frontguard 0.2.9 → 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/dist/index.d.ts CHANGED
@@ -58,8 +58,9 @@ interface FrontGuardConfig {
58
58
  gateWhenAiDisclosureAmbiguous: CheckGate;
59
59
  };
60
60
  /**
61
- * Stricter heuristics on AI-generated code.
62
- * Prefer marking regions with `// @frontguard-ai:start` `// @frontguard-ai:end` (or `// written by AI: start` … `end`).
61
+ * **Under development — disabled by default.** Stricter static heuristics on AI-generated code
62
+ * (decorator regions or PR disclosure). When enabled, also drives escalation in `ai-escalation.ts`.
63
+ * Prefer marking regions with `// @frontguard-ai:start` … `// @frontguard-ai:end`.
63
64
  * See `strictScanMode` for interaction with PR AI disclosure.
64
65
  */
65
66
  aiAssistedReview: {
@@ -121,25 +122,41 @@ interface FrontGuardConfig {
121
122
  maxReportLines: number;
122
123
  };
123
124
  /**
124
- * Static bundle size: optionally run `buildCommand`, then sum bytes under `measureGlobs`.
125
- * For default `npm run build`, the run is skipped (info) when `package.json` has no matching script — use `buildCommand` / `runBuild: false` for non-standard repos.
125
+ * Bundle size: run `buildCommand`, extract the size metric, compare to a baseline from the base branch.
126
+ *
127
+ * **`bundleSizeStrategy`** selects how to read the bundle size:
128
+ * - `'auto'` — pick from detected stack (Next→next, Vite→vite, CRA→cra, else glob).
129
+ * - `'next'` — parse `next build` stdout for "First Load JS shared by all" (kB).
130
+ * - `'vite'` — sum `dist/assets/*.js` (or `measureGlobs`) after `vite build`.
131
+ * - `'cra'` — parse `react-scripts build` stdout for the gzipped JS total.
132
+ * - `'glob'` — sum raw bytes under `measureGlobs` (generic fallback).
133
+ * - `'custom'` — run `bundleSizeCommand`, read a single number (bytes) from stdout.
126
134
  */
127
135
  bundle: {
128
136
  enabled: boolean;
129
137
  gate: CheckGate;
130
- /** When false, only measures existing files (no build). Set false for libs/backends with no web bundle. */
138
+ /** When false, only measures existing files (no build). */
131
139
  runBuild: boolean;
132
- /** E.g. `npm run build`, `pnpm run vite-build`, or any command that produces artifacts under measureGlobs */
140
+ /** E.g. `npm run build`, `yarn build:prod`, or any command that produces output. */
133
141
  buildCommand: string;
142
+ /** How to extract the size number after build. See JSDoc above. */
143
+ bundleSizeStrategy: 'auto' | 'next' | 'vite' | 'cra' | 'glob' | 'custom';
144
+ /**
145
+ * Command that prints a single number (bytes) to stdout. Used only when `bundleSizeStrategy: 'custom'`.
146
+ * E.g. `"node scripts/bundle-size.js"`.
147
+ */
148
+ bundleSizeCommand: string | null;
149
+ /** Globs for `'glob'` and `'vite'` strategies. */
134
150
  measureGlobs: string[];
135
151
  baselinePath: string;
136
152
  /**
137
153
  * Git ref used to read `baselinePath` from the base branch when the file is not on disk.
138
- * Bitbucket PR pipelines set `BITBUCKET_PR_DESTINATION_BRANCH`, which is preferred for resolving the ref.
139
- * Separate from `tsAnyDelta.baseRef` so bundle can track a release branch.
154
+ * Bitbucket PR pipelines set `BITBUCKET_PR_DESTINATION_BRANCH`, which is preferred.
140
155
  */
141
156
  baselineRef: string;
157
+ /** Max allowed growth vs baseline in bytes; null = no limit. */
142
158
  maxDeltaBytes: number | null;
159
+ /** Absolute cap in bytes; null = no limit. */
143
160
  maxTotalBytes: number | null;
144
161
  };
145
162
  /**
@@ -152,6 +169,9 @@ interface FrontGuardConfig {
152
169
  scanGlobs: string[];
153
170
  maxFileBytes: number;
154
171
  };
172
+ /**
173
+ * **Under development — disabled by default.** Optional automated review / fix hints via OpenAI, Anthropic, or Ollama.
174
+ */
155
175
  llm: {
156
176
  enabled: boolean;
157
177
  provider: 'openai' | 'anthropic' | 'ollama';
package/dist/index.js CHANGED
@@ -75,7 +75,7 @@ var defaultConfig = {
75
75
  gateWhenAiDisclosureAmbiguous: "warn"
76
76
  },
77
77
  aiAssistedReview: {
78
- enabled: true,
78
+ enabled: false,
79
79
  gate: "warn",
80
80
  strictScanMode: "both",
81
81
  escalate: {
@@ -107,6 +107,8 @@ var defaultConfig = {
107
107
  gate: "warn",
108
108
  runBuild: true,
109
109
  buildCommand: "npm run build",
110
+ bundleSizeStrategy: "auto",
111
+ bundleSizeCommand: null,
110
112
  measureGlobs: ["dist/**/*", "build/static/**/*", ".next/static/**/*"],
111
113
  baselinePath: ".frontguard/bundle-baseline.json",
112
114
  baselineRef: "main",
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../node_modules/defu/dist/defu.mjs","../src/config/defaults.ts","../src/config/migrate.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["function isPlainObject(value) {\n if (value === null || typeof value !== \"object\") {\n return false;\n }\n const prototype = Object.getPrototypeOf(value);\n if (prototype !== null && prototype !== Object.prototype && Object.getPrototypeOf(prototype) !== null) {\n return false;\n }\n if (Symbol.iterator in value) {\n return false;\n }\n if (Symbol.toStringTag in value) {\n return Object.prototype.toString.call(value) === \"[object Module]\";\n }\n return true;\n}\n\nfunction _defu(baseObject, defaults, namespace = \".\", merger) {\n if (!isPlainObject(defaults)) {\n return _defu(baseObject, {}, namespace, merger);\n }\n const object = { ...defaults };\n for (const key of Object.keys(baseObject)) {\n if (key === \"__proto__\" || key === \"constructor\") {\n continue;\n }\n const value = baseObject[key];\n if (value === null || value === void 0) {\n continue;\n }\n if (merger && merger(object, key, value, namespace)) {\n continue;\n }\n if (Array.isArray(value) && Array.isArray(object[key])) {\n object[key] = [...value, ...object[key]];\n } else if (isPlainObject(value) && isPlainObject(object[key])) {\n object[key] = _defu(\n value,\n object[key],\n (namespace ? `${namespace}.` : \"\") + key.toString(),\n merger\n );\n } else {\n object[key] = value;\n }\n }\n return object;\n}\nfunction createDefu(merger) {\n return (...arguments_) => (\n // eslint-disable-next-line unicorn/no-array-reduce\n arguments_.reduce((p, c) => _defu(p, c, \"\", merger), {})\n );\n}\nconst defu = createDefu();\nconst defuFn = createDefu((object, key, currentValue) => {\n if (object[key] !== void 0 && typeof currentValue === \"function\") {\n object[key] = currentValue(object[key]);\n return true;\n }\n});\nconst defuArrayFn = createDefu((object, key, currentValue) => {\n if (Array.isArray(object[key]) && typeof currentValue === \"function\") {\n object[key] = currentValue(object[key]);\n return true;\n }\n});\n\nexport { createDefu, defu as default, defu, defuArrayFn, defuFn };\n","import type { FrontGuardConfig } from './schema.js'\n\nexport const defaultConfig: FrontGuardConfig = {\n mode: 'warn',\n rules: {},\n checks: {\n eslint: { enabled: true, glob: '**/*.{js,cjs,mjs,jsx,ts,tsx}' },\n prettier: {\n enabled: true,\n glob: '**/*.{js,cjs,mjs,jsx,ts,tsx,json,md,css,scss,yml,yaml}',\n },\n typescript: { enabled: true },\n secrets: { enabled: true },\n prHygiene: {\n enabled: true,\n minBodyLength: 80,\n requireSections: false,\n sectionHints: ['what', 'why', 'test', 'how to test', 'screenshot'],\n requireAiDisclosureSection: true,\n gateWhenAiDisclosureAmbiguous: 'warn',\n },\n aiAssistedReview: {\n enabled: true,\n gate: 'warn',\n strictScanMode: 'both',\n escalate: {\n secretFindingsToBlock: true,\n tsAnyDeltaToBlock: true,\n },\n },\n prSize: { enabled: true, warnLines: 400, softBlockLines: 800 },\n\n tsAnyDelta: {\n enabled: true,\n gate: 'warn',\n baseRef: 'main',\n maxAdded: 0,\n },\n cycles: {\n enabled: true,\n gate: 'warn',\n entries: ['src'],\n extraArgs: [],\n },\n deadCode: {\n enabled: true,\n gate: 'info',\n extraArgs: [],\n maxReportLines: 80,\n },\n bundle: {\n enabled: true,\n gate: 'warn',\n runBuild: true,\n buildCommand: 'npm run build',\n measureGlobs: ['dist/**/*', 'build/static/**/*', '.next/static/**/*'],\n baselinePath: '.frontguard/bundle-baseline.json',\n baselineRef: 'main',\n maxDeltaBytes: null,\n maxTotalBytes: null,\n },\n coreWebVitals: {\n enabled: true,\n gate: 'warn',\n scanGlobs: ['app/**/*.{tsx,jsx}', 'pages/**/*.{tsx,jsx}', 'src/**/*.{tsx,jsx}'],\n maxFileBytes: 400_000,\n },\n llm: {\n enabled: false,\n provider: 'openai',\n model: 'gpt-4o-mini',\n apiKeyEnv: 'OPENAI_API_KEY',\n maxDiffChars: 48_000,\n timeoutMs: 60_000,\n ollamaUrl: 'http://127.0.0.1:11434',\n perFindingFixes: false,\n maxFixSuggestions: 12,\n maxFileContextChars: 24_000,\n },\n },\n}\n","import type { FrontGuardConfig } from './schema.js'\n\n/** Normalize legacy config keys before merging with defaults. */\nexport function migrateLegacyConfigKeys(config: Partial<FrontGuardConfig>): void {\n const ch = config.checks as Record<string, unknown> | undefined\n if (!ch) return\n if ('cwv' in ch && !('coreWebVitals' in ch)) {\n ch.coreWebVitals = ch.cwv\n delete ch.cwv\n }\n}\n","import defu from 'defu'\nimport type { FrontGuardConfig } from './config/schema.js'\nimport { defaultConfig } from './config/defaults.js'\nimport { migrateLegacyConfigKeys } from './config/migrate.js'\n\nexport type {\n FrontGuardConfig,\n GuardMode,\n CheckGate,\n RulesMap,\n CustomRuleDefinition,\n RuleFileContext,\n PrSizeTier,\n} from './config/schema.js'\nexport { defaultConfig } from './config/defaults.js'\n\n/**\n * Define repo-level config; shallow-merged over built-in defaults.\n * Org packages can be referenced via `extends` in config file.\n */\nexport function defineConfig(partial: Partial<FrontGuardConfig>): FrontGuardConfig {\n migrateLegacyConfigKeys(partial)\n return defu(partial, defaultConfig) as FrontGuardConfig\n}\n"]}
1
+ {"version":3,"sources":["../node_modules/defu/dist/defu.mjs","../src/config/defaults.ts","../src/config/migrate.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["function isPlainObject(value) {\n if (value === null || typeof value !== \"object\") {\n return false;\n }\n const prototype = Object.getPrototypeOf(value);\n if (prototype !== null && prototype !== Object.prototype && Object.getPrototypeOf(prototype) !== null) {\n return false;\n }\n if (Symbol.iterator in value) {\n return false;\n }\n if (Symbol.toStringTag in value) {\n return Object.prototype.toString.call(value) === \"[object Module]\";\n }\n return true;\n}\n\nfunction _defu(baseObject, defaults, namespace = \".\", merger) {\n if (!isPlainObject(defaults)) {\n return _defu(baseObject, {}, namespace, merger);\n }\n const object = { ...defaults };\n for (const key of Object.keys(baseObject)) {\n if (key === \"__proto__\" || key === \"constructor\") {\n continue;\n }\n const value = baseObject[key];\n if (value === null || value === void 0) {\n continue;\n }\n if (merger && merger(object, key, value, namespace)) {\n continue;\n }\n if (Array.isArray(value) && Array.isArray(object[key])) {\n object[key] = [...value, ...object[key]];\n } else if (isPlainObject(value) && isPlainObject(object[key])) {\n object[key] = _defu(\n value,\n object[key],\n (namespace ? `${namespace}.` : \"\") + key.toString(),\n merger\n );\n } else {\n object[key] = value;\n }\n }\n return object;\n}\nfunction createDefu(merger) {\n return (...arguments_) => (\n // eslint-disable-next-line unicorn/no-array-reduce\n arguments_.reduce((p, c) => _defu(p, c, \"\", merger), {})\n );\n}\nconst defu = createDefu();\nconst defuFn = createDefu((object, key, currentValue) => {\n if (object[key] !== void 0 && typeof currentValue === \"function\") {\n object[key] = currentValue(object[key]);\n return true;\n }\n});\nconst defuArrayFn = createDefu((object, key, currentValue) => {\n if (Array.isArray(object[key]) && typeof currentValue === \"function\") {\n object[key] = currentValue(object[key]);\n return true;\n }\n});\n\nexport { createDefu, defu as default, defu, defuArrayFn, defuFn };\n","import type { FrontGuardConfig } from './schema.js'\n\nexport const defaultConfig: FrontGuardConfig = {\n mode: 'warn',\n rules: {},\n checks: {\n eslint: { enabled: true, glob: '**/*.{js,cjs,mjs,jsx,ts,tsx}' },\n prettier: {\n enabled: true,\n glob: '**/*.{js,cjs,mjs,jsx,ts,tsx,json,md,css,scss,yml,yaml}',\n },\n typescript: { enabled: true },\n secrets: { enabled: true },\n prHygiene: {\n enabled: true,\n minBodyLength: 80,\n requireSections: false,\n sectionHints: ['what', 'why', 'test', 'how to test', 'screenshot'],\n requireAiDisclosureSection: true,\n gateWhenAiDisclosureAmbiguous: 'warn',\n },\n aiAssistedReview: {\n enabled: false,\n gate: 'warn',\n strictScanMode: 'both',\n escalate: {\n secretFindingsToBlock: true,\n tsAnyDeltaToBlock: true,\n },\n },\n prSize: { enabled: true, warnLines: 400, softBlockLines: 800 },\n\n tsAnyDelta: {\n enabled: true,\n gate: 'warn',\n baseRef: 'main',\n maxAdded: 0,\n },\n cycles: {\n enabled: true,\n gate: 'warn',\n entries: ['src'],\n extraArgs: [],\n },\n deadCode: {\n enabled: true,\n gate: 'info',\n extraArgs: [],\n maxReportLines: 80,\n },\n bundle: {\n enabled: true,\n gate: 'warn',\n runBuild: true,\n buildCommand: 'npm run build',\n bundleSizeStrategy: 'auto',\n bundleSizeCommand: null,\n measureGlobs: ['dist/**/*', 'build/static/**/*', '.next/static/**/*'],\n baselinePath: '.frontguard/bundle-baseline.json',\n baselineRef: 'main',\n maxDeltaBytes: null,\n maxTotalBytes: null,\n },\n coreWebVitals: {\n enabled: true,\n gate: 'warn',\n scanGlobs: ['app/**/*.{tsx,jsx}', 'pages/**/*.{tsx,jsx}', 'src/**/*.{tsx,jsx}'],\n maxFileBytes: 400_000,\n },\n llm: {\n enabled: false,\n provider: 'openai',\n model: 'gpt-4o-mini',\n apiKeyEnv: 'OPENAI_API_KEY',\n maxDiffChars: 48_000,\n timeoutMs: 60_000,\n ollamaUrl: 'http://127.0.0.1:11434',\n perFindingFixes: false,\n maxFixSuggestions: 12,\n maxFileContextChars: 24_000,\n },\n },\n}\n","import type { FrontGuardConfig } from './schema.js'\n\n/** Normalize legacy config keys before merging with defaults. */\nexport function migrateLegacyConfigKeys(config: Partial<FrontGuardConfig>): void {\n const ch = config.checks as Record<string, unknown> | undefined\n if (!ch) return\n if ('cwv' in ch && !('coreWebVitals' in ch)) {\n ch.coreWebVitals = ch.cwv\n delete ch.cwv\n }\n}\n","import defu from 'defu'\nimport type { FrontGuardConfig } from './config/schema.js'\nimport { defaultConfig } from './config/defaults.js'\nimport { migrateLegacyConfigKeys } from './config/migrate.js'\n\nexport type {\n FrontGuardConfig,\n GuardMode,\n CheckGate,\n RulesMap,\n CustomRuleDefinition,\n RuleFileContext,\n PrSizeTier,\n} from './config/schema.js'\nexport { defaultConfig } from './config/defaults.js'\n\n/**\n * Define repo-level config; shallow-merged over built-in defaults.\n * Org packages can be referenced via `extends` in config file.\n */\nexport function defineConfig(partial: Partial<FrontGuardConfig>): FrontGuardConfig {\n migrateLegacyConfigKeys(partial)\n return defu(partial, defaultConfig) as FrontGuardConfig\n}\n"]}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@cleartrip/frontguard",
3
- "version": "0.2.9",
4
- "description": "Org-wide frontend PR guardrails: linting, hygiene, any-delta, cycles, dead code, bundle/CWV hints, custom rules, optional LLM brief",
3
+ "version": "0.3.1",
4
+ "description": "Org-wide frontend PR guardrails for Bitbucket: lint, types, secrets, bundle, PR hygiene, and more (AI-assisted / LLM layers WIP, off by default)",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "frontguard": "./dist/cli.js"
@@ -26,7 +26,6 @@
26
26
  "build": "tsup",
27
27
  "dev": "tsup --watch",
28
28
  "typecheck": "tsc --noEmit",
29
- "test": "npm run build && tsx --test src/lib/bitbucket-checks-image-md.test.ts src/runner/checks-snapshot-png.integration.test.ts",
30
29
  "format": "prettier --write .",
31
30
  "prepack": "npm run build"
32
31
  },
@@ -39,7 +38,6 @@
39
38
  ],
40
39
  "license": "MIT",
41
40
  "dependencies": {
42
- "@resvg/resvg-js": "^2.6.2",
43
41
  "fast-glob": "^3.3.3",
44
42
  "typescript": "^5.8.2"
45
43
  },
@@ -50,7 +48,6 @@
50
48
  "picocolors": "^1.1.1",
51
49
  "prettier": "^3.5.3",
52
50
  "tinyexec": "^1.0.1",
53
- "tsup": "^8.4.0",
54
- "tsx": "^4.21.0"
51
+ "tsup": "^8.4.0"
55
52
  }
56
53
  }
@@ -1,15 +1,11 @@
1
- # FrontGuard — PR comment: checks screenshot + FreeKit full report
1
+ # FrontGuard — reference pipeline (Bitbucket Cloud)
2
2
  #
3
- # Bitbucket’s PR renderer does NOT load data:image/... (base64) — it shows “Preview unavailable”.
4
- # This pipeline uploads the PNG to Repository Downloads, then uses an https://bitbucket.org/.../downloads/... URL in Markdown.
5
- # Token must allow: pull request comments AND POST …/downloads (Repositories: write / equivalent API scope).
6
- # • Raster: @resvg/resvg-js (no browser). Full HTML report → FreeKit; comment links there for details.
3
+ # This is the FULL pipeline that `frontguard init` generates.
4
+ # In most cases, run `frontguard init` instead of copying this manually
5
+ # it merges the FrontGuard step into your existing bitbucket-pipelines.yml.
7
6
  #
8
- # Secured variable: BITBUCKET_ACCESS_TOKEN comments + downloads upload (see above).
9
- #
10
- # Optional: FREEKIT_BASE_URL — override FreeKit API host (default https://freekit.dev).
11
- #
12
- # Deeper clone avoids shallow-git errors (e.g. merge-base / parent rev) for diff-based checks.
7
+ # Required secured variable: BITBUCKET_ACCESS_TOKEN (PR comment permission)
8
+ # Optional: FREEKIT_BASE_URL — override the report hosting endpoint
13
9
 
14
10
  image: node:20
15
11
 
@@ -26,40 +22,24 @@ pipelines:
26
22
  artifacts:
27
23
  - frontguard-report.html
28
24
  - frontguard-report.md
29
- - frontguard-checks.html
30
- - frontguard-checks.png
25
+ - frontguard-pr-comment.partial.md
31
26
  script:
32
27
  - corepack enable
33
- - apt-get update && apt-get install -y --no-install-recommends fonts-dejavu-core fonts-liberation
34
28
  - yarn install --immutable || yarn install
35
29
  - |
36
30
  yarn frontguard run --markdown \
37
31
  --htmlOut frontguard-report.html \
38
- --checksSnapshotOut frontguard-checks.html \
39
- --checksPngOut frontguard-checks.png \
32
+ --prCommentOut frontguard-pr-comment.partial.md \
40
33
  > frontguard-report.md
34
+ - test -n "${BITBUCKET_ACCESS_TOKEN:-}" || { echo "Missing secured var BITBUCKET_ACCESS_TOKEN"; exit 1; }
41
35
  - |
42
- test -n "${BITBUCKET_ACCESS_TOKEN:-}" || { echo "Missing secured var BITBUCKET_ACCESS_TOKEN"; exit 1; }
43
- BB_PNG_NAME="frontguard-checks-pipeline-${BITBUCKET_BUILD_NUMBER}-pr-${BITBUCKET_PR_ID}.png"
44
- cp frontguard-checks.png "${BB_PNG_NAME}"
45
- if curl --silent --show-error --fail --request POST \
46
- --url "https://api.bitbucket.org/2.0/repositories/${BITBUCKET_REPO_FULL_NAME}/downloads" \
47
- --header "Authorization: Bearer ${BITBUCKET_ACCESS_TOKEN}" \
48
- --form "files=@${BB_PNG_NAME}"; then
49
- export FRONTGUARD_CHECKS_IMAGE_URL="https://bitbucket.org/${BITBUCKET_REPO_FULL_NAME}/downloads/${BB_PNG_NAME}"
50
- echo "Uploaded checks PNG to Downloads; image URL set for PR comment."
51
- else
52
- echo "WARNING: Downloads upload failed (token needs repository write for uploads). PR comment will not include an inline image."
53
- export FRONTGUARD_CHECKS_IMAGE_URL=""
54
- fi
55
36
  python3 << 'PY'
56
37
  import json
57
38
  import os
58
39
  from urllib.error import HTTPError
59
40
  from urllib.request import Request, urlopen
60
41
 
61
- DETAILED = "For detailed check analysis, please open the full interactive report:"
62
- PNG_SIG = bytes([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A])
42
+ PLACEHOLDER = "__FRONTGUARD_REPORT_URL__"
63
43
 
64
44
  base = os.environ.get("FREEKIT_BASE_URL", "https://freekit.dev").rstrip("/")
65
45
  with open("frontguard-report.html", encoding="utf-8") as f:
@@ -87,22 +67,14 @@ pipelines:
87
67
  if not report_url:
88
68
  raise SystemExit(f"FreeKit: missing data.url in {json.dumps(parsed)[:2000]}")
89
69
 
90
- with open("frontguard-checks.png", "rb") as f:
91
- raw = f.read()
92
- if len(raw) < 8 or raw[:8] != PNG_SIG:
93
- raise SystemExit("frontguard-checks.png is missing or not a valid PNG file.")
94
-
95
- img_url = (os.environ.get("FRONTGUARD_CHECKS_IMAGE_URL") or "").strip()
96
- if img_url:
97
- # Bitbucket PR comments: only normal https:// URLs render; data: URIs show “Preview unavailable”.
98
- img_line = f"![FrontGuard checks summary]({img_url})"
99
- else:
100
- img_line = (
101
- "_Checks summary image: open **Pipeline artifacts** (`frontguard-checks.png`) "
102
- "or grant the pipeline token permission to **upload to Repository → Downloads**._"
70
+ with open("frontguard-pr-comment.partial.md", encoding="utf-8") as f:
71
+ body = f.read()
72
+ if PLACEHOLDER not in body:
73
+ raise SystemExit(
74
+ f"Expected {PLACEHOLDER!r} in frontguard-pr-comment.partial.md — regenerate with current FrontGuard."
103
75
  )
76
+ body = body.replace(PLACEHOLDER, report_url, 1)
104
77
 
105
- body = f"{img_line}\n\n{DETAILED}\n{report_url}\n"
106
78
  with open("frontguard-pr-comment.md", "w", encoding="utf-8") as out:
107
79
  out.write(body)
108
80
  with open("frontguard-payload.json", "w", encoding="utf-8") as out:
@@ -1 +0,0 @@
1
- # Deprecated: use `templates/bitbucket-pipelines.yml` — it now uses `--checksPngOut` + inline PNG.
@@ -1,33 +0,0 @@
1
- # FreeKit.dev + FrontGuard (Bitbucket Pipelines)
2
-
3
- The Bitbucket template runs FrontGuard with **`--checksPngOut`** (SVG → PNG via **`@resvg/resvg-js`**), uploads the full HTML to **FreeKit**, **uploads the checks PNG to Repository → Downloads**, and posts a PR comment with **`![alt](https://bitbucket.org/.../downloads/...png)`** plus the FreeKit URL. Bitbucket does **not** render `data:image/...` in PR comments (you get “Preview unavailable”), so the template uses an HTTPS Downloads link instead. **`BITBUCKET_ACCESS_TOKEN`** must allow **PR comments** and **POST** to **`/2.0/repositories/{workspace}/{repo}/downloads`** (Repositories write / equivalent).
4
-
5
- The same step uploads `frontguard-report.html` to FreeKit’s public API:
6
-
7
- - **Docs:** [FreeKit API](https://freekit.dev/docs)
8
- - **Endpoint:** `POST https://freekit.dev/api/v1/sites` with JSON `{"html": "<full report html>"}`
9
- - **Response:** `data.url` like `https://freekit.dev/s/<siteId>` — this URL is included in the PR comment below the checks screenshot caption.
10
-
11
- Optional env (advanced): **`FREEKIT_BASE_URL`** — override the API host (e.g. self‑hosted instance per FreeKit docs).
12
-
13
- ## PR checks (`pr-hygiene`, `pr-size`)
14
-
15
- On **Bitbucket Pipelines** pull-request builds, FrontGuard reads **`BITBUCKET_PR_ID`** (and related branch env vars) and computes diff size with **`git diff <destination>...HEAD --numstat`**, so **`pr-size` runs** when the destination ref exists in the clone.
16
-
17
- - **`pr-hygiene`** needs the PR description text. Bitbucket does not expose the body in default env vars — set **`FRONTGUARD_PR_BODY`** in the pipeline (e.g. fetch from the [Bitbucket PR API](https://developer.atlassian.com/cloud/bitbucket/rest/api-group-pullrequests/) in a prior step and `export` it) if you want body-length / section / AI-disclosure rules to apply.
18
- - **Shallow clones:** If `git` cannot resolve `BITBUCKET_PR_DESTINATION_BRANCH`, add a **`git fetch`** of the base branch so `pr-size` can measure the PR (see Bitbucket “clone depth” docs).
19
-
20
- ## Things to know
21
-
22
- - **Public:** Anyone with the link can open the report. Do not use for highly sensitive content unless you add FreeKit **password** protection via their API (requires passing `password` in the JSON — not in this template).
23
- - **Third party:** Uptime, limits, and terms are those of [FreeKit](https://freekit.dev); not controlled by Cleartrip/FrontGuard.
24
- - **Limits:** Per FreeKit docs — e.g. HTML payload size caps, **rate limits** (creates per minute per IP), possible `429`. Large reports or busy runners may need retries or another host.
25
- - **No delete token in CI:** Each run creates a **new** site URL. Old FreeKit URLs may still work until you delete them with the `deleteToken` returned in the API response (this template does not store it).
26
-
27
- ## Checks table image in PR comments
28
-
29
- The pipeline writes **`frontguard-checks.png`**, uploads it via the Downloads API, and references that **HTTPS** URL in Markdown. If the upload fails (token scope), the comment falls back to text pointing at pipeline artifacts. You can still set **`FRONTGUARD_CHECKS_IMAGE_URL`** before a custom comment step if you host the PNG elsewhere.
30
-
31
- ## Compared to Surge
32
-
33
- Surge credentials (`SURGE_LOGIN` / `SURGE_TOKEN`) are **not** required for FreeKit’s open POST flow. If you remove Surge, you can delete any old Surge-related secured variables from the repository.