Deploy Foundry Core from main with Actions credentials
Foundry Go checks / check (push) Successful in 1m30s
Foundry Go checks / check (pull_request) Has been cancelled

This commit is contained in:
2026-11-05 00:18:53 +08:00
parent 8cbd83f0e5
commit 4e5832c14b
4 changed files with 199 additions and 1 deletions
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env bash
set -euo pipefail
umask 077
base=/opt/endoleap-infra
foundry=$base/foundry
origin=https://api.foundry.entroia.com
candidate=foundry-core-candidate
network=endoleap-dev_default
for name in GOOGLE_CLIENT_ID GOOGLE_CLIENT_SECRET FOUNDRY_PROJECTS; do
value=${!name-}
if [[ -z "$value" || "$value" == *$'\n'* || "$value" == *$'\r'* ]]; then
printf 'Missing or invalid %s\n' "$name" >&2
exit 1
fi
done
test -s "$foundry/database.env"
docker network inspect "$network" >/dev/null
release=$(mktemp -d "$foundry/release.XXXXXXXX")
install -m 755 "$1" "$release/foundry"
export HOST=0.0.0.0 PORT=8080 PUBLIC_ORIGIN="$origin"
docker run -d --name "$candidate" --network "$network" \
--env-file "$foundry/database.env" -e HOST -e PORT -e PUBLIC_ORIGIN \
-e GOOGLE_CLIENT_ID -e GOOGLE_CLIENT_SECRET -e FOUNDRY_PROJECTS \
--user 1000:1000 --cap-drop ALL --security-opt no-new-privileges \
--read-only --memory 128m \
--mount "type=bind,source=$release/foundry,target=/opt/foundry,readonly" \
node:24-bookworm-slim /opt/foundry >/dev/null
cleanup_candidate() {
docker rm -f "${live:-$candidate}" >/dev/null 2>&1 || true
}
trap cleanup_candidate EXIT
docker exec "$candidate" node -e '
const assert = require("node:assert/strict");
async function verify() {
const base = "http://127.0.0.1:8080";
for (let attempt = 0; ; attempt++) {
try {
const ready = await fetch(base + "/readyz", {signal: AbortSignal.timeout(3000)});
if (ready.ok) break;
} catch (error) {
if (attempt === 9) throw error;
}
assert.ok(attempt < 9);
await new Promise(resolve => setTimeout(resolve, 1000));
}
const project = Object.keys(JSON.parse(process.env.FOUNDRY_PROJECTS))[0];
const route = base + "/projects/" + encodeURIComponent(project) + "/auth";
assert.equal((await fetch(route + "/me")).status, 401);
const challenge = require("node:crypto").randomBytes(32).toString("base64url");
const start = await fetch(route + "/google?challenge=" + challenge, {redirect: "manual"});
assert.ok([302, 303].includes(start.status));
const location = new URL(start.headers.get("location"));
assert.equal(location.hostname, "accounts.google.com");
assert.equal(location.searchParams.get("client_id"), process.env.GOOGLE_CLIENT_ID);
assert.equal(location.searchParams.get("redirect_uri"), process.env.PUBLIC_ORIGIN + "/auth/google/callback");
}
verify().catch(() => { console.error("Foundry readiness or OAuth entry check failed"); process.exit(1); });
'
# Keep the previous running container until the new candidate has passed checks.
# Traefik reads the route atomically from its watched config directory.
id=$(docker inspect -f '{{.Id}}' "$candidate")
live=foundry-core-${id:0:12}
docker rename "$candidate" "$live"
docker update --restart unless-stopped "$live" >/dev/null
bash "$(dirname "$0")/route.sh" "$live"
trap - EXIT
printf 'Foundry readiness, unauthenticated rejection and Google redirect verified.\n'
+80
View File
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
set -euo pipefail
umask 077
cd /opt/endoleap-infra
live=$1
[[ "$live" =~ ^foundry-core-[a-f0-9]{12}$ ]]
install -d -m 700 dev/foundry-edge
edge=dev/foundry-edge
route=$edge/foundry.yml
previous=
if [[ -f "$route" ]]; then
previous=$(sed -n 's@.*url: http://\(foundry-core-[a-f0-9]\{12\}\):8080.*@\1@p' "$route")
fi
backup=$(mktemp)
if [[ -f "$route" ]]; then cp "$route" "$backup"; fi
rollback() {
if [[ -s "$backup" ]]; then
cp "$backup" "$edge/.restore"
mv "$edge/.restore" "$route"
else
rm -f "$route"
fi
rm -f "$backup"
}
trap rollback EXIT
touch "$edge/acme.json"
chmod 600 "$edge/acme.json"
cat > "$edge/.foundry.yml" <<YAML
http:
routers:
foundry:
entryPoints: [websecure]
rule: Host(\`api.foundry.entroia.com\`)
middlewares: [foundry-limit]
service: foundry
tls:
certResolver: public
middlewares:
foundry-limit:
rateLimit:
average: 30
burst: 60
services:
foundry:
loadBalancer:
servers:
- url: http://$live:8080
YAML
mv "$edge/.foundry.yml" "$route"
cat > foundry-edge.yml <<'YAML'
services:
traefik:
command:
- --entrypoints.web.address=:8080
- --entrypoints.websecure.address=:8443
- --ping=true
- --ping.entrypoint=web
- --api.dashboard=false
- --providers.file.directory=/edge
- --certificatesresolvers.public.acme.storage=/edge/acme.json
- --certificatesresolvers.public.acme.tlschallenge=true
ports:
- "443:8443"
volumes:
- ./dev/foundry-edge:/edge
YAML
docker compose -p endoleap-dev --env-file dev/env -f compose.yml -f dev.yml -f foundry-edge.yml up -d --no-deps traefik
curl --fail --silent --show-error --output /dev/null --max-time 6 \
--retry 15 --retry-delay 4 --retry-all-errors \
--resolve api.foundry.entroia.com:443:127.0.0.1 \
https://api.foundry.entroia.com/readyz
trap - EXIT
rm -f "$backup"
if [[ "$previous" =~ ^foundry-core-[a-f0-9]{12}$ && "$previous" != "$live" ]]; then
docker stop "$previous" >/dev/null || printf 'Previous container needs manual cleanup.\n' >&2
fi