Skip to content

Commit 7a2b50a

Browse files
committed
test(e2e): verify login context notice for issue/pr workspaces
1 parent 86c9fec commit 7a2b50a

4 files changed

Lines changed: 281 additions & 12 deletions

File tree

.github/workflows/check.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,16 @@ jobs:
9797
run: docker version && docker compose version
9898
- name: OpenCode autoconnect
9999
run: bash scripts/e2e/opencode-autoconnect.sh
100+
101+
e2e-login-context:
102+
name: E2E (Login context)
103+
runs-on: ubuntu-latest
104+
timeout-minutes: 20
105+
steps:
106+
- uses: actions/checkout@v6
107+
- name: Install dependencies
108+
uses: ./.github/actions/setup
109+
- name: Docker info
110+
run: docker version && docker compose version
111+
- name: Login context notice
112+
run: bash scripts/e2e/login-context.sh

packages/docker-git/tests/core/templates.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,48 @@ describe("planFiles", () => {
139139
expect(entrypointSpec.contents).toContain("Issue AGENTS.md:")
140140
expect(entrypointSpec.contents).toContain("ISSUE_AGENTS_PATH=\"$TARGET_DIR/AGENTS.md\"")
141141
expect(entrypointSpec.contents).toContain("grep -qx \"AGENTS.md\" \"$EXCLUDE_PATH\"")
142+
expect(entrypointSpec.contents).toContain("docker_git_workspace_context_line()")
143+
expect(entrypointSpec.contents).toContain("Контекст workspace: issue #$ISSUE_ID_VALUE ($ISSUE_URL_VALUE)")
144+
}
145+
}))
146+
147+
it.effect("embeds PR workspace URL context in entrypoint", () =>
148+
Effect.sync(() => {
149+
const config: TemplateConfig = {
150+
containerName: "dg-repo-pr-42",
151+
serviceName: "dg-repo-pr-42",
152+
sshUser: "dev",
153+
sshPort: 2222,
154+
repoUrl: "https://github.com/org/repo.git",
155+
repoRef: "refs/pull/42/head",
156+
targetDir: "/home/dev/org/repo/pr-42",
157+
volumeName: "dg-repo-pr-42-home",
158+
authorizedKeysPath: "./authorized_keys",
159+
envGlobalPath: "./.orch/env/global.env",
160+
envProjectPath: "./.orch/env/project.env",
161+
codexAuthPath: "./.orch/auth/codex",
162+
codexSharedAuthPath: "../../.orch/auth/codex",
163+
codexHome: "/home/dev/.codex",
164+
enableMcpPlaywright: false,
165+
pnpmVersion: "10.27.0"
166+
}
167+
168+
const specs = planFiles(config)
169+
const entrypointSpec = specs.find(
170+
(spec) => spec._tag === "File" && spec.relativePath === "entrypoint.sh"
171+
)
172+
expect(entrypointSpec !== undefined && entrypointSpec._tag === "File").toBe(true)
173+
if (entrypointSpec && entrypointSpec._tag === "File") {
174+
expect(entrypointSpec.contents).toContain(
175+
"PR_ID=\"$(printf \"%s\" \"$REPO_REF\" | sed -nE 's#^refs/pull/([0-9]+)/head$#\\1#p')\""
176+
)
177+
expect(entrypointSpec.contents).toContain(
178+
"PR_URL=\"https://github.com/$PR_REPO/pull/$PR_ID\""
179+
)
180+
expect(entrypointSpec.contents).toContain(
181+
"WORKSPACE_INFO_LINE=\"Контекст workspace: PR #$PR_ID ($PR_URL)\""
182+
)
183+
expect(entrypointSpec.contents).toContain("Контекст workspace: PR #$PR_ID_VALUE ($PR_URL_VALUE)")
142184
}
143185
}))
144186
})

packages/lib/src/core/templates-entrypoint/codex.ts

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -102,27 +102,76 @@ export const renderEntrypointMcpPlaywright = (config: TemplateConfig): string =>
102102
.replaceAll("__CODEX_HOME__", config.codexHome)
103103
.replaceAll("__SERVICE_NAME__", config.serviceName)
104104

