Skip to content

Commit 3de1a07

Browse files
committed
feat: unify README badge sync across superproject and submodule
1 parent 0fac943 commit 3de1a07

9 files changed

Lines changed: 246 additions & 11 deletions

File tree

AGENTS.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ just run <args> # Run CLI with arguments (e.g., just run status)
3737
just dev # Start local runtime (local submodule + cached sandbox rebuild)
3838
```
3939

40+
## README Badge Sync
41+
42+
`README.md` (superproject) and `packages/opencode/README.md` (submodule) use distinct generated badge blocks for different audiences.
43+
The source of truth is `packages/opencode/packages/fork-ui/src/readme-badge-catalog.ts`.
44+
45+
- Run `just sync-readme-badges` after badge catalog changes.
46+
- `just lint` and `just pre-commit` run `just check-readme-badges` and fail on drift.
47+
- Do not manually edit badge lines between generated marker comments.
48+
4049
## UAT Testing
4150

4251
When performing manual UAT tests with the user, use justfile commands instead of the installed `occ` binary:

CLAUDE.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,15 @@ just run <args> # Run CLI with arguments (e.g., just run status)
3737
just dev # Start local runtime (local submodule + cached sandbox rebuild)
3838
```
3939

40+
## README Badge Sync
41+
42+
`README.md` (superproject) and `packages/opencode/README.md` (submodule) use distinct generated badge blocks for different audiences.
43+
The source of truth is `packages/opencode/packages/fork-ui/src/readme-badge-catalog.ts`.
44+
45+
- Run `just sync-readme-badges` after badge catalog changes.
46+
- `just lint` and `just pre-commit` run `just check-readme-badges` and fail on drift.
47+
- Do not manually edit badge lines between generated marker comments.
48+
4049
## UAT Testing
4150

4251
When performing manual UAT tests with the user, use justfile commands instead of the installed `occ` binary:

CONTRIBUTING.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,21 @@ just lint
7171
just fmt
7272
```
7373

74+
### README Badge Sync
75+
76+
The root README (`README.md`) and submodule README (`packages/opencode/README.md`) have distinct generated badge blocks.
77+
Both are sourced from `packages/opencode/packages/fork-ui/src/readme-badge-catalog.ts`.
78+
79+
```bash
80+
# Regenerate both README badge sections
81+
just sync-readme-badges
82+
83+
# Validate badge sections are in sync (also run by just lint)
84+
just check-readme-badges
85+
```
86+
87+
Do not hand-edit badge lines between generated marker comments in either README.
88+
7489
## Commit Messages
7590

7691
We follow [Conventional Commits](https://www.conventionalcommits.org/):

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# opencode-cloud
22

3+
<!-- BEGIN:opencode-cloud-readme-badges -->
34
[![GitHub Stars](https://img.shields.io/github/stars/pRizz/opencode-cloud)](https://github.com/pRizz/opencode-cloud)
45
[![CI](https://github.com/pRizz/opencode-cloud/actions/workflows/ci.yml/badge.svg)](https://github.com/pRizz/opencode-cloud/actions/workflows/ci.yml)
56
[![Mirror](https://img.shields.io/badge/mirror-gitea-blue?logo=gitea)](https://gitea.com/pRizz/opencode-cloud)
@@ -12,6 +13,7 @@
1213
[![docs.rs](https://docs.rs/opencode-cloud/badge.svg)](https://docs.rs/opencode-cloud)
1314
[![MSRV](https://img.shields.io/badge/MSRV-1.85-blue.svg)](https://blog.rust-lang.org/2025/02/20/Rust-1.85.0.html)
1415
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
16+
<!-- END:opencode-cloud-readme-badges -->
1517

1618
> [!WARNING]
1719
> This tool is still a work in progress and is rapidly evolving. Expect frequent updates and breaking changes. Follow updates at https://github.com/pRizz/opencode-cloud (mirror: https://gitea.com/pRizz/opencode-cloud). Stability will be announced at some point. Use with caution.

bun.lock

Lines changed: 8 additions & 8 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

justfile

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,7 @@ test-doc-slow:
257257
cargo test --workspace --doc
258258

259259
# Lint everything
260-
lint: lint-rust lint-node lint-shell lint-workflows lint-opencode lint-opencode-broker
260+
lint: lint-rust lint-node lint-shell lint-workflows check-readme-badges lint-opencode lint-opencode-broker
261261

262262
# Lint Rust code
263263
lint-rust: check-rust-format check-rust-clippy
@@ -327,6 +327,14 @@ ci-checks: ci-lint ci-build ci-test ci-verify
327327
check-updates:
328328
./scripts/check-dockerfile-updates.sh
329329

330+
# Sync generated README badge blocks from fork-ui badge catalog
331+
sync-readme-badges: opencode-submodule-check
332+
bun scripts/sync-readme-badges.ts
333+
334+
# Validate generated README badge blocks are up to date
335+
check-readme-badges: opencode-submodule-check
336+
bun scripts/sync-readme-badges.ts --check
337+
330338
# --- DigitalOcean Marketplace ---
331339

332340
# Validate the DigitalOcean Marketplace Packer template

packages/core/src/docker/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -525,7 +525,7 @@ COPY --chown=opencoder:opencoder packages/opencode /tmp/opencode-local
525525
# Update it by running: ./scripts/update-opencode-commit.sh
526526
RUN set -eux; \
527527
OPENCODE_COMMIT_OVERRIDE="${OPENCODE_COMMIT:-}"; \
528-
OPENCODE_COMMIT="c3074def613f8eed68db31e88ba1ceda01af5472"; \
528+
OPENCODE_COMMIT="192f6857ba4d84f7f773f42e2ef557f13469a869"; \
529529
if [ -n "${OPENCODE_COMMIT_OVERRIDE}" ]; then OPENCODE_COMMIT="${OPENCODE_COMMIT_OVERRIDE}"; fi; \
530530
rm -rf /tmp/opencode-repo; \
531531
if [ "${OPENCODE_SOURCE}" = "local" ]; then \

scripts/sync-readme-badges.ts

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
#!/usr/bin/env bun
2+
import { readFileSync, writeFileSync } from "node:fs"
3+
import { resolve } from "node:path"
4+
import { getBadges, type BadgeVariant } from "../packages/opencode/packages/fork-ui/src/readme-badge-catalog"
5+
6+
type Target = "all" | "superproject" | "submodule"
7+
type ReadmeTarget = Exclude<Target, "all">
8+
9+
const ROOT_README_PATH = resolve(import.meta.dir, "..", "README.md")
10+
const SUBMODULE_README_PATH = resolve(import.meta.dir, "..", "packages/opencode/README.md")
11+
12+
const ROOT_BEGIN_MARKER = "<!-- BEGIN:opencode-cloud-readme-badges -->"
13+
const ROOT_END_MARKER = "<!-- END:opencode-cloud-readme-badges -->"
14+
const SUBMODULE_BEGIN_MARKER = "<!-- BEGIN:opencode-submodule-readme-badges -->"
15+
const SUBMODULE_END_MARKER = "<!-- END:opencode-submodule-readme-badges -->"
16+
17+
const DEFAULT_VARIANT_BY_TARGET: Record<ReadmeTarget, BadgeVariant> = {
18+
superproject: "full",
19+
submodule: "full",
20+
}
21+
22+
interface Options {
23+
check: boolean
24+
target: Target
25+
variantOverride?: BadgeVariant
26+
}
27+
28+
interface Update {
29+
target: ReadmeTarget
30+
filePath: string
31+
changed: boolean
32+
}
33+
34+
function usage(): never {
35+
console.error(`Usage: bun scripts/sync-readme-badges.ts [--check] [--target=all|superproject|submodule] [--variant=core|full]\n\nOptions:\n --check Validate files without writing changes.\n --target=... Limit sync/check scope. Default: all\n --variant=core|full Override per-target variant defaults for this run.\n\nExamples:\n bun scripts/sync-readme-badges.ts\n bun scripts/sync-readme-badges.ts --check\n bun scripts/sync-readme-badges.ts --target=submodule --variant=core`)
36+
process.exit(1)
37+
}
38+
39+
function parseVariant(value: string): BadgeVariant {
40+
if (value === "core" || value === "full") return value
41+
console.error(`Invalid --variant value: ${value}`)
42+
usage()
43+
}
44+
45+
function parseTarget(value: string): Target {
46+
if (value === "all" || value === "superproject" || value === "submodule") return value
47+
console.error(`Invalid --target value: ${value}`)
48+
usage()
49+
}
50+
51+
function parseArgs(argv: string[]): Options {
52+
const options: Options = { check: false, target: "all" }
53+
54+
for (let i = 0; i < argv.length; i += 1) {
55+
const arg = argv[i]
56+
57+
if (arg === "--check") {
58+
options.check = true
59+
continue
60+
}
61+
62+
if (arg === "--target") {
63+
const value = argv[i + 1]
64+
if (!value) usage()
65+
options.target = parseTarget(value)
66+
i += 1
67+
continue
68+
}
69+
70+
if (arg.startsWith("--target=")) {
71+
options.target = parseTarget(arg.slice("--target=".length))
72+
continue
73+
}
74+
75+
if (arg === "--variant") {
76+
const value = argv[i + 1]
77+
if (!value) usage()
78+
options.variantOverride = parseVariant(value)
79+
i += 1
80+
continue
81+
}
82+
83+
if (arg.startsWith("--variant=")) {
84+
options.variantOverride = parseVariant(arg.slice("--variant=".length))
85+
continue
86+
}
87+
88+
if (arg === "-h" || arg === "--help") {
89+
usage()
90+
}
91+
92+
console.error(`Unknown argument: ${arg}`)
93+
usage()
94+
}
95+
96+
return options
97+
}
98+
99+
function replaceMarkerBlock(content: string, beginMarker: string, endMarker: string, generatedBody: string, filePath: string): string {
100+
const beginIndex = content.indexOf(beginMarker)
101+
const endIndex = content.indexOf(endMarker)
102+
103+
if (beginIndex < 0 || endIndex < 0 || endIndex <= beginIndex) {
104+
throw new Error(`Markers not found or invalid in ${filePath}: ${beginMarker} ... ${endMarker}`)
105+
}
106+
107+
const before = content.slice(0, beginIndex + beginMarker.length)
108+
const after = content.slice(endIndex)
109+
return `${before}\n${generatedBody.trimEnd()}\n${after}`
110+
}
111+
112+
function renderRootBadges(variant: BadgeVariant): string {
113+
return getBadges("opencode-cloud", variant)
114+
.map((badge) => `[![${badge.label}](${badge.imageUrl})](${badge.linkUrl})`)
115+
.join("\n")
116+
}
117+
118+
function renderSubmoduleBadges(variant: BadgeVariant): string {
119+
return getBadges("opencode-submodule", variant)
120+
.map((badge) => ` <a href="${badge.linkUrl}"><img alt="${badge.label}" src="${badge.imageUrl}" /></a>`)
121+
.join("\n")
122+
}
123+
124+
function resolveVariant(target: ReadmeTarget, variantOverride?: BadgeVariant): BadgeVariant {
125+
return variantOverride ?? DEFAULT_VARIANT_BY_TARGET[target]
126+
}
127+
128+
function includesTarget(scope: Target, target: ReadmeTarget): boolean {
129+
return scope === "all" || scope === target
130+
}
131+
132+
function syncRootReadme(variant: BadgeVariant, check: boolean): Update {
133+
const current = readFileSync(ROOT_README_PATH, "utf8")
134+
const generated = renderRootBadges(variant)
135+
const next = replaceMarkerBlock(current, ROOT_BEGIN_MARKER, ROOT_END_MARKER, generated, ROOT_README_PATH)
136+
const changed = next !== current
137+
138+
if (changed && !check) writeFileSync(ROOT_README_PATH, next)
139+
140+
return { target: "superproject", filePath: ROOT_README_PATH, changed }
141+
}
142+
143+
function syncSubmoduleReadme(variant: BadgeVariant, check: boolean): Update {
144+
const current = readFileSync(SUBMODULE_README_PATH, "utf8")
145+
const generated = renderSubmoduleBadges(variant)
146+
const next = replaceMarkerBlock(current, SUBMODULE_BEGIN_MARKER, SUBMODULE_END_MARKER, generated, SUBMODULE_README_PATH)
147+
const changed = next !== current
148+
149+
if (changed && !check) writeFileSync(SUBMODULE_README_PATH, next)
150+
151+
return { target: "submodule", filePath: SUBMODULE_README_PATH, changed }
152+
}
153+
154+
function main() {
155+
const options = parseArgs(process.argv.slice(2))
156+
const updates: Update[] = []
157+
158+
if (includesTarget(options.target, "superproject")) {
159+
updates.push(syncRootReadme(resolveVariant("superproject", options.variantOverride), options.check))
160+
}
161+
162+
if (includesTarget(options.target, "submodule")) {
163+
updates.push(syncSubmoduleReadme(resolveVariant("submodule", options.variantOverride), options.check))
164+
}
165+
166+
if (options.check) {
167+
const drifted = updates.filter((update) => update.changed)
168+
if (drifted.length > 0) {
169+
console.error("README badge blocks are out of sync:")
170+
for (const update of drifted) {
171+
console.error(`- ${update.target}: ${update.filePath}`)
172+
}
173+
console.error("Run: just sync-readme-badges")
174+
process.exit(1)
175+
}
176+
177+
console.log("README badge blocks are in sync.")
178+
return
179+
}
180+
181+
const changed = updates.filter((update) => update.changed)
182+
if (changed.length === 0) {
183+
console.log("README badge blocks already up to date.")
184+
return
185+
}
186+
187+
for (const update of changed) {
188+
console.log(`Updated ${update.target} badges: ${update.filePath}`)
189+
}
190+
}
191+
192+
main()

0 commit comments

Comments
 (0)