fix(ci): require factory auth for droid live docker

This commit is contained in:
Vincent Koc
2026-05-23 12:20:24 +02:00
parent a4a1abbe30
commit cc6c3728c7
12 changed files with 104 additions and 4 deletions

View File

@@ -150,3 +150,9 @@ runs:
echo "ANTHROPIC_API_TOKEN or ANTHROPIC_API_KEY is required for selected Docker E2E lanes." >&2
exit 1
fi
if [[ "$credentials" == *",factory,"* ]]; then
[[ -n "${FACTORY_API_KEY:-}" ]] || {
echo "FACTORY_API_KEY is required for selected Docker E2E lanes." >&2
exit 1
}
fi

View File

@@ -103,6 +103,7 @@ jobs:
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}

View File

@@ -134,6 +134,7 @@ jobs:
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
FIREWORKS_API_KEY: ${{ secrets.FIREWORKS_API_KEY }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}

View File

@@ -219,6 +219,8 @@ on:
required: false
ANTHROPIC_API_TOKEN:
required: false
FACTORY_API_KEY:
required: false
BYTEPLUS_API_KEY:
required: false
CEREBRAS_API_KEY:
@@ -696,6 +698,7 @@ jobs:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
@@ -851,6 +854,12 @@ jobs:
echo "ANTHROPIC_API_TOKEN or ANTHROPIC_API_KEY is required for selected Docker E2E lanes." >&2
exit 1
fi
if [[ "$credentials" == *",factory,"* ]]; then
[[ -n "${FACTORY_API_KEY:-}" ]] || {
echo "FACTORY_API_KEY is required for selected Docker E2E lanes." >&2
exit 1
}
fi
- name: Run Docker E2E chunk
if: contains(matrix.profiles, inputs.release_test_profile)
@@ -937,6 +946,7 @@ jobs:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
@@ -1088,6 +1098,12 @@ jobs:
echo "ANTHROPIC_API_TOKEN or ANTHROPIC_API_KEY is required for selected Docker E2E lanes." >&2
exit 1
fi
if [[ "$credentials" == *",factory,"* ]]; then
[[ -n "${FACTORY_API_KEY:-}" ]] || {
echo "FACTORY_API_KEY is required for selected Docker E2E lanes." >&2
exit 1
}
fi
- name: Run targeted Docker E2E lanes
shell: bash

View File

@@ -596,6 +596,7 @@ jobs:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}
@@ -688,6 +689,7 @@ jobs:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}

View File

@@ -38,6 +38,7 @@ jobs:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}

View File

@@ -190,6 +190,8 @@ on:
required: false
ANTHROPIC_API_TOKEN:
required: false
FACTORY_API_KEY:
required: false
BYTEPLUS_API_KEY:
required: false
CEREBRAS_API_KEY:
@@ -561,6 +563,7 @@ jobs:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
ANTHROPIC_API_KEY_OLD: ${{ secrets.ANTHROPIC_API_KEY_OLD }}
ANTHROPIC_API_TOKEN: ${{ secrets.ANTHROPIC_API_TOKEN }}
FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}
BYTEPLUS_API_KEY: ${{ secrets.BYTEPLUS_API_KEY }}
CEREBRAS_API_KEY: ${{ secrets.CEREBRAS_API_KEY }}
DEEPINFRA_API_KEY: ${{ secrets.DEEPINFRA_API_KEY }}

View File

@@ -51,6 +51,7 @@ for env_key in \
OPENCLAW_LIVE_SETUP_TOKEN_MODEL \
OPENCLAW_LIVE_SETUP_TOKEN_PROFILE \
OPENCLAW_LIVE_SETUP_TOKEN_VALUE \
FACTORY_API_KEY \
GEMINI_API_KEY \
GOOGLE_API_KEY \
OPENROUTER_API_KEY \

View File

