mirror of
https://github.com/mattermost/mattermost.git
synced 2026-10-13 01:25:24 +08:00
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:
co-authored by
mattermost-code
Cursor
Mattermost Build
parent
786d22bce3
commit
59cd9d6282
+4
-2
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user