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
117 changes: 117 additions & 0 deletions .github/scripts/classify-candidate.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { appendFileSync, existsSync, readFileSync, readdirSync } from "node:fs";
import { execFileSync } from "node:child_process";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";

const MAX_CHANGED_PATH_BYTES = 4 * 1024 * 1024;
const MAX_CHANGED_PATHS = 10_000;

function globPattern(pattern) {
let normalized = pattern.startsWith("/") ? pattern.slice(1) : pattern;
let expression = "";
for (let index = 0; index < normalized.length; index += 1) {
const character = normalized[index];
if (character === "*" && normalized[index + 1] === "*") {
expression += ".*";
index += 1;
} else if (character === "*") {
expression += "[^/]*";
} else if (character === "?") {
expression += "[^/]";
} else {
expression += character.replace(/[|\\{}()[\]^$+?.]/gu, "\\$&");
}
}
return new RegExp(`^${expression}$`, "u");
}

function changedPaths(candidateRoot, baseSha, headSha) {
const output = execFileSync(
"git",
[
"-C",
candidateRoot,
"diff",
"--no-renames",
"--name-only",
"-z",
baseSha,
headSha,
],
{ encoding: "utf8", maxBuffer: MAX_CHANGED_PATH_BYTES },
);
const paths = output.split("\0").filter(Boolean);
if (paths.length > MAX_CHANGED_PATHS) {
throw new Error("candidate changes exceed the trusted path budget");
}
return paths;
}

function requireNoCandidateWorkflows(candidateRoot) {
const workflowRoot = resolve(candidateRoot, ".github/workflows");
if (!existsSync(workflowRoot)) return;
const workflows = readdirSync(workflowRoot).filter((name) => /\.ya?ml$/u.test(name));
if (workflows.length > 0) {
throw new Error(
`target repositories must delegate automatic workflows to organization controls: ${workflows.join(",")}`,
);
}
}

export function classifyCandidate({
baseSha,
candidateRoot,
exactPolicyOutcome,
headSha,
trustedRoot,
}) {
requireNoCandidateWorkflows(candidateRoot);
const policy = JSON.parse(
readFileSync(resolve(trustedRoot, ".github/merge-policy.json"), "utf8"),
);
if (!Array.isArray(policy.protected_paths) || policy.protected_paths.length === 0) {
throw new Error("trusted merge policy must define protected paths");
}
const matchers = policy.protected_paths.map((pattern) => {
if (typeof pattern !== "string" || pattern.startsWith("!")) {
throw new Error("trusted protected paths must be positive string patterns");
}
return globPattern(pattern);
});
const paths = changedPaths(candidateRoot, baseSha, headSha);
const protectedChanges = paths.filter((path) =>
matchers.some((matcher) => matcher.test(path)),
);
const sensitive = exactPolicyOutcome !== "success" || protectedChanges.length > 0;
return Object.freeze({
sensitive,
protectedChanges: Object.freeze(protectedChanges),
changedPathCount: paths.length,
});
}

function main() {
const result = classifyCandidate({
baseSha: process.env.BASE_SHA,
candidateRoot: process.env.CANDIDATE_ROOT,
exactPolicyOutcome: process.env.EXACT_POLICY_OUTCOME,
headSha: process.env.HEAD_SHA,
trustedRoot: process.env.TRUSTED_ROOT,
});
appendFileSync(
process.env.GITHUB_OUTPUT,
`sensitive=${result.sensitive ? "true" : "false"}\n`,
);
process.stdout.write(
`${JSON.stringify({ status: "classified", ...result })}\n`,
);
}

if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
try {
main();
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
}
1 change: 1 addition & 0 deletions .github/scripts/reject-candidate-authorities.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ if awk '$1 == "120000" { found = 1 } END { exit !found }' "$index_entries"; then
fi

test ! -e "$candidate_root/.npmrc"
test ! -e "$candidate_root/.github/policy-parser/.npmrc"
test ! -e "$candidate_root/npm-shrinkwrap.json"
test ! -L "$candidate_root/package.json"
test ! -L "$candidate_root/package-lock.json"
79 changes: 79 additions & 0 deletions .github/scripts/run-candidate-quality.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
set -euo pipefail

repository="${1:?target repository is required}"
candidate_root="${2:?candidate repository root is required}"
candidate_home="${HOME:?}"
candidate_tmp="${RUNNER_TEMP:?}"
npm_globalconfig="$candidate_tmp/npm-globalconfig"
npm_userconfig="$candidate_tmp/npm-userconfig"

