From 2f4eb55a447754f7c14502fcc6eb25e2106f7314 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Wed, 11 Nov 2026 16:35:14 -0600 Subject: [PATCH 01/15] amp: Run Sourcegraph in an orb and serve it in a portal Adds the Amp orb setup script, which installs Docker Compose and pulls the images, and a service that starts the Docker Compose deployment, creates the first site admin with a password made in the orb, and serves the frontend through a portal. Amp-Thread-ID: https://ampcode.com/threads/T-01a1c7a5-6b65-723b-980c-b85e1e9008b5 Co-authored-by: Amp --- .agents/setup | 40 ++++++++++++++++++++++++++++++++++++++++ .amp/services.yaml | 11 +++++++++++ .amp/sourcegraph.sh | 39 +++++++++++++++++++++++++++++++++++++++ .gitignore | 1 + 4 files changed, 91 insertions(+) create mode 100755 .agents/setup create mode 100644 .amp/services.yaml create mode 100755 .amp/sourcegraph.sh diff --git a/.agents/setup b/.agents/setup new file mode 100755 index 000000000..443832ead --- /dev/null +++ b/.agents/setup @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Prepares an Amp orb (https://ampcode.com/docs/orbs) to run Sourcegraph with Docker Compose. +# Amp saves the result as a snapshot for new orbs, so this only installs and downloads. +# .amp/services.yaml starts Sourcegraph. +set -euo pipefail + +step() { echo "==> $* (${SECONDS}s)"; } + +step 'Start Docker' +sudo systemctl enable --now docker + +if ! sudo docker compose version >/dev/null 2>&1; then + step 'Install Docker Compose' + sudo apt-get update + sudo apt-get install -y docker-compose-plugin +fi + +step 'Pull images' +pull_images() { sudo docker compose --file docker-compose/docker-compose.yaml pull --quiet; } +# Docker Hub times out now and then +pull_images || { sleep 10 && pull_images; } + +step 'Write guidance for agents' +guidance_file="$HOME/.config/amp/AGENTS.md" +guidance_heading='## Sourcegraph in this orb' +if ! grep --fixed-strings --line-regexp --quiet "$guidance_heading" "$guidance_file" 2>/dev/null; then + mkdir -p "$(dirname "$guidance_file")" + cat >>"$guidance_file" <"$password_file") +fi +admin_password=$(<"$password_file") + +# Sourcegraph refuses this request once it has a user, so a restart keeps the first admin +curl --silent --output /dev/null --request POST "$frontend_url/-/site-init" \ + --header 'Content-Type: application/json' \ + --data "{\"email\":\"$admin_email\",\"username\":\"$admin_username\",\"password\":\"$admin_password\"}" + +# Stop here if the admin cannot sign in +curl --silent --fail --output /dev/null --request POST "$frontend_url/-/sign-in" \ + --header 'Content-Type: application/json' \ + --data "{\"email\":\"$admin_email\",\"password\":\"$admin_password\"}" + +echo "Sourcegraph is ready at ${PUBLIC_URL}" +echo "Sign in as $admin_username with the password in $password_file" + +# Stay in the foreground. Stopping the service stops the containers +exec "${compose[@]}" up diff --git a/.gitignore b/.gitignore index 9cf3cdc72..e04bedc32 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ .vagrant *.log node_modules/ +.amp/portals/ From 8b115963516f694ac884df881af2ced290ba1fec Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:05:00 +0000 Subject: [PATCH 02/15] amp: Add Jaeger and an executor, and fix the service in a real orb Fixes found by running the service in an orb: - Amp checks a service's port as a normal user, who cannot open port 80, and Amp's portal proxy in the orb listens on port 443, which Caddy also publishes. An override file now makes Caddy publish only the port Amp chooses. - When the service process dies while the containers keep running, Amp chooses another free port and the script moves Caddy to it. - The script removes its temporary files when it stops. Adds the optional Jaeger and executor deployments. Each orb makes its own executor token. On the first start, the script sets it as executors.accessToken, and sets externalURL to the portal URL, which removes the site admin warning about externalURL. Restarts do not change the site configuration. The Portal tab now shows the admin password in the note of the Sourcegraph link, and links to the executors page and Jaeger. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-01a1ccc1-035e-77c5-8b50-caa8352fd32d --- .agents/setup | 11 +++-- .amp/docker-compose.override.yaml | 13 ++++++ .amp/services.yaml | 11 ++--- .amp/sourcegraph.sh | 71 ++++++++++++++++++++++--------- 4 files changed, 79 insertions(+), 27 deletions(-) create mode 100644 .amp/docker-compose.override.yaml diff --git a/.agents/setup b/.agents/setup index 443832ead..a3abb7c8a 100755 --- a/.agents/setup +++ b/.agents/setup @@ -16,7 +16,10 @@ if ! sudo docker compose version >/dev/null 2>&1; then fi step 'Pull images' -pull_images() { sudo docker compose --file docker-compose/docker-compose.yaml pull --quiet; } +pull_images() { + sudo docker compose --file docker-compose/docker-compose.yaml --file docker-compose/jaeger/docker-compose.yaml \ + --file docker-compose/executors/executor.docker-compose.yaml pull --quiet +} # Docker Hub times out now and then pull_images || { sleep 10 && pull_images; } @@ -29,11 +32,13 @@ if ! grep --fixed-strings --line-regexp --quiet "$guidance_heading" "$guidance_f $guidance_heading -- \`amp orb services ensure\` starts Sourcegraph from \`docker-compose/docker-compose.yaml\` and prints its portal URL. +- \`amp orb services ensure\` starts Sourcegraph with Jaeger and an executor, from the Compose files in + \`docker-compose/\`, \`docker-compose/jaeger/\`, and \`docker-compose/executors/\`, and prints its portal URL. - The first site admin is \`admin\`. Its password is in \`~/.config/sourcegraph/admin-password\`. When you start Sourcegraph for the user, give them the portal URL, the username, and the password. - \`amp orb service logs sourcegraph\` shows the logs of every container. -- Docker needs \`sudo\`, such as \`sudo docker compose --file docker-compose/docker-compose.yaml ps\`. + After changing a Compose file, run \`amp orb service restart sourcegraph\`. +- Docker needs \`sudo\`, such as \`sudo docker ps\` or \`sudo docker logs executor\`. EOF fi diff --git a/.amp/docker-compose.override.yaml b/.amp/docker-compose.override.yaml new file mode 100644 index 000000000..be074863b --- /dev/null +++ b/.amp/docker-compose.override.yaml @@ -0,0 +1,13 @@ +# Changes the Docker Compose deployment for an Amp orb. .amp/sourcegraph.sh sets both variables +services: + # Publishes only the port Amp chose for the service. Amp checks ports as a user who cannot open port 80, + # and the portal proxy in the orb already listens on port 443 + caddy: + ports: !override + - '${PORT:?}:80' + # Connects the executor from docker-compose/executors/ to Sourcegraph, + # with the token .amp/sourcegraph.sh also sets as executors.accessToken in the site configuration + executor: + environment: + - EXECUTOR_FRONTEND_PASSWORD=${EXECUTOR_TOKEN:?} + - EXECUTOR_QUEUE_NAME=batches diff --git a/.amp/services.yaml b/.amp/services.yaml index ac8f17080..01f487df2 100644 --- a/.amp/services.yaml +++ b/.amp/services.yaml @@ -3,9 +3,10 @@ services: sourcegraph: command: .amp/sourcegraph.sh - # The host port Caddy publishes in docker-compose/docker-compose.yaml - port: 80 health: /healthz - portal: - title: Sourcegraph - description: Sign in as admin. The password is in ~/.config/sourcegraph/admin-password in the orb. Ask Amp for it, or read it in the Terminal tab. + # .amp/sourcegraph.sh adds the Sourcegraph link, with the admin password in its note + portals: + - url: /admin/executors + title: Executors + - url: /-/debug/jaeger/ + title: Jaeger diff --git a/.amp/sourcegraph.sh b/.amp/sourcegraph.sh index 284f21d85..b97a73de3 100755 --- a/.amp/sourcegraph.sh +++ b/.amp/sourcegraph.sh @@ -1,39 +1,72 @@ #!/usr/bin/env bash # Runs Sourcegraph in an Amp orb, as the `sourcegraph` service in .amp/services.yaml. -# Starts the Docker Compose deployment, creates the first site admin, +# Starts the Docker Compose deployment with Jaeger and an executor, creates the first site admin, # then follows the logs until Amp stops the service. set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")/.." -compose=(sudo docker compose --file docker-compose/docker-compose.yaml) frontend_url="http://localhost:${PORT}" admin_username='admin' admin_email='admin@example.com' -password_file="$HOME/.config/sourcegraph/admin-password" +secrets_directory="$HOME/.config/sourcegraph" -# Start every service, then wait until the frontend is healthy +# Each orb makes its own secrets, so the repository holds none +make_secret() { + if [[ ! -f "$secrets_directory/$1" ]]; then + (umask 077 && mkdir -p "$secrets_directory" && openssl rand -hex 16 >"$secrets_directory/$1") + fi + cat "$secrets_directory/$1" +} +admin_password=$(make_secret admin-password) +executor_token=$(make_secret executor-token) + +# Variables for .amp/docker-compose.override.yaml. A file keeps the token out of the sudo log +compose_variables="$secrets_directory/compose.env" +(umask 077 && printf 'PORT=%s\nEXECUTOR_TOKEN=%s\n' "$PORT" "$executor_token" >"$compose_variables") +compose=(sudo docker compose --env-file "$compose_variables" + --file docker-compose/docker-compose.yaml + --file docker-compose/jaeger/docker-compose.yaml + --file docker-compose/executors/executor.docker-compose.yaml + --file .amp/docker-compose.override.yaml) + +# Start every service, then wait until the frontend is healthy. +# Waiting for every service fails, because searcher-0 reports unhealthy for a few seconds while it starts "${compose[@]}" up --detach "${compose[@]}" up --detach --wait sourcegraph-frontend-0 -# Each orb makes its own password, so the repository holds none -if [[ ! -f "$password_file" ]]; then - mkdir -p "$(dirname "$password_file")" - (umask 077 && openssl rand -hex 16 >"$password_file") -fi -admin_password=$(<"$password_file") - -# Sourcegraph refuses this request once it has a user, so a restart keeps the first admin -curl --silent --output /dev/null --request POST "$frontend_url/-/site-init" \ +# Sourcegraph refuses this request once it has a user, so only the first start configures the site below +site_init_status=$(curl --silent --output /dev/null --write-out '%{http_code}' --request POST "$frontend_url/-/site-init" \ --header 'Content-Type: application/json' \ - --data "{\"email\":\"$admin_email\",\"username\":\"$admin_username\",\"password\":\"$admin_password\"}" + --data "{\"email\":\"$admin_email\",\"username\":\"$admin_username\",\"password\":\"$admin_password\"}") -# Stop here if the admin cannot sign in -curl --silent --fail --output /dev/null --request POST "$frontend_url/-/sign-in" \ +# Stop here if the admin cannot sign in. The session cookie also authorizes the GraphQL requests below +cookie_jar=$(mktemp) +portal_name=sourcegraph-admin +trap 'rm -f "$cookie_jar" ".amp/portals/$portal_name.json"' EXIT +# When Amp stops the service, wait for Docker Compose to stop the containers, then remove the files above +trap exit TERM +curl --silent --fail --output /dev/null --cookie-jar "$cookie_jar" --request POST "$frontend_url/-/sign-in" \ --header 'Content-Type: application/json' \ --data "{\"email\":\"$admin_email\",\"password\":\"$admin_password\"}" -echo "Sourcegraph is ready at ${PUBLIC_URL}" -echo "Sign in as $admin_username with the password in $password_file" +if [[ "$site_init_status" == 200 ]]; then + # Prints the response, and fails if it has errors + graphql() { + curl --silent --fail --cookie "$cookie_jar" --header 'Content-Type: application/json' --data "$1" "$frontend_url/.api/graphql" | + jq --exit-status 'select(.errors == null)' + } + configuration_id=$(graphql '{"query":"{ site { configuration { id } } }"}' | jq '.data.site.configuration.id') + # Lets the executor connect, and gives Sourcegraph its portal URL + site_configuration=$(jq --null-input --arg token "$executor_token" --arg url "$PUBLIC_URL" \ + '{"externalURL": $url, "auth.providers": [{"type": "builtin"}], "executors.accessToken": $token}') + graphql "$(jq --null-input --argjson id "$configuration_id" --arg input "$site_configuration" \ + '{query: "mutation($id: Int!, $input: String!) { updateSiteConfiguration(lastID: $id, input: $input) }", + variables: {id: $id, input: $input}}')" >/dev/null +fi + +# Shows the sign-in in the Portal tab, as a note next to the Sourcegraph link, until the service stops +amp orb portal "$PORT" --name "$portal_name" --title Sourcegraph \ + --description "Sign in as $admin_username with the password $admin_password" # Stay in the foreground. Stopping the service stops the containers -exec "${compose[@]}" up +"${compose[@]}" up From 2160c12ad3bae6e99a51882719af3fe904b36ca7 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:13:28 +0000 Subject: [PATCH 03/15] amp: Configure the site whenever it has no executor token The service wrote the site configuration only when /-/site-init had just created the admin, so a first start that failed after that step left the executor without a token for good. The script now reads the site configuration and adds externalURL and executors.accessToken whenever the token is missing, keeping the other settings, and leaves the configuration alone on every other start. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-01a1ccc1-035e-77c5-8b50-caa8352fd32d --- .amp/sourcegraph.sh | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/.amp/sourcegraph.sh b/.amp/sourcegraph.sh index b97a73de3..a72ef689c 100755 --- a/.amp/sourcegraph.sh +++ b/.amp/sourcegraph.sh @@ -34,10 +34,10 @@ compose=(sudo docker compose --env-file "$compose_variables" "${compose[@]}" up --detach "${compose[@]}" up --detach --wait sourcegraph-frontend-0 -# Sourcegraph refuses this request once it has a user, so only the first start configures the site below -site_init_status=$(curl --silent --output /dev/null --write-out '%{http_code}' --request POST "$frontend_url/-/site-init" \ +# Sourcegraph refuses this request once it has a user, so a restart keeps the first admin +curl --silent --output /dev/null --request POST "$frontend_url/-/site-init" \ --header 'Content-Type: application/json' \ - --data "{\"email\":\"$admin_email\",\"username\":\"$admin_username\",\"password\":\"$admin_password\"}") + --data "{\"email\":\"$admin_email\",\"username\":\"$admin_username\",\"password\":\"$admin_password\"}" # Stop here if the admin cannot sign in. The session cookie also authorizes the GraphQL requests below cookie_jar=$(mktemp) @@ -49,19 +49,23 @@ curl --silent --fail --output /dev/null --cookie-jar "$cookie_jar" --request POS --header 'Content-Type: application/json' \ --data "{\"email\":\"$admin_email\",\"password\":\"$admin_password\"}" -if [[ "$site_init_status" == 200 ]]; then - # Prints the response, and fails if it has errors - graphql() { - curl --silent --fail --cookie "$cookie_jar" --header 'Content-Type: application/json' --data "$1" "$frontend_url/.api/graphql" | - jq --exit-status 'select(.errors == null)' - } - configuration_id=$(graphql '{"query":"{ site { configuration { id } } }"}' | jq '.data.site.configuration.id') +# Prints the response, and fails if it has errors +graphql() { + curl --silent --fail --cookie "$cookie_jar" --header 'Content-Type: application/json' --data "$1" "$frontend_url/.api/graphql" | + jq --exit-status 'select(.errors == null)' +} +# The site configuration is JSON with comments, so drop whole-line comments and trailing commas to read it +site=$(graphql '{"query":"{ site { configuration { id effectiveContents } } }"}' | jq '.data.site.configuration | + {id, contents: (.effectiveContents | gsub("(?m)^\\s*//.*$"; "") | gsub(",(?=\\s*[]}])"; "") | fromjson? // null)}') +# Configure the site only while it has no executor token, so restarts keep later changes. +# Leave it alone if it does not parse +if jq --exit-status '.contents != null and (.contents | has("executors.accessToken") | not)' <<<"$site" >/dev/null; then # Lets the executor connect, and gives Sourcegraph its portal URL - site_configuration=$(jq --null-input --arg token "$executor_token" --arg url "$PUBLIC_URL" \ - '{"externalURL": $url, "auth.providers": [{"type": "builtin"}], "executors.accessToken": $token}') - graphql "$(jq --null-input --argjson id "$configuration_id" --arg input "$site_configuration" \ + site_configuration=$(jq --arg token "$executor_token" --arg url "$PUBLIC_URL" \ + '.contents + {"externalURL": $url, "executors.accessToken": $token}' <<<"$site") + graphql "$(jq --arg input "$site_configuration" \ '{query: "mutation($id: Int!, $input: String!) { updateSiteConfiguration(lastID: $id, input: $input) }", - variables: {id: $id, input: $input}}')" >/dev/null + variables: {id, input: $input}}' <<<"$site")" >/dev/null fi # Shows the sign-in in the Portal tab, as a note next to the Sourcegraph link, until the service stops From b6271f8fb34f031fdbb8edbc04d8715b93d4d46d Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Wed, 11 Nov 2026 23:21:27 +0000 Subject: [PATCH 04/15] amp: Let the executor take jobs from every queue Sets EXECUTOR_QUEUE_NAMES to batches, batches-hooks, and codeintel instead of EXECUTOR_QUEUE_NAME=batches. The base executor file leaves EXECUTOR_QUEUE_NAME empty, which the executor requires when EXECUTOR_QUEUE_NAMES is set. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-01a1ccc1-035e-77c5-8b50-caa8352fd32d --- .amp/docker-compose.override.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.amp/docker-compose.override.yaml b/.amp/docker-compose.override.yaml index be074863b..98feef3e6 100644 --- a/.amp/docker-compose.override.yaml +++ b/.amp/docker-compose.override.yaml @@ -10,4 +10,5 @@ services: executor: environment: - EXECUTOR_FRONTEND_PASSWORD=${EXECUTOR_TOKEN:?} - - EXECUTOR_QUEUE_NAME=batches + # Every queue. The base file leaves EXECUTOR_QUEUE_NAME empty, which the executor requires with this + - EXECUTOR_QUEUE_NAMES=batches,batches-hooks,codeintel From 944818d580c53c0ab3d00495d83cf94ea9693f3c Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Wed, 11 Nov 2026 22:05:57 -0600 Subject: [PATCH 05/15] amp: Let the executor run code intelligence jobs, and send traces to Jaeger Found by running jobs in an orb. A server-side batch change already ran. An auto-indexing job did not: Sourcegraph refused to queue it, and once queued, its upload step got the portal URL, reached it from outside Amp's sign-in, and received an HTML page. Sourcegraph also sent Jaeger no traces. - Caddy also publishes port 80, and the executor sets EXECUTOR_DOCKER_ADD_HOST_GATEWAY, so the containers the executor starts reach Sourcegraph at http://host.docker.internal, which the script sets as executors.frontendURL. - The script sets codeIntelAutoIndexing.enabled, which lets index jobs be queued. With the default policies, it indexes nothing on its own. - The script sets observability.tracing. Sourcegraph exports the requests made with ?trace=1. - externalURL no longer ends in a slash, which put "//" in trace links. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-01a1c7a5-6b65-723b-980c-b85e1e9008b5 --- .amp/docker-compose.override.yaml | 7 +++++-- .amp/sourcegraph.sh | 12 +++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.amp/docker-compose.override.yaml b/.amp/docker-compose.override.yaml index 98feef3e6..641c59c1c 100644 --- a/.amp/docker-compose.override.yaml +++ b/.amp/docker-compose.override.yaml @@ -1,10 +1,11 @@ # Changes the Docker Compose deployment for an Amp orb. .amp/sourcegraph.sh sets both variables services: - # Publishes only the port Amp chose for the service. Amp checks ports as a user who cannot open port 80, - # and the portal proxy in the orb already listens on port 443 + # Publishes the port Amp chose for the service, which Amp checks as a user who cannot open port 80, + # and port 80 for the containers the executor starts. Not port 443, where the portal proxy in the orb listens caddy: ports: !override - '${PORT:?}:80' + - '80:80' # Connects the executor from docker-compose/executors/ to Sourcegraph, # with the token .amp/sourcegraph.sh also sets as executors.accessToken in the site configuration executor: @@ -12,3 +13,5 @@ services: - EXECUTOR_FRONTEND_PASSWORD=${EXECUTOR_TOKEN:?} # Every queue. The base file leaves EXECUTOR_QUEUE_NAME empty, which the executor requires with this - EXECUTOR_QUEUE_NAMES=batches,batches-hooks,codeintel + # Lets the containers the executor starts reach Caddy at host.docker.internal, the executors.frontendURL + - EXECUTOR_DOCKER_ADD_HOST_GATEWAY=true diff --git a/.amp/sourcegraph.sh b/.amp/sourcegraph.sh index a72ef689c..54eb7e8df 100755 --- a/.amp/sourcegraph.sh +++ b/.amp/sourcegraph.sh @@ -60,9 +60,15 @@ site=$(graphql '{"query":"{ site { configuration { id effectiveContents } } }"}' # Configure the site only while it has no executor token, so restarts keep later changes. # Leave it alone if it does not parse if jq --exit-status '.contents != null and (.contents | has("executors.accessToken") | not)' <<<"$site" >/dev/null; then - # Lets the executor connect, and gives Sourcegraph its portal URL - site_configuration=$(jq --arg token "$executor_token" --arg url "$PUBLIC_URL" \ - '.contents + {"externalURL": $url, "executors.accessToken": $token}' <<<"$site") + # Gives Sourcegraph its portal URL, lets the executor and the containers it starts connect, + # lets code intelligence jobs be queued, and sends traces to Jaeger + site_configuration=$(jq --arg url "${PUBLIC_URL%/}" --arg token "$executor_token" '.contents + { + "externalURL": $url, + "executors.accessToken": $token, + "executors.frontendURL": "http://host.docker.internal", + "codeIntelAutoIndexing.enabled": true, + "observability.tracing": {"urlTemplate": "{{ .ExternalURL }}/-/debug/jaeger/trace/{{ .TraceID }}"} + }' <<<"$site") graphql "$(jq --arg input "$site_configuration" \ '{query: "mutation($id: Int!, $input: String!) { updateSiteConfiguration(lastID: $id, input: $input) }", variables: {id, input: $input}}' <<<"$site")" >/dev/null From 3de84eca78a058fa8f5ef113cb409d9dc0e11ed4 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Wed, 11 Nov 2026 22:05:57 -0600 Subject: [PATCH 06/15] amp: Run in an orb with 4 vCPUs zoekt-indexserver-0 and zoekt-webserver-0 set cpus: 8, and Docker refuses a limit above the host's CPU count, so in a 4 vCPU orb no container started. The override lowers both to 4. With it, the deployment starts in a 4 vCPU, 8 GB orb, and uses 2.8 GB at idle. Co-authored-by: Amp Amp-Thread-ID: https://ampcode.com/threads/T-01a1c7a5-6b65-723b-980c-b85e1e9008b5 --- .amp/docker-compose.override.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.amp/docker-compose.override.yaml b/.amp/docker-compose.override.yaml index 641c59c1c..929a79e66 100644 --- a/.amp/docker-compose.override.yaml +++ b/.amp/docker-compose.override.yaml @@ -15,3 +15,8 @@ services: - EXECUTOR_QUEUE_NAMES=batches,batches-hooks,codeintel # Lets the containers the executor starts reach Caddy at host.docker.internal, the executors.frontendURL - EXECUTOR_DOCKER_ADD_HOST_GATEWAY=true + # Docker refuses a CPU limit above the CPUs in the orb, and an a1.medium orb has 4 + zoekt-indexserver-0: + cpus: 4 + zoekt-webserver-0: + cpus: 4 From f6086eb377ffd551277e8705c61a435fc33eab08 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Thu, 12 Nov 2026 20:04:09 -0600 Subject: [PATCH 07/15] amp: Prune unused images and volumes once Sourcegraph is running Images and volumes from earlier versions stay on the orb's disk after an upgrade. Every container of the deployment exists once the frontend is healthy, so Docker keeps the images and volumes they use. Amp-Thread-ID: https://ampcode.com/threads/T-01a1d2a1-43a0-7092-998e-43c135c772e8 Co-authored-by: Amp --- .amp/sourcegraph.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.amp/sourcegraph.sh b/.amp/sourcegraph.sh index 54eb7e8df..675a66375 100755 --- a/.amp/sourcegraph.sh +++ b/.amp/sourcegraph.sh @@ -78,5 +78,10 @@ fi amp orb portal "$PORT" --name "$portal_name" --title Sourcegraph \ --description "Sign in as $admin_username with the password $admin_password" +# Free the disk that images and volumes of earlier versions hold. +# Every container of this deployment exists by now, so Docker keeps their images and volumes +sudo docker image prune --all --force | tail -n 1 +sudo docker volume prune --all --force | tail -n 1 + # Stay in the foreground. Stopping the service stops the containers "${compose[@]}" up From 14b2a325fbddd06fa1596ff1afdb34277ae3eb87 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Thu, 12 Nov 2026 20:51:24 -0600 Subject: [PATCH 08/15] amp: Pull images on the first start, not in setup Setup output goes into the project snapshot, and the images are built artifacts, not dev dependencies. Docker Compose pulls missing images on the first start instead. In a 16 vCPU orb, setup drops from 15 s to under 1 s and the first start grows from about 37 s to about 54 s. Amp waits 60 s for the service; a slower start makes amp orb services ensure exit 1, but the service keeps running and the portal appears once Sourcegraph answers. Amp-Thread-ID: https://ampcode.com/threads/T-01a1d2a1-43a0-7092-998e-43c135c772e8 Co-authored-by: Amp --- .agents/setup | 10 +--------- .amp/sourcegraph.sh | 5 +++-- 2 files changed, 4 insertions(+), 11 deletions(-) diff --git a/.agents/setup b/.agents/setup index a3abb7c8a..d8daf1321 100755 --- a/.agents/setup +++ b/.agents/setup @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Prepares an Amp orb (https://ampcode.com/docs/orbs) to run Sourcegraph with Docker Compose. -# Amp saves the result as a snapshot for new orbs, so this only installs and downloads. +# Amp saves the result as a snapshot for new orbs, so this only installs. # .amp/services.yaml starts Sourcegraph. set -euo pipefail @@ -15,14 +15,6 @@ if ! sudo docker compose version >/dev/null 2>&1; then sudo apt-get install -y docker-compose-plugin fi -step 'Pull images' -pull_images() { - sudo docker compose --file docker-compose/docker-compose.yaml --file docker-compose/jaeger/docker-compose.yaml \ - --file docker-compose/executors/executor.docker-compose.yaml pull --quiet -} -# Docker Hub times out now and then -pull_images || { sleep 10 && pull_images; } - step 'Write guidance for agents' guidance_file="$HOME/.config/amp/AGENTS.md" guidance_heading='## Sourcegraph in this orb' diff --git a/.amp/sourcegraph.sh b/.amp/sourcegraph.sh index 675a66375..a1a284834 100755 --- a/.amp/sourcegraph.sh +++ b/.amp/sourcegraph.sh @@ -30,8 +30,9 @@ compose=(sudo docker compose --env-file "$compose_variables" --file .amp/docker-compose.override.yaml) # Start every service, then wait until the frontend is healthy. -# Waiting for every service fails, because searcher-0 reports unhealthy for a few seconds while it starts -"${compose[@]}" up --detach +# Waiting for every service fails, because searcher-0 reports unhealthy for a few seconds while it starts. +# The first start pulls the images, and Docker Hub times out now and then +"${compose[@]}" up --detach || { sleep 10 && "${compose[@]}" up --detach; } "${compose[@]}" up --detach --wait sourcegraph-frontend-0 # Sourcegraph refuses this request once it has a user, so a restart keeps the first admin From da8abf755904886509748a618e110a9fff51f4f0 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Thu, 12 Nov 2026 21:14:05 -0600 Subject: [PATCH 09/15] amp: Let Sourcegraph load in the orb's Portal tab The Portal tab shows the portal in an iframe, and Sourcegraph refuses that: its sign-in page, admin actions, and the Jaeger proxy reject requests with Sec-Fetch-Dest: iframe, and every response carries X-Frame-Options: DENY. The orb's Caddy now removes both, so Sourcegraph, the Executors page, and Jaeger load in the tab. Customers' Caddy configuration is unchanged. Amp-Thread-ID: https://ampcode.com/threads/T-01a1d2a1-43a0-7092-998e-43c135c772e8 Co-authored-by: Amp --- .amp/Caddyfile | 11 +++++++++++ .amp/docker-compose.override.yaml | 3 +++ 2 files changed, 14 insertions(+) create mode 100644 .amp/Caddyfile diff --git a/.amp/Caddyfile b/.amp/Caddyfile new file mode 100644 index 000000000..4b242a5b4 --- /dev/null +++ b/.amp/Caddyfile @@ -0,0 +1,11 @@ +# caddy/builtins/http.Caddyfile, changed so Sourcegraph loads in the orb's Portal tab, which shows it in an iframe. +# Sourcegraph refuses requests from an iframe, and tells browsers not to show its pages in one. +# This turns off Sourcegraph's clickjacking protection, which is fine only for a disposable test deployment +:80 + +reverse_proxy { + to {$SRC_FRONTEND_ADDRESSES} + trusted_proxies 0.0.0.0/0 + header_up -Sec-Fetch-Dest + header_down -X-Frame-Options +} diff --git a/.amp/docker-compose.override.yaml b/.amp/docker-compose.override.yaml index 929a79e66..0ec524f57 100644 --- a/.amp/docker-compose.override.yaml +++ b/.amp/docker-compose.override.yaml @@ -6,6 +6,9 @@ services: ports: !override - '${PORT:?}:80' - '80:80' + # Relative to docker-compose/, the directory of the first Compose file + volumes: + - '../.amp/Caddyfile:/etc/caddy/Caddyfile' # Connects the executor from docker-compose/executors/ to Sourcegraph, # with the token .amp/sourcegraph.sh also sets as executors.accessToken in the site configuration executor: From 780e3578605c15a1177852fdb2262643e299049c Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Thu, 12 Nov 2026 21:16:49 -0600 Subject: [PATCH 10/15] amp: Give the site admin the same password in every orb People open Sourcegraph in many orbs, and a password that differs per orb has to be looked up each time. The executor token is still made per orb. Amp-Thread-ID: https://ampcode.com/threads/T-01a1d2a1-43a0-7092-998e-43c135c772e8 Co-authored-by: Amp --- .agents/setup | 2 +- .amp/sourcegraph.sh | 17 ++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/.agents/setup b/.agents/setup index d8daf1321..d586f332e 100755 --- a/.agents/setup +++ b/.agents/setup @@ -26,7 +26,7 @@ $guidance_heading - \`amp orb services ensure\` starts Sourcegraph with Jaeger and an executor, from the Compose files in \`docker-compose/\`, \`docker-compose/jaeger/\`, and \`docker-compose/executors/\`, and prints its portal URL. -- The first site admin is \`admin\`. Its password is in \`~/.config/sourcegraph/admin-password\`. +- The first site admin is \`admin\`, with the password \`Sourcegraph123\`. When you start Sourcegraph for the user, give them the portal URL, the username, and the password. - \`amp orb service logs sourcegraph\` shows the logs of every container. After changing a Compose file, run \`amp orb service restart sourcegraph\`. diff --git a/.amp/sourcegraph.sh b/.amp/sourcegraph.sh index a1a284834..ea079c740 100755 --- a/.amp/sourcegraph.sh +++ b/.amp/sourcegraph.sh @@ -8,17 +8,16 @@ cd "$(dirname "${BASH_SOURCE[0]}")/.." frontend_url="http://localhost:${PORT}" admin_username='admin' admin_email='admin@example.com' +# The same in every orb, so nobody has to look it up +admin_password='Sourcegraph123' secrets_directory="$HOME/.config/sourcegraph" -# Each orb makes its own secrets, so the repository holds none -make_secret() { - if [[ ! -f "$secrets_directory/$1" ]]; then - (umask 077 && mkdir -p "$secrets_directory" && openssl rand -hex 16 >"$secrets_directory/$1") - fi - cat "$secrets_directory/$1" -} -admin_password=$(make_secret admin-password) -executor_token=$(make_secret executor-token) +# Each orb makes its own executor token +executor_token_file="$secrets_directory/executor-token" +if [[ ! -f "$executor_token_file" ]]; then + (umask 077 && mkdir -p "$secrets_directory" && openssl rand -hex 16 >"$executor_token_file") +fi +executor_token=$(<"$executor_token_file") # Variables for .amp/docker-compose.override.yaml. A file keeps the token out of the sudo log compose_variables="$secrets_directory/compose.env" From c06470521957ffd4a7499cb4cdcc9a09f497013a Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Thu, 12 Nov 2026 21:42:14 -0600 Subject: [PATCH 11/15] amp: Remove containers of services the Compose files no longer have After a branch renames or removes a service, its old container keeps running and holds its image and volumes, so the prune cannot free them. Every up in the service now passes --remove-orphans. Amp-Thread-ID: https://ampcode.com/threads/T-01a1d2a1-43a0-7092-998e-43c135c772e8 Co-authored-by: Amp --- .amp/sourcegraph.sh | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.amp/sourcegraph.sh b/.amp/sourcegraph.sh index ea079c740..b68351a88 100755 --- a/.amp/sourcegraph.sh +++ b/.amp/sourcegraph.sh @@ -30,9 +30,10 @@ compose=(sudo docker compose --env-file "$compose_variables" # Start every service, then wait until the frontend is healthy. # Waiting for every service fails, because searcher-0 reports unhealthy for a few seconds while it starts. -# The first start pulls the images, and Docker Hub times out now and then -"${compose[@]}" up --detach || { sleep 10 && "${compose[@]}" up --detach; } -"${compose[@]}" up --detach --wait sourcegraph-frontend-0 +# The first start pulls the images, and Docker Hub times out now and then. +# --remove-orphans removes the containers of services the Compose files no longer have, such as after a rename +"${compose[@]}" up --remove-orphans --detach || { sleep 10 && "${compose[@]}" up --remove-orphans --detach; } +"${compose[@]}" up --remove-orphans --detach --wait sourcegraph-frontend-0 # Sourcegraph refuses this request once it has a user, so a restart keeps the first admin curl --silent --output /dev/null --request POST "$frontend_url/-/site-init" \ @@ -84,4 +85,4 @@ sudo docker image prune --all --force | tail -n 1 sudo docker volume prune --all --force | tail -n 1 # Stay in the foreground. Stopping the service stops the containers -"${compose[@]}" up +"${compose[@]}" up --remove-orphans From 781ce8b7d4889251b17168d4c31ec3006eaa4b59 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Thu, 12 Nov 2026 21:54:28 -0600 Subject: [PATCH 12/15] amp: Read the site admin from the Amp project's environment variables SOURCEGRAPH_ADMIN_USERNAME, SOURCEGRAPH_ADMIN_EMAIL, and SOURCEGRAPH_ADMIN_PASSWORD set the first site admin, so every orb in the project gets the same one and the repository holds no password. The service stops with a message when one is missing. The sign-in requests build their JSON with jq, so any character works in the values. Amp-Thread-ID: https://ampcode.com/threads/T-01a1d2a1-43a0-7092-998e-43c135c772e8 Co-authored-by: Amp --- .agents/setup | 3 ++- .amp/sourcegraph.sh | 13 +++++++------ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.agents/setup b/.agents/setup index d586f332e..5a2089242 100755 --- a/.agents/setup +++ b/.agents/setup @@ -26,7 +26,8 @@ $guidance_heading - \`amp orb services ensure\` starts Sourcegraph with Jaeger and an executor, from the Compose files in \`docker-compose/\`, \`docker-compose/jaeger/\`, and \`docker-compose/executors/\`, and prints its portal URL. -- The first site admin is \`admin\`, with the password \`Sourcegraph123\`. +- The first site admin's username, email, and password are in \`\$SOURCEGRAPH_ADMIN_USERNAME\`, + \`\$SOURCEGRAPH_ADMIN_EMAIL\`, and \`\$SOURCEGRAPH_ADMIN_PASSWORD\`, from the Amp project's environment variables. When you start Sourcegraph for the user, give them the portal URL, the username, and the password. - \`amp orb service logs sourcegraph\` shows the logs of every container. After changing a Compose file, run \`amp orb service restart sourcegraph\`. diff --git a/.amp/sourcegraph.sh b/.amp/sourcegraph.sh index b68351a88..bf2749fdb 100755 --- a/.amp/sourcegraph.sh +++ b/.amp/sourcegraph.sh @@ -6,10 +6,10 @@ set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")/.." frontend_url="http://localhost:${PORT}" -admin_username='admin' -admin_email='admin@example.com' -# The same in every orb, so nobody has to look it up -admin_password='Sourcegraph123' +# The first site admin, from the Amp project's environment variables, so it is the same in every orb +admin_username=${SOURCEGRAPH_ADMIN_USERNAME:?"Set it in the Amp project's environment variables"} +admin_email=${SOURCEGRAPH_ADMIN_EMAIL:?"Set it in the Amp project's environment variables"} +admin_password=${SOURCEGRAPH_ADMIN_PASSWORD:?"Set it in the Amp project's environment variables"} secrets_directory="$HOME/.config/sourcegraph" # Each orb makes its own executor token @@ -38,7 +38,8 @@ compose=(sudo docker compose --env-file "$compose_variables" # Sourcegraph refuses this request once it has a user, so a restart keeps the first admin curl --silent --output /dev/null --request POST "$frontend_url/-/site-init" \ --header 'Content-Type: application/json' \ - --data "{\"email\":\"$admin_email\",\"username\":\"$admin_username\",\"password\":\"$admin_password\"}" + --data "$(jq --null-input --arg email "$admin_email" --arg username "$admin_username" \ + --arg password "$admin_password" '{$email, $username, $password}')" # Stop here if the admin cannot sign in. The session cookie also authorizes the GraphQL requests below cookie_jar=$(mktemp) @@ -48,7 +49,7 @@ trap 'rm -f "$cookie_jar" ".amp/portals/$portal_name.json"' EXIT trap exit TERM curl --silent --fail --output /dev/null --cookie-jar "$cookie_jar" --request POST "$frontend_url/-/sign-in" \ --header 'Content-Type: application/json' \ - --data "{\"email\":\"$admin_email\",\"password\":\"$admin_password\"}" + --data "$(jq --null-input --arg email "$admin_email" --arg password "$admin_password" '{$email, $password}')" # Prints the response, and fails if it has errors graphql() { From 0ead9d5625697d283d03979be4a1bc0194237b59 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Thu, 12 Nov 2026 22:07:52 -0600 Subject: [PATCH 13/15] amp: Say what the orb is for, and where its portal URLs come from The orb is for developing and testing this repository's Docker Compose deployment, not Sourcegraph itself, which sg in sourcegraph/sourcegraph is for. amp orb services ensure prints only the Executors and Jaeger links: the script adds the Sourcegraph link a few seconds later. Amp-Thread-ID: https://ampcode.com/threads/T-01a1d2a1-43a0-7092-998e-43c135c772e8 Co-authored-by: Amp --- .agents/setup | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.agents/setup b/.agents/setup index 5a2089242..d3c4c67f7 100755 --- a/.agents/setup +++ b/.agents/setup @@ -24,8 +24,12 @@ if ! grep --fixed-strings --line-regexp --quiet "$guidance_heading" "$guidance_f $guidance_heading +This orb is for developing and testing the Docker Compose deployment in this repository. To develop +Sourcegraph itself, use \`sg\` in the sourcegraph/sourcegraph repository instead. + - \`amp orb services ensure\` starts Sourcegraph with Jaeger and an executor, from the Compose files in - \`docker-compose/\`, \`docker-compose/jaeger/\`, and \`docker-compose/executors/\`, and prints its portal URL. + \`docker-compose/\`, \`docker-compose/jaeger/\`, and \`docker-compose/executors/\`, and prints the + Executors and Jaeger portal URLs. The Sourcegraph portal URL is the Executors URL without its path. - The first site admin's username, email, and password are in \`\$SOURCEGRAPH_ADMIN_USERNAME\`, \`\$SOURCEGRAPH_ADMIN_EMAIL\`, and \`\$SOURCEGRAPH_ADMIN_PASSWORD\`, from the Amp project's environment variables. When you start Sourcegraph for the user, give them the portal URL, the username, and the password. From b0c18d8b06c2227075d14f12bd390d5f2a34dd07 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Thu, 12 Nov 2026 23:01:19 -0600 Subject: [PATCH 14/15] amp: Report failures, stop containers on exit, and prune images only on new versions - Print why Sourcegraph refused the sign-in, the site admin, or a GraphQL request, and say when the site configuration does not parse - Stop the containers when the service fails or stops before it follows the logs - Prune unused images only when the deployment's images change, so executor job images do not pull again after every restart, and stop pruning volumes, which hold the data that upgrade and rename tests need - Rewrite the site configuration when the executor token or the portal URL changed - Include docker-compose/docker-compose.override.yaml when it exists - Replace the orb guidance on every setup run, so changes to it reach new snapshots - Say in AGENTS.md what .amp/ is, and that it follows the base files Amp-Thread-ID: https://ampcode.com/threads/T-01a1d323-9e56-72e1-ac97-48cbf9823c29 Co-authored-by: Amp --- .agents/setup | 21 ++++++++---- .amp/sourcegraph.sh | 80 +++++++++++++++++++++++++++++---------------- AGENTS.md | 1 + 3 files changed, 66 insertions(+), 36 deletions(-) diff --git a/.agents/setup b/.agents/setup index d3c4c67f7..423007269 100755 --- a/.agents/setup +++ b/.agents/setup @@ -18,18 +18,24 @@ fi step 'Write guidance for agents' guidance_file="$HOME/.config/amp/AGENTS.md" guidance_heading='## Sourcegraph in this orb' -if ! grep --fixed-strings --line-regexp --quiet "$guidance_heading" "$guidance_file" 2>/dev/null; then - mkdir -p "$(dirname "$guidance_file")" - cat >>"$guidance_file" <"$guidance_file.new" +mv "$guidance_file.new" "$guidance_file" step 'Done' diff --git a/.amp/sourcegraph.sh b/.amp/sourcegraph.sh index bf2749fdb..470d73268 100755 --- a/.amp/sourcegraph.sh +++ b/.amp/sourcegraph.sh @@ -6,6 +6,7 @@ set -euo pipefail cd "$(dirname "${BASH_SOURCE[0]}")/.." frontend_url="http://localhost:${PORT}" +portal_url=${PUBLIC_URL%/} # The first site admin, from the Amp project's environment variables, so it is the same in every orb admin_username=${SOURCEGRAPH_ADMIN_USERNAME:?"Set it in the Amp project's environment variables"} admin_email=${SOURCEGRAPH_ADMIN_EMAIL:?"Set it in the Amp project's environment variables"} @@ -14,10 +15,12 @@ secrets_directory="$HOME/.config/sourcegraph" # Each orb makes its own executor token executor_token_file="$secrets_directory/executor-token" -if [[ ! -f "$executor_token_file" ]]; then +if [[ ! -s "$executor_token_file" ]]; then (umask 077 && mkdir -p "$secrets_directory" && openssl rand -hex 16 >"$executor_token_file") fi executor_token=$(<"$executor_token_file") +# The token last written to the site configuration, which shows it only as REDACTED +configured_token_file="$secrets_directory/configured-executor-token" # Variables for .amp/docker-compose.override.yaml. A file keeps the token out of the sudo log compose_variables="$secrets_directory/compose.env" @@ -25,8 +28,18 @@ compose_variables="$secrets_directory/compose.env" compose=(sudo docker compose --env-file "$compose_variables" --file docker-compose/docker-compose.yaml --file docker-compose/jaeger/docker-compose.yaml - --file docker-compose/executors/executor.docker-compose.yaml - --file .amp/docker-compose.override.yaml) + --file docker-compose/executors/executor.docker-compose.yaml) +# Naming files turns off Compose's own pick-up of the override file customers keep their changes in +if [[ -f docker-compose/docker-compose.override.yaml ]]; then + compose+=(--file docker-compose/docker-compose.override.yaml) +fi +compose+=(--file .amp/docker-compose.override.yaml) + +# When Amp stops the service, or a step fails, stop the containers and remove these files +cookie_jar=$(mktemp) +portal_name=sourcegraph-admin +trap '"${compose[@]}" stop; rm -f "$cookie_jar" ".amp/portals/$portal_name.json"' EXIT +trap exit TERM # Start every service, then wait until the frontend is healthy. # Waiting for every service fails, because searcher-0 reports unhealthy for a few seconds while it starts. @@ -35,36 +48,39 @@ compose=(sudo docker compose --env-file "$compose_variables" "${compose[@]}" up --remove-orphans --detach || { sleep 10 && "${compose[@]}" up --remove-orphans --detach; } "${compose[@]}" up --remove-orphans --detach --wait sourcegraph-frontend-0 -# Sourcegraph refuses this request once it has a user, so a restart keeps the first admin -curl --silent --output /dev/null --request POST "$frontend_url/-/site-init" \ - --header 'Content-Type: application/json' \ - --data "$(jq --null-input --arg email "$admin_email" --arg username "$admin_username" \ - --arg password "$admin_password" '{$email, $username, $password}')" - -# Stop here if the admin cannot sign in. The session cookie also authorizes the GraphQL requests below -cookie_jar=$(mktemp) -portal_name=sourcegraph-admin -trap 'rm -f "$cookie_jar" ".amp/portals/$portal_name.json"' EXIT -# When Amp stops the service, wait for Docker Compose to stop the containers, then remove the files above -trap exit TERM -curl --silent --fail --output /dev/null --cookie-jar "$cookie_jar" --request POST "$frontend_url/-/sign-in" \ - --header 'Content-Type: application/json' \ - --data "$(jq --null-input --arg email "$admin_email" --arg password "$admin_password" '{$email, $password}')" +# Posts JSON, keeps the session cookie, and prints Sourcegraph's reason when it refuses +post() { + curl --silent --show-error --fail-with-body --cookie-jar "$cookie_jar" \ + --header 'Content-Type: application/json' --data "$2" "$frontend_url$1" +} +credentials=$(jq --null-input --arg email "$admin_email" --arg username "$admin_username" \ + --arg password "$admin_password" '{$email, $username, $password}') +# Sign in, or create the first site admin, which also signs in. The session authorizes the GraphQL requests below +if ! post /-/sign-in "$credentials" >/dev/null 2>&1 && ! post /-/site-init "$credentials"; then + echo "Could not sign in as $admin_username, or create that site admin. Sourcegraph creates only the first one," \ + "so after changing the SOURCEGRAPH_ADMIN_* variables, change that user to match" >&2 + exit 1 +fi -# Prints the response, and fails if it has errors +# Prints the response, and fails with its errors graphql() { - curl --silent --fail --cookie "$cookie_jar" --header 'Content-Type: application/json' --data "$1" "$frontend_url/.api/graphql" | - jq --exit-status 'select(.errors == null)' + curl --silent --show-error --fail-with-body --cookie "$cookie_jar" --header 'Content-Type: application/json' \ + --data "$1" "$frontend_url/.api/graphql" | + jq --exit-status 'if .errors then "GraphQL errors: \(.errors)\n" | halt_error else . end' } # The site configuration is JSON with comments, so drop whole-line comments and trailing commas to read it site=$(graphql '{"query":"{ site { configuration { id effectiveContents } } }"}' | jq '.data.site.configuration | {id, contents: (.effectiveContents | gsub("(?m)^\\s*//.*$"; "") | gsub(",(?=\\s*[]}])"; "") | fromjson? // null)}') -# Configure the site only while it has no executor token, so restarts keep later changes. -# Leave it alone if it does not parse -if jq --exit-status '.contents != null and (.contents | has("executors.accessToken") | not)' <<<"$site" >/dev/null; then +# Configure the site when it has no executor token, has an older one, or has another portal URL. +# Otherwise leave it alone, so restarts keep later changes and its comments +if jq --exit-status '.contents == null' <<<"$site" >/dev/null; then + echo 'Could not parse the site configuration, so left it alone' >&2 +elif [[ "$executor_token" != "$(cat "$configured_token_file" 2>/dev/null)" ]] || + jq --exit-status --arg url "$portal_url" \ + '.contents | (has("executors.accessToken") | not) or .externalURL != $url' <<<"$site" >/dev/null; then # Gives Sourcegraph its portal URL, lets the executor and the containers it starts connect, # lets code intelligence jobs be queued, and sends traces to Jaeger - site_configuration=$(jq --arg url "${PUBLIC_URL%/}" --arg token "$executor_token" '.contents + { + site_configuration=$(jq --arg url "$portal_url" --arg token "$executor_token" '.contents + { "externalURL": $url, "executors.accessToken": $token, "executors.frontendURL": "http://host.docker.internal", @@ -74,16 +90,22 @@ if jq --exit-status '.contents != null and (.contents | has("executors.accessTok graphql "$(jq --arg input "$site_configuration" \ '{query: "mutation($id: Int!, $input: String!) { updateSiteConfiguration(lastID: $id, input: $input) }", variables: {id, input: $input}}' <<<"$site")" >/dev/null + (umask 077 && cp "$executor_token_file" "$configured_token_file") fi # Shows the sign-in in the Portal tab, as a note next to the Sourcegraph link, until the service stops amp orb portal "$PORT" --name "$portal_name" --title Sourcegraph \ --description "Sign in as $admin_username with the password $admin_password" -# Free the disk that images and volumes of earlier versions hold. -# Every container of this deployment exists by now, so Docker keeps their images and volumes -sudo docker image prune --all --force | tail -n 1 -sudo docker volume prune --all --force | tail -n 1 +# Free the disk that images of earlier versions hold, once the deployment's images change. +# Every container of this deployment exists by now, so Docker keeps their images. +# Not on every start, because that also removes the images of executor jobs, which then pull again +images_file="$secrets_directory/images" +images=$("${compose[@]}" config --images | sort) +if [[ "$images" != "$(cat "$images_file" 2>/dev/null)" ]]; then + sudo docker image prune --all --force | tail -n 1 + echo "$images" >"$images_file" +fi # Stay in the foreground. Stopping the service stops the containers "${compose[@]}" up --remove-orphans diff --git a/AGENTS.md b/AGENTS.md index e6ccfebd3..58e2a8457 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,6 +8,7 @@ This repository is the deployment reference for [Sourcegraph with Docker Compose - `docker-compose/db-only-migrate.docker-compose.yaml`: a copy of the `pgsql` and `codeintel-db` services for backup and restore. Make every change to those two services in both files. - `docker-compose/examples/`: example override files. `docker-compose/executors/` and `docker-compose/jaeger/` are optional extra compose files. `docker-compose/dev/` is an overlay for developing this repository. - `test/` and `.buildkite/`: the smoke test and the CI pipeline. +- `.agents/setup` and `.amp/`: run the deployment in an [Amp orb](https://ampcode.com/docs/orbs), to test changes to it. `.amp/docker-compose.override.yaml` names base services, and `.amp/Caddyfile` copies `caddy/builtins/http.Caddyfile`; keep both in step with the base files, because CI checks neither. - `release.yaml`: the release steps that `sg release` runs. ## Checks From feebb261062bd6534583296d3721f292acb7f576 Mon Sep 17 00:00:00 2001 From: Marc LeBlanc <7050295+marcleblanc2@users.noreply.github.com> Date: Thu, 12 Nov 2026 23:31:50 -0600 Subject: [PATCH 15/15] amp: Report ready once the site admin exists, and log why sign-in failed - The health check is /-/amp-ready, which Caddy serves only after the script has created the site admin and configured the site. /healthz passed about 2 seconds before the admin existed, so a sign-in right after ensure got a 401 - When both sign-in and site-init fail, log both reasons: a wrong password on an existing site logged only site-init's "Signup failed unexpectedly." Amp-Thread-ID: https://ampcode.com/threads/T-01a1d323-9e56-72e1-ac97-48cbf9823c29 Co-authored-by: Amp --- .amp/Caddyfile | 19 ++++++++++++++----- .amp/docker-compose.override.yaml | 1 + .amp/services.yaml | 3 ++- .amp/sourcegraph.sh | 21 ++++++++++++++++----- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/.amp/Caddyfile b/.amp/Caddyfile index 4b242a5b4..948c87843 100644 --- a/.amp/Caddyfile +++ b/.amp/Caddyfile @@ -3,9 +3,18 @@ # This turns off Sourcegraph's clickjacking protection, which is fine only for a disposable test deployment :80 -reverse_proxy { - to {$SRC_FRONTEND_ADDRESSES} - trusted_proxies 0.0.0.0/0 - header_up -Sec-Fetch-Dest - header_down -X-Frame-Options +# The health check in .amp/services.yaml: 404 until .amp/sourcegraph.sh has set up the site admin and site configuration +handle /-/amp-ready { + root * /amp-ready + rewrite * /ready + file_server +} + +handle { + reverse_proxy { + to {$SRC_FRONTEND_ADDRESSES} + trusted_proxies 0.0.0.0/0 + header_up -Sec-Fetch-Dest + header_down -X-Frame-Options + } } diff --git a/.amp/docker-compose.override.yaml b/.amp/docker-compose.override.yaml index 0ec524f57..f697a9576 100644 --- a/.amp/docker-compose.override.yaml +++ b/.amp/docker-compose.override.yaml @@ -9,6 +9,7 @@ services: # Relative to docker-compose/, the directory of the first Compose file volumes: - '../.amp/Caddyfile:/etc/caddy/Caddyfile' + - '${READY_DIRECTORY:?}:/amp-ready:ro' # Connects the executor from docker-compose/executors/ to Sourcegraph, # with the token .amp/sourcegraph.sh also sets as executors.accessToken in the site configuration executor: diff --git a/.amp/services.yaml b/.amp/services.yaml index 01f487df2..916ea41a5 100644 --- a/.amp/services.yaml +++ b/.amp/services.yaml @@ -3,7 +3,8 @@ services: sourcegraph: command: .amp/sourcegraph.sh - health: /healthz + # Served by Caddy once .amp/sourcegraph.sh has created the site admin and configured the site + health: /-/amp-ready # .amp/sourcegraph.sh adds the Sourcegraph link, with the admin password in its note portals: - url: /admin/executors diff --git a/.amp/sourcegraph.sh b/.amp/sourcegraph.sh index 470d73268..c6a7c238e 100755 --- a/.amp/sourcegraph.sh +++ b/.amp/sourcegraph.sh @@ -22,9 +22,16 @@ executor_token=$(<"$executor_token_file") # The token last written to the site configuration, which shows it only as REDACTED configured_token_file="$secrets_directory/configured-executor-token" +# Caddy serves this file at /-/amp-ready, the health check in .amp/services.yaml, so Amp reports the service +# ready only once the site admin exists and the site is configured +ready_directory="$secrets_directory/ready" +mkdir -p "$ready_directory" +rm -f "$ready_directory/ready" + # Variables for .amp/docker-compose.override.yaml. A file keeps the token out of the sudo log compose_variables="$secrets_directory/compose.env" -(umask 077 && printf 'PORT=%s\nEXECUTOR_TOKEN=%s\n' "$PORT" "$executor_token" >"$compose_variables") +(umask 077 && printf 'PORT=%s\nEXECUTOR_TOKEN=%s\nREADY_DIRECTORY=%s\n' "$PORT" "$executor_token" "$ready_directory" \ + >"$compose_variables") compose=(sudo docker compose --env-file "$compose_variables" --file docker-compose/docker-compose.yaml --file docker-compose/jaeger/docker-compose.yaml @@ -38,7 +45,7 @@ compose+=(--file .amp/docker-compose.override.yaml) # When Amp stops the service, or a step fails, stop the containers and remove these files cookie_jar=$(mktemp) portal_name=sourcegraph-admin -trap '"${compose[@]}" stop; rm -f "$cookie_jar" ".amp/portals/$portal_name.json"' EXIT +trap 'rm -f "$ready_directory/ready"; "${compose[@]}" stop; rm -f "$cookie_jar" ".amp/portals/$portal_name.json"' EXIT trap exit TERM # Start every service, then wait until the frontend is healthy. @@ -56,9 +63,12 @@ post() { credentials=$(jq --null-input --arg email "$admin_email" --arg username "$admin_username" \ --arg password "$admin_password" '{$email, $username, $password}') # Sign in, or create the first site admin, which also signs in. The session authorizes the GraphQL requests below -if ! post /-/sign-in "$credentials" >/dev/null 2>&1 && ! post /-/site-init "$credentials"; then - echo "Could not sign in as $admin_username, or create that site admin. Sourcegraph creates only the first one," \ - "so after changing the SOURCEGRAPH_ADMIN_* variables, change that user to match" >&2 +if ! sign_in_error=$(post /-/sign-in "$credentials" 2>&1) && + ! site_init_error=$(post /-/site-init "$credentials" 2>&1); then + echo "Could not sign in as $admin_username: ${sign_in_error//$'\n'/ }" >&2 + echo "Could not create that site admin: ${site_init_error//$'\n'/ }" >&2 + echo "Sourcegraph creates only the first site admin, so after changing the SOURCEGRAPH_ADMIN_* variables," \ + "change that user to match" >&2 exit 1 fi @@ -96,6 +106,7 @@ fi # Shows the sign-in in the Portal tab, as a note next to the Sourcegraph link, until the service stops amp orb portal "$PORT" --name "$portal_name" --title Sourcegraph \ --description "Sign in as $admin_username with the password $admin_password" +touch "$ready_directory/ready" # Free the disk that images of earlier versions hold, once the deployment's images change. # Every container of this deployment exists by now, so Docker keeps their images.