build(ci): gate config generation behind bun checks
This commit is contained in:
1 parent
d3f67f9e8e
commit
f72b0dae3c
10 files changed
+305
-49
No files matched your search
+34
-47
@@ -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
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
META.yaml
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
[install]
|
||||
auto = "disable"
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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 });
|
||||
});
|
||||
Reference in new issue
Block a user