Skip to content

Commit a2a88b1

Browse files
authored
Create and configure new actions workflow for E2E tests (#2721)
Add new e2e.yaml workflow for running Cypress E2E tests in CI Update pull-request.yaml to trigger E2E workflow on PRs Configure cypress.config.js for retries and extended timeouts in CI Add DEBUG_CYPRESS env variable and conditional debug steps for enhanced diagnostics and video replay recordings Enhance failure diagnostics with detailed iframe and body HTML logging Add health checks and wait loops to ensure services are ready before starting tests Ensure VSIX extension is installed and verified before running tests Improve iframe detection and fallback logic for publisher webview in tests Add helper functions for robust waits and retries to stabilize CI runs Add uncaught exception handler for VS Code web, Quarto extension, and common browser errors Create utility script for repeatable test runs with per-spec execution and detailed summaries Update .gitignore and CONTRIBUTING.md for new CI workflow and local files
1 parent 65e8d9b commit a2a88b1

18 files changed

+935
-80
lines changed

.github/workflows/e2e.yaml

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
name: E2E Tests
2+
on:
3+
workflow_call:
4+
secrets:
5+
CONNECT_LICENSE:
6+
required: true
7+
8+
jobs:
9+
cypress:
10+
name: e2e tests (cypress)
11+
runs-on: ubuntu-latest
12+
env:
13+
DEBUG_CYPRESS: "false" # Set to true to enable extra logging and video recording replays
14+
steps:
15+
- name: Checkout code
16+
uses: actions/checkout@v3
17+
with:
18+
fetch-depth: 0
19+
20+
- name: Set up Python
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: "3.11"
24+
cache: pip
25+
cache-dependency-path: test/e2e/requirements.txt
26+
27+
- name: Set up Node.js
28+
uses: actions/setup-node@v4
29+
with:
30+
node-version: 20
31+
cache: npm
32+
cache-dependency-path: test/e2e/package-lock.json
33+
34+
- name: Install Python dependencies
35+
working-directory: test/e2e
36+
run: pip install -r requirements.txt
37+
38+
- name: Install npm dependencies
39+
working-directory: test/e2e
40+
run: npm ci
41+
42+
- uses: extractions/setup-just@v2
43+
44+
- name: Build publisher binary
45+
run: just build
46+
47+
- name: Download VSIX artifact
48+
uses: actions/download-artifact@v4
49+
with:
50+
name: dist
51+
path: dist
52+
53+
- name: Write Connect license file
54+
if: env.CONNECT_LICENSE != ''
55+
run: |
56+
mkdir -p ./test/e2e/licenses
57+
echo "$CONNECT_LICENSE" > ./test/e2e/licenses/connect-license.lic
58+
env:
59+
CONNECT_LICENSE: ${{ secrets.CONNECT_LICENSE }}
60+
61+
- name: Build Docker images
62+
working-directory: test/e2e
63+
run: |
64+
docker build --build-arg GH_DOWNLOAD_TOKEN=${{ secrets.GH_DOWNLOAD_TOKEN }} -f Dockerfile.base -t e2ebase --platform linux/amd64 .
65+
docker compose build connect-publisher-e2e code-server
66+
env:
67+
CONNECT_LICENSE: ${{ secrets.CONNECT_LICENSE }}
68+
69+
- name: "[DEBUG] Print host user and group info"
70+
if: always() && (env.DEBUG_CYPRESS == 'true' || runner.debug)
71+
run: |
72+
echo "USER: $(whoami)"
73+
echo "UID: $(id -u)"
74+
echo "GID: $(id -g)"
75+
id
76+
ls -ld .
77+
78+
- name: Ensure content-workspace exists
79+
run: mkdir -p ./test/e2e/content-workspace
80+
81+
- name: Start containers
82+
working-directory: test/e2e
83+
run: |
84+
just start "connect-publisher-e2e"
85+
just start "code-server"
86+
87+
- name: Wait for Connect server to be ready
88+
run: |
89+
echo "Waiting for Connect server to be ready..."
90+
for i in {1..30}; do
91+
if curl -sf http://localhost:3939/__ping__ > /dev/null; then
92+
echo "Connect server is ready!"
93+
break
94+
fi
95+
if [ $i -eq 30 ]; then
96+
echo "WARNING: Timeout waiting for Connect server to be ready"
97+
# Continue anyway, don't exit with error
98+
fi
99+
echo "Waiting for Connect server (attempt $i/30)..."
100+
sleep 2
101+
done
102+
103+
- name: Stop any processes using port 8080
104+
run: |
105+
PID=$(docker exec publisher-e2e.code-server sh -c "lsof -ti :8080 || netstat -tulnp 2>/dev/null | grep ':8080' | awk '{print \$7}' | cut -d'/' -f1 | head -n1" || true)
106+
if [ -n "$PID" ]; then
107+
echo "Killing process $PID using port 8080"
108+
docker exec publisher-e2e.code-server kill -9 $PID || true
109+
else
110+
echo "No process using port 8080"
111+
fi
112+
113+
- name: Wait for code-server to be ready
114+
run: |
115+
for i in {1..30}; do
116+
if curl -sf http://localhost:8080 > /dev/null; then
117+
echo "code-server is ready!"
118+
break
119+
fi
120+
if [ $i -eq 30 ]; then
121+
echo "code-server did not become ready in time"
122+
exit 1
123+
fi
124+
sleep 2
125+
done
126+
127+
- name: Install and wait for VS Code extension to be ready
128+
working-directory: test/e2e
129+
run: |
130+
# Install the Publisher extension
131+
VSIX_FILENAME=$(ls -Art ../../dist | grep linux-amd64 | tail -n 1)
132+
docker compose exec code-server code-server --install-extension "/home/coder/vsix/${VSIX_FILENAME}"
133+
134+
# Wait for the extension to be installed (more reliable than process grep)
135+
echo "Waiting for Publisher extension to be installed..."
136+
for i in {1..60}; do
137+
if docker compose exec code-server code-server --list-extensions | grep -q posit.publisher; then
138+
echo "Publisher extension appears to be installed!"
139+
break
140+
fi
141+
if [ $i -eq 60 ]; then
142+
echo "WARNING: Timeout waiting for Publisher extension to be installed"
143+
fi
144+
echo "Waiting for extension installation (attempt $i/60)..."
145+
sleep 2
146+
done
147+
148+
- name: "[DEBUG] Print workspace file ownership before tests"
149+
if: always() && (env.DEBUG_CYPRESS == 'true' || runner.debug)
150+
run: |
151+
docker exec publisher-e2e.code-server ls -lR /home/coder/workspace || true
152+
153+
- name: Prepare test environment
154+
working-directory: test/e2e
155+
run: |
156+
# Clean up any static TOML files before tests
157+
docker exec publisher-e2e.code-server rm -f /home/coder/workspace/static*.toml || true
158+
159+
- name: Run Cypress tests
160+
working-directory: test/e2e
161+
run: npx cypress run
162+
env:
163+
CONNECT_LICENSE: ${{ secrets.CONNECT_LICENSE }}
164+
CONNECT_CLOUD_ENV: ${{ env.CONNECT_CLOUD_ENV }} # defaults to staging
165+
CI: true
166+
DEBUG_CYPRESS: ${{ env.DEBUG_CYPRESS }}
167+
ACTIONS_STEP_DEBUG: ${{ runner.debug && 'true' || 'false' }}
168+
169+
- name: Upload Cypress screenshots on failure
170+
uses: actions/upload-artifact@v4
171+
if: failure()
172+
with:
173+
name: cypress-screenshots
174+
path: test/e2e/cypress/screenshots
175+
if-no-files-found: ignore
176+
177+
- name: "[DEBUG] Upload Cypress videos"
178+
uses: actions/upload-artifact@v4
179+
if: always() && (env.DEBUG_CYPRESS == 'true' || runner.debug)
180+
with:
181+
name: cypress-videos
182+
path: test/e2e/cypress/videos
183+
if-no-files-found: ignore
184+
185+
- name: "[DEBUG] List installed VS Code extensions"
186+
if: always() && (env.DEBUG_CYPRESS == 'true' || runner.debug)
187+
working-directory: test/e2e
188+
run: docker compose exec code-server code-server --list-extensions --show-versions || true
189+
190+
- name: "[DEBUG] Display test/e2e directory tree"
191+
if: always() && (env.DEBUG_CYPRESS == 'true' || runner.debug)
192+
run: |
193+
echo "Directory tree for test/e2e:"
194+
if command -v tree > /dev/null; then
195+
tree -I "node_modules|.venv" test/e2e
196+
else
197+
find test/e2e -path "*/node_modules" -prune -o -path "*/.venv" -prune -o -print
198+
fi
199+
200+
- name: "[DEBUG] Print code-server logs"
201+
if: always() && (env.DEBUG_CYPRESS == 'true' || runner.debug)
202+
run: |
203+
echo "=== Code Server Log Files ==="
204+
docker exec publisher-e2e.code-server bash -c 'find /root/.local -type f -name "*.log" 2>/dev/null | while read -r file; do echo "FILE: $file"; done'
205+
echo "=== Main Code Server Logs (sample) ==="
206+
docker exec publisher-e2e.code-server bash -c 'cat /root/.local/share/code-server/coder-logs/code-server-*.log 2>/dev/null | tail -n 50 || echo "No logs found"'
207+
echo "=== Extension Host Logs (sample) ==="
208+
docker exec publisher-e2e.code-server bash -c 'find /root/.local -name "remoteexthost.log" -type f 2>/dev/null | head -n 1 | xargs cat | tail -n 50 || echo "No logs found"'
209+
210+
- name: "[DEBUG] Print Publisher extension logs"
211+
if: always() && (env.DEBUG_CYPRESS == 'true' || runner.debug)
212+
run: |
213+
echo "=== Finding Publisher logs ==="
214+
docker exec publisher-e2e.code-server bash -c 'find /root -type f -name "*.log" 2>/dev/null | grep -i "posit\|publisher" || echo "No publisher logs found"'
215+
docker exec publisher-e2e.code-server bash -c 'find /home -type f -name "*.log" 2>/dev/null | grep -i "posit\|publisher" || echo "No publisher logs found"'
216+
echo "======================================="
217+
218+
- name: "[DEBUG] Print code-server container logs"
219+
if: always() && (env.DEBUG_CYPRESS == 'true' || runner.debug)
220+
working-directory: test/e2e
221+
run: docker compose logs code-server || true
222+
223+
- name: "[DEBUG] List /home/coder directory contents"
224+
if: always() && (env.DEBUG_CYPRESS == 'true' || runner.debug)
225+
run: docker exec publisher-e2e.code-server ls -lR /home/coder || true
226+
227+
- name: Stop containers
228+
if: always()
229+
working-directory: test/e2e
230+
run: just stop

.github/workflows/pull-request.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ jobs:
1616
uses: ./.github/workflows/agent.yaml
1717
vscode:
1818
uses: ./.github/workflows/vscode.yaml
19+
# E2E Tests
20+
e2e:
21+
needs: [home-view-unit-tests, agent, vscode, package]
22+
uses: ./.github/workflows/e2e.yaml
23+
secrets: inherit
1924

2025
# Build
2126
build:

.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ node_modules
1010

1111
.DS_Store
1212

13+
*renv/
14+
*.venv/
15+
.envrc
16+
1317
# Visual Studio Code
1418
.vscode/*
1519
!.vscode/recommended.settings.json
@@ -31,6 +35,7 @@ cmd/publisher/__debug_bin*
3135

3236
# Possible e2e tests deployment assets
3337
test/e2e/content-workspace/**/.posit
38+
screenshots/
3439

3540
# license files should not be commited to this repository
3641
*.lic

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- Added Cypress E2E CI Setup and Test Reliability Improvements (#2721)
1213
- Added endpoints for performing OAuth Device Authorization Grant with Posit Cloud login. (#2692)
1314
- Added support for one-click token authentication with Connect. (#2769)
1415
- Added schema and agent support for publishing to Connect Cloud. (#2729, #2747, #2771)

CONTRIBUTING.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,9 @@ Once complete, a coverage report will open in your default browser.
9494

9595
End-to-end tests are written in JavaScript and utilize Cypress for testing the Posit Publisher VSCode extension.
9696

97-
They are not currently run within the CI pipeline, but can be run locally to verify that the extension works as expected in a Connect environment.
97+
These tests can be run locally to verify that the extension works as expected in a Connect environment.
98+
99+
These tests also run automatically in the GitHub Actions CI pipeline for pull requests after the unit tests have passed. The workflow uses the `CONNECT_LICENSE` secret stored in the repository settings to authenticate with Connect during testing. Results, including screenshots of failed tests of test runs, are uploaded as artifacts for troubleshooting. (Video replays can be optionally enabled by setting local environment variable `DEBUG_CYPRESS` or `ACTIONS_STEP_DEBUG` to true).
98100

99101
#### Requirements
100102

@@ -190,6 +192,14 @@ just stop
190192
**NOTE: ** If you are updating the images in any way, where you need to rebuild the images with `just build-images`,
191193
you will need to run the `just stop` command to remove the existing containers before running `just dev`.
192194

195+
#### Repeat Tests Headless Script
196+
197+
Allows you to run your Cypress E2E tests multiple times in headless mode, either for all tests or for specific test files. It is useful for detecting flaky tests and verifying test suite stability.
198+
199+
```bash
200+
./repeat-cypress-headless.sh [REPEAT=N] [spec1] [spec2] [...]
201+
```
202+
193203
## Development
194204

195205
### Build Tools

test/e2e/Dockerfile.base

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -123,34 +123,41 @@ RUN curl -O https://cdn.rstudio.com/r/${OS_IDENTIFIER}/pkgs/r-${R_VERSION}_1_amd
123123

124124
FROM base AS quarto-1.4.556
125125
ENV QUARTO_VERSION=1.4.556
126-
# from https://docs.posit.co/resources/install-quarto.html#download-install-quarto-tar
126+
ARG GH_DOWNLOAD_TOKEN
127127
RUN mkdir -p /opt/quarto/${QUARTO_VERSION} \
128-
&& curl -o quarto.tar.gz -L \
129-
"https://github.com/quarto-dev/quarto-cli/releases/download/v${QUARTO_VERSION}/quarto-${QUARTO_VERSION}-linux-amd64.tar.gz" \
128+
&& curl -H "Authorization: token ${GH_DOWNLOAD_TOKEN}" -fSL -o quarto.tar.gz \
129+
"https://github.com/quarto-dev/quarto-cli/releases/download/v${QUARTO_VERSION}/quarto-${QUARTO_VERSION}-linux-amd64.tar.gz" \
130+
&& file quarto.tar.gz | grep 'gzip compressed data' \
130131
&& tar -zxvf quarto.tar.gz -C "/opt/quarto/${QUARTO_VERSION}" --strip-components=1 \
131132
&& rm quarto.tar.gz
132133

133134
FROM base AS quarto-1.5.52
134135
ENV QUARTO_VERSION=1.5.52
136+
ARG GH_DOWNLOAD_TOKEN
135137
RUN mkdir -p /opt/quarto/${QUARTO_VERSION} \
136-
&& curl -o quarto.tar.gz -L \
137-
"https://github.com/quarto-dev/quarto-cli/releases/download/v${QUARTO_VERSION}/quarto-${QUARTO_VERSION}-linux-amd64.tar.gz" \
138+
&& curl -H "Authorization: token ${GH_DOWNLOAD_TOKEN}" -fSL -o quarto.tar.gz \
139+
"https://github.com/quarto-dev/quarto-cli/releases/download/v${QUARTO_VERSION}/quarto-${QUARTO_VERSION}-linux-amd64.tar.gz" \
140+
&& file quarto.tar.gz | grep 'gzip compressed data' \
138141
&& tar -zxvf quarto.tar.gz -C "/opt/quarto/${QUARTO_VERSION}" --strip-components=1 \
139142
&& rm quarto.tar.gz
140143

141144
FROM base AS quarto-1.6.42
142145
ENV QUARTO_VERSION=1.6.42
146+
ARG GH_DOWNLOAD_TOKEN
143147
RUN mkdir -p /opt/quarto/${QUARTO_VERSION} \
144-
&& curl -o quarto.tar.gz -L \
145-
"https://github.com/quarto-dev/quarto-cli/releases/download/v${QUARTO_VERSION}/quarto-${QUARTO_VERSION}-linux-amd64.tar.gz" \
148+
&& curl -H "Authorization: token ${GH_DOWNLOAD_TOKEN}" -fSL -o quarto.tar.gz \
149+
"https://github.com/quarto-dev/quarto-cli/releases/download/v${QUARTO_VERSION}/quarto-${QUARTO_VERSION}-linux-amd64.tar.gz" \
150+
&& file quarto.tar.gz | grep 'gzip compressed data' \
146151
&& tar -zxvf quarto.tar.gz -C "/opt/quarto/${QUARTO_VERSION}" --strip-components=1 \
147152
&& rm quarto.tar.gz
148153

149154
FROM base AS quarto-1.7.6
150155
ENV QUARTO_VERSION=1.7.6
156+
ARG GH_DOWNLOAD_TOKEN
151157
RUN mkdir -p /opt/quarto/${QUARTO_VERSION} \
152-
&& curl -o quarto.tar.gz -L \
153-
"https://github.com/quarto-dev/quarto-cli/releases/download/v${QUARTO_VERSION}/quarto-${QUARTO_VERSION}-linux-amd64.tar.gz" \
158+
&& curl -H "Authorization: token ${GH_DOWNLOAD_TOKEN}" -fSL -o quarto.tar.gz \
159+
"https://github.com/quarto-dev/quarto-cli/releases/download/v${QUARTO_VERSION}/quarto-${QUARTO_VERSION}-linux-amd64.tar.gz" \
160+
&& file quarto.tar.gz | grep 'gzip compressed data' \
154161
&& tar -zxvf quarto.tar.gz -C "/opt/quarto/${QUARTO_VERSION}" --strip-components=1 \
155162
&& rm quarto.tar.gz
156163

@@ -195,4 +202,4 @@ ARG QUARTO_VERSION=1.4.556
195202
RUN ln -s /opt/quarto/${QUARTO_VERSION}/bin/quarto /usr/bin/quarto
196203

197204
# clean up from our apt-update
198-
RUN rm -rf /var/lib/apt/lists/*
205+
RUN rm -rf /var/lib/apt/lists/*

test/e2e/code-server-entry.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,5 @@ cat <<EOF > /home/coder/.local/share/code-server/User/settings.json
2020
2121
EOF
2222

23-
# Run the original code server entrypoint that starts the service
24-
/usr/bin/code-server --disable-workspace-trust --auth none --bind-addr 0.0.0.0:8080 .
23+
# Code server entrypoint that starts the service
24+
exec /usr/bin/code-server --disable-workspace-trust --auth none --bind-addr 0.0.0.0:8080 .

0 commit comments

Comments
 (0)