Skip to content
Open
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
19 changes: 14 additions & 5 deletions .github/merge-policy.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
{
"merge_method": "squash",
"required_approvals": 0,
"required_code_owner_reviews": 0,
"required_code_owner_reviews": 1,
"required_last_push_approvals": 0,
"merge_queue": false,
"required_events": ["pull_request"],
"eligible_author_associations": ["OWNER", "MEMBER"],
"eligible_bot_logins": ["dependabot[bot]"],
"required_events": [
"pull_request"
],
"eligible_author_associations": [
"OWNER",
"MEMBER"
],
"eligible_bot_logins": [
"dependabot[bot]"
],
"custom_merge_controller": false,
"required_checks": [
{
Expand All @@ -31,7 +38,9 @@
"/package.json",
"/package-lock.json",
"/npm-shrinkwrap.json",
"/skills/**"
"/skills/**",
"/.gitignore",
"/.gitattributes"
],
"codeql_enforcement": "trusted_central_aggregate",
"sensitive_review": {
Expand Down
318 changes: 20 additions & 298 deletions .github/ci-policy.mjs → .github/verify.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from "node:assert/strict";
import { execFileSync } from "node:child_process";
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
import { existsSync, lstatSync, readFileSync, readdirSync } from "node:fs";
import { resolve } from "node:path";
import { parseDocument } from "yaml";

Expand All @@ -11,62 +11,20 @@ const trackedFiles = execFileSync("git", ["-C", root, "ls-files", "-z"], {
})
.split("\0")
.filter(Boolean);
function trackedEntries(directory = ".") {
const prefix = directory === "." ? "" : `${directory.replace(/\/$/u, "")}/`;
const entries = new Set();
for (const file of trackedFiles) {
if (!file.startsWith(prefix)) continue;
const remainder = file.slice(prefix.length);
if (!remainder) continue;
entries.add(remainder.split("/")[0]);
}
return [...entries].sort();
}
function checkoutEntries(directory = ".") {
const entries = trackedEntries(directory);
if (directory === ".") entries.push(".git");
return entries.sort();
}
const TRUSTED_CONTROL_SHA = "f33da6bbcdfebd0693ff7673d750f369629e000e";

assert.equal(existsSync(resolve(root, ".npmrc")), false);
assert.equal(existsSync(resolve(root, "npm-shrinkwrap.json")), false);
assert.deepEqual(
readdirSync(resolve(root, ".github/workflows")).sort(),
["trusted.yml"],
);
assert.equal(
readFileSync(resolve(root, ".github/workflows/trusted.yml"), "utf8"),
`name: OpenBoa Coffee trusted gate

on:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review]

permissions: {}

jobs:
trusted:
name: OpenBoa Coffee trusted required
permissions:
actions: read
contents: read
security-events: write
uses: openboa-ai/.github/.github/workflows/coffee-trusted-gate.yml@${TRUSTED_CONTROL_SHA}
with:
control_sha: ${TRUSTED_CONTROL_SHA}
`,
"trusted wrapper must remain exact",
);

assert.deepEqual(checkoutEntries(), [
".claude-plugin",
".codex-plugin",
// Compare complete paths: a same-named directory is not an allowed file.
// This is the published repository layout, not central security policy.
assert.deepEqual(trackedFiles.slice().sort(), [
".claude-plugin/plugin.json",
".codex-plugin/plugin.json",
".editorconfig",
".git",
".gitattributes",
".githooks",
".github",
".githooks/pre-commit",
".github/PULL_REQUEST_TEMPLATE.md",
".github/dependabot.yml",
".github/merge-policy.json",
".github/verify.mjs",
".github/verify.test.mjs",
".github/workflows/trusted.yml",
".gitignore",
"AGENTS.md",
"CODEOWNERS",
Expand All @@ -76,112 +34,12 @@ assert.deepEqual(checkoutEntries(), [
"package-lock.json",
"package.json",
"plugin.json",
"skills",
]);

assert.deepEqual(readJson("package.json"), {
name: "@openboa-ai/coffee-chat",
version: "0.0.0",
private: true,
type: "module",
scripts: {
"hooks:install": "git config core.hooksPath .githooks",
verify: "node .github/ci-policy.mjs",
},
dependencies: { yaml: "2.9.0" },
});
assert.deepEqual(readJson("package-lock.json"), {
name: "@openboa-ai/coffee-chat",
version: "0.0.0",
lockfileVersion: 3,
requires: true,
packages: {
"": {
name: "@openboa-ai/coffee-chat",
version: "0.0.0",
dependencies: { yaml: "2.9.0" },
},
"node_modules/yaml": {
version: "2.9.0",
resolved: "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
integrity: "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
bin: { yaml: "bin.mjs" },
engines: { node: ">= 14.6" },
funding: { url: "https://github.com/sponsors/eemeli" },
},
},
});
assert.deepEqual(readdirSync(resolve(root, ".github")).sort(), [
"PULL_REQUEST_TEMPLATE.md",
"ci-policy.mjs",
"dependabot.yml",
"merge-policy.json",
"workflows",
]);
assert.deepEqual(readdirSync(resolve(root, ".githooks")).sort(), ["pre-commit"]);
const expectedHook = [
"#!/bin/sh",
"set -eu",
"",
"scanner=${GITLEAKS_BIN:-gitleaks}",
'if ! command -v "$scanner" >/dev/null 2>&1; then',
" printf '%s\\n' 'Gitleaks is required; install Gitleaks before committing.' >&2",
" exit 1",
"fi",
"",
"if [ -e .gitleaks.toml ] || [ -e .gitleaksignore ]; then",
" printf '%s\\n' 'Repository-local Gitleaks controls are not permitted.' >&2",
" exit 1",
"fi",
"unset GITLEAKS_CONFIG GITLEAKS_CONFIG_TOML",
'"$scanner" git --pre-commit --staged --gitleaks-ignore-path /dev/null \\',
" --ignore-gitleaks-allow --redact --no-banner .",
'staged_dir="$(mktemp -d)"',
`trap 'rm -rf "$staged_dir"' EXIT HUP INT TERM`,
'git checkout-index --all --prefix="$staged_dir/"',
'"$scanner" dir --gitleaks-ignore-path /dev/null --ignore-gitleaks-allow \\',
' --redact --no-banner "$staged_dir"',
"",
].join("\n");
assert.equal(readFileSync(resolve(root, ".githooks/pre-commit"), "utf8"), expectedHook);
assert.notEqual(statSync(resolve(root, ".githooks/pre-commit")).mode & 0o111, 0);
assert.equal(
readFileSync(resolve(root, ".gitignore"), "utf8"),
`node_modules/
build/
coverage/
__pycache__/
*.py[cod]
*.log
.DS_Store

# Local credentials
.env
.env.*
!.env.example
credentials.json
secrets.json
*.private.pem
private-key.pem
*.private.key
private.key
private-key.key
id_rsa
id_dsa
id_ecdsa
id_ed25519
tls.key
server.key
server-key.pem
*-private-key.pem
*-private-key.key
privkey*.pem
*.p12
*.pfx
*.jks
`,
".gitignore must preserve the credential and local-artifact ignore contract",
);
"skills/brew/SKILL.md",
"skills/roast/SKILL.md"
], "unexpected or missing repository file");
for (const path of trackedFiles) {
assert.equal(lstatSync(resolve(root, path)).isFile(), true, `${path}: regular file required`);
}

const portable = readJson("plugin.json");
const manifestKeys = [
Expand Down Expand Up @@ -310,50 +168,6 @@ assert.equal(typeof codex.interface.longDescription, "string");
assert.ok(codex.interface.longDescription.trim().length > 0, "codex.interface.longDescription must be non-empty");
assertHttpsUrl(codex.interface.websiteURL, "codex.interface.websiteURL");

assert.deepEqual(readJson(".github/merge-policy.json"), {
merge_method: "squash",
required_approvals: 0,
required_code_owner_reviews: 0,
required_last_push_approvals: 0,
merge_queue: false,
required_events: ["pull_request"],
eligible_author_associations: ["OWNER", "MEMBER"],
eligible_bot_logins: ["dependabot[bot]"],
custom_merge_controller: false,
required_checks: [
{
context: "OpenBoa Coffee trusted required / OpenBoa Coffee trusted required",
integration_id: 15368,
},
],
protected_paths: [
"/plugin.json",
"/.github/**",
"/.githooks/**",
"/.gitleaksignore",
"/.gitleaks.toml",
"/.codex-plugin/**",
"/.claude-plugin/**",
"/AGENTS.md",
"/CODEOWNERS",
"/README.md",
"/LICENSE",
"/SECURITY.md",
"/.npmrc",
"/package.json",
"/package-lock.json",
"/npm-shrinkwrap.json",
"/skills/**",
],
codeql_enforcement: "trusted_central_aggregate",
sensitive_review: {
enforcement: "github_environment",
environment: "coffee-security",
required_approvals: 1,
prevent_self_review: false,
},
});

for (const forbidden of [
"mcp.json",
".mcp.json",
Expand All @@ -373,98 +187,6 @@ for (const forbidden of [
assert.equal(existsSync(resolve(root, forbidden)), false, forbidden);
}

assert.equal(
readFileSync(resolve(root, ".github/dependabot.yml"), "utf8"),
`version: 2

updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 5
commit-message:
prefix: deps
allow:
- dependency-name: "*"
update-types:
- version-update:semver-minor
- version-update:semver-patch
groups:
security:
applies-to: security-updates
patterns:
- "*"
production-minor-patch:
applies-to: version-updates
dependency-type: production
update-types:
- minor
- patch
patterns:
- "*"
development-minor-patch:
applies-to: version-updates
dependency-type: development
update-types:
- minor
- patch
patterns:
- "*"

- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 5
commit-message:
prefix: deps
allow:
- dependency-name: "*"
update-types:
- version-update:semver-minor
- version-update:semver-patch
groups:
security:
applies-to: security-updates
patterns:
- "*"
compatible-actions:
applies-to: version-updates
update-types:
- minor
- patch
patterns:
- "*"
`,
"Dependabot policy must remain bounded to approved update lanes",
);
assert.equal(
readFileSync(resolve(root, "CODEOWNERS"), "utf8"),
`# Ownership routes review context; GitHub requires zero human approvals.
/.github/** @openboa
/.codex-plugin/** @openboa
/.claude-plugin/** @openboa
/AGENTS.md @openboa
/CODEOWNERS @openboa
/README.md @openboa
/.npmrc @openboa-ai/security-maintainers
/LICENSE @openboa
/SECURITY.md @openboa
/skills/** @openboa
/package.json @openboa
/package-lock.json @openboa
/npm-shrinkwrap.json @openboa-ai/security-maintainers
/plugin.json @openboa
`,
"CODEOWNERS must preserve the product ownership routes",
);
assert.match(
readFileSync(resolve(root, "SECURITY.md"), "utf8"),
/security@openboa\.ai/u,
"SECURITY.md must provide a private reporting channel",
);

const skillRoot = resolve(root, "skills");
const skills = readdirSync(skillRoot).sort();
assert.deepEqual(skills, ["brew", "roast"]);
Expand Down Expand Up @@ -509,4 +231,4 @@ for (const skill of skills) {
assert.equal(fields.name, skill, `${skill}: frontmatter name`);
}

console.log("Coffee Chat structure and manifest policy passed.");
console.log("Coffee Chat structure and manifest verification passed.");
Loading
Loading