Skip to content

Commit c34cc43

Browse files
committed
Set up Matomo smoke instance
1 parent c5229bf commit c34cc43

2 files changed

Lines changed: 349 additions & 0 deletions

File tree

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
4+
MATOMO_DIR=${MATOMO_DIR:?MATOMO_DIR is required}
5+
BASE_URL=${BASE_URL:-http://127.0.0.1:8000}
6+
MYSQL_HOST=${MYSQL_HOST:?MYSQL_HOST is required}
7+
MYSQL_PORT=${MYSQL_PORT:-3306}
8+
MYSQL_USER=${MYSQL_USER:?MYSQL_USER is required}
9+
MYSQL_PASSWORD=${MYSQL_PASSWORD:?MYSQL_PASSWORD is required}
10+
MYSQL_DATABASE=${MYSQL_DATABASE:?MYSQL_DATABASE is required}
11+
STATE_FILE=${STATE_FILE:-$PWD/.github/scripts/mcp-smoke/.state.json}
12+
13+
export MYSQL_PWD="$MYSQL_PASSWORD"
14+
15+
wait_for_mysql() {
16+
for _ in $(seq 1 60); do
17+
if mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -e 'SELECT 1' >/dev/null 2>&1; then
18+
return 0
19+
fi
20+
sleep 2
21+
done
22+
echo "MySQL did not become ready in time" >&2
23+
return 1
24+
}
25+
26+
write_config() {
27+
cat > "$MATOMO_DIR/config/config.ini.php" <<EOF
28+
; <?php exit; ?> DO NOT REMOVE THIS LINE
29+
[database]
30+
host = "$MYSQL_HOST"
31+
username = "$MYSQL_USER"
32+
password = "$MYSQL_PASSWORD"
33+
dbname = "$MYSQL_DATABASE"
34+
tables_prefix = ""
35+
charset = "utf8mb4"
36+
collation = "utf8mb4_general_ci"
37+
38+
[General]
39+
trusted_hosts[] = "127.0.0.1"
40+
enable_logging = 1
41+
42+
[log]
43+
log_writers[] = file
44+
log_level = DEBUG
45+
logger_file_path = tmp/logs/matomo.log
46+
47+
[McpServer]
48+
enable_mcp = 1
49+
log_tool_calls = 1
50+
log_tool_call_level = DEBUG
51+
log_tool_call_parameters_full = 1
52+
EOF
53+
}
54+
55+
start_php_server() {
56+
mkdir -p "$MATOMO_DIR/tmp/logs"
57+
: > "$MATOMO_DIR/tmp/logs/matomo.log"
58+
php -S 127.0.0.1:8000 -t "$MATOMO_DIR" > "$MATOMO_DIR/tmp/logs/php-server.log" 2>&1 &
59+
echo $! > "$MATOMO_DIR/tmp/php-server.pid"
60+
61+
for _ in $(seq 1 60); do
62+
if curl -sS "$BASE_URL/index.php" >/dev/null 2>&1; then
63+
return 0
64+
fi
65+
sleep 1
66+
done
67+
68+
echo "PHP server did not start in time" >&2
69+
return 1
70+
}
71+
72+
api_get_json() {
73+
local url="$1"
74+
curl -sS "$url" | sed 's/^\xEF\xBB\xBF//'
75+
}
76+
77+
require_non_empty() {
78+
local key="$1"
79+
local value="$2"
80+
if [ -z "$value" ] || [ "$value" = "null" ]; then
81+
echo "Required fixture value '$key' is missing from OmniFixture/API discovery." >&2
82+
exit 1
83+
fi
84+
}
85+
86+
extract_php_constant() {
87+
local file="$1"
88+
local constant_name="$2"
89+
90+
php -r '
91+
$file = $argv[1];
92+
$constantName = $argv[2];
93+
$content = @file_get_contents($file);
94+
if ($content === false) {
95+
fwrite(STDERR, "Unable to read file: {$file}\n");
96+
exit(2);
97+
}
98+
99+
$linePattern = "/^\\s*public\\s+const\\s+" . preg_quote($constantName, "/") . "\\s*=\\s*\\x27(.*)\\x27;\\s*$/";
100+
foreach (preg_split("/\\R/", $content) as $line) {
101+
if (preg_match($linePattern, $line, $matches)) {
102+
echo stripcslashes($matches[1]);
103+
exit(0);
104+
}
105+
}
106+
107+
fwrite(STDERR, "Could not extract constant {$constantName} from {$file}\n");
108+
exit(3);
109+
' "$file" "$constant_name"
110+
}
111+
112+
main() {
113+
wait_for_mysql
114+
115+
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" -e "DROP DATABASE IF EXISTS \`$MYSQL_DATABASE\`; CREATE DATABASE \`$MYSQL_DATABASE\` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;"
116+
mysql -h "$MYSQL_HOST" -P "$MYSQL_PORT" -u "$MYSQL_USER" "$MYSQL_DATABASE" < "$MATOMO_DIR/tests/resources/OmniFixture-dump.sql"
117+
118+
write_config
119+
start_php_server
120+
121+
# Run updates as the first Matomo console interaction after startup.
122+
(cd "$MATOMO_DIR" && php ./console core:update --yes)
123+
124+
(cd "$MATOMO_DIR" && php ./console plugin:activate McpServer)
125+
if ! (cd "$MATOMO_DIR" && php ./console plugin:list | grep -qE '\|[[:space:]]*McpServer[[:space:]]*\|.*\|[[:space:]]*Activated[[:space:]]*\|'); then
126+
echo "McpServer plugin is not activated after setup." >&2
127+
exit 1
128+
fi
129+
130+
local fixture_file="$MATOMO_DIR/tests/PHPUnit/Framework/Fixture.php"
131+
local omnifixture_file="$MATOMO_DIR/tests/PHPUnit/Fixtures/OmniFixture.php"
132+
local fixture_admin_login
133+
local fixture_admin_password
134+
local omnifixture_superuser_token
135+
fixture_admin_login=$(extract_php_constant "$fixture_file" "ADMIN_USER_LOGIN")
136+
fixture_admin_password=$(extract_php_constant "$fixture_file" "ADMIN_USER_PASSWORD")
137+
omnifixture_superuser_token=$(extract_php_constant "$omnifixture_file" "OMNIFIXTURE_SUPERUSER_TOKEN")
138+
139+
require_non_empty "ADMIN_USER_LOGIN" "$fixture_admin_login"
140+
require_non_empty "ADMIN_USER_PASSWORD" "$fixture_admin_password"
141+
require_non_empty "OMNIFIXTURE_SUPERUSER_TOKEN" "$omnifixture_superuser_token"
142+
143+
local token_response
144+
token_response=$(curl -sS -X POST "$BASE_URL/index.php" \
145+
--data-urlencode "module=API" \
146+
--data-urlencode "method=UsersManager.createAppSpecificTokenAuth" \
147+
--data-urlencode "format=JSON" \
148+
--data-urlencode "token_auth=${omnifixture_superuser_token}" \
149+
--data-urlencode "userLogin=${fixture_admin_login}" \
150+
--data-urlencode "passwordConfirmation=${fixture_admin_password}" \
151+
--data-urlencode "description=mcp smoke ci token")
152+
153+
local token
154+
if ! echo "$token_response" | jq -e 'type == "string" or (type == "object" and ((.value // .token_auth // .token // null) | type == "string"))' >/dev/null 2>&1; then
155+
echo "Unexpected token response schema from UsersManager.createAppSpecificTokenAuth: $token_response" >&2
156+
exit 1
157+
fi
158+
token=$(echo "$token_response" | jq -r 'if type == "string" then . else (.value // .token_auth // .token // empty) end')
159+
if [ -z "$token" ] || [ "$token" = "null" ] || [ "$token" = "false" ]; then
160+
echo "Failed to create app token via API. Response: $token_response" >&2
161+
exit 1
162+
fi
163+
164+
local sites_json metadata_json
165+
local id_site report_unique_id
166+
local -a skip_cases
167+
168+
sites_json=$(api_get_json "$BASE_URL/index.php?module=API&method=SitesManager.getSitesWithAtLeastViewAccess&format=JSON&token_auth=${token}")
169+
id_site=$(echo "$sites_json" | jq -r '.[0].idsite // empty')
170+
require_non_empty "idSite" "$id_site"
171+
172+
metadata_json=$(api_get_json "$BASE_URL/index.php?module=API&method=API.getReportMetadata&idSite=${id_site}&format=JSON&token_auth=${token}")
173+
report_unique_id=$(echo "$metadata_json" | jq -r '.[] | select(.module=="Actions" and .action=="getPageUrls") | .uniqueId' | head -n1)
174+
if [ -z "$report_unique_id" ]; then
175+
report_unique_id=$(echo "$metadata_json" | jq -r '.[] | .uniqueId // empty' | head -n1)
176+
fi
177+
if [ -z "$report_unique_id" ]; then
178+
skip_cases+=("report_processed")
179+
fi
180+
181+
jq -n \
182+
--arg base_url "$BASE_URL" \
183+
--arg token_auth "$token" \
184+
--arg idSite "$id_site" \
185+
--arg reportUniqueId "$report_unique_id" \
186+
--argjson skip_cases "$(printf '%s\n' "${skip_cases[@]:-}" | sed '/^$/d' | jq -R . | jq -s 'unique')" \
187+
'{base_url: $base_url, token_auth: $token_auth, idSite: $idSite, reportUniqueId: $reportUniqueId, skip_cases: $skip_cases}' > "$STATE_FILE"
188+
189+
echo "State written to $STATE_FILE"
190+
}
191+
192+
main "$@"

.github/workflows/mcp-ai-smoke.yml

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
name: MCP AI Smoke (Gemini)
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize]
6+
workflow_dispatch:
7+
inputs:
8+
gemini_model:
9+
description: Gemini model
10+
required: false
11+
default: gemini-2.5-flash
12+
13+
permissions:
14+
actions: read
15+
checks: none
16+
contents: read
17+
deployments: none
18+
issues: none
19+
packages: none
20+
pull-requests: read
21+
repository-projects: none
22+
security-events: none
23+
statuses: none
24+
25+
concurrency:
26+
group: mcp-ai-smoke-${{ github.ref }}
27+
cancel-in-progress: true
28+
29+
env:
30+
PLUGIN_NAME: McpServer
31+
MATOMO_DIR: ${{ github.workspace }}/matomo
32+
BASE_URL: http://127.0.0.1:8000
33+
MYSQL_HOST: 127.0.0.1
34+
MYSQL_PORT: 3306
35+
MYSQL_USER: root
36+
MYSQL_PASSWORD: root
37+
MYSQL_DATABASE: matomo
38+
GEMINI_MODEL: ${{ github.event.inputs.gemini_model || 'gemini-2.5-flash' }}
39+
40+
jobs:
41+
preflight:
42+
name: Preflight
43+
runs-on: ubuntu-24.04
44+
outputs:
45+
should_run: ${{ steps.check.outputs.should_run }}
46+
reason: ${{ steps.check.outputs.reason }}
47+
steps:
48+
- id: check
49+
name: Check secret availability
50+
env:
51+
GEMINI_APIKEY: ${{ secrets.GEMINI_APIKEY }}
52+
run: |
53+
set -euo pipefail
54+
if [ -z "${GEMINI_APIKEY:-}" ]; then
55+
echo "should_run=false" >> "$GITHUB_OUTPUT"
56+
echo "reason=GEMINI_APIKEY is not available in this context (likely fork PR)." >> "$GITHUB_OUTPUT"
57+
else
58+
echo "should_run=true" >> "$GITHUB_OUTPUT"
59+
echo "reason=Ready to run smoke checks." >> "$GITHUB_OUTPUT"
60+
fi
61+
62+
- name: Preflight summary
63+
run: |
64+
{
65+
echo "## MCP AI Smoke Preflight"
66+
echo
67+
echo "- should_run: ${{ steps.check.outputs.should_run }}"
68+
echo "- reason: ${{ steps.check.outputs.reason }}"
69+
} >> "$GITHUB_STEP_SUMMARY"
70+
71+
smoke:
72+
name: API + Gemini smoke
73+
runs-on: ubuntu-24.04
74+
needs: preflight
75+
if: needs.preflight.outputs.should_run == 'true'
76+
continue-on-error: true
77+
services:
78+
mysql:
79+
image: mysql:5.7
80+
env:
81+
MYSQL_ROOT_PASSWORD: root
82+
MYSQL_DATABASE: matomo
83+
ports:
84+
- 3306:3306
85+
options: >-
86+
--health-cmd="mysqladmin ping -h 127.0.0.1 -uroot -proot"
87+
--health-interval=10s
88+
--health-timeout=5s
89+
--health-retries=30
90+
91+
steps:
92+
- uses: actions/checkout@v6
93+
with:
94+
lfs: false
95+
persist-credentials: false
96+
97+
- name: Set up PHP
98+
uses: shivammathur/setup-php@44454db4f0199b8b9685a5d763dc37cbf79108e1 # v2
99+
with:
100+
php-version: '8.1'
101+
extensions: mbstring, mysqli
102+
103+
- name: Set up Node.js
104+
uses: actions/setup-node@v4
105+
with:
106+
node-version: '22'
107+
108+
- name: Check out github-action-tests repository
109+
uses: actions/checkout@v6
110+
with:
111+
repository: matomo-org/github-action-tests
112+
ref: main
113+
path: github-action-tests
114+
115+
- name: Check out Matomo for plugin builds
116+
run: ${{ github.workspace }}/github-action-tests/scripts/bash/checkout_matomo.sh
117+
env:
118+
PLUGIN_NAME: ${{ env.PLUGIN_NAME }}
119+
WORKSPACE: ${{ github.workspace }}
120+
ACTION_PATH: ${{ github.workspace }}/github-action-tests
121+
MATOMO_TEST_TARGET: maximum_supported_matomo
122+
123+
- name: Prepare Matomo composer setup
124+
run: composer install --ignore-platform-reqs --no-interaction --no-progress
125+
working-directory: matomo
126+
127+
- name: Setup Matomo with OmniFixture
128+
run: ./.github/scripts/mcp-smoke/setup-matomo-omnifixture.sh
129+
env:
130+
MATOMO_DIR: ${{ env.MATOMO_DIR }}
131+
BASE_URL: ${{ env.BASE_URL }}
132+
MYSQL_HOST: ${{ env.MYSQL_HOST }}
133+
MYSQL_PORT: ${{ env.MYSQL_PORT }}
134+
MYSQL_USER: ${{ env.MYSQL_USER }}
135+
MYSQL_PASSWORD: ${{ env.MYSQL_PASSWORD }}
136+
MYSQL_DATABASE: ${{ env.MYSQL_DATABASE }}
137+
138+
- name: Install Gemini CLI
139+
run: npm install -g @google/gemini-cli
140+
141+
summary:
142+
name: Final Summary
143+
runs-on: ubuntu-24.04
144+
needs: [preflight, smoke]
145+
if: always()
146+
steps:
147+
- name: Final status summary
148+
run: |
149+
{
150+
echo "## MCP AI Smoke Result"
151+
echo
152+
echo "- preflight: ${{ needs.preflight.outputs.should_run }}"
153+
echo "- reason: ${{ needs.preflight.outputs.reason }}"
154+
echo "- smoke result: ${{ needs.smoke.result || 'skipped' }}"
155+
echo
156+
echo "Prototype smoke is intentionally non-blocking."
157+
} >> "$GITHUB_STEP_SUMMARY"

0 commit comments

Comments
 (0)