105-
export const renderEntrypointCodexResumeHint = (): string =>
106-
`# Ensure codex resume hint is shown for interactive shells
105+
const entrypointCodexResumeHintTemplate = `# Ensure codex resume hint is shown for interactive shells
107106
CODEX_HINT_PATH="/etc/profile.d/zz-codex-resume.sh"
108107
if [[ ! -s "$CODEX_HINT_PATH" ]]; then
109108
cat <<'EOF' > "$CODEX_HINT_PATH"
109+
docker_git_workspace_context_line() {
110+
REPO_REF_VALUE="\${REPO_REF:-}"
111+
REPO_URL_VALUE="\${REPO_URL:-}"
112+
113+
if [[ "$REPO_REF_VALUE" == issue-* ]]; then
114+
ISSUE_ID_VALUE="$(printf "%s" "$REPO_REF_VALUE" | sed -E 's#^issue-##')"
115+
ISSUE_URL_VALUE=""
116+
if [[ "$REPO_URL_VALUE" == https://github.com/* ]]; then
117+
ISSUE_REPO_VALUE="$(printf "%s" "$REPO_URL_VALUE" | sed -E 's#^https://github.com/##; s#[.]git$##; s#/*$##')"
118+
if [[ -n "$ISSUE_REPO_VALUE" ]]; then
119+
ISSUE_URL_VALUE="https://github.com/$ISSUE_REPO_VALUE/issues/$ISSUE_ID_VALUE"
120+
fi
121+
fi
122+
if [[ -n "$ISSUE_URL_VALUE" ]]; then
123+
printf "%s\n" "Контекст workspace: issue #$ISSUE_ID_VALUE ($ISSUE_URL_VALUE)"
124+
else
125+
printf "%s\n" "Контекст workspace: issue #$ISSUE_ID_VALUE"
126+
fi
127+
return
128+
fi
129+
130+
if [[ "$REPO_REF_VALUE" == refs/pull/*/head ]]; then
131+
PR_ID_VALUE="$(printf "%s" "$REPO_REF_VALUE" | sed -nE 's#^refs/pull/([0-9]+)/head$#\\1#p')"
132+
PR_URL_VALUE=""
133+
if [[ "$REPO_URL_VALUE" == https://github.com/* && -n "$PR_ID_VALUE" ]]; then
134+
PR_REPO_VALUE="$(printf "%s" "$REPO_URL_VALUE" | sed -E 's#^https://github.com/##; s#[.]git$##; s#/*$##')"
135+
if [[ -n "$PR_REPO_VALUE" ]]; then
136+
PR_URL_VALUE="https://github.com/$PR_REPO_VALUE/pull/$PR_ID_VALUE"
137+
fi
138+
fi
139+
if [[ -n "$PR_ID_VALUE" && -n "$PR_URL_VALUE" ]]; then
140+
printf "%s\n" "Контекст workspace: PR #$PR_ID_VALUE ($PR_URL_VALUE)"
141+
elif [[ -n "$PR_ID_VALUE" ]]; then
142+
printf "%s\n" "Контекст workspace: PR #$PR_ID_VALUE"
143+
elif [[ -n "$REPO_REF_VALUE" ]]; then
144+
printf "%s\n" "Контекст workspace: pull request ($REPO_REF_VALUE)"
145+
fi
146+
return
147+
fi
148+
149+
if [[ -n "$REPO_URL_VALUE" ]]; then
150+
printf "%s\n" "Контекст workspace: $REPO_URL_VALUE"
151+
fi
152+
}
153+
154+
docker_git_print_codex_resume_hint() {
155+
if [ -z "\${CODEX_RESUME_HINT_SHOWN-}" ]; then
156+
DOCKER_GIT_CONTEXT_LINE="$(docker_git_workspace_context_line)"
157+
if [[ -n "$DOCKER_GIT_CONTEXT_LINE" ]]; then
158+
echo "$DOCKER_GIT_CONTEXT_LINE"
159+
fi
160+
echo "Старые сессии можно запустить с помощью codex resume или codex resume <id>, если знаешь айди."
161+
export CODEX_RESUME_HINT_SHOWN=1
162+
fi
163+
}
164+
110165
if [ -n "$BASH_VERSION" ]; then
111166
case "$-" in
112167
*i*)
113-
if [ -z "\${CODEX_RESUME_HINT_SHOWN-}" ]; then
114-
echo "Старые сессии можно запустить с помощью codex resume или codex resume <id>, если знаешь айди."
115-
export CODEX_RESUME_HINT_SHOWN=1
116-
fi
168+
docker_git_print_codex_resume_hint
117169
;;
118170
esac
119171
fi
120172
if [ -n "$ZSH_VERSION" ]; then
121173
if [[ "$-" == *i* ]]; then
122-
if [[ -z "\${CODEX_RESUME_HINT_SHOWN-}" ]]; then
123-
echo "Старые сессии можно запустить с помощью codex resume или codex resume <id>, если знаешь айди."
124-
export CODEX_RESUME_HINT_SHOWN=1
125-
fi
174+
docker_git_print_codex_resume_hint
126175
fi
127176
fi
128177
EOF
@@ -135,6 +184,8 @@ if [[ -s /etc/zsh/zshrc ]] && ! grep -q "zz-codex-resume.sh" /etc/zsh/zshrc 2>/d
135184
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
136185
fi`
137186

187+
export const renderEntrypointCodexResumeHint = (): string => entrypointCodexResumeHintTemplate
188+
138189
const entrypointAgentsNoticeTemplate = String.raw`# Ensure global AGENTS.md exists for container context
139190
AGENTS_PATH="__CODEX_HOME__/AGENTS.md"
140191
LEGACY_AGENTS_PATH="/home/__SSH_USER__/AGENTS.md"
@@ -160,8 +211,17 @@ if [[ "$REPO_REF" == issue-* ]]; then
160211
fi
161212
ISSUE_AGENTS_HINT_LINE="Issue AGENTS.md: __TARGET_DIR__/AGENTS.md"
162213
elif [[ "$REPO_REF" == refs/pull/*/head ]]; then
163-
PR_ID="$(printf "%s" "$REPO_REF" | sed -E 's#^refs/pull/([0-9]+)/head$#\1#')"
164-
if [[ -n "$PR_ID" ]]; then
214+
PR_ID="$(printf "%s" "$REPO_REF" | sed -nE 's#^refs/pull/([0-9]+)/head$#\1#p')"
215+
PR_URL=""
216+
if [[ "$REPO_URL" == https://github.com/* && -n "$PR_ID" ]]; then
217+
PR_REPO="$(printf "%s" "$REPO_URL" | sed -E 's#^https://github.com/##; s#[.]git$##; s#/*$##')"
218+
if [[ -n "$PR_REPO" ]]; then
219+
PR_URL="https://github.com/$PR_REPO/pull/$PR_ID"
220+
fi
221+
fi
222+
if [[ -n "$PR_ID" && -n "$PR_URL" ]]; then
223+
WORKSPACE_INFO_LINE="Контекст workspace: PR #$PR_ID ($PR_URL)"
224+
elif [[ -n "$PR_ID" ]]; then
165225
WORKSPACE_INFO_LINE="Контекст workspace: PR #$PR_ID"
166226
else
167227
WORKSPACE_INFO_LINE="Контекст workspace: pull request ($REPO_REF)"

scripts/e2e/login-context.sh

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
RUN_ID="$(date +%s)-$RANDOM"
5+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
6+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
7+
ROOT_BASE="${DOCKER_GIT_E2E_ROOT_BASE:-$REPO_ROOT/.docker-git/e2e-root}"
8+
mkdir -p "$ROOT_BASE"
9+
ROOT="$(mktemp -d "$ROOT_BASE/login-context.XXXXXX")"
10+
# docker-git containers may `chown -R` the `.docker-git` bind mount to UID 1000.
11+
# `mktemp -d` creates 0700 dirs; if ownership changes, the host runner may lose access.
12+
chmod 0755 "$ROOT"
13+
KEEP="${KEEP:-0}"
14+
15+
export DOCKER_GIT_PROJECTS_ROOT="$ROOT"
16+
export DOCKER_GIT_STATE_AUTO_SYNC=0
17+
18+
ACTIVE_OUT_DIR=""
19+
ACTIVE_CONTAINER=""
20+
ACTIVE_SERVICE=""
21+
22+
fail() {
23+
echo "e2e/login-context: $*" >&2
24+
exit 1
25+
}
26+
27+
on_error() {
28+
local line="$1"
29+
echo "e2e/login-context: failed at line $line" >&2
30+
docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | head -n 80 || true
31+
if [[ -n "$ACTIVE_OUT_DIR" ]] && [[ -f "$ACTIVE_OUT_DIR/docker-compose.yml" ]]; then
32+
(cd "$ACTIVE_OUT_DIR" && docker compose ps) || true
33+
(cd "$ACTIVE_OUT_DIR" && docker compose logs --no-color --tail 200) || true
34+
fi
35+
}
36+
37+
cleanup_active_case() {
38+
if [[ -n "$ACTIVE_OUT_DIR" ]] && [[ -f "$ACTIVE_OUT_DIR/docker-compose.yml" ]]; then
39+
(cd "$ACTIVE_OUT_DIR" && docker compose down -v --remove-orphans) >/dev/null 2>&1 || true
40+
fi
41+
ACTIVE_OUT_DIR=""
42+
ACTIVE_CONTAINER=""
43+
ACTIVE_SERVICE=""
44+
}
45+
46+
cleanup() {
47+
if [[ "$KEEP" == "1" ]]; then
48+
echo "e2e/login-context: KEEP=1 set; preserving temp dir: $ROOT" >&2
49+
if [[ -n "$ACTIVE_CONTAINER" ]]; then
50+
echo "e2e/login-context: active container: $ACTIVE_CONTAINER" >&2
51+
fi
52+
if [[ -n "$ACTIVE_OUT_DIR" ]]; then
53+
echo "e2e/login-context: active out dir: $ACTIVE_OUT_DIR" >&2
54+
fi
55+
return
56+
fi
57+
cleanup_active_case
58+
rm -rf "$ROOT" >/dev/null 2>&1 || true
59+
}
60+
61+
trap 'on_error $LINENO' ERR
62+
trap cleanup EXIT
63+
64+
command -v ssh >/dev/null 2>&1 || fail "missing 'ssh' command"
65+
command -v timeout >/dev/null 2>&1 || fail "missing 'timeout' command"
66+
command -v ssh-keygen >/dev/null 2>&1 || fail "missing 'ssh-keygen' command"
67+
68+
ssh-keygen -q -t ed25519 -N "" -f "$ROOT/dev_ssh_key" >/dev/null
69+
cp "$ROOT/dev_ssh_key.pub" "$ROOT/authorized_keys"
70+
chmod 0600 "$ROOT/dev_ssh_key"
71+
chmod 0644 "$ROOT/authorized_keys"
72+
73+
wait_for_ssh() {
74+
local ssh_port="$1"
75+
local attempts=30
76+
local attempt=1
77+
78+
while [[ "$attempt" -le "$attempts" ]]; do
79+
if timeout 1 bash -lc "cat < /dev/null > /dev/tcp/127.0.0.1/$ssh_port" >/dev/null 2>&1; then
80+
return 0
81+
fi
82+
sleep 1
83+
attempt="$((attempt + 1))"
84+
done
85+
86+
return 1
87+
}
88+
89+
run_case() {
90+
local case_name="$1"
91+
local repo_url="$2"
92+
local expected_context_line="$3"
93+
local out_dir_rel=".docker-git/e2e/login-context-${case_name}-${RUN_ID}"
94+
local out_dir="$ROOT/e2e/login-context-${case_name}-${RUN_ID}"
95+
local container_name="dg-e2e-login-${case_name}-${RUN_ID}"
96+
local service_name="dg-e2e-login-${case_name}-${RUN_ID}"
97+
local volume_name="dg-e2e-login-${case_name}-${RUN_ID}-home"
98+
local ssh_port="$(( (RANDOM % 1000) + 21000 ))"
99+
local login_log="$ROOT/login-${case_name}.log"
100+
101+
mkdir -p "$out_dir/.orch/env"
102+
cat > "$out_dir/.orch/env/project.env" <<'EOF_ENV'
103+
# docker-git project env (e2e)
104+
CODEX_AUTO_UPDATE=0
105+
CODEX_SHARE_AUTH=1
106+
EOF_ENV
107+
108+
ACTIVE_OUT_DIR="$out_dir"
109+
ACTIVE_CONTAINER="$container_name"
110+
ACTIVE_SERVICE="$service_name"
111+
112+
(
113+
cd "$REPO_ROOT"
114+
pnpm run docker-git clone "$repo_url" \
115+
--force \
116+
--no-ssh \
117+
--authorized-keys "$ROOT/authorized_keys" \
118+
--ssh-port "$ssh_port" \
119+
--out-dir "$out_dir_rel" \
120+
--container-name "$container_name" \
121+
--service-name "$service_name" \
122+
--volume-name "$volume_name"
123+
)
124+
125+
wait_for_ssh "$ssh_port" || fail "ssh port did not open for $case_name (port: $ssh_port)"
126+
127+
set +e
128+
timeout 30s bash -lc "printf 'exit\n' | ssh -i \"$ROOT/dev_ssh_key\" -tt -o LogLevel=ERROR -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p \"$ssh_port\" dev@localhost" > "$login_log" 2>&1
129+
local ssh_exit=$?
130+
set -e
131+
132+
if [[ "$ssh_exit" -ne 0 ]]; then
133+
cat "$login_log" >&2 || true
134+
fail "ssh login failed for $case_name (exit: $ssh_exit)"
135+
fi
136+
137+
grep -Fq -- "$expected_context_line" "$login_log" \
138+
|| fail "expected context line not found for $case_name: $expected_context_line"
139+
140+
grep -Fq -- "Старые сессии можно запустить с помощью codex resume" "$login_log" \
141+
|| fail "expected codex resume hint for $case_name"
142+
143+
cleanup_active_case
144+
}
145+
146+
run_case \
147+
"issue" \
148+
"https://github.com/octocat/Hello-World/issues/1" \
149+
"Контекст workspace: issue #1 (https://github.com/octocat/Hello-World/issues/1)"
150+
151+
run_case \
152+
"pr" \
153+
"https://github.com/octocat/Hello-World/pull/1" \
154+
"Контекст workspace: PR #1 (https://github.com/octocat/Hello-World/pull/1)"

0 commit comments

Comments
 (0)