diff --git a/scripts/setup-github-datasource.sh b/scripts/setup-github-datasource.sh index 6d7a2cefd7..17daee5ce5 100755 --- a/scripts/setup-github-datasource.sh +++ b/scripts/setup-github-datasource.sh @@ -20,10 +20,22 @@ set -euo pipefail GRAFANA_URL="${GRAFANA_URL:-http://localhost:3000}" -[ -f .env ] && { set -a; . ./.env; set +a; } +[ -f .env ] && . ./.env : "${GITHUB_TOKEN:?Set GITHUB_TOKEN (a read-only fine-grained PAT) in the environment or .env}" : "${GRAFANA_ADMIN_PASSWORD:?Set GRAFANA_ADMIN_PASSWORD in the environment or .env}" -AUTH="admin:${GRAFANA_ADMIN_PASSWORD}" + +TMP_DIR="$(mktemp -d)" +NETRC_FILE="$TMP_DIR/netrc" +trap 'rm -rf "$TMP_DIR"' EXIT +GRAFANA_HOSTPORT="${GRAFANA_URL#*://}" +GRAFANA_HOSTPORT="${GRAFANA_HOSTPORT%%/*}" +GRAFANA_HOST="${GRAFANA_HOSTPORT%%:*}" +printf 'machine %s login %s password %s\n' "$GRAFANA_HOST" admin "$GRAFANA_ADMIN_PASSWORD" >"$NETRC_FILE" +chmod 600 "$NETRC_FILE" + +grafana_curl() { + env -u GRAFANA_ADMIN_PASSWORD -u GITHUB_TOKEN curl -sf --netrc-file "$NETRC_FILE" "$@" +} payload() { cat </dev/null 2>&1; then +if grafana_curl "$GRAFANA_URL/api/datasources/uid/github" >/dev/null 2>&1; then echo "Updating existing GitHub data source…" - curl -sf -u "$AUTH" -H 'content-type: application/json' -X PUT \ - "$GRAFANA_URL/api/datasources/uid/github" -d "$(payload)" >/dev/null + payload | grafana_curl -H 'content-type: application/json' -X PUT \ + "$GRAFANA_URL/api/datasources/uid/github" --data-binary @- >/dev/null else echo "Creating GitHub data source…" - curl -sf -u "$AUTH" -H 'content-type: application/json' -X POST \ - "$GRAFANA_URL/api/datasources" -d "$(payload)" >/dev/null + payload | grafana_curl -H 'content-type: application/json' -X POST \ + "$GRAFANA_URL/api/datasources" --data-binary @- >/dev/null fi echo "Done. Verifying health…" -curl -sf -u "$AUTH" -X POST "$GRAFANA_URL/api/datasources/uid/github/health" 2>/dev/null \ +grafana_curl -X POST "$GRAFANA_URL/api/datasources/uid/github/health" 2>/dev/null \ | grep -q '"status":"OK"' && echo "✓ GitHub data source healthy" || echo "⚠ Added, but health check did not return OK — verify the token scopes." diff --git a/test/unit/selfhost-grafana-github-datasource.test.ts b/test/unit/selfhost-grafana-github-datasource.test.ts new file mode 100644 index 0000000000..87394bb8ea --- /dev/null +++ b/test/unit/selfhost-grafana-github-datasource.test.ts @@ -0,0 +1,34 @@ +import { readFileSync, statSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +describe("Grafana GitHub data source", () => { + it("keeps GITHUB_TOKEN and GRAFANA_ADMIN_PASSWORD out of curl argv and child environments", () => { + const script = readFileSync(join(process.cwd(), "scripts/setup-github-datasource.sh"), "utf8"); + + expect(script).not.toContain("set -a"); + expect(script).not.toContain('AUTH="admin:${GRAFANA_ADMIN_PASSWORD}"'); + expect(script).not.toContain('-u "$AUTH"'); + expect(script).not.toContain('-d "$(payload)"'); + expect(script).toContain('--netrc-file "$NETRC_FILE"'); + expect(script).toContain("--data-binary @-"); + expect(script).toMatch(/env -u GRAFANA_ADMIN_PASSWORD -u GITHUB_TOKEN curl/); + }); + + it("is executable, matching setup-sentry-datasource.sh's own mode", () => { + const mode = statSync(join(process.cwd(), "scripts/setup-github-datasource.sh")).mode; + // Owner-execute bit (0o100). + expect(mode & 0o100).not.toBe(0); + }); + + it("remains idempotent (update-vs-create) and preserves the health check after the credential-handling rewrite", () => { + const script = readFileSync(join(process.cwd(), "scripts/setup-github-datasource.sh"), "utf8"); + + expect(script).toContain("api/datasources/uid/github"); + expect(script).toMatch(/-X PUT/); + expect(script).toMatch(/-X POST/); + expect(script).toContain("secureJsonData"); + expect(script).toContain("accessToken"); + expect(script).toContain("/health"); + }); +});