build(ci): gate config generation behind bun checks

This commit is contained in:
xixu-me committed 2026-03-31 03:10:32 +08:00
1 parent d3f67f9e8e
commit f72b0dae3c
10 files changed
+305 -49

No files matched your search

+34 -47
View File
@@ -7,20 +7,38 @@ on:
workflow_dispatch:
jobs:
check-script:
ci:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
node-version: 22.x
- name: Run script
run: node META.js
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Run test suite
run: bun test META.test.js workflow.test.js
run:
runs-on: ubuntu-latest
needs: ci
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install --frozen-lockfile
- name: Validate runnable scenarios
run: bun run run
purge-dns-cache:
runs-on: ubuntu-latest
needs: check-script
needs: run
steps:
- name: Checkout repository
uses: actions/checkout@v4
@@ -29,56 +47,25 @@ jobs:
for file in $(ls); do
curl -i "https://purge.jsdelivr.net/gh/${{ github.repository }}@script/${file}"
done
generate-config:
runs-on: ubuntu-latest
needs: check-script
needs: run
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
node-version: "20"
- name: Remove validation function
run: |
sed -i '/function validateOriginalConfig/,/^}/d' META.js
sed -i 's/validateOriginalConfig(config);//' META.js
bun-version: latest
- name: Install dependencies
run: npm install yaml
- name: Create conversion script
run: |
cat > generate.js << 'EOL'
const fs = require('fs');
const vm = require('vm');
const yaml = require('yaml');
// Read the JS config file
const jsContent = fs.readFileSync('./META.js', 'utf8');
// Create a sandbox context
const context = {
console,
require,
process,
__dirname,
module,
exports,
};
// Create empty config object
const baseConfig = {};
// Prepare the script
const fullScript = `${jsContent}\nmodule.exports = main(${JSON.stringify(baseConfig)});`;
// Execute in sandbox
vm.createContext(context);
vm.runInContext(fullScript, context);
// Get the result and convert to YAML
const config = context.module.exports;
const yamlContent = yaml.stringify(config);
// Write output
fs.writeFileSync('./META.yaml', yamlContent);
EOL
run: bun install --frozen-lockfile
- name: Generate YAML configuration
run: |
mkdir -p publish
node generate.js
bun run generate
cat > publish/META.yaml << 'EOL'
proxy-providers-defaults: &ppd
type: http
+2
View File
@@ -0,0 +1,2 @@
node_modules/
META.yaml
+40
View File
@@ -0,0 +1,40 @@
const fs = require("node:fs");
const vm = require("node:vm");
const yaml = require("yaml");
function loadMain() {
const content = fs.readFileSync("./META.js", "utf8");
const wrapped = `${content}
module.exports = { main };`;
const context = {
module: { exports: {} },
console: {
log() {},
warn() {},
error() {},
},
};
vm.runInNewContext(wrapped, context, { filename: "META.js" });
return context.module.exports.main;
}
function main() {
const generateConfig = loadMain();
const config = generateConfig({}, { skipValidation: true });
if (config.error) {
throw new Error(config.error);
}
fs.writeFileSync("./META.yaml", yaml.stringify(config));
console.log("Generated META.yaml");
}
try {
main();
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
+4 -2
View File
@@ -763,9 +763,11 @@ function validateOriginalConfig(config) {
return true;
}
function main(config) {
function main(config, options = {}) {
try {
validateOriginalConfig(config);
if (!options.skipValidation) {
validateOriginalConfig(config);
}
Object.assign(config, generalConfig, {
dns,
hosts,
+88
View File
@@ -0,0 +1,88 @@
const fs = require("node:fs");
const vm = require("node:vm");
function loadMetaModule() {
const content = fs.readFileSync("./META.js", "utf8");
const wrapped = `${content}
module.exports = {
main,
validateOriginalConfig,
};`;
const context = {
module: { exports: {} },
console: {
log() {},
warn() {},
error() {},
},
};
vm.runInNewContext(wrapped, context, { filename: "META.js" });
return context.module.exports;
}
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
function runScenario(name, input, options) {
const { main } = loadMetaModule();
const config = main(structuredClone(input), options);
assert(!config.error, `${name}: ${config.error}`);
assert(Array.isArray(config.rules), `${name}: rules should be an array`);
assert(
Array.isArray(config["proxy-groups"]),
`${name}: proxy-groups should be an array`
);
assert(
config["rule-providers"] && typeof config["rule-providers"] === "object",
`${name}: rule-providers should be present`
);
}
function main() {
runScenario(
"proxy-based config",
{
proxies: [
{
name: "sample",
type: "socks5",
server: "127.0.0.1",
port: 1080,
},
],
},
{}
);
runScenario(
"provider-based config",
{
"proxy-providers": {
provider: {
type: "http",
url: "https://example.com/provider.yaml",
path: "./providers/provider.yaml",
},
},
},
{}
);
runScenario("publish generation", {}, { skipValidation: true });
console.log("Validated META.js against 3 scenarios.");
}
try {
main();
} catch (error) {
console.error(error.message);
process.exitCode = 1;
}
+10
View File
@@ -59,6 +59,16 @@ test("generated config does not force deprecated global fingerprinting", () => {
assert.equal(config["global-client-fingerprint"], undefined);
});
test("main can skip source validation for publish-time generation", () => {
const { main } = loadMetaModule();
const config = main({}, { skipValidation: true });
assert.equal(config.error, undefined);
assert.ok(Array.isArray(config.rules));
assert.ok(Array.isArray(config["proxy-groups"]));
});
test("generated tun config fully replaces the source tun settings", () => {
const { main } = loadMetaModule();
+15
View File
@@ -0,0 +1,15 @@
{
"lockfileVersion": 1,
"configVersion": 0,
"workspaces": {
"": {
"name": "meta",
"dependencies": {
"yaml": "^2.8.1",
},
},
},
"packages": {
"yaml": ["yaml@2.8.3", "", { "bin": "bin.mjs" }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="],
}
}
+2
View File
@@ -0,0 +1,2 @@
[install]
auto = "disable"
+12
View File
@@ -0,0 +1,12 @@
{
"name": "meta",
"private": true,
"scripts": {
"generate": "bun META.generate.js",
"test": "bun test META.test.js workflow.test.js",
"run": "bun META.run.js"
},
"dependencies": {
"yaml": "^2.8.1"
}
}
+98
View File
@@ -0,0 +1,98 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const path = require("node:path");
const childProcess = require("node:child_process");
const repoRoot = "E:/github/meta";
const workflowPath = path.join(repoRoot, ".github", "workflows", "run.yml");
const runScriptPath = path.join(repoRoot, "META.run.js");
function runBun(args) {
return childProcess.spawnSync("bun", args, {
cwd: repoRoot,
encoding: "utf8",
});
}
function ensureDependenciesInstalled() {
const result = runBun(["install", "--frozen-lockfile"]);
assert.equal(result.status, 0, result.stderr || result.stdout);
}
test("workflow runs CI before run and publish jobs", () => {
const workflow = fs.readFileSync(workflowPath, "utf8");
assert.match(workflow, /^ ci:\s*$/m, "workflow should define a ci job");
assert.match(
workflow,
/uses:\s+oven-sh\/setup-bun@v2/,
"workflow should install Bun via the official setup action"
);
assert.match(
workflow,
/^ run:\s*$(?:\r?\n)(?:\s+.*\r?\n)*?\s+needs:\s+ci\s*$/m,
"run job should depend on ci"
);
assert.match(
workflow,
/^ purge-dns-cache:\s*$(?:\r?\n)(?:\s+.*\r?\n)*?\s+needs:\s+run\s*$/m,
"purge-dns-cache should depend on run"
);
assert.match(
workflow,
/^ generate-config:\s*$(?:\r?\n)(?:\s+.*\r?\n)*?\s+needs:\s+run\s*$/m,
"generate-config should depend on run"
);
});
test("workflow installs dependencies and runs tests in ci job", () => {
const workflow = fs.readFileSync(workflowPath, "utf8");
assert.match(
workflow,
/bun install --frozen-lockfile/,
"ci job should install dependencies with Bun"
);
assert.match(workflow, /bun test/, "ci job should run the test suite with Bun");
assert.match(workflow, /bun run run/, "run job should use Bun scripts");
assert.match(
workflow,
/bun run generate/,
"generate-config job should use Bun scripts"
);
assert.doesNotMatch(
workflow,
/sed -i .*validateOriginalConfig/,
"workflow should not mutate META.js to bypass validation"
);
});
test("META.run.js executes the generator against sample inputs", () => {
const result = runBun([runScriptPath]);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.match(
result.stdout,
/validated/i,
"run script should report successful validation"
);
});
test("META.generate.js creates YAML output without mutating source", () => {
const outputPath = path.join(repoRoot, "META.yaml");
fs.rmSync(outputPath, { force: true });
ensureDependenciesInstalled();
const result = runBun(["META.generate.js"]);
assert.equal(result.status, 0, result.stderr || result.stdout);
assert.ok(fs.existsSync(outputPath), "generate script should create META.yaml");
assert.match(
fs.readFileSync(outputPath, "utf8"),
/mixed-port:\s*7890/,
"generated YAML should include core config values"
);
fs.rmSync(outputPath, { force: true });
});