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
68 changes: 68 additions & 0 deletions .cursor/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Cloud agent image for building the plugin and running it against the stack in
# e2e/integration/docker-compose.yml (Mattermost and Postgres) and the fake GitHub from
# e2e/integration/fake-github/, from the agent's browser or the Playwright suite. Adapted from
# mattermost-plugin-gitlab's .cursor environment.
FROM ubuntu:24.04

ARG DEBIAN_FRONTEND=noninteractive
# install.sh warns when these drift from go.mod and .nvmrc.
ARG GO_VERSION=1.25.8
ARG NODE_VERSION=24.13.1
ARG DOCKER_VERSION=5:28.5.2-1~ubuntu.24.04~noble

RUN apt-get update && apt-get install -y --no-install-recommends \
acl \
build-essential \
ca-certificates \
curl \
fuse-overlayfs \
git \
gnupg \
iptables \
jq \
make \
procps \
python3 \
sudo \
xz-utils \
&& rm -rf /var/lib/apt/lists/*

RUN install -m 0755 -d /etc/apt/keyrings \
&& curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu noble stable" \
> /etc/apt/sources.list.d/docker.list \
&& apt-get update && apt-get install -y --no-install-recommends \
docker-ce=${DOCKER_VERSION} \
docker-ce-cli=${DOCKER_VERSION} \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin \
&& rm -rf /var/lib/apt/lists/*

# Docker runs nested inside the agent's container.
RUN mkdir -p /etc/docker \
&& echo '{"storage-driver": "fuse-overlayfs"}' > /etc/docker/daemon.json \
&& update-alternatives --set iptables /usr/sbin/iptables-legacy \
&& update-alternatives --set ip6tables /usr/sbin/ip6tables-legacy

RUN arch="$(dpkg --print-architecture)" \
&& curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-${arch}.tar.gz" | tar -C /usr/local -xz \
&& node_arch="$([ "$arch" = amd64 ] && echo x64 || echo "$arch")" \
&& mkdir -p /usr/local/node \
&& curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${node_arch}.tar.xz" \
| tar -C /usr/local/node -xJ --strip-components=1 \
# Cursor can run the hooks and terminals without the ENV lines below (like the Playbooks, core
# and Wrangler images, which do the same), so the tools are also on the default PATH.
&& for bin in go gofmt; do ln -sf /usr/local/go/bin/$bin /usr/local/bin/$bin; done \
&& for bin in node npm npx; do ln -sf /usr/local/node/bin/$bin /usr/local/bin/$bin; done

RUN (id ubuntu >/dev/null 2>&1 || useradd -m -s /bin/bash ubuntu) \
&& usermod -aG docker,sudo ubuntu \
&& echo 'ubuntu ALL=(ALL) NOPASSWD:ALL' > /etc/sudoers.d/ubuntu \
&& chmod 0440 /etc/sudoers.d/ubuntu \
&& printf '\nexport GOPATH=/home/ubuntu/go\nexport PATH=/usr/local/go/bin:/usr/local/node/bin:/home/ubuntu/go/bin:$PATH\n' >> /home/ubuntu/.bashrc

USER ubuntu
ENV GOPATH=/home/ubuntu/go
ENV PATH=/usr/local/go/bin:/usr/local/node/bin:/home/ubuntu/go/bin:${PATH}
WORKDIR /home/ubuntu
22 changes: 22 additions & 0 deletions .cursor/environment.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "mattermost-plugin-github",
"build": {
"dockerfile": "Dockerfile"
},
"user": "ubuntu",
"install": ".cursor/install.sh",
"start": ".cursor/start.sh",
"terminals": [
{
"name": "github-stack",
"command": "make e2e-env-up && echo 'E2E stack is healthy' && cd e2e/integration && npx --yes tsx@4.23.15 ../../.cursor/fake-github.ts",
"description": "Boots Mattermost and Postgres from e2e/integration/docker-compose.yml, then runs the fake GitHub (.cursor/fake-github.ts, built on e2e/integration/fake-github/) in the foreground. Wait for '[fake-github] Listening'. There is no real GitHub here: never use `make e2e-real`, `npm run test:real` or GitHub credentials. Open Mattermost at http://127.0.0.1:8069 (use 127.0.0.1, not localhost: it is the SiteURL and the OAuth callback). The server starts with no users; create the admin with `cd e2e/integration && docker compose exec mattermost /mattermost/bin/mmctl --local user create --email sysadmin@example.com --username sysadmin --password 'Sys@dmin-sample1' --system-admin --email-verified`, then a team from the browser. Build and install the plugin with `make e2e-dist && MM_SERVICESETTINGS_SITEURL=http://127.0.0.1:8069 MM_ADMIN_USERNAME=sysadmin MM_ADMIN_PASSWORD='Sys@dmin-sample1' ./build/bin/pluginctl deploy github dist/e2e/github-*-e2e.tar.gz` (the e2e bundle sends GitHub OAuth, REST and GraphQL to http://fake-github:8093; `make dist` and `make deploy` would talk to real GitHub). Rerun it after every change. In System Console > Plugins > GitHub set GitHub OAuth Client ID Iv1.e2e0123456789abc, Client Secret e2e-github-oauth-client-secret-0000000000, Webhook Secret e2e-github-webhook-secret, and generate the At Rest Encryption Key. `/github connect` opens the fake's Authorize page, already signed in as octo-agent. The fake is seeded with org acme, repos acme/widgets (two PRs, an issue and a webhook to the plugin) and octo-agent/sandbox. Shape it with the control API at http://localhost:8093/__control/ (see e2e/integration/support/fake_github.ts: users, repos, items, threads, sign-in, faults) and, after subscribing a channel with `/github subscriptions add acme/widgets`, send webhook events with POST /__control/webhooks, e.g. `{\"event\": \"issues\", \"action\": \"opened\", \"repo\": \"acme/widgets\", \"number\": 3, \"sender\": \"octo-author\"}` (see e2e/integration/fake-github/webhooks.ts). Reset and reseed with `cd e2e/integration && npx --yes tsx@4.23.15 ../../.cursor/fake-github.ts --seed`. Server logs: `cd e2e/integration && docker compose logs mattermost`. GitHub calls the plugin made: http://localhost:8093/__control/requests; responses that break GitHub's API contract: http://localhost:8093/__control/contract-violations. Tests: stop the fake first (Ctrl-C here; the suite starts its own on 8093), then `make e2e` or `cd e2e/integration && npx playwright test tests/<spec>.spec.ts`. The suite reinstalls the plugin with its own settings and creates sysadmin / Sys@dmin-sample1 if missing. Reset all data with `make e2e-env-down && make e2e-env-up`."
}
],
"ports": [
{
"name": "Mattermost",
"port": 8069
}
]
}
59 changes: 59 additions & 0 deletions .cursor/fake-github.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Copyright (c) 2018-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

// Runs the E2E suite's fake GitHub on its own so an agent can use the plugin from a browser, and
// seeds a small world to start from. The Playwright suite starts its own copy in global_setup, so
// stop this one (Ctrl-C in the github-stack terminal) before `make e2e`.
//
// npx --yes tsx@4.23.15 ../../.cursor/fake-github.ts start on :8093 and seed (run from e2e/integration)
// npx --yes tsx@4.23.15 ../../.cursor/fake-github.ts --seed reset a running fake and seed it again

import {startFakeGitHub} from '../e2e/integration/fake-github/server';
import {fakeGitHub} from '../e2e/integration/support/fake_github';
import {fakeGitHubPort, fakeGitHubURL, oauthApp, webhookSecret, webhookURL} from '../e2e/integration/support/constants';

const login = 'octo-agent';
const repo = 'acme/widgets';

async function seed() {
await fakeGitHub.reset();
await fakeGitHub.createUser(login, {name: 'Octo Agent', email: 'octo-agent@fake-github.test'});
await fakeGitHub.createUser('octo-author', {name: 'Octo Author'});
await fakeGitHub.createOrg('acme', {members: [login, 'octo-author'], admins: [login]});
await fakeGitHub.createRepo(repo, {admins: [login], writers: ['octo-author'], labels: ['bug', 'enhancement'], milestones: ['v1.0']});
await fakeGitHub.createRepo(`${login}/sandbox`, {admins: [login]});
const pr = await fakeGitHub.createItem(repo, {kind: 'pull', title: 'Polish the widgets', author: login, labels: ['enhancement'], body: 'Splits polishing into stages.'});
await fakeGitHub.createItem(repo, {kind: 'pull', title: 'Please review the gizmo', author: 'octo-author', reviewers: [login]});
await fakeGitHub.createItem(repo, {kind: 'issue', title: 'Widgets squeak on Mondays', author: 'octo-author', assignees: [login], labels: ['bug'], milestone: 1});
await fakeGitHub.createThread(login, repo, {number: pr.number, reason: 'mention'});

// Deliveries from POST /__control/webhooks reach the plugin once its webhook secret matches.
await fakeGitHub.createHook('acme', repo.split('/')[1], webhookURL, webhookSecret);

// The OAuth page then goes straight to "Authorize"; POST /__control/sign-in {"login": null} shows the sign-in form instead.
await fakeGitHub.signIn(login);
}

async function main() {
if (process.argv.includes('--seed')) {
await seed();
console.log(`[fake-github] Reset and seeded the fake at ${fakeGitHubURL}.`);
return;
}

const fake = await startFakeGitHub({port: fakeGitHubPort, publicURL: fakeGitHubURL, app: oauthApp});
await seed();
console.log(`[fake-github] Listening at ${fakeGitHubURL} (control API at http://localhost:${fakeGitHubPort}/__control/), signed in as ${login}; repos ${repo} and ${login}/sandbox.`);

const stop = async () => {
await fake.close();
process.exit(0);
};
process.on('SIGINT', stop);
process.on('SIGTERM', stop);
}

main().catch((err) => {
console.error(err);
process.exit(1);
});
46 changes: 46 additions & 0 deletions .cursor/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Runs on every Build and must stay idempotent. Only disk state (including /var/lib/docker)
# survives, so the stack is booted once here and left stopped: agents then skip pulling the
# Mattermost and Postgres images.
set -euo pipefail

cd "$(dirname "$0")/.."

if ! command -v docker >/dev/null 2>&1; then
echo "Docker is not installed, so this agent did not boot from .cursor/Dockerfile." >&2
echo "Check the agent's environment: a saved or default environment may have been used instead." >&2
exit 1
fi

image_go="$(GOTOOLCHAIN=local go env GOVERSION)"
repo_go="go$(go mod edit -json | jq -r .Go)"
if [ "$image_go" != "$repo_go" ]; then
echo "WARNING: image has $image_go but go.mod requires $repo_go; update GO_VERSION in .cursor/Dockerfile." >&2
fi

image_node="$(node --version)"
repo_node="v$(tr -d '[:space:]' < .nvmrc)"
if [ "$image_node" != "$repo_node" ]; then
echo "WARNING: image has Node $image_node but .nvmrc requires $repo_node; update NODE_VERSION in .cursor/Dockerfile." >&2
fi

.cursor/start.sh

make e2e-dist

(
cd e2e/integration
npm ci
npx playwright install --with-deps chromium
# Mark the suite's dependencies as installed for the Makefile's e2e/integration/node_modules target.
touch node_modules
# Caches the TypeScript runner the github-stack terminal uses for .cursor/fake-github.ts.
npx --yes tsx@4.23.15 --version >/dev/null
)

if make e2e-env-up; then
(cd e2e/integration && docker compose stop)
else
echo "WARNING: the E2E stack did not become healthy during the build; agents will boot it from scratch." >&2
(cd e2e/integration && docker compose logs --no-color --tail 200 && docker compose down -v) || true
fi
78 changes: 78 additions & 0 deletions .cursor/start.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env bash
# Starts Docker for the E2E stack and, like the GitLab plugin's start hook, maps the hostname the
# agent's browser needs: the e2e plugin bundle (`make e2e-dist`) sends GitHub OAuth, REST and
# GraphQL to http://fake-github:8093, which is the fake GitHub (.cursor/fake-github.ts) on this
# machine. Mattermost reaches it through extra_hosts in e2e/integration/docker-compose.yml; the
# Playwright suite's Chromium maps it with --host-resolver-rules and does not rely on /etc/hosts.
#
# Hardened like the Playbooks and core repos' start hooks: falls back to starting dockerd directly
# when the service script fails, fixes socket access for the current session, prints Docker's logs
# on failure, and logs in to Docker Hub when DOCKERHUB_USERNAME and DOCKERHUB_TOKEN are set as
# Cursor environment secrets (anonymous pulls can hit rate limits).
set -euo pipefail

log() { echo "[cloud-agent-start] $*"; }

if ! command -v docker >/dev/null 2>&1; then
log "Docker is not installed, so this agent did not boot from .cursor/Dockerfile." >&2
log "Check the agent's environment: a saved or default environment may have been used instead." >&2
exit 1
fi

if ! grep -qE '^127\.0\.0\.1[[:space:]]+fake-github$' /etc/hosts; then
echo "127.0.0.1 fake-github" | sudo tee -a /etc/hosts >/dev/null
fi

ensure_socket_access() {
[[ -S /var/run/docker.sock ]] || return 0
sudo chgrp docker /var/run/docker.sock 2>/dev/null || true
sudo chmod g+rw /var/run/docker.sock 2>/dev/null || true
if command -v setfacl >/dev/null 2>&1; then
sudo setfacl -m "u:$(id -un):rw" /var/run/docker.sock 2>/dev/null || true
fi
}

docker_login_if_configured() {
if [[ -z "${DOCKERHUB_USERNAME:-}" || -z "${DOCKERHUB_TOKEN:-}" ]]; then
log "Docker Hub credentials not configured; anonymous pulls may hit rate limits."
return 0
fi
if echo "${DOCKERHUB_TOKEN}" | docker login -u "${DOCKERHUB_USERNAME}" --password-stdin >/tmp/docker-login.log 2>&1; then
log "Logged in to Docker Hub as ${DOCKERHUB_USERNAME}."
else
log "Docker Hub login failed; see /tmp/docker-login.log." >&2
fi
}

ready() {
docker info >/dev/null 2>&1 && docker compose version >/dev/null 2>&1
}

ensure_socket_access
if ! ready; then
sudo sh -c 'service docker start >/tmp/docker-service-start.log 2>&1' ||
log "service docker start failed; starting dockerd directly."
if ! pgrep -x dockerd >/dev/null 2>&1; then
sudo rm -f /var/run/docker.pid
sudo sh -c 'nohup dockerd --host=unix:///var/run/docker.sock >/tmp/dockerd.log 2>&1 &'
fi
for _ in $(seq 1 60); do
ensure_socket_access
ready && break
sleep 1
done
fi

if ! ready; then
log "Docker did not become ready within 60 seconds." >&2
for f in /tmp/docker-service-start.log /var/log/docker.log /tmp/dockerd.log; do
if [[ -f "$f" ]]; then
log "$f:" >&2
sudo tail -n 80 "$f" >&2 || true
fi
done
exit 1
fi

docker_login_if_configured
log "Docker is ready."
Loading