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
13 changes: 13 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,16 @@ jobs:
run: docker version && docker compose version
- name: OpenCode autoconnect
run: bash scripts/e2e/opencode-autoconnect.sh

e2e-login-context:
name: E2E (Login context)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v6
- name: Install dependencies
uses: ./.github/actions/setup
- name: Docker info
run: docker version && docker compose version
- name: Login context notice
run: bash scripts/e2e/login-context.sh
46 changes: 46 additions & 0 deletions packages/docker-git/tests/core/templates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,52 @@ describe("planFiles", () => {
expect(entrypointSpec.contents).toContain("Issue AGENTS.md:")
expect(entrypointSpec.contents).toContain("ISSUE_AGENTS_PATH=\"$TARGET_DIR/AGENTS.md\"")
expect(entrypointSpec.contents).toContain("grep -qx \"AGENTS.md\" \"$EXCLUDE_PATH\"")
expect(entrypointSpec.contents).toContain("docker_git_workspace_context_line()")
expect(entrypointSpec.contents).toContain("REPO_REF_VALUE=\"${REPO_REF:-issue-5}\"")
expect(entrypointSpec.contents).toContain("REPO_URL_VALUE=\"${REPO_URL:-https://github.com/org/repo.git}\"")
expect(entrypointSpec.contents).toContain("Контекст workspace: issue #$ISSUE_ID_VALUE ($ISSUE_URL_VALUE)")
}
}))

it.effect("embeds PR workspace URL context in entrypoint", () =>
Effect.sync(() => {
const config: TemplateConfig = {
containerName: "dg-repo-pr-42",
serviceName: "dg-repo-pr-42",
sshUser: "dev",
sshPort: 2222,
repoUrl: "https://github.com/org/repo.git",
repoRef: "refs/pull/42/head",
targetDir: "/home/dev/org/repo/pr-42",
volumeName: "dg-repo-pr-42-home",
authorizedKeysPath: "./authorized_keys",
envGlobalPath: "./.orch/env/global.env",
envProjectPath: "./.orch/env/project.env",
codexAuthPath: "./.orch/auth/codex",
codexSharedAuthPath: "../../.orch/auth/codex",
codexHome: "/home/dev/.codex",
enableMcpPlaywright: false,
pnpmVersion: "10.27.0"
}

const specs = planFiles(config)
const entrypointSpec = specs.find(
(spec) => spec._tag === "File" && spec.relativePath === "entrypoint.sh"
)
expect(entrypointSpec !== undefined && entrypointSpec._tag === "File").toBe(true)
if (entrypointSpec && entrypointSpec._tag === "File") {
expect(entrypointSpec.contents).toContain("REPO_REF_VALUE=\"${REPO_REF:-refs/pull/42/head}\"")
expect(entrypointSpec.contents).toContain("REPO_URL_VALUE=\"${REPO_URL:-https://github.com/org/repo.git}\"")
expect(entrypointSpec.contents).toContain(
"PR_ID=\"$(printf \"%s\" \"$REPO_REF\" | sed -nE 's#^refs/pull/([0-9]+)/head$#\\1#p')\""
)
expect(entrypointSpec.contents).toContain(
"PR_URL=\"https://github.com/$PR_REPO/pull/$PR_ID\""
)
expect(entrypointSpec.contents).toContain(
"WORKSPACE_INFO_LINE=\"Контекст workspace: PR #$PR_ID ($PR_URL)\""
)
expect(entrypointSpec.contents).toContain("Контекст workspace: PR #$PR_ID_VALUE ($PR_URL_VALUE)")
}
}))
})
2 changes: 1 addition & 1 deletion packages/lib/src/core/templates-entrypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export const renderEntrypoint = (config: TemplateConfig): string =>
renderEntrypointBashHistory(),
renderEntrypointInputRc(config),
renderEntrypointZshConfig(),
renderEntrypointCodexResumeHint(),
renderEntrypointCodexResumeHint(config),
renderEntrypointAgentsNotice(config),
renderEntrypointDockerSocket(config),
renderEntrypointGitConfig(config),
Expand Down
97 changes: 85 additions & 12 deletions packages/lib/src/core/templates-entrypoint/codex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,27 +102,76 @@ export const renderEntrypointMcpPlaywright = (config: TemplateConfig): string =>
.replaceAll("__CODEX_HOME__", config.codexHome)
.replaceAll("__SERVICE_NAME__", config.serviceName)