: > "$npm_globalconfig"
: > "$npm_userconfig"

candidate_environment=(
"CI=true"
"HOME=$candidate_home"
"LANG=${LANG:-C.UTF-8}"
"NPM_CONFIG_CACHE=$candidate_tmp/npm-cache"
"NPM_CONFIG_GLOBALCONFIG=$npm_globalconfig"
"NPM_CONFIG_REGISTRY=https://registry.npmjs.org"
"NPM_CONFIG_REPLACE_REGISTRY_HOST=never"
"NPM_CONFIG_USERCONFIG=$npm_userconfig"
"PATH=$PATH"
"PWD=$candidate_root"
"RUNNER_TEMP=$candidate_tmp"
"TMPDIR=${TMPDIR:-$candidate_tmp}"
)
if test -n "${DOCKER_HOST:-}"; then
candidate_environment+=("DOCKER_HOST=$DOCKER_HOST")
fi

run_clean() {
env -i "${candidate_environment[@]}" "$@"
}

cd "$candidate_root"
run_clean npm ci --ignore-scripts
run_clean npm audit --audit-level=moderate
run_clean npm ci --ignore-scripts --prefix .github/policy-parser
run_clean npm audit --audit-level=moderate --prefix .github/policy-parser

case "$repository" in
openboa-ai/coffee-chat)
run_clean npm run format:check
run_clean npm run typecheck
run_clean npm test
run_clean npm run readme:assets:verify
run_clean npm run build
run_clean npm run package:smoke
;;
openboa-ai/coffee-chat-roastery)
run_clean npm run format:check
run_clean npm run typecheck
run_clean npm run dist:check
run_clean npm run repository:check
run_clean npm run smoke
run_clean npm run package:check
;;
openboa-ai/coffee-chat-eval)
run_clean npm run format:check
run_clean npm run typecheck
run_clean npm run build
run_clean npm run canary:check
run_clean npm test
run_clean npm run dry-run
run_clean npm run smoke
run_clean npm run pcda:calibrate
;;
openboa-ai/coffee-chat-bench)
run_clean npm run format:check
run_clean npm run check:inactive
run_clean npm run typecheck
run_clean npm test
;;
*)
echo "unsupported Coffee repository: $repository" >&2
exit 1
;;
esac
51 changes: 51 additions & 0 deletions .github/scripts/run-eval-harbor.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
set -euo pipefail

candidate_root="${1:?candidate repository root is required}"
candidate_tmp="${RUNNER_TEMP:?}"
uv_root="$candidate_tmp/uv-venv"
harbor_root="$candidate_tmp/harbor-venv"

clean_environment=(
"CI=true"
"HOME=${HOME:?}"
"LANG=${LANG:-C.UTF-8}"
"PATH=$PATH"
"PIP_CONFIG_FILE=/dev/null"
"PIP_DISABLE_PIP_VERSION_CHECK=1"
"PIP_INDEX_URL=https://pypi.org/simple"
"RUNNER_TEMP=$candidate_tmp"
"TMPDIR=${TMPDIR:-$candidate_tmp}"
"UV_INDEX_URL=https://pypi.org/simple"
"UV_NO_CONFIG=1"
)
if test -n "${DOCKER_HOST:-}"; then
clean_environment+=("DOCKER_HOST=$DOCKER_HOST")
fi

run_clean() {
env -i "${clean_environment[@]}" "$@"
}

test -f "$candidate_root/.github/uv-requirements.txt"
test -f "$candidate_root/.github/harbor-requirements.txt"
test -f "$candidate_root/src/canary-cli.ts"
test -f "$candidate_root/src/harbor.ts"

run_clean python3 -m venv "$uv_root"
run_clean "$uv_root/bin/python" -m pip install \
--disable-pip-version-check --require-hashes --no-deps \
-r "$candidate_root/.github/uv-requirements.txt"
run_clean "$uv_root/bin/uv" venv --python python3 --no-python-downloads \
"$harbor_root"
run_clean "$uv_root/bin/uv" pip install --require-hashes --no-deps \
--only-binary :all: --python "$harbor_root/bin/python" \
-r "$candidate_root/.github/harbor-requirements.txt"

cd "$candidate_root"
env -i "${clean_environment[@]}" \
"HARBOR_COMMAND=$harbor_root/bin/harbor" \
node --experimental-strip-types src/canary-cli.ts calibrate
env -i "${clean_environment[@]}" \
"HARBOR_COMMAND=$harbor_root/bin/harbor" \
node --experimental-strip-types src/canary-cli.ts benchmark-calibrate
Loading