diff --git a/packages/app/tests/docker-git/entrypoint-auth.test.ts b/packages/app/tests/docker-git/entrypoint-auth.test.ts new file mode 100644 index 00000000..827e55f4 --- /dev/null +++ b/packages/app/tests/docker-git/entrypoint-auth.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from "@effect/vitest" +import { Effect } from "effect" + +import { defaultTemplateConfig } from "@effect-template/lib/core/domain" +import { renderEntrypoint } from "@effect-template/lib/core/templates-entrypoint" + +describe("renderEntrypoint auth bridge", () => { + it.effect("maps GH token fallback to git auth and sets git credential helper", () => + Effect.sync(() => { + const entrypoint = renderEntrypoint({ + ...defaultTemplateConfig, + repoUrl: "https://github.com/org/repo.git", + enableMcpPlaywright: false + }) + + expect(entrypoint).toContain( + "GIT_AUTH_TOKEN=\"${GIT_AUTH_TOKEN:-${GITHUB_TOKEN:-${GH_TOKEN:-}}}\"" + ) + expect(entrypoint).toContain("GITHUB_TOKEN=\"${GITHUB_TOKEN:-${GH_TOKEN:-}}\"") + expect(entrypoint).toContain("if [[ -n \"$GH_TOKEN\" || -n \"$GITHUB_TOKEN\" ]]; then") + expect(entrypoint).toContain(String.raw`printf "export GITHUB_TOKEN=%q\n" "$EFFECTIVE_GITHUB_TOKEN"`) + expect(entrypoint).toContain(String.raw`printf "%s\n" "GITHUB_TOKEN=$EFFECTIVE_GITHUB_TOKEN" >> "$SSH_ENV_PATH"`) + expect(entrypoint).toContain("GIT_CREDENTIAL_HELPER_PATH=\"/usr/local/bin/docker-git-credential-helper\"") + expect(entrypoint).toContain("token=\"$GITHUB_TOKEN\"") + expect(entrypoint).toContain("token=\"$GH_TOKEN\"") + expect(entrypoint).toContain(String.raw`printf "%s\n" "password=$token"`) + expect(entrypoint).toContain("git config --global credential.helper") + })) +}) diff --git a/packages/docker-git/tests/core/templates.test.ts b/packages/docker-git/tests/core/templates.test.ts index 8356b5dd..b919777c 100644 --- a/packages/docker-git/tests/core/templates.test.ts +++ b/packages/docker-git/tests/core/templates.test.ts @@ -39,11 +39,15 @@ describe("planFiles", () => { const dockerfileSpec = specs.find( (spec) => spec._tag === "File" && spec.relativePath === "Dockerfile" ) + const entrypointSpec = specs.find( + (spec) => spec._tag === "File" && spec.relativePath === "entrypoint.sh" + ) expect(composeSpec !== undefined && composeSpec._tag === "File").toBe(true) expect(ignoreSpec !== undefined && ignoreSpec._tag === "File").toBe(true) expect(configSpec !== undefined && configSpec._tag === "File").toBe(true) expect(dockerfileSpec !== undefined && dockerfileSpec._tag === "File").toBe(true) + expect(entrypointSpec !== undefined && entrypointSpec._tag === "File").toBe(true) if (configSpec && configSpec._tag === "File") { expect(configSpec.contents).toContain(config.repoUrl) @@ -61,6 +65,13 @@ describe("planFiles", () => { expect(dockerfileSpec.contents).toContain("ncurses-term") expect(dockerfileSpec.contents).toContain("tag-order builtins commands") } + + if (entrypointSpec && entrypointSpec._tag === "File") { + expect(entrypointSpec.contents).toContain( + "GIT_CREDENTIAL_HELPER_PATH=\"/usr/local/bin/docker-git-credential-helper\"" + ) + expect(entrypointSpec.contents).toContain("token=\"$GITHUB_TOKEN\"") + } })) it.effect("includes Playwright sidecar files when enabled", () => diff --git a/packages/lib/src/core/templates-entrypoint/base.ts b/packages/lib/src/core/templates-entrypoint/base.ts index 22a815e9..d61f8f47 100644 --- a/packages/lib/src/core/templates-entrypoint/base.ts +++ b/packages/lib/src/core/templates-entrypoint/base.ts @@ -10,8 +10,9 @@ REPO_REF="\${REPO_REF:-}" FORK_REPO_URL="\${FORK_REPO_URL:-}" TARGET_DIR="\${TARGET_DIR:-${config.targetDir}}" GIT_AUTH_USER="\${GIT_AUTH_USER:-\${GITHUB_USER:-x-access-token}}" -GIT_AUTH_TOKEN="\${GIT_AUTH_TOKEN:-\${GITHUB_TOKEN:-}}" +GIT_AUTH_TOKEN="\${GIT_AUTH_TOKEN:-\${GITHUB_TOKEN:-\${GH_TOKEN:-}}}" GH_TOKEN="\${GH_TOKEN:-\${GIT_AUTH_TOKEN:-}}" +GITHUB_TOKEN="\${GITHUB_TOKEN:-\${GH_TOKEN:-}}" GIT_USER_NAME="\${GIT_USER_NAME:-}" GIT_USER_EMAIL="\${GIT_USER_EMAIL:-}" CODEX_AUTO_UPDATE="\${CODEX_AUTO_UPDATE:-1}" diff --git a/packages/lib/src/core/templates-entrypoint/git.ts b/packages/lib/src/core/templates-entrypoint/git.ts index a6d8d44e..de1390d6 100644 --- a/packages/lib/src/core/templates-entrypoint/git.ts +++ b/packages/lib/src/core/templates-entrypoint/git.ts @@ -1,20 +1,72 @@ import type { TemplateConfig } from "../domain.js" -export const renderEntrypointGitConfig = (config: TemplateConfig): string => - String.raw`# 2) Ensure GH_TOKEN is available for SSH sessions if provided -if [[ -n "$GH_TOKEN" ]]; then - printf "export GH_TOKEN=%q\n" "$GH_TOKEN" > /etc/profile.d/gh-token.sh +const renderEntrypointAuthEnvBridge = (config: TemplateConfig): string => + String.raw`# 2) Ensure GitHub auth vars are available for SSH sessions if provided +if [[ -n "$GH_TOKEN" || -n "$GITHUB_TOKEN" ]]; then + EFFECTIVE_GITHUB_TOKEN="$GITHUB_TOKEN" + if [[ -z "$EFFECTIVE_GITHUB_TOKEN" ]]; then + EFFECTIVE_GITHUB_TOKEN="$GH_TOKEN" + fi + + EFFECTIVE_GH_TOKEN="$GH_TOKEN" + if [[ -z "$EFFECTIVE_GH_TOKEN" ]]; then + EFFECTIVE_GH_TOKEN="$EFFECTIVE_GITHUB_TOKEN" + fi + + printf "export GH_TOKEN=%q\n" "$EFFECTIVE_GH_TOKEN" > /etc/profile.d/gh-token.sh + printf "export GITHUB_TOKEN=%q\n" "$EFFECTIVE_GITHUB_TOKEN" >> /etc/profile.d/gh-token.sh chmod 0644 /etc/profile.d/gh-token.sh SSH_ENV_PATH="/home/${config.sshUser}/.ssh/environment" - printf "%s\n" "GH_TOKEN=$GH_TOKEN" > "$SSH_ENV_PATH" - if [[ -n "$GITHUB_TOKEN" ]]; then - printf "%s\n" "GITHUB_TOKEN=$GITHUB_TOKEN" >> "$SSH_ENV_PATH" - fi + printf "%s\n" "GH_TOKEN=$EFFECTIVE_GH_TOKEN" > "$SSH_ENV_PATH" + printf "%s\n" "GITHUB_TOKEN=$EFFECTIVE_GITHUB_TOKEN" >> "$SSH_ENV_PATH" chmod 600 "$SSH_ENV_PATH" chown 1000:1000 "$SSH_ENV_PATH" || true + + SAFE_GH_TOKEN="$(printf "%q" "$GH_TOKEN")" + # Keep git+https auth in sync with gh auth so push/pull works without manual setup. + su - ${config.sshUser} -c "GH_TOKEN=$SAFE_GH_TOKEN gh auth setup-git --hostname github.com --force" || true + + GH_LOGIN="$(su - ${config.sshUser} -c "GH_TOKEN=$SAFE_GH_TOKEN gh api user --jq .login" 2>/dev/null || true)" + GH_ID="$(su - ${config.sshUser} -c "GH_TOKEN=$SAFE_GH_TOKEN gh api user --jq .id" 2>/dev/null || true)" + GH_LOGIN="$(printf "%s" "$GH_LOGIN" | tr -d '\r\n')" + GH_ID="$(printf "%s" "$GH_ID" | tr -d '\r\n')" + + if [[ -z "$GIT_USER_NAME" && -n "$GH_LOGIN" ]]; then + GIT_USER_NAME="$GH_LOGIN" + fi + if [[ -z "$GIT_USER_EMAIL" && -n "$GH_LOGIN" && -n "$GH_ID" ]]; then + GIT_USER_EMAIL="${"${"}GH_ID}+${"${"}GH_LOGIN}@users.noreply.github.com" + fi +fi` + +const renderEntrypointGitCredentialHelper = (config: TemplateConfig): string => + String.raw`# 3) Configure git credential helper for HTTPS remotes +GIT_CREDENTIAL_HELPER_PATH="/usr/local/bin/docker-git-credential-helper" +cat <<'EOF' > "$GIT_CREDENTIAL_HELPER_PATH" +#!/usr/bin/env bash +set -euo pipefail + +if [[ "$#" -lt 1 || "$1" != "get" ]]; then + exit 0 +fi + +token="$GITHUB_TOKEN" +if [[ -z "$token" ]]; then + token="$GH_TOKEN" fi -# 3) Configure git identity for the dev user if provided +if [[ -z "$token" ]]; then + exit 0 +fi + +printf "%s\n" "username=x-access-token" +printf "%s\n" "password=$token" +EOF +chmod 0755 "$GIT_CREDENTIAL_HELPER_PATH" +su - ${config.sshUser} -c "git config --global credential.helper '$GIT_CREDENTIAL_HELPER_PATH'"` + +const renderEntrypointGitIdentity = (config: TemplateConfig): string => + String.raw`# 4) Configure git identity for the dev user if provided if [[ -n "$GIT_USER_NAME" ]]; then SAFE_GIT_USER_NAME="$(printf "%q" "$GIT_USER_NAME")" su - ${config.sshUser} -c "git config --global user.name $SAFE_GIT_USER_NAME" @@ -25,6 +77,13 @@ if [[ -n "$GIT_USER_EMAIL" ]]; then su - ${config.sshUser} -c "git config --global user.email $SAFE_GIT_USER_EMAIL" fi` +export const renderEntrypointGitConfig = (config: TemplateConfig): string => + [ + renderEntrypointAuthEnvBridge(config), + renderEntrypointGitCredentialHelper(config), + renderEntrypointGitIdentity(config) + ].join("\n\n") + export const renderEntrypointGitHooks = (): string => String.raw`# 3) Install global git hooks to protect main/master HOOKS_DIR="/opt/docker-git/hooks"