export const renderEntrypointCodexResumeHint = (): string =>
`# Ensure codex resume hint is shown for interactive shells
const entrypointCodexResumeHintTemplate = `# Ensure codex resume hint is shown for interactive shells
CODEX_HINT_PATH="/etc/profile.d/zz-codex-resume.sh"
if [[ ! -s "$CODEX_HINT_PATH" ]]; then
cat <<'EOF' > "$CODEX_HINT_PATH"
docker_git_workspace_context_line() {
REPO_REF_VALUE="\${REPO_REF:-__REPO_REF_DEFAULT__}"
REPO_URL_VALUE="\${REPO_URL:-__REPO_URL_DEFAULT__}"

if [[ "$REPO_REF_VALUE" == issue-* ]]; then
ISSUE_ID_VALUE="$(printf "%s" "$REPO_REF_VALUE" | sed -E 's#^issue-##')"
ISSUE_URL_VALUE=""
if [[ "$REPO_URL_VALUE" == https://github.com/* ]]; then
ISSUE_REPO_VALUE="$(printf "%s" "$REPO_URL_VALUE" | sed -E 's#^https://github.com/##; s#[.]git$##; s#/*$##')"
if [[ -n "$ISSUE_REPO_VALUE" ]]; then
ISSUE_URL_VALUE="https://github.com/$ISSUE_REPO_VALUE/issues/$ISSUE_ID_VALUE"
fi
fi
if [[ -n "$ISSUE_URL_VALUE" ]]; then
printf "%s\n" "Контекст workspace: issue #$ISSUE_ID_VALUE ($ISSUE_URL_VALUE)"
else
printf "%s\n" "Контекст workspace: issue #$ISSUE_ID_VALUE"
fi
return
fi

if [[ "$REPO_REF_VALUE" == refs/pull/*/head ]]; then
PR_ID_VALUE="$(printf "%s" "$REPO_REF_VALUE" | sed -nE 's#^refs/pull/([0-9]+)/head$#\\1#p')"
PR_URL_VALUE=""
if [[ "$REPO_URL_VALUE" == https://github.com/* && -n "$PR_ID_VALUE" ]]; then
PR_REPO_VALUE="$(printf "%s" "$REPO_URL_VALUE" | sed -E 's#^https://github.com/##; s#[.]git$##; s#/*$##')"
if [[ -n "$PR_REPO_VALUE" ]]; then
PR_URL_VALUE="https://github.com/$PR_REPO_VALUE/pull/$PR_ID_VALUE"
fi
fi
if [[ -n "$PR_ID_VALUE" && -n "$PR_URL_VALUE" ]]; then
printf "%s\n" "Контекст workspace: PR #$PR_ID_VALUE ($PR_URL_VALUE)"
elif [[ -n "$PR_ID_VALUE" ]]; then
printf "%s\n" "Контекст workspace: PR #$PR_ID_VALUE"
elif [[ -n "$REPO_REF_VALUE" ]]; then
printf "%s\n" "Контекст workspace: pull request ($REPO_REF_VALUE)"
fi
return
fi

if [[ -n "$REPO_URL_VALUE" ]]; then
printf "%s\n" "Контекст workspace: $REPO_URL_VALUE"
fi
}

docker_git_print_codex_resume_hint() {
if [ -z "\${CODEX_RESUME_HINT_SHOWN-}" ]; then
DOCKER_GIT_CONTEXT_LINE="$(docker_git_workspace_context_line)"
if [[ -n "$DOCKER_GIT_CONTEXT_LINE" ]]; then
echo "$DOCKER_GIT_CONTEXT_LINE"
fi
echo "Старые сессии можно запустить с помощью codex resume или codex resume <id>, если знаешь айди."
export CODEX_RESUME_HINT_SHOWN=1
fi
}

if [ -n "$BASH_VERSION" ]; then
case "$-" in
*i*)
if [ -z "\${CODEX_RESUME_HINT_SHOWN-}" ]; then
echo "Старые сессии можно запустить с помощью codex resume или codex resume <id>, если знаешь айди."
export CODEX_RESUME_HINT_SHOWN=1
fi
docker_git_print_codex_resume_hint
;;
esac
fi
if [ -n "$ZSH_VERSION" ]; then
if [[ "$-" == *i* ]]; then
if [[ -z "\${CODEX_RESUME_HINT_SHOWN-}" ]]; then
echo "Старые сессии можно запустить с помощью codex resume или codex resume <id>, если знаешь айди."
export CODEX_RESUME_HINT_SHOWN=1
fi
docker_git_print_codex_resume_hint
fi
fi
EOF
Expand All @@ -135,6 +184,21 @@ if [[ -s /etc/zsh/zshrc ]] && ! grep -q "zz-codex-resume.sh" /etc/zsh/zshrc 2>/d
printf "%s\\n" "if [ -f /etc/profile.d/zz-codex-resume.sh ]; then source /etc/profile.d/zz-codex-resume.sh; fi" >> /etc/zsh/zshrc
fi`

const escapeForDoubleQuotes = (value: string): string => {
const backslash = String.fromCodePoint(92)
const quote = String.fromCodePoint(34)
const escapedBackslash = `${backslash}${backslash}`
const escapedQuote = `${backslash}${quote}`
return value
.replaceAll(backslash, escapedBackslash)
.replaceAll(quote, escapedQuote)
}

export const renderEntrypointCodexResumeHint = (config: TemplateConfig): string =>
entrypointCodexResumeHintTemplate
.replaceAll("__REPO_REF_DEFAULT__", escapeForDoubleQuotes(config.repoRef))
.replaceAll("__REPO_URL_DEFAULT__", escapeForDoubleQuotes(config.repoUrl))

const entrypointAgentsNoticeTemplate = String.raw`# Ensure global AGENTS.md exists for container context
AGENTS_PATH="__CODEX_HOME__/AGENTS.md"
LEGACY_AGENTS_PATH="/home/__SSH_USER__/AGENTS.md"
Expand All @@ -160,8 +224,17 @@ if [[ "$REPO_REF" == issue-* ]]; then
fi
ISSUE_AGENTS_HINT_LINE="Issue AGENTS.md: __TARGET_DIR__/AGENTS.md"
elif [[ "$REPO_REF" == refs/pull/*/head ]]; then
PR_ID="$(printf "%s" "$REPO_REF" | sed -E 's#^refs/pull/([0-9]+)/head$#\1#')"
if [[ -n "$PR_ID" ]]; then
PR_ID="$(printf "%s" "$REPO_REF" | sed -nE 's#^refs/pull/([0-9]+)/head$#\1#p')"
PR_URL=""
if [[ "$REPO_URL" == https://github.com/* && -n "$PR_ID" ]]; then
PR_REPO="$(printf "%s" "$REPO_URL" | sed -E 's#^https://github.com/##; s#[.]git$##; s#/*$##')"
if [[ -n "$PR_REPO" ]]; then
PR_URL="https://github.com/$PR_REPO/pull/$PR_ID"
fi
fi
if [[ -n "$PR_ID" && -n "$PR_URL" ]]; then
WORKSPACE_INFO_LINE="Контекст workspace: PR #$PR_ID ($PR_URL)"
elif [[ -n "$PR_ID" ]]; then
WORKSPACE_INFO_LINE="Контекст workspace: PR #$PR_ID"
else
WORKSPACE_INFO_LINE="Контекст workspace: pull request ($REPO_REF)"
Expand Down
162 changes: 162 additions & 0 deletions scripts/e2e/login-context.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
#!/usr/bin/env bash
set -euo pipefail

RUN_ID="$(date +%s)-$RANDOM"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
ROOT_BASE="${DOCKER_GIT_E2E_ROOT_BASE:-$REPO_ROOT/.docker-git/e2e-root}"
mkdir -p "$ROOT_BASE"
ROOT="$(mktemp -d "$ROOT_BASE/login-context.XXXXXX")"
SSH_KEY_BASE="$(mktemp -d /tmp/docker-git-login-context-key.XXXXXX)"
# docker-git containers may `chown -R` the `.docker-git` bind mount to UID 1000.
# Use world-writable permissions so the host runner can still create files
# even if ownership changes inside the container.
chmod 0777 "$ROOT"
mkdir -p "$ROOT/e2e"
chmod 0777 "$ROOT/e2e"
KEEP="${KEEP:-0}"

export DOCKER_GIT_PROJECTS_ROOT="$ROOT"
export DOCKER_GIT_STATE_AUTO_SYNC=0

ACTIVE_OUT_DIR=""
ACTIVE_CONTAINER=""
ACTIVE_SERVICE=""

fail() {
echo "e2e/login-context: $*" >&2
exit 1
}

on_error() {
local line="$1"
echo "e2e/login-context: failed at line $line" >&2
docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | head -n 80 || true
if [[ -n "$ACTIVE_OUT_DIR" ]] && [[ -f "$ACTIVE_OUT_DIR/docker-compose.yml" ]]; then
(cd "$ACTIVE_OUT_DIR" && docker compose ps) || true
(cd "$ACTIVE_OUT_DIR" && docker compose logs --no-color --tail 200) || true
fi
}

cleanup_active_case() {
if [[ -n "$ACTIVE_OUT_DIR" ]] && [[ -f "$ACTIVE_OUT_DIR/docker-compose.yml" ]]; then
(cd "$ACTIVE_OUT_DIR" && docker compose down -v --remove-orphans) >/dev/null 2>&1 || true
fi
ACTIVE_OUT_DIR=""
ACTIVE_CONTAINER=""
ACTIVE_SERVICE=""
}

cleanup() {
if [[ "$KEEP" == "1" ]]; then
echo "e2e/login-context: KEEP=1 set; preserving temp dir: $ROOT" >&2
if [[ -n "$ACTIVE_CONTAINER" ]]; then
echo "e2e/login-context: active container: $ACTIVE_CONTAINER" >&2
fi
if [[ -n "$ACTIVE_OUT_DIR" ]]; then
echo "e2e/login-context: active out dir: $ACTIVE_OUT_DIR" >&2
fi
return
fi
cleanup_active_case
rm -rf "$ROOT" >/dev/null 2>&1 || true
rm -rf "$SSH_KEY_BASE" >/dev/null 2>&1 || true
}

trap 'on_error $LINENO' ERR
trap cleanup EXIT

command -v ssh >/dev/null 2>&1 || fail "missing 'ssh' command"
command -v timeout >/dev/null 2>&1 || fail "missing 'timeout' command"
command -v ssh-keygen >/dev/null 2>&1 || fail "missing 'ssh-keygen' command"

ssh-keygen -q -t ed25519 -N "" -f "$SSH_KEY_BASE/dev_ssh_key" >/dev/null
cp "$SSH_KEY_BASE/dev_ssh_key.pub" "$ROOT/authorized_keys"
chmod 0600 "$SSH_KEY_BASE/dev_ssh_key"
chmod 0644 "$ROOT/authorized_keys"

wait_for_ssh() {
local ssh_port="$1"
local attempts=30
local attempt=1

while [[ "$attempt" -le "$attempts" ]]; do
if timeout 1 bash -lc "cat < /dev/null > /dev/tcp/127.0.0.1/$ssh_port" >/dev/null 2>&1; then
return 0
fi
sleep 1
attempt="$((attempt + 1))"
done

return 1
}

run_case() {
local case_name="$1"
local repo_url="$2"
local expected_context_line="$3"
local out_dir_rel=".docker-git/e2e/login-context-${case_name}-${RUN_ID}"
local out_dir="$ROOT/e2e/login-context-${case_name}-${RUN_ID}"
local container_name="dg-e2e-login-${case_name}-${RUN_ID}"
local service_name="dg-e2e-login-${case_name}-${RUN_ID}"
local volume_name="dg-e2e-login-${case_name}-${RUN_ID}-home"
local ssh_port="$(( (RANDOM % 1000) + 21000 ))"
local login_log="/tmp/docker-git-login-context-${RUN_ID}-${case_name}.log"

mkdir -p "$out_dir/.orch/env"
chmod 0777 "$out_dir" "$out_dir/.orch" "$out_dir/.orch/env"
cat > "$out_dir/.orch/env/project.env" <<'EOF_ENV'
# docker-git project env (e2e)
CODEX_AUTO_UPDATE=0
CODEX_SHARE_AUTH=1
EOF_ENV

ACTIVE_OUT_DIR="$out_dir"
ACTIVE_CONTAINER="$container_name"
ACTIVE_SERVICE="$service_name"

(
cd "$REPO_ROOT"
pnpm run docker-git clone "$repo_url" \
--force \
--no-ssh \
--authorized-keys "$ROOT/authorized_keys" \
--ssh-port "$ssh_port" \
--out-dir "$out_dir_rel" \
--container-name "$container_name" \
--service-name "$service_name" \
--volume-name "$volume_name"
)

wait_for_ssh "$ssh_port" || fail "ssh port did not open for $case_name (port: $ssh_port)"

rm -f "$login_log"

set +e
timeout 30s bash -lc "printf 'exit\n' | ssh -i \"$SSH_KEY_BASE/dev_ssh_key\" -tt -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p \"$ssh_port\" dev@localhost" > "$login_log" 2>&1
local ssh_exit=$?
set -e

if [[ "$ssh_exit" -ne 0 ]]; then
cat "$login_log" >&2 || true
fail "ssh login failed for $case_name (exit: $ssh_exit)"
fi

grep -Fq -- "$expected_context_line" "$login_log" \
|| fail "expected context line not found for $case_name: $expected_context_line"

grep -Fq -- "Старые сессии можно запустить с помощью codex resume" "$login_log" \
|| fail "expected codex resume hint for $case_name"

cleanup_active_case
}

run_case \
"issue" \
"https://github.com/octocat/Hello-World/issues/1" \
"Контекст workspace: issue #1 (https://github.com/octocat/Hello-World/issues/1)"

run_case \
"pr" \
"https://github.com/octocat/Hello-World/pull/1" \
"Контекст workspace: PR #1 (https://github.com/octocat/Hello-World/pull/1)"