Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
verify-tomls:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: latest
- name: Install dependencies
run: bun install
working-directory: ci/bun
- name: Run all tests
run: bun run "ci/bun/scripts/verifyTomls.ts"
- name: Upload log artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: verification-log
path: verification-log.json
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# python
venv
site
__pycache__
*:Zone.Identifier
.DS_Store

# bun
ci/bun/node_modules/
ci/bun/tmp/*.toml
29 changes: 29 additions & 0 deletions ci/bun/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions ci/bun/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "bun",
"module": "index.ts",
"type": "module",
"private": true,
"devDependencies": {
"@types/bun": "latest"
},
"peerDependencies": {
"typescript": "^5"
}
}
224 changes: 224 additions & 0 deletions ci/bun/scripts/verifyTomls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// -----------------------------------------------------------------------------
// ----- Constants -------------------------------------------------------------

const TOML_SNIPPET_REGEX = /^([ \t]*)```toml[^\n]*\r?\n([\s\S]*?)^\1```/gm;

const DOCS_ROOT_DIRECTORY = `${process.cwd()}/docs`;
const OUTPUT_DIRECTORY = `${process.cwd()}/tests/bun/tmp`;

// TODO -> Remove hash when Docker image is updated
const DOCKER_IMAGE =
"ghcr.io/pgdogdev/pgdog:main@sha256:3036d2ac7b684643dd187c42971f003f9d76e5f54cd129dcba742c309d7debd0";

const SnippetsByMd5 = new Map<string, Snippet>();

// -----------------------------------------------------------------------------
// ----- Invoke main() ---------------------------------------------------------

main();

// -----------------------------------------------------------------------------
// ----- Main ------------------------------------------------------------------

async function main() {
await setup();

// Scan docs for TOML snippets
const snippets = await extractTomlSnippets();

// Create one temporary {md5}.toml file for each snippet
await Promise.all(snippets.map(createFileForSnippet));

// Verify each snippet off the latest Docker image
const results = await Promise.all(snippets.map(verifySnippet));

// Remove temporary files
await cleanup();

// Exit with error code if any verification failed
const errors = results.filter((result) => !result.success);
if (errors.length > 0) {
console.error("Errors occurred during verification:");
errors.forEach((result) => console.error(`- ${result.errorMessage}`));
process.exit(1);
}

// Exit with success status code
console.log("All snippets verified successfully!");
process.exit(0);
}

// -----------------------------------------------------------------------------
// ----- Types -----------------------------------------------------------------

type Snippet = {
file: string;
content: string;
line: number;
};

// -----------------------------------------------------------------------------
// ----- Helpers ---------------------------------------------------------------

async function extractTomlSnippets() {
const glob = new Bun.Glob("**/*.md");
const paths = glob.scan(DOCS_ROOT_DIRECTORY);

const snippets: Snippet[] = [];

for await (const relativePath of paths) {
const fullPath = `${DOCS_ROOT_DIRECTORY}/${relativePath}`;
const fileText = await Bun.file(fullPath).text();

while (true) {
const match = TOML_SNIPPET_REGEX.exec(fileText);
if (match === null) {
break;
}

const codeIndent = match[1];
const deindentedContent = match[2]
.split(/\r?\n/)
.map((line) =>
line.startsWith(codeIndent) ? line.slice(codeIndent.length) : line,
)
.join("\n")
.trim();

const startIndex = match.index;
const startLineNumber = fileText
.slice(0, startIndex)
.split(/\r?\n/).length;

const snippet = {
file: relativePath,
content: deindentedContent,
line: startLineNumber,
};

const md5 = hash(snippet);

SnippetsByMd5.set(md5, snippet);
snippets.push(snippet);
}
}

return snippets;
}

// -----------------------------------------------------------------------------

function hash(snippet: Snippet): string {
const hasher = new Bun.CryptoHasher("md5");
hasher.update(JSON.stringify(snippet));
const md5 = hasher.digest("hex");
return md5;
}

// -----------------------------------------------------------------------------

async function setup() {
await Bun.$`mkdir -p ${OUTPUT_DIRECTORY}`;
await Bun.$`rm -rf ${OUTPUT_DIRECTORY}`.nothrow();
}

// -----------------------------------------------------------------------------

async function cleanup() {
await Bun.$`rm -rf ${OUTPUT_DIRECTORY}`.nothrow();
}

// -----------------------------------------------------------------------------

async function createFileForSnippet(snippet: Snippet) {
const md5 = hash(snippet);
const filename = `${md5}.toml`;
const outputFilePath = `${OUTPUT_DIRECTORY}/${filename}`;

// Write snippet content with trailing newline
await Bun.write(outputFilePath, `${snippet.content}\n`);
}

// -----------------------------------------------------------------------------

type VerificationResult =
| { success: true }
| {
success: false;
errorMessage: string;
};

async function verifySnippet(snippet: Snippet): Promise<VerificationResult> {
// The TOML snippets are either pgdog.toml or users.toml
// We don't annotate the config type so we need to check for both.
const configValid = await verifyConfigSnippet(snippet);
const usersValid = await verifyUsersSnippet(snippet);

const overallSuccess = configValid || usersValid;
if (overallSuccess) {
return { success: true };
}

return {
success: false,
errorMessage: `Validation failed for ${snippet?.file ?? "unknown"}:${snippet?.line ?? "?"}`,
};
}

async function verifyConfigSnippet(snippet: Snippet): Promise<boolean> {
const outputFilePath = `${OUTPUT_DIRECTORY}/${snippet.file}`;
const containerFilePath = "/pgdog.toml";

const cmd = [
"docker",
"run",
"--rm",
"--entrypoint",
"pgdog",
"-v",
`${outputFilePath}:${containerFilePath}`,
DOCKER_IMAGE,
"configcheck",
"--config",
containerFilePath,
];

const result = Bun.spawnSync({
cmd,
stdout: "pipe",
stderr: "pipe",
});

return result.success;
}

async function verifyUsersSnippet(snippet: Snippet): Promise<boolean> {
const md5 = hash(snippet);
const outputFilePath = `${OUTPUT_DIRECTORY}/${md5}.toml`;
const containerFilePath = "/users.toml";

const cmd = [
"docker",
"run",
"--rm",
"--entrypoint",
"pgdog",
"-v",
`${outputFilePath}:${containerFilePath}`,
DOCKER_IMAGE,
"configcheck",
"--users",
containerFilePath,
];

const result = Bun.spawnSync({
cmd,
stdout: "pipe",
stderr: "pipe",
});

return result.success;
}

// -----------------------------------------------------------------------------
// -----------------------------------------------------------------------------
27 changes: 27 additions & 0 deletions ci/bun/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"compilerOptions": {
// Enable latest features
"lib": ["ESNext", "DOM"],
"target": "ESNext",
"module": "ESNext",
"moduleDetection": "force",
"jsx": "react-jsx",
"allowJs": true,

// Bundler mode
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"noEmit": true,

// Best practices
"strict": true,
"skipLibCheck": true,
"noFallthroughCasesInSwitch": true,

// Some stricter flags (disabled by default)
"noUnusedLocals": false,
"noUnusedParameters": false,
"noPropertyAccessFromIndexSignature": false
}
}
Loading