Skip to content
Merged
39 changes: 20 additions & 19 deletions .github/renovate.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,26 @@
"extends": ["config:js-lib"]
},
// By Package Manager
{
// Generated from apps/playgrounds/*/package.json arkenvExamples metadata.
// Hand-maintained examples, including StackBlitz, remain Renovate-managed.
"description": "Disable sync-generated example apps",
"matchFileNames": [
// BEGIN GENERATED SYNC EXAMPLES
"examples/basic/**",
"examples/basic-js/**",
"examples/with-bun/**",
"examples/with-bun-react/**",
"examples/with-nextjs/**",
"examples/with-nextjs-strict/**",
"examples/with-nuxt/**",
"examples/with-solid-start/**",
"examples/with-standard-schema/**",
"examples/with-vite-react/**",
// END GENERATED SYNC EXAMPLES
],
"enabled": false
},
{
// Group pnpm workspace updates (minor/patch) across selected dirs
"groupName": "the pnpm group",
Expand Down Expand Up @@ -97,25 +117,6 @@
"matchPackagePatterns": ["^@arkenv/"],
"automerge": true,
"automergeType": "pr"
},
{
// Sync-generated examples only — packageManager comes from catalog + transform.js.
// Hand-maintained examples (with-zod, with-valibot) stay Renovate-updatable via the pnpm group.
"description": "Disable packageManager bumps for sync-generated examples; versions come from the catalog + transform.js",
"matchPackageNames": ["npm", "pnpm", "bun"],
"matchFileNames": [
"examples/basic/**",
"examples/basic-js/**",
"examples/with-nextjs/**",
"examples/with-nextjs-strict/**",
"examples/with-nuxt/**",
"examples/with-solid-start/**",
"examples/with-standard-schema/**",
"examples/with-vite-react/**",
"examples/with-bun/**",
"examples/with-bun-react/**"
],
"enabled": false
}
]
}
4 changes: 2 additions & 2 deletions .github/workflows/sync-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -82,8 +82,8 @@ jobs:
run: |
git config --global user.email "237618717+arkenv-bot[bot]@users.noreply.github.com"
git config --global user.name "arkenv-bot[bot]"
if [ -n "$(git status --porcelain examples)" ]; then
git add examples
if [ -n "$(git status --porcelain examples .github/renovate.json)" ]; then
git add examples .github/renovate.json
git commit -m "chore(examples): sync generated examples"
git push origin HEAD || {
echo "❌ Failed to push example sync changes. Please re-run the workflow."
Expand Down
10 changes: 10 additions & 0 deletions scripts/sync-examples.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { existsSync, readdirSync, readFileSync } from "node:fs";
import { basename, join } from "node:path";
import { argv } from "node:process";
import { PLAYGROUNDS_DIR } from "./sync-lib/constants.js";
import { syncRenovateConfig } from "./sync-lib/renovate.js";
import { syncPlayground } from "./sync-lib/sync.js";
import { parseCatalog } from "./sync-lib/workspace.js";

Expand All @@ -39,6 +40,7 @@ function main() {
.map((d) => join(PLAYGROUNDS_DIR, d.name));

let hasChanges = false;
const generatedExampleNames = [];

for (const playgroundPath of playgroundDirs) {
const pkgPath = join(playgroundPath, "package.json");
Expand All @@ -65,6 +67,7 @@ function main() {
continue;
}

generatedExampleNames.push(exampleConfig.name);
const changes = syncPlayground(
playgroundPath,
exampleConfig,
Expand All @@ -82,6 +85,13 @@ function main() {
}
}

if (syncRenovateConfig(generatedExampleNames, checkOnly)) {
hasChanges = true;
console.log(
"\n ✗ Renovate config is out of sync with playground metadata.",
);
}

if (checkOnly) {
if (hasChanges) {
console.log("\n❌ Examples are out of sync with playgrounds.");
Expand Down
55 changes: 55 additions & 0 deletions scripts/sync-lib/renovate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { ROOT_DIR } from "./constants.js";

const RENOVATE_CONFIG_PATH = join(ROOT_DIR, ".github", "renovate.json");
const START_MARKER = "// BEGIN GENERATED SYNC EXAMPLES";
const END_MARKER = "// END GENERATED SYNC EXAMPLES";

/**
* Emit every generated `matchFileNames` entry with a trailing comma so the
* marker region stays self-contained JSONC whether or not hand-written entries
* follow the END marker before the closing `]`.
*
* @param {string[]} exampleNames
*/
export function generatedMatchFileNames(exampleNames) {
return [
`\t\t\t\t${START_MARKER}`,
...exampleNames.map((name) => `\t\t\t\t"examples/${name}/**",`),
Comment thread
yamcodes marked this conversation as resolved.
`\t\t\t\t${END_MARKER}`,
].join("\n");
}

/**
* Keep Renovate's generated-example exclusion in sync with playground metadata.
*/
export function syncRenovateConfig(exampleNames, checkOnly = false) {
const content = readFileSync(RENOVATE_CONFIG_PATH, "utf8");
const markerStart = content.indexOf(START_MARKER);
const start = content.lastIndexOf("\n", markerStart) + 1;
const end = content.indexOf(END_MARKER);

if (markerStart === -1 || end === -1 || end < start) {
throw new Error(
`Renovate config must contain ${START_MARKER} and ${END_MARKER}`,
);
}

const generated = generatedMatchFileNames([...new Set(exampleNames)].sort());
const current = content.slice(start, end + END_MARKER.length);
const updated =
content.slice(0, start) +
generated +
content.slice(end + END_MARKER.length);

if (checkOnly) {
return current !== generated;
}

if (current !== generated) {
writeFileSync(RENOVATE_CONFIG_PATH, updated);
}

return false;
}
23 changes: 23 additions & 0 deletions scripts/sync-lib/renovate.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { describe, expect, it } from "vitest";
import { generatedMatchFileNames } from "./renovate.js";

describe("generatedMatchFileNames", () => {
it("always trailing-commas generated entries so the region is self-contained", () => {
const block = generatedMatchFileNames(["basic", "with-vite-react"]);
const entries = block
.split("\n")
.map((line) => line.trim())
.filter((line) => line.startsWith('"examples/'));

expect(entries).toEqual([
'"examples/basic/**",',
'"examples/with-vite-react/**",',
]);
});

it("trailing-commas a single generated entry", () => {
const block = generatedMatchFileNames(["basic"]);
expect(block).toContain('"examples/basic/**",');
expect(block).not.toMatch(/"examples\/basic\/\*\*"\n/);
});
});
Loading