@michaelbel/cuckcoder-mcp 1.6.13 → 1.6.14
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/assets/agents/architect-auditor.md +182 -0
- package/assets/agents/bug-hunter.md +185 -0
- package/assets/agents/build-engineer.md +148 -0
- package/assets/agents/business-analyst.md +178 -0
- package/assets/agents/code-refine.md +150 -0
- package/assets/agents/code-reviewer.md +180 -0
- package/assets/agents/compose-builder.md +192 -0
- package/assets/agents/device-ui-tester.md +133 -0
- package/assets/agents/devops-expert.md +177 -0
- package/assets/agents/explorer.md +132 -0
- package/assets/agents/github-project-manager.md +203 -0
- package/assets/agents/guide-android-builder.md +81 -0
- package/assets/agents/guide-writer.md +63 -0
- package/assets/agents/kotlin-engineer.md +215 -0
- package/assets/agents/mechanical-operator.md +166 -0
- package/assets/agents/notion-project-manager.md +185 -0
- package/assets/agents/performance-reviewer.md +188 -0
- package/assets/agents/security-auditor.md +203 -0
- package/assets/agents/swift-engineer.md +223 -0
- package/assets/agents/swiftui-builder.md +236 -0
- package/assets/agents/tech-writer.md +214 -0
- package/assets/agents/ux-reviewer.md +235 -0
- package/assets/rules/kotlin-datetime.md +10 -0
- package/assets/rules/resource.md +7 -1
- package/assets/workflows/architecture-sweep.js +98 -0
- package/assets/workflows/business-feature-sweep.js +218 -0
- package/assets/workflows/full-review.js +190 -0
- package/assets/workflows/mvi-compliance-sweep.js +74 -0
- package/assets/workflows/redesign-sweep.js +152 -0
- package/assets/workflows/refactoring-sweep.js +199 -0
- package/assets/workflows/security-sweep.js +98 -0
- package/assets/workflows/task-batch-create.js +104 -0
- package/dist/search.js +38 -0
- package/dist/server.js +161 -2
- package/dist/source/bundled.js +78 -0
- package/dist/source/github-source.js +76 -0
- package/dist/validation.js +16 -0
- package/dist/workflow-meta.js +29 -0
- package/package.json +1 -1
- package/assets/rules/app-badging.md +0 -170
package/dist/source/bundled.js
CHANGED
|
@@ -3,6 +3,7 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { WorkflowError } from "../errors.js";
|
|
5
5
|
import { parseFrontmatter } from "../frontmatter.js";
|
|
6
|
+
import { parseWorkflowMeta } from "../workflow-meta.js";
|
|
6
7
|
/**
|
|
7
8
|
* Directory the npm package ships rules/skills in. `scripts/copy-assets.ts` populates this
|
|
8
9
|
* directory (from the repository's `rules/`/`skills/`) before every build/dev/test/pack, and
|
|
@@ -20,11 +21,15 @@ export class BundledSource {
|
|
|
20
21
|
ref;
|
|
21
22
|
rulesDir;
|
|
22
23
|
skillsDir;
|
|
24
|
+
agentsDir;
|
|
25
|
+
workflowsDir;
|
|
23
26
|
constructor(ref) {
|
|
24
27
|
this.ref = ref;
|
|
25
28
|
const base = assetsDir();
|
|
26
29
|
this.rulesDir = join(base, "rules");
|
|
27
30
|
this.skillsDir = join(base, "skills");
|
|
31
|
+
this.agentsDir = join(base, "agents");
|
|
32
|
+
this.workflowsDir = join(base, "workflows");
|
|
28
33
|
}
|
|
29
34
|
info() {
|
|
30
35
|
return { kind: "bundled", ref: this.ref };
|
|
@@ -75,4 +80,77 @@ export class BundledSource {
|
|
|
75
80
|
const { fields, body } = parseFrontmatter(raw);
|
|
76
81
|
return { description: fields.description ?? "", content: body.trim() };
|
|
77
82
|
}
|
|
83
|
+
async listAgents() {
|
|
84
|
+
const names = readdirSync(this.agentsDir, { withFileTypes: true })
|
|
85
|
+
.filter((entry) => entry.isFile() && /^[a-z0-9]+(?:-[a-z0-9]+)*\.md$/.test(entry.name))
|
|
86
|
+
.map((entry) => entry.name.replace(/\.md$/, ""))
|
|
87
|
+
.sort();
|
|
88
|
+
const summaries = [];
|
|
89
|
+
for (const name of names) {
|
|
90
|
+
try {
|
|
91
|
+
const raw = readFileSync(join(this.agentsDir, `${name}.md`), "utf8");
|
|
92
|
+
const { fields } = parseFrontmatter(raw);
|
|
93
|
+
summaries.push({ name, description: fields.description ?? "" });
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// An agent file without readable frontmatter is not listed; get_agent will still
|
|
97
|
+
// surface NOT_FOUND if it's requested directly.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return summaries;
|
|
101
|
+
}
|
|
102
|
+
async getAgent(name) {
|
|
103
|
+
const path = join(this.agentsDir, `${name}.md`);
|
|
104
|
+
let raw;
|
|
105
|
+
try {
|
|
106
|
+
raw = readFileSync(path, "utf8");
|
|
107
|
+
}
|
|
108
|
+
catch {
|
|
109
|
+
throw new WorkflowError("NOT_FOUND", `Agent '${name}' was not found in the bundled snapshot.`);
|
|
110
|
+
}
|
|
111
|
+
const { fields, body } = parseFrontmatter(raw);
|
|
112
|
+
return {
|
|
113
|
+
description: fields.description ?? "",
|
|
114
|
+
tools: fields.tools ?? "",
|
|
115
|
+
disallowedTools: fields.disallowedTools ?? "",
|
|
116
|
+
content: body.trim(),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
async listWorkflows() {
|
|
120
|
+
const names = readdirSync(this.workflowsDir, { withFileTypes: true })
|
|
121
|
+
.filter((entry) => entry.isFile() && /^[a-z0-9]+(?:-[a-z0-9]+)*\.js$/.test(entry.name))
|
|
122
|
+
.map((entry) => entry.name.replace(/\.js$/, ""))
|
|
123
|
+
.sort();
|
|
124
|
+
const summaries = [];
|
|
125
|
+
for (const name of names) {
|
|
126
|
+
try {
|
|
127
|
+
const raw = readFileSync(join(this.workflowsDir, `${name}.js`), "utf8");
|
|
128
|
+
const meta = parseWorkflowMeta(raw);
|
|
129
|
+
summaries.push({ name, description: meta.description });
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// A workflow file whose meta block can't be parsed is not listed; get_workflow will
|
|
133
|
+
// still surface an error if it's requested directly.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return summaries;
|
|
137
|
+
}
|
|
138
|
+
async getWorkflow(name) {
|
|
139
|
+
const path = join(this.workflowsDir, `${name}.js`);
|
|
140
|
+
let raw;
|
|
141
|
+
try {
|
|
142
|
+
raw = readFileSync(path, "utf8");
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
throw new WorkflowError("NOT_FOUND", `Workflow '${name}' was not found in the bundled snapshot.`);
|
|
146
|
+
}
|
|
147
|
+
let meta;
|
|
148
|
+
try {
|
|
149
|
+
meta = parseWorkflowMeta(raw);
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
throw new WorkflowError("INTERNAL_ERROR", `Workflow '${name}' has an unparsable meta block: ${error instanceof Error ? error.message : String(error)}`);
|
|
153
|
+
}
|
|
154
|
+
return { description: meta.description, whenToUse: meta.whenToUse, content: raw };
|
|
155
|
+
}
|
|
78
156
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { WorkflowError } from "../errors.js";
|
|
2
2
|
import { parseFrontmatter } from "../frontmatter.js";
|
|
3
|
+
import { parseWorkflowMeta } from "../workflow-meta.js";
|
|
3
4
|
import { GithubClient } from "./github.js";
|
|
4
5
|
const OWNER = "michaelbel";
|
|
5
6
|
const REPO = "cuckcoder";
|
|
@@ -65,4 +66,79 @@ export class GithubSource {
|
|
|
65
66
|
const { fields, body } = parseFrontmatter(raw);
|
|
66
67
|
return { description: fields.description ?? "", content: body.trim() };
|
|
67
68
|
}
|
|
69
|
+
async listAgents() {
|
|
70
|
+
const tree = await this.client.listTree(this.ref);
|
|
71
|
+
const paths = tree
|
|
72
|
+
.filter((item) => item.type === "blob" && /^agents\/[a-z0-9]+(?:-[a-z0-9]+)*\.md$/.test(item.path))
|
|
73
|
+
.map((item) => item.path)
|
|
74
|
+
.sort();
|
|
75
|
+
const summaries = [];
|
|
76
|
+
for (const path of paths) {
|
|
77
|
+
const name = path.replace(/^agents\//, "").replace(/\.md$/, "");
|
|
78
|
+
const raw = await this.client.fetchFile(this.ref, path);
|
|
79
|
+
const { fields } = parseFrontmatter(raw);
|
|
80
|
+
summaries.push({ name, description: fields.description ?? "" });
|
|
81
|
+
}
|
|
82
|
+
return summaries;
|
|
83
|
+
}
|
|
84
|
+
async getAgent(name) {
|
|
85
|
+
let raw;
|
|
86
|
+
try {
|
|
87
|
+
raw = await this.client.fetchFile(this.ref, `agents/${name}.md`);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (error instanceof WorkflowError && error.code === "NOT_FOUND") {
|
|
91
|
+
throw new WorkflowError("NOT_FOUND", `Agent '${name}' was not found at ref '${this.ref}'.`);
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
const { fields, body } = parseFrontmatter(raw);
|
|
96
|
+
return {
|
|
97
|
+
description: fields.description ?? "",
|
|
98
|
+
tools: fields.tools ?? "",
|
|
99
|
+
disallowedTools: fields.disallowedTools ?? "",
|
|
100
|
+
content: body.trim(),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
async listWorkflows() {
|
|
104
|
+
const tree = await this.client.listTree(this.ref);
|
|
105
|
+
const paths = tree
|
|
106
|
+
.filter((item) => item.type === "blob" && /^workflows\/[a-z0-9]+(?:-[a-z0-9]+)*\.js$/.test(item.path))
|
|
107
|
+
.map((item) => item.path)
|
|
108
|
+
.sort();
|
|
109
|
+
const summaries = [];
|
|
110
|
+
for (const path of paths) {
|
|
111
|
+
const name = path.replace(/^workflows\//, "").replace(/\.js$/, "");
|
|
112
|
+
const raw = await this.client.fetchFile(this.ref, path);
|
|
113
|
+
try {
|
|
114
|
+
const meta = parseWorkflowMeta(raw);
|
|
115
|
+
summaries.push({ name, description: meta.description });
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// A workflow file whose meta block can't be parsed is not listed; get_workflow will
|
|
119
|
+
// still surface an error if it's requested directly.
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return summaries;
|
|
123
|
+
}
|
|
124
|
+
async getWorkflow(name) {
|
|
125
|
+
let raw;
|
|
126
|
+
try {
|
|
127
|
+
raw = await this.client.fetchFile(this.ref, `workflows/${name}.js`);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
if (error instanceof WorkflowError && error.code === "NOT_FOUND") {
|
|
131
|
+
throw new WorkflowError("NOT_FOUND", `Workflow '${name}' was not found at ref '${this.ref}'.`);
|
|
132
|
+
}
|
|
133
|
+
throw error;
|
|
134
|
+
}
|
|
135
|
+
let meta;
|
|
136
|
+
try {
|
|
137
|
+
meta = parseWorkflowMeta(raw);
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
throw new WorkflowError("INTERNAL_ERROR", `Workflow '${name}' has an unparsable meta block: ${error instanceof Error ? error.message : String(error)}`);
|
|
141
|
+
}
|
|
142
|
+
return { description: meta.description, whenToUse: meta.whenToUse, content: raw };
|
|
143
|
+
}
|
|
68
144
|
}
|
package/dist/validation.js
CHANGED
|
@@ -31,3 +31,19 @@ export function validateRuleName(rawName) {
|
|
|
31
31
|
}
|
|
32
32
|
return rawName;
|
|
33
33
|
}
|
|
34
|
+
/** Agent name: the lowercase kebab-case basename without `.md`, e.g. `kotlin-engineer`. */
|
|
35
|
+
const AGENT_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
36
|
+
export function validateAgentName(rawName) {
|
|
37
|
+
if (!AGENT_NAME_PATTERN.test(rawName)) {
|
|
38
|
+
throw new WorkflowError("INVALID_NAME", `Invalid agent name '${rawName}'. Expected a lowercase kebab-case agent name, e.g. 'kotlin-engineer'.`);
|
|
39
|
+
}
|
|
40
|
+
return rawName;
|
|
41
|
+
}
|
|
42
|
+
/** Workflow name: the lowercase kebab-case basename without `.js`, e.g. `full-review`. */
|
|
43
|
+
const WORKFLOW_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
44
|
+
export function validateWorkflowName(rawName) {
|
|
45
|
+
if (!WORKFLOW_NAME_PATTERN.test(rawName)) {
|
|
46
|
+
throw new WorkflowError("INVALID_NAME", `Invalid workflow name '${rawName}'. Expected a lowercase kebab-case workflow name, e.g. 'full-review'.`);
|
|
47
|
+
}
|
|
48
|
+
return rawName;
|
|
49
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const META_BLOCK_PATTERN = /export\s+const\s+meta\s*=\s*(\{[\s\S]*?\n\})/;
|
|
2
|
+
/**
|
|
3
|
+
* Extracts `export const meta = { name, description, whenToUse? }` from a workflow module's
|
|
4
|
+
* source text without importing the module. Workflow files reference sweep-runtime globals
|
|
5
|
+
* (`args`, `agent`, `phase`, ...) at the top level, so `import()`-ing one directly would throw;
|
|
6
|
+
* this only evaluates the isolated `meta` object literal, which in every workflow is just string
|
|
7
|
+
* literals joined with `+`.
|
|
8
|
+
*/
|
|
9
|
+
export function parseWorkflowMeta(source) {
|
|
10
|
+
const match = source.match(META_BLOCK_PATTERN);
|
|
11
|
+
if (!match) {
|
|
12
|
+
throw new Error("no 'export const meta = {...}' block found");
|
|
13
|
+
}
|
|
14
|
+
let meta;
|
|
15
|
+
try {
|
|
16
|
+
meta = new Function(`"use strict"; return (${match[1]});`)();
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
throw new Error(`failed to evaluate the workflow meta block: ${error instanceof Error ? error.message : String(error)}`);
|
|
20
|
+
}
|
|
21
|
+
if (typeof meta !== "object" ||
|
|
22
|
+
meta === null ||
|
|
23
|
+
typeof meta.name !== "string" ||
|
|
24
|
+
typeof meta.description !== "string") {
|
|
25
|
+
throw new Error("workflow meta block did not evaluate to { name: string, description: string, ... }");
|
|
26
|
+
}
|
|
27
|
+
const { name, description, whenToUse } = meta;
|
|
28
|
+
return { name, description, whenToUse: typeof whenToUse === "string" ? whenToUse : "" };
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
description: >-
|
|
3
|
-
Регресс-гард доступности устройств: aapt2 dump badging в golden-файл app/badging, обязательный
|
|
4
|
-
check<Variant>Badging в CI, необязательное железо через android:required=false
|
|
5
|
-
paths:
|
|
6
|
-
- "**/*.gradle.kts"
|
|
7
|
-
- "**/AndroidManifest.xml"
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
`aapt2 dump badging` по собранному APK показывает эффективный набор `uses-feature` /
|
|
11
|
-
`uses-permission` / `uses-sdk` — то, из-за чего Google Play скрывает приложение на планшетах,
|
|
12
|
-
Android TV, Wear OS, складных устройствах и авто. Обновление зависимости может молча добавить
|
|
13
|
-
`android:required` hardware feature (камера, телефония, автофокус) и срезать доступность, а в
|
|
14
|
-
диффе исходников этого не видно. Гард фиксирует бейдж в golden-файл и валит CI при расхождении.
|
|
15
|
-
|
|
16
|
-
## Что заводится в проекте
|
|
17
|
-
|
|
18
|
-
- Convention-плагин `build-logic/`, который на каждый application-вариант регистрирует три задачи:
|
|
19
|
-
- `generate<Variant>Badging` — `aapt2 dump badging` по универсальному APK варианта → генерируемый
|
|
20
|
-
файл в `build/`.
|
|
21
|
-
- `update<Variant>Badging` — копирует генерируемый файл в golden `app/badging/<variant>.txt`
|
|
22
|
-
(запускается локально, результат коммитится и проходит ревью).
|
|
23
|
-
- `check<Variant>Badging` — сравнивает генерируемый с golden; при расхождении фейлит с диффом.
|
|
24
|
-
- Golden-файлы `app/badging/*.txt` — под контролем версий; изменение бейджа всегда осознанное и
|
|
25
|
-
видно в PR.
|
|
26
|
-
|
|
27
|
-
## Правила
|
|
28
|
-
|
|
29
|
-
- `check<Variant>Badging` (как минимум для release-варианта) — обязательный шаг CI на каждый PR.
|
|
30
|
-
- Изменение `app/badging/*.txt` в PR без соответствующей причины (новая фича, осознанное
|
|
31
|
-
требование к железу) — стоп-фактор ревью.
|
|
32
|
-
- Все `uses-feature` для необязательного железа объявляй с `android:required="false"`; жёсткое
|
|
33
|
-
требование железа — только когда приложение без него нефункционально.
|
|
34
|
-
- Не добавляй `check*Badging` в `dependsOn` обычной сборки — это отдельный verification-шаг, чтобы
|
|
35
|
-
не замедлять локальный цикл.
|
|
36
|
-
|
|
37
|
-
## Скелет convention-плагина
|
|
38
|
-
|
|
39
|
-
```kotlin
|
|
40
|
-
// build-logic/convention/src/main/kotlin/AndroidApplicationBadgingConventionPlugin.kt
|
|
41
|
-
import com.android.build.api.artifact.SingleArtifact
|
|
42
|
-
import com.android.build.api.variant.ApplicationAndroidComponentsExtension
|
|
43
|
-
import org.gradle.api.DefaultTask
|
|
44
|
-
import org.gradle.api.Plugin
|
|
45
|
-
import org.gradle.api.Project
|
|
46
|
-
import org.gradle.api.file.RegularFileProperty
|
|
47
|
-
import org.gradle.api.provider.Property
|
|
48
|
-
import org.gradle.api.tasks.CacheableTask
|
|
49
|
-
import org.gradle.api.tasks.Copy
|
|
50
|
-
import org.gradle.api.tasks.InputFile
|
|
51
|
-
import org.gradle.api.tasks.OutputFile
|
|
52
|
-
import org.gradle.api.tasks.PathSensitive
|
|
53
|
-
import org.gradle.api.tasks.PathSensitivity
|
|
54
|
-
import org.gradle.api.tasks.TaskAction
|
|
55
|
-
import org.gradle.kotlin.dsl.register
|
|
56
|
-
|
|
57
|
-
class AndroidApplicationBadgingConventionPlugin : Plugin<Project> {
|
|
58
|
-
override fun apply(target: Project) {
|
|
59
|
-
val components = target.extensions
|
|
60
|
-
.getByType(ApplicationAndroidComponentsExtension::class.java)
|
|
61
|
-
|
|
62
|
-
components.onVariants { variant ->
|
|
63
|
-
val capitalised = variant.name.replaceFirstChar { it.uppercaseChar() }
|
|
64
|
-
val golden = target.rootDir.resolve("app/badging/${variant.name}.txt")
|
|
65
|
-
val generated = target.layout.buildDirectory.file("badging/${variant.name}.txt")
|
|
66
|
-
val apk = variant.artifacts.get(SingleArtifact.APK)
|
|
67
|
-
|
|
68
|
-
val generate = target.tasks.register<GenerateBadgingTask>("generate${capitalised}Badging") {
|
|
69
|
-
apkDir.set(apk)
|
|
70
|
-
aapt2.set(resolveAapt2(target))
|
|
71
|
-
output.set(generated)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
target.tasks.register<Copy>("update${capitalised}Badging") {
|
|
75
|
-
group = "badging"
|
|
76
|
-
description = "Обновляет app/badging/${variant.name}.txt — закоммить результат"
|
|
77
|
-
dependsOn(generate)
|
|
78
|
-
from(generated)
|
|
79
|
-
into(golden.parentFile.also { it.mkdirs() })
|
|
80
|
-
rename { golden.name }
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
target.tasks.register<CheckBadgingTask>("check${capitalised}Badging") {
|
|
84
|
-
group = "verification"
|
|
85
|
-
description = "Фейлит, если требуемые фичи ${variant.name}-APK разошлись с golden-файлом"
|
|
86
|
-
dependsOn(generate)
|
|
87
|
-
generated.set(generated)
|
|
88
|
-
this.golden.set(golden)
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
@CacheableTask
|
|
95
|
-
abstract class GenerateBadgingTask : DefaultTask() {
|
|
96
|
-
@get:InputFile @get:PathSensitive(PathSensitivity.NONE)
|
|
97
|
-
abstract val apkDir: RegularFileProperty
|
|
98
|
-
|
|
99
|
-
@get:org.gradle.api.tasks.Input
|
|
100
|
-
abstract val aapt2: Property<String>
|
|
101
|
-
|
|
102
|
-
@get:OutputFile
|
|
103
|
-
abstract val output: RegularFileProperty
|
|
104
|
-
|
|
105
|
-
@TaskAction
|
|
106
|
-
fun run() {
|
|
107
|
-
val apk = apkDir.get().asFile.parentFile
|
|
108
|
-
.listFiles { f -> f.extension == "apk" }?.firstOrNull()
|
|
109
|
-
?: error("APK не найден в ${apkDir.get().asFile.parentFile}")
|
|
110
|
-
val out = output.get().asFile.also { it.parentFile.mkdirs() }
|
|
111
|
-
project.exec {
|
|
112
|
-
commandLine(aapt2.get(), "dump", "badging", apk.absolutePath)
|
|
113
|
-
standardOutput = out.outputStream()
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
@CacheableTask
|
|
119
|
-
abstract class CheckBadgingTask : DefaultTask() {
|
|
120
|
-
@get:InputFile @get:PathSensitive(PathSensitivity.RELATIVE)
|
|
121
|
-
abstract val generated: RegularFileProperty
|
|
122
|
-
|
|
123
|
-
@get:InputFile @get:PathSensitive(PathSensitivity.RELATIVE)
|
|
124
|
-
abstract val golden: RegularFileProperty
|
|
125
|
-
|
|
126
|
-
@TaskAction
|
|
127
|
-
fun run() {
|
|
128
|
-
val g = generated.get().asFile.readText()
|
|
129
|
-
val expected = golden.get().asFile.let {
|
|
130
|
-
if (!it.exists()) error("Нет golden-файла ${it.path} — запусти update…Badging и закоммить")
|
|
131
|
-
it.readText()
|
|
132
|
-
}
|
|
133
|
-
if (g.trim() != expected.trim()) {
|
|
134
|
-
error(
|
|
135
|
-
"Бейдж APK разошёлся с golden-файлом ${golden.get().asFile.path}.\n" +
|
|
136
|
-
"Если изменение осознанное — запусти ./gradlew update…Badging и закоммить новый golden."
|
|
137
|
-
)
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
private fun resolveAapt2(project: Project): String {
|
|
143
|
-
val android = project.extensions
|
|
144
|
-
.getByType(com.android.build.gradle.BaseExtension::class.java)
|
|
145
|
-
return android.sdkDirectory
|
|
146
|
-
.resolve("build-tools/${android.buildToolsVersion}/aapt2")
|
|
147
|
-
.absolutePath
|
|
148
|
-
}
|
|
149
|
-
```
|
|
150
|
-
|
|
151
|
-
## CI
|
|
152
|
-
|
|
153
|
-
```yaml
|
|
154
|
-
# .github/workflows/pr-check.yml (фрагмент)
|
|
155
|
-
- name: App badging check
|
|
156
|
-
run: ./gradlew checkReleaseBadging
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
## Как выглядит golden-файл
|
|
160
|
-
|
|
161
|
-
```
|
|
162
|
-
# app/badging/release.txt
|
|
163
|
-
package: name='com.example.app' versionCode='142' versionName='1.4.2'
|
|
164
|
-
sdkVersion:'26'
|
|
165
|
-
targetSdkVersion:'35'
|
|
166
|
-
uses-permission: name='android.permission.INTERNET'
|
|
167
|
-
uses-feature-not-required: name='android.hardware.camera'
|
|
168
|
-
feature-group: label=''
|
|
169
|
-
uses-feature: name='android.hardware.faketouch'
|
|
170
|
-
```
|