Clone missing enterprise checkout in Cloud Agent install (#38117)

* Clone enterprise in Cloud Agent install when the multi-repo layout is missing.

Git-triggered automations only add mattermost/enterprise to the GitHub token scope; they do not check it out, so the install hook clones it when needed.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Reject bare Git repos when locating the enterprise checkout.

git rev-parse --is-inside-work-tree exits 0 for bare repositories while
printing false, so require a real work tree. Document the $HOME/enterprise
fallback in AGENTS.md.

Co-authored-by: mattermost-code <matty-code@mattermost.com>

* Honor config.override.mk during Enterprise detection and skip unusable sibling clone dests.

make evaluated BUILD_TAGS before reading the override, so a $HOME/enterprise fallback still shipped Team Edition; a bare ../enterprise also aborted install instead of falling back.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Match Enterprise branches in Cloud Agent clones

Prefer the corresponding Enterprise branch for PR-triggered environments while keeping clone publication safe and Cloud-only guidance scoped under .cursor.

Co-authored-by: Cursor <cursoragent@cursor.com>

* Simplify Enterprise clone publication

Use an atomic no-clobber move instead of maintaining lock state in the ephemeral Cloud Agent environment.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: mattermost-code <matty-code@mattermost.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
This commit is contained in:
Nick Misasi
2026-09-09 17:20:12 -04:00
committed by GitHub
co-authored by mattermost-code Cursor Mattermost Build
parent 786d22bce3
commit 59cd9d6282
5 changed files with 265 additions and 14 deletions
+4 -2
View File
@@ -16,10 +16,12 @@ The Docker build context is `.cursor/` only. The Dockerfile intentionally does n
## Runtime Hooks
- `cloud-agent-install.sh` runs after Cursor checks out the repo. It refreshes nvm, verifies Cursor's multi-repo `mattermost/enterprise` checkout, runs `server` Go dependency hydration, installs webapp dependencies, and runs Playwright `npm ci`.
- `cloud-agent-install.sh` runs after Cursor checks out the repo. It refreshes nvm, locates or clones `mattermost/enterprise`, runs `server` Go dependency hydration, installs webapp dependencies, and runs Playwright `npm ci`.
- `cloud-agent-start.sh` materializes `.cursor/cursor.md` as `.cursor/AGENTS.md`, fixes current-session Docker socket access, starts Docker, waits until `docker info` and `docker compose version` succeed, then logs in to Docker Hub when credentials are configured.
The environment declares `github.com/mattermost/enterprise` in `repositoryDependencies` so Cursor can provide it as part of the multi-repo workspace. Cursor currently clones the repositories as siblings, such as `/agent/repos/mattermost` and `/agent/repos/enterprise`, which matches `server/Makefile`'s default `../../enterprise` path. The install hook does not clone, pull, or symlink enterprise.
The environment declares `github.com/mattermost/enterprise` in `repositoryDependencies` so Cursor can provide it as part of the multi-repo workspace and so the Cloud Agent GitHub token can read that private repo. Cursor currently clones the repositories as siblings when you explicitly select the multi-repo environment, such as `/agent/repos/mattermost` and `/agent/repos/enterprise`, which matches `server/Makefile`'s default `../../enterprise` path.
Git-triggered automations (for example, a PR label) still launch in a single-repo layout. `repositoryDependencies` only adds enterprise to the token scope in that mode; it does not check the repo out. The install hook therefore clones `https://github.com/mattermost/enterprise.git` into `../enterprise` when no usable checkout is present. It uses `ENTERPRISE_BRANCH` when set, otherwise preferring an exact branch matching the Mattermost checkout and falling back to Enterprise's default branch. If the sibling path is not writable or is not a usable work tree (for example a bare repo), the hook clones into `$HOME/enterprise` and writes `BUILD_ENTERPRISE_DIR` to `server/config.override.mk`. `server/Makefile` includes that file before Enterprise detection so `make run-server` honors the fallback path.
## Useful Skips
+2 -1
View File
@@ -6,7 +6,8 @@ These instructions apply to Cursor Cloud Agents after `.cursor/scripts/cloud-age
- Docker must be available. If `docker info` fails, inspect `/tmp/docker-service-start.log` and `/tmp/dockerd.log`; do not assume a snapshot will provide Docker.
- The image includes Go, Node/npm, Docker Compose, and AWS CLI v2.
- Cursor should provide `mattermost/enterprise` through the multi-repo environment. The expected layout is sibling repositories, such as `/agent/repos/mattermost` and `/agent/repos/enterprise`; this matches `server/Makefile`'s default `../../enterprise` path.
- Prefer Cursor's multi-repo layout for `mattermost/enterprise` when it is available: sibling repositories such as `/agent/repos/mattermost` and `/agent/repos/enterprise`, matching `server/Makefile`'s default `../../enterprise` path.
- Git-triggered automations start as a single-repo checkout. If enterprise is missing, `cloud-agent-install.sh` clones it with the token scoped by `repositoryDependencies`, preferring an exact like-named branch and falling back to Enterprise's default branch. If that clone lands somewhere other than `../enterprise`, `BUILD_ENTERPRISE_DIR` is written to `server/config.override.mk`, which `server/Makefile` reads before Enterprise detection.
## Running Mattermost
+255 -6
View File
@@ -12,6 +12,10 @@ is_true() {
esac
}
is_git_work_tree() {
[ "$(git -C "${1:-}" rev-parse --is-inside-work-tree 2>/dev/null)" = "true" ]
}
ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
cd "$ROOT"
@@ -220,6 +224,10 @@ enterprise_build_dir() {
esac
}
default_enterprise_checkout() {
realpath -m "$ROOT/../enterprise"
}
find_enterprise_checkout() {
local candidates=()
if [ -n "${ENTERPRISE_CHECKOUT_DIR:-}" ]; then
@@ -228,16 +236,22 @@ find_enterprise_checkout() {
if [ -n "${ENTERPRISE_DIR:-}" ]; then
candidates+=("$ENTERPRISE_DIR")
fi
if [ -n "${BUILD_ENTERPRISE_DIR:-}" ]; then
case "$BUILD_ENTERPRISE_DIR" in
/*) candidates+=("$BUILD_ENTERPRISE_DIR") ;;
*) candidates+=("$ROOT/server/$BUILD_ENTERPRISE_DIR") ;;
esac
fi
candidates+=(
"$ROOT/../enterprise"
"$(default_enterprise_checkout)"
"$ROOT/../../enterprise"
"$HOME/enterprise"
)
local candidate
for candidate in "${candidates[@]}"; do
if git -C "$candidate" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
if is_git_work_tree "$candidate"; then
realpath -m "$candidate"
return 0
fi
@@ -246,18 +260,253 @@ find_enterprise_checkout() {
return 1
}
verify_enterprise_checkout() {
enterprise_clone_url() {
printf 'https://github.com/mattermost/enterprise.git\n'
}
requested_enterprise_branch() {
local branch
if [ -n "${ENTERPRISE_BRANCH:-}" ]; then
branch="$ENTERPRISE_BRANCH"
elif ! branch="$(git -C "$ROOT" symbolic-ref --quiet --short HEAD 2>/dev/null)"; then
return 1
fi
if ! git check-ref-format --branch "$branch" >/dev/null 2>&1; then
return 1
fi
printf '%s\n' "$branch"
}
enterprise_branch_exists() {
local branch="$1"
local url
url="$(enterprise_clone_url)"
GIT_TERMINAL_PROMPT=0 git ls-remote --exit-code --heads "$url" "refs/heads/$branch" >/dev/null 2>&1 && return 0
local status=$?
if [ "$status" -eq 2 ]; then
return 1
fi
if ! command -v gh >/dev/null 2>&1; then
return 2
fi
local exists
if ! exists="$(
gh api graphql \
-f query="query(\$qualifiedName: String!) { repository(owner: \"mattermost\", name: \"enterprise\") { ref(qualifiedName: \$qualifiedName) { id } } }" \
-f qualifiedName="refs/heads/$branch" \
--jq '.data.repository.ref != null' 2>/dev/null
)"; then
return 2
fi
[ "$exists" = "true" ]
}
matching_enterprise_branch() {
local branch
if ! branch="$(requested_enterprise_branch)"; then
log "No valid Enterprise branch was requested; using Enterprise's default branch."
return 1
fi
local status
if enterprise_branch_exists "$branch"; then
printf '%s\n' "$branch"
return 0
else
status=$?
fi
if [ "$status" -eq 1 ]; then
log "Enterprise branch $branch does not exist; using Enterprise's default branch."
return 1
fi
log "Could not verify Enterprise branch $branch; attempting it before the default branch."
printf '%s\n' "$branch"
}
can_clone_into() {
local dest="$1"
local parent
parent="$(dirname "$dest")"
if [ ! -d "$parent" ] || [ ! -w "$parent" ] || [ ! -x "$parent" ]; then
return 1
fi
if [ -L "$dest" ]; then
return 1
fi
if [ ! -e "$dest" ]; then
return 0
fi
if is_git_work_tree "$dest"; then
return 0
fi
if [ ! -d "$dest" ] || [ ! -w "$dest" ] || [ ! -x "$dest" ]; then
return 1
fi
local entries
if ! entries="$(ls -A "$dest" 2>/dev/null)"; then
return 1
fi
[ -z "$entries" ]
}
publish_enterprise_checkout() {
local source="$1"
local dest="$2"
if mv -T -n "$source" "$dest" && [ ! -e "$source" ]; then
return 0
fi
rm -rf "$source"
if is_git_work_tree "$dest"; then
log "Another process populated Enterprise checkout at $dest; reusing it."
return 0
fi
log "Could not publish Enterprise checkout at $dest."
return 1
}
clone_enterprise_ref() {
local dest="$1"
local url="$2"
shift 2
local clone_args=("$@")
local temp
if ! temp="$(mktemp -d "$(dirname "$dest")/.enterprise-clone.XXXXXX")"; then
return 1
fi
if GIT_TERMINAL_PROMPT=0 git clone "${clone_args[@]}" "$url" "$temp"; then
publish_enterprise_checkout "$temp" "$dest"
return $?
fi
rm -rf "$temp"
if ! command -v gh >/dev/null 2>&1; then
return 1
fi
log "git clone failed; retrying with GitHub CLI."
if ! temp="$(mktemp -d "$(dirname "$dest")/.enterprise-clone.XXXXXX")"; then
return 1
fi
if gh repo clone mattermost/enterprise "$temp" -- "${clone_args[@]}"; then
publish_enterprise_checkout "$temp" "$dest"
return $?
fi
rm -rf "$temp"
return 1
}
enterprise_clone_dest() {
local sibling
sibling="$(default_enterprise_checkout)"
if can_clone_into "$sibling"; then
printf '%s\n' "$sibling"
return 0
fi
realpath -m "$HOME/enterprise"
}
clone_enterprise_checkout() {
local dest="$1"
local url
url="$(enterprise_clone_url)"
if [ -z "$dest" ] || [ "$dest" = "/" ] || [ "$(basename "$dest")" != "enterprise" ]; then
log "Refusing unsafe enterprise clone dest: ${dest:-<empty>}"
return 1
fi
if ! can_clone_into "$dest"; then
log "Refusing unusable enterprise clone dest: $dest"
return 1
fi
if [ -e "$dest" ]; then
if is_git_work_tree "$dest"; then
return 0
fi
rmdir "$dest"
fi
log "Enterprise checkout missing; cloning mattermost/enterprise into $dest."
log "Git-triggered automations stay single-repo; repositoryDependencies only scopes the GitHub token."
mkdir -p "$(dirname "$dest")"
local clone_args=(--depth 1 --single-branch)
local branch
if branch="$(matching_enterprise_branch)"; then
clone_args+=(--branch "$branch")
log "Trying matching Enterprise branch $branch."
fi
if clone_enterprise_ref "$dest" "$url" "${clone_args[@]}"; then
return 0
fi
if [ -n "${branch:-}" ]; then
log "Could not clone matching Enterprise branch $branch; retrying with the default branch."
clone_args=(--depth 1 --single-branch)
clone_enterprise_ref "$dest" "$url" "${clone_args[@]}" && return 0
fi
log "Failed to clone mattermost/enterprise. repositoryDependencies must include github.com/mattermost/enterprise so the GitHub token can access it."
return 1
}
persist_build_enterprise_dir() {
local target="$1"
export BUILD_ENTERPRISE_DIR="$target"
local default_target
default_target="$(default_enterprise_checkout)"
if [ "$target" = "$default_target" ]; then
return 0
fi
local override="$ROOT/server/config.override.mk"
if [ -f "$override" ] && grep -q '^BUILD_ENTERPRISE_DIR' "$override"; then
return 0
fi
printf 'BUILD_ENTERPRISE_DIR := %s\n' "$target" >> "$override"
log "Wrote BUILD_ENTERPRISE_DIR to $override so make uses the cloned checkout."
}
ensure_enterprise_checkout() {
if is_true "${CLOUD_AGENT_SKIP_ENTERPRISE:-false}"; then
log "Skipping enterprise verification because CLOUD_AGENT_SKIP_ENTERPRISE is set."
return 0
fi
local target
if ! target="$(find_enterprise_checkout)"; then
log "Enterprise checkout not found. Ensure the Cursor multi-repo environment includes github.com/mattermost/enterprise."
if target="$(find_enterprise_checkout)"; then
persist_build_enterprise_dir "$target"
log "Enterprise checkout ready at $target."
return 0
fi
local dest
dest="$(enterprise_clone_dest)"
clone_enterprise_checkout "$dest"
target="$(realpath -m "$dest")"
if ! is_git_work_tree "$target"; then
log "Enterprise clone completed but checkout was not found at $dest."
return 1
fi
persist_build_enterprise_dir "$target"
log "Enterprise checkout ready at $target."
}
@@ -320,7 +569,7 @@ hydrate_playwright_dependencies() {
ensure_go
ensure_node
verify_enterprise_checkout
ensure_enterprise_checkout
hydrate_go_dependencies
hydrate_webapp_dependencies
hydrate_playwright_dependencies
+1 -3
View File
@@ -13,7 +13,5 @@ When creating a pull request, follow `.github/PULL_REQUEST_TEMPLATE.md` exactly:
## Cursor Cloud Agents
This repository has a checked-in Cloud Agent environment under `.cursor/`. Docker is started by `.cursor/scripts/cloud-agent-start.sh`; if Docker is unavailable in Cloud, treat that as an environment failure rather than falling back to snapshot assumptions.
The environment declares `mattermost/enterprise` as a Cursor multi-repo dependency. Cursor clones the repositories as siblings, so `server/Makefile` can use its default `../../enterprise` path; the install hook does not clone or symlink enterprise.
Cursor Cloud Agents must follow `.cursor/AGENTS.md`, materialized by `.cursor/scripts/cloud-agent-start.sh`.
+3 -2
View File
@@ -1,5 +1,8 @@
ROOT := $(dir $(abspath $(lastword $(MAKEFILE_LIST))))
# Must be read before parse-time Enterprise detection so BUILD_ENTERPRISE_DIR applies.
-include $(ROOT)config.override.mk
ifeq ($(OS),Windows_NT)
PLATFORM := Windows
else
@@ -195,8 +198,6 @@ CONFIG_FILE_PATH ?= ./config/config.json
.PHONY: all
all: run ## Alias for 'run'.
-include config.override.mk
# Make sure not to modify an overridden ENABLED_DOCKER_SERVICES variable
DOCKER_SERVICES_OVERRIDE=false
ifneq (,$(ENABLED_DOCKER_SERVICES))