@@ -332,6 +332,9 @@ function laneCredentialRequirements(poolLane) {
if (poolLane.name === "install-e2e-anthropic") {
credentials.push("anthropic");
}
if (poolLane.name === "live-acp-bind-droid") {
credentials.push("factory");
}
if (
poolLane.name === "openwebui" ||
poolLane.name === "openai-chat-tools" ||

View File

@@ -173,8 +173,8 @@ WRAP
fi
droid --version
if [ -z "${FACTORY_API_KEY:-}" ]; then
echo "SKIP: Droid Docker ACP bind requires FACTORY_API_KEY; Factory OAuth/keyring auth in ~/.factory is not portable into the container." >&2
exit 0
echo "ERROR: Droid Docker ACP bind requires FACTORY_API_KEY; Factory OAuth/keyring auth in ~/.factory is not portable into the container." >&2
exit 1
fi
;;
gemini)
@@ -306,8 +306,8 @@ for ACP_AGENT in "${ACP_AGENTS[@]}"; do
echo "==> Profile file: $PROFILE_STATUS"
echo "==> Auth dirs: ${AUTH_DIRS_CSV:-none}"
echo "==> Auth files: ${AUTH_FILES_CSV:-none}"
echo "SKIP: Droid Docker ACP bind requires FACTORY_API_KEY; Factory OAuth/keyring auth in ~/.factory is not portable into the container." >&2
continue
echo "ERROR: Droid Docker ACP bind requires FACTORY_API_KEY; Factory OAuth/keyring auth in ~/.factory is not portable into the container." >&2
exit 1
fi
EXTERNAL_AUTH_MOUNTS=()

View File

@@ -748,6 +748,23 @@ describe("scripts/lib/docker-e2e-plan", () => {
expect(plan.needs.package).toBe(true);
});
it("plans the Droid ACP bind live lane as Factory-auth proof", () => {
const plan = planFor({ selectedLaneNames: ["live-acp-bind-droid"] });
expect(plan.credentials).toEqual(["factory"]);
expect(plan.lanes).toHaveLength(1);
const lane = requireFirstLane(plan);
expect(lane.command).toBe(
'OPENCLAW_LIVE_ACP_BIND_AGENT=droid OPENCLAW_LIVE_ACP_BIND_REQUIRE_TRANSCRIPT=1 OPENCLAW_SKIP_DOCKER_BUILD=1 bash -c \'harness="${OPENCLAW_DOCKER_E2E_TRUSTED_HARNESS_DIR:-}"; if [ -z "$harness" ]; then if [ -d .release-harness/scripts ]; then harness=.release-harness; else harness=.; fi; fi; OPENCLAW_LIVE_DOCKER_REPO_ROOT="${OPENCLAW_DOCKER_E2E_REPO_ROOT:-$PWD}" bash "$harness/scripts/test-live-acp-bind-docker.sh"\'',
);
expect(lane.imageKind).toBeUndefined();
expect(lane.live).toBe(true);
expect(lane.name).toBe("live-acp-bind-droid");
expect(lane.resources).toEqual(["docker", "live", "live:droid", "npm"]);
expect(lane.timeoutMs).toBe(1_200_000);
expect(plan.needs.liveImage).toBe(true);
});
it("plans Open WebUI as a live-auth functional image lane", () => {
const plan = planFor({
includeOpenWebUI: true,

View File

@@ -7,11 +7,16 @@ const LIVE_E2E_WORKFLOW = ".github/workflows/openclaw-live-and-e2e-checks-reusab
const NPM_TELEGRAM_WORKFLOW = ".github/workflows/npm-telegram-beta-e2e.yml";
const PACKAGE_JSON = "package.json";
const SETUP_PNPM_STORE_CACHE_ACTION = ".github/actions/setup-pnpm-store-cache/action.yml";
const DOCKER_E2E_PLAN_ACTION = ".github/actions/docker-e2e-plan/action.yml";
const RELEASE_CHECKS_WORKFLOW = ".github/workflows/openclaw-release-checks.yml";
const RELEASE_PUBLISH_WORKFLOW = ".github/workflows/openclaw-release-publish.yml";
const FULL_RELEASE_VALIDATION_WORKFLOW = ".github/workflows/full-release-validation.yml";
const QA_LIVE_TRANSPORTS_WORKFLOW = ".github/workflows/qa-live-transports-convex.yml";
const UPDATE_MIGRATION_WORKFLOW = ".github/workflows/update-migration.yml";
const CI_CHECK_TESTBOX_WORKFLOW = ".github/workflows/ci-check-testbox.yml";
const CRABBOX_HYDRATE_WORKFLOW = ".github/workflows/crabbox-hydrate.yml";
const SCHEDULED_LIVE_CHECKS_WORKFLOW = ".github/workflows/openclaw-scheduled-live-checks.yml";
const CI_HYDRATE_LIVE_AUTH_SCRIPT = "scripts/ci-hydrate-live-auth.sh";
const UPGRADE_SURVIVOR_RUN_SCRIPT = "scripts/e2e/lib/upgrade-survivor/run.sh";
type WorkflowStep = {
@@ -614,6 +619,50 @@ describe("package artifact reuse", () => {
expect(stage).toContain('node --import tsx "$scripts_dir/live-docker-normalize-config.ts"');
});
it("fails Droid ACP Docker live proof when Factory auth is missing", () => {
const script = readFileSync("scripts/test-live-acp-bind-docker.sh", "utf8");
expect(script).toContain(
"ERROR: Droid Docker ACP bind requires FACTORY_API_KEY; Factory OAuth/keyring auth in ~/.factory is not portable into the container.",
);
expect(script).not.toContain(
"SKIP: Droid Docker ACP bind requires FACTORY_API_KEY; Factory OAuth/keyring auth in ~/.factory is not portable into the container.",
);
expect(script).not.toMatch(
/Droid Docker ACP bind requires FACTORY_API_KEY[\s\S]{0,160}(exit 0|continue)/u,
);
});
it("plumbs Factory credentials through planned Docker E2E live lanes", () => {
const reusableWorkflow = readFileSync(LIVE_E2E_WORKFLOW, "utf8");
const releaseChecksWorkflow = readFileSync(RELEASE_CHECKS_WORKFLOW, "utf8");
const scheduledWorkflow = readFileSync(SCHEDULED_LIVE_CHECKS_WORKFLOW, "utf8");
const packageAcceptanceWorkflow = readFileSync(PACKAGE_ACCEPTANCE_WORKFLOW, "utf8");
const testboxWorkflow = readFileSync(CI_CHECK_TESTBOX_WORKFLOW, "utf8");
const crabboxHydrateWorkflow = readFileSync(CRABBOX_HYDRATE_WORKFLOW, "utf8");
const dockerPlanAction = readFileSync(DOCKER_E2E_PLAN_ACTION, "utf8");
const hydrateScript = readFileSync(CI_HYDRATE_LIVE_AUTH_SCRIPT, "utf8");
expect(hydrateScript).toContain(" FACTORY_API_KEY \\");
expect(dockerPlanAction).toContain('if [[ "$credentials" == *",factory,"* ]]; then');
expect(dockerPlanAction).toContain(
"FACTORY_API_KEY is required for selected Docker E2E lanes.",
);
for (const workflow of [
reusableWorkflow,
releaseChecksWorkflow,
scheduledWorkflow,
packageAcceptanceWorkflow,
testboxWorkflow,
crabboxHydrateWorkflow,
]) {
expect(workflow).toContain("FACTORY_API_KEY: ${{ secrets.FACTORY_API_KEY }}");
}
expect(reusableWorkflow).toContain("FACTORY_API_KEY:\n required: false");
expect(packageAcceptanceWorkflow).toContain("FACTORY_API_KEY:\n required: false");
expect(reusableWorkflow).toContain('if [[ "$credentials" == *",factory,"* ]]; then');
});
it("allows the Telegram lane to run from reusable package acceptance artifacts", () => {
const workflow = readFileSync(NPM_TELEGRAM_WORKFLOW, "utf8");