Compare commits

..

1 Commits

Author SHA1 Message Date
Simon Larsen
57052d2301 feat: Enhance Navbar with mobile menu functionality and active child detection 2025-05-20 10:18:06 +00:00
4868 changed files with 273308 additions and 599302 deletions

View File

@@ -33,15 +33,6 @@ stop
nohup.out*
# Large directories not needed for Docker builds
E2E/playwright-report
E2E/test-results
Terraform
HelmChart
Scripts
.git
GoSDK
encrypted-credentials.tar
encrypted-credentials/

View File

@@ -1,17 +0,0 @@
---
applyTo: '**'
---
# Building and Compiling
If you would like to compile or build any project. Please cd into the directory and run the following command:
```
npm run compile
```
Ths will make sure there are no type / syntax errors.
# Typescript Types.
Please do not use "any" types. Please create proper types where required.

View File

@@ -10,6 +10,36 @@ on:
jobs:
docker-build-accounts:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./Accounts/Dockerfile .
docker-build-isolated-vm:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./IsolatedVM/Dockerfile .
docker-build-home:
runs-on: ubuntu-latest
env:
@@ -19,20 +49,87 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
run: npm run prerun
# build image for accounts service
- name: build docker image
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build --no-cache -f ./Home/Dockerfile .
run: sudo docker build -f ./Home/Dockerfile .
docker-build-worker:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./Worker/Dockerfile .
docker-build-workflow:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./Workflow/Dockerfile .
docker-build-api-reference:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./APIReference/Dockerfile .
docker-build-docs:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./Docs/Dockerfile .
docker-build-otel-collector:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./OTelCollector/Dockerfile .
docker-build-app:
runs-on: ubuntu-latest
@@ -43,22 +140,29 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
run: npm run prerun
# build image for accounts service
- name: build docker image
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build --no-cache -f ./App/Dockerfile .
run: sudo docker build -f ./App/Dockerfile .
docker-build-copilot:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image for accounts service
- name: build docker image
run: sudo docker build -f ./Copilot/Dockerfile .
docker-build-e2e:
runs-on: ubuntu-latest
env:
@@ -68,20 +172,58 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
run: npm run prerun
# build image for accounts service
- name: build docker image
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build --no-cache -f ./E2E/Dockerfile .
run: sudo docker build -f ./E2E/Dockerfile .
docker-build-admin-dashboard:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image for home
- name: build docker image
run: sudo docker build -f ./AdminDashboard/Dockerfile .
docker-build-dashboard:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image for home
- name: build docker image
run: sudo docker build -f ./Dashboard/Dockerfile .
docker-build-haraka:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build images
- name: build docker image
run: sudo docker build -f ./Haraka/Dockerfile .
docker-build-probe:
runs-on: ubuntu-latest
@@ -92,62 +234,113 @@ jobs:
uses: actions/checkout@v4
- name: Preinstall
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
run: npm run prerun
# build image probe api
- name: build docker image
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build --no-cache -f ./Probe/Dockerfile .
run: sudo docker build -f ./Probe/Dockerfile .
docker-build-probe-ingest:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image probe api
- name: build docker image
run: sudo docker build -f ./ProbeIngest/Dockerfile .
docker-build-server-monitor-ingest:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image probe api
- name: build docker image
run: sudo docker build -f ./ServerMonitorIngest/Dockerfile .
docker-build-open-telemetry-ingest:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image probe api
- name: build docker image
run: sudo docker build -f ./OpenTelemetryIngest/Dockerfile .
docker-build-incoming-request-ingest:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image probe api
- name: build docker image
run: sudo docker build -f ./IncomingRequestIngest/Dockerfile .
docker-build-fluent-ingest:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image probe api
- name: build docker image
run: sudo docker build -f ./FluentIngest/Dockerfile .
docker-build-status-page:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
run: npm run prerun
# build image for home
- name: build docker image
run: sudo docker build -f ./StatusPage/Dockerfile .
docker-build-test-server:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
- name: Preinstall
run: npm run prerun
# build image for mail service
- name: build docker image
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build --no-cache -f ./TestServer/Dockerfile .
docker-build-ai-agent:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Preinstall
uses: nick-fields/retry@v3
with:
timeout_minutes: 10
max_attempts: 3
command: npm run prerun
# build image for ai agent service
- name: build docker image
uses: nick-fields/retry@v3
with:
timeout_minutes: 45
max_attempts: 3
command: sudo docker build --no-cache -f ./AIAgent/Dockerfile .
run: sudo docker build -f ./TestServer/Dockerfile .

View File

@@ -31,6 +31,8 @@ jobs:
contents: read
security-events: write
strategy:
fail-fast: false
matrix:
language: [ 'javascript', 'typescript', 'go' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ]

View File

@@ -18,10 +18,9 @@ jobs:
- name: Install Helm
run: |
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
- name: Lint Helm Chart
- name: Lint Helm Chart
run: |
helm lint ./HelmChart/Public/oneuptime
helm lint ./HelmChart/Public/kubernetes-agent
js-lint:
runs-on: ubuntu-latest

View File

@@ -20,12 +20,19 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- name: Compile Accounts
uses: nick-fields/retry@v3
- run: cd Accounts && npm install && npm run compile && npm run dep-check
compile-isolated-vm:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
timeout_minutes: 30
max_attempts: 3
command: cd App/FeatureSet/Accounts && npm install && npm run compile && npm run dep-check
node-version: latest
- run: cd Common && npm install
- run: cd IsolatedVM && npm install && npm run compile && npm run dep-check
compile-common:
runs-on: ubuntu-latest
@@ -36,12 +43,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: latest
- name: Compile Common
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd Common && npm install && npm run compile && npm run dep-check
- run: cd Common && npm install && npm run compile && npm run dep-check
compile-app:
runs-on: ubuntu-latest
@@ -53,12 +55,7 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- name: Compile App
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd App && npm install && npm run compile && npm run dep-check
- run: cd App && npm install && npm run compile && npm run dep-check
compile-home:
runs-on: ubuntu-latest
@@ -70,13 +67,67 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- name: Compile Home
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd Home && npm install && npm run compile && npm run dep-check
- run: cd Home && npm install && npm run compile && npm run dep-check
compile-worker:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd Worker && npm install && npm run compile && npm run dep-check
compile-workflow:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd Workflow && npm install && npm run compile && npm run dep-check
compile-api-reference:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd APIReference && npm install && npm run compile && npm run dep-check
compile-docs-reference:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd Docs && npm install && npm run compile && npm run dep-check
compile-copilot:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd Copilot && npm install && npm run compile && npm run dep-check
compile-nginx:
runs-on: ubuntu-latest
@@ -89,12 +140,7 @@ jobs:
node-version: latest
- run: cd Common && npm install
- name: Compile Nginx
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd Nginx && npm install && npm run compile && npm run dep-check
- run: cd Nginx && npm install && npm run compile && npm run dep-check
compile-infrastructure-agent:
runs-on: ubuntu-latest
@@ -104,12 +150,7 @@ jobs:
- uses: actions/checkout@v4
# Setup Go
- uses: actions/setup-go@v5
- name: Compile Infrastructure Agent
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd InfrastructureAgent && go build .
- run: cd InfrastructureAgent && go build .
compile-admin-dashboard:
@@ -123,12 +164,7 @@ jobs:
node-version: latest
- run: cd Common && npm install
- name: Compile Admin Dashboard
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd App/FeatureSet/AdminDashboard && npm install && npm run compile && npm run dep-check
- run: cd AdminDashboard && npm install && npm run compile && npm run dep-check
compile-dashboard:
runs-on: ubuntu-latest
@@ -141,12 +177,7 @@ jobs:
node-version: latest
- run: cd Common && npm install
- name: Compile Dashboard
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd App/FeatureSet/Dashboard && npm install && npm run compile && npm run dep-check
- run: cd Dashboard && npm install && npm run compile && npm run dep-check
compile-e2e:
@@ -160,12 +191,7 @@ jobs:
node-version: latest
- run: sudo apt-get update
- run: cd Common && npm install
- name: Compile E2E
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd E2E && npm install && npm run compile && npm run dep-check
- run: cd E2E && npm install && npm run compile && npm run dep-check
compile-probe:
runs-on: ubuntu-latest
@@ -177,12 +203,69 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- name: Compile Probe
uses: nick-fields/retry@v3
- run: cd Probe && npm install && npm run compile && npm run dep-check
compile-probe-ingest:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
timeout_minutes: 30
max_attempts: 3
command: cd Probe && npm install && npm run compile && npm run dep-check
node-version: latest
- run: cd Common && npm install
- run: cd ProbeIngest && npm install && npm run compile && npm run dep-check
compile-server-monitor-ingest:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd ServerMonitorIngest && npm install && npm run compile && npm run dep-check
compile-open-telemetry-ingest:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd OpenTelemetryIngest && npm install && npm run compile && npm run dep-check
compile-incoming-request-ingest:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd IncomingRequestIngest && npm install && npm run compile && npm run dep-check
compile-fluent-ingest:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd FluentIngest && npm install && npm run compile && npm run dep-check
compile-status-page:
runs-on: ubuntu-latest
@@ -195,12 +278,7 @@ jobs:
node-version: latest
- run: cd Common && npm install
- name: Compile Status Page
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd App/FeatureSet/StatusPage && npm install && npm run compile && npm run dep-check
- run: cd StatusPage && npm install && npm run compile && npm run dep-check
compile-test-server:
runs-on: ubuntu-latest
@@ -212,60 +290,4 @@ jobs:
with:
node-version: latest
- run: cd Common && npm install
- name: Compile Test Server
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd TestServer && npm install && npm run compile && npm run dep-check
compile-mobile-app:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install && npm run compile
- name: Compile MobileApp
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd MobileApp && npm install && npm run compile
compile-ai-agent:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- name: Compile AIAgent
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd AIAgent && npm install && npm run compile && npm run dep-check
compile-cli:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- name: Compile CLI
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd CLI && npm install && npm run compile && npm run dep-check
- run: cd TestServer && npm install && npm run compile && npm run dep-check

View File

@@ -1,49 +0,0 @@
name: NPM Audit Fix
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
permissions:
contents: write
pull-requests: write
jobs:
npm-audit-fix:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Run npm audit fix across packages
run: npm run audit-fix
- name: Detect changes
id: changes
run: |
if git status --porcelain | grep .; then
echo "has_changes=true" >> $GITHUB_OUTPUT
else
echo "has_changes=false" >> $GITHUB_OUTPUT
fi
- name: Create pull request
if: steps.changes.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@v6
with:
commit-message: "chore: npm audit fix"
title: "chore: npm audit fix"
body: |
Automated npm audit fix run.
Workflow: ${{ github.workflow }}
Run ID: ${{ github.run_id }}
branch: chore/npm-audit-fix
delete-branch: true

View File

@@ -1,74 +0,0 @@
name: OpenAPI Spec Generation
on:
pull_request:
push:
branches-ignore:
- 'hotfix-*'
- 'release'
jobs:
generate-openapi-spec:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{ github.run_number }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: latest
cache: 'npm'
- name: Install Common dependencies
run: cd Common && npm install
- name: Install root dependencies
run: npm install
- name: Install Script dependencies
run: cd Scripts && npm install
- name: Generate OpenAPI specification
run: npm run generate-openapi-spec
- name: Check if OpenAPI spec was generated
run: |
if [ -f "./openapi.json" ]; then
echo "✅ OpenAPI spec file generated successfully"
echo "📄 File size: $(du -h ./openapi.json | cut -f1)"
echo "📊 Spec contains $(jq '.paths | length' ./openapi.json) API paths"
echo "🏷️ API version: $(jq -r '.info.version' ./openapi.json)"
echo "📝 API title: $(jq -r '.info.title' ./openapi.json)"
else
echo "❌ OpenAPI spec file was not generated"
exit 1
fi
- name: Validate OpenAPI spec format
run: |
# Check if the file is valid JSON
if jq empty ./openapi.json; then
echo "✅ OpenAPI spec is valid JSON"
else
echo "❌ OpenAPI spec is not valid JSON"
exit 1
fi
# Check if it has required OpenAPI fields
if jq -e '.openapi and .info and .paths' ./openapi.json > /dev/null; then
echo "✅ OpenAPI spec has required fields"
else
echo "❌ OpenAPI spec missing required fields (openapi, info, paths)"
exit 1
fi
- name: Upload OpenAPI spec as artifact
uses: actions/upload-artifact@v4
with:
name: openapi-spec
path: ./openapi.json
retention-days: 30

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,32 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
name: "OneUptime Reliability Copilot"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
# Run every day at midnight UTC
- cron: '0 0 * * *'
jobs:
analyze:
name: Analyze Code
runs-on: ubuntu-latest
steps:
# Run Reliability Copilot in Docker Container
- name: Run Copilot
run: |
docker run --rm \
-e ONEUPTIME_URL="https://test.oneuptime.com" \
-e ONEUPTIME_REPOSITORY_SECRET_KEY="${{ secrets.COPILOT_ONEUPTIME_REPOSITORY_SECRET_KEY }}" \
-e CODE_REPOSITORY_PASSWORD="${{ github.token }}" \
-e CODE_REPOSITORY_USERNAME="simlarsen" \
-e OPENAI_API_KEY="${{ secrets.OPENAI_API_KEY }}" \
--net=host oneuptime/copilot:test

View File

@@ -1,143 +0,0 @@
name: Terraform Provider E2E Tests
permissions:
contents: read
on:
pull_request:
push:
branches:
- main
- master
- develop
workflow_dispatch:
jobs:
terraform-e2e-tests:
runs-on: ubuntu-latest
timeout-minutes: 120
env:
CI_PIPELINE_ID: ${{ github.run_number }}
APP_TAG: latest
steps:
- name: Free Disk Space (Ubuntu)
uses: jlumbroso/free-disk-space@main
with:
tool-cache: true
android: true
dotnet: true
haskell: true
large-packages: true
docker-images: true
swap-storage: true
- name: Additional Disk Cleanup
run: |
echo "=== Initial disk space ==="
df -h
echo "=== Removing unnecessary tools and libraries ==="
# Remove Android SDK (if not already removed)
sudo rm -rf /usr/local/lib/android || true
# Remove .NET SDK and runtime
sudo rm -rf /usr/share/dotnet || true
sudo rm -rf /etc/skel/.dotnet || true
# Remove Haskell/GHC
sudo rm -rf /opt/ghc || true
sudo rm -rf /usr/local/.ghcup || true
# Remove CodeQL
sudo rm -rf /opt/hostedtoolcache/CodeQL || true
# Remove Boost
sudo rm -rf /usr/local/share/boost || true
# Remove Swift
sudo rm -rf /usr/share/swift || true
# Remove Julia
sudo rm -rf /usr/local/julia* || true
# Remove Rust (cargo/rustup)
sudo rm -rf /usr/share/rust || true
sudo rm -rf /home/runner/.rustup || true
sudo rm -rf /home/runner/.cargo || true
# Remove unnecessary hostedtoolcache items
sudo rm -rf /opt/hostedtoolcache/Python || true
sudo rm -rf /opt/hostedtoolcache/PyPy || true
sudo rm -rf /opt/hostedtoolcache/Ruby || true
sudo rm -rf /opt/hostedtoolcache/Java* || true
# Remove additional large directories
sudo rm -rf /usr/share/miniconda || true
sudo rm -rf /usr/local/graalvm || true
sudo rm -rf /usr/local/share/chromium || true
sudo rm -rf /usr/local/share/powershell || true
sudo rm -rf /usr/share/az_* || true
# Remove documentation
sudo rm -rf /usr/share/doc || true
sudo rm -rf /usr/share/man || true
# Remove unnecessary locales
sudo rm -rf /usr/share/locale || true
# Clean apt cache
sudo apt-get clean || true
sudo rm -rf /var/lib/apt/lists/* || true
sudo rm -rf /var/cache/apt/archives/* || true
# Clean tmp
sudo rm -rf /tmp/* || true
echo "=== Moving Docker data to /mnt for more space ==="
# Stop docker
sudo systemctl stop docker || true
# Move docker data directory to /mnt (which has ~70GB)
sudo mv /var/lib/docker /mnt/docker || true
sudo mkdir -p /var/lib/docker || true
sudo mount --bind /mnt/docker /var/lib/docker || true
# Restart docker
sudo systemctl start docker || true
echo "=== Final disk space ==="
df -h
echo "=== Docker info ==="
docker info | grep -E "Docker Root Dir|Storage Driver" || true
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: latest
cache: 'npm'
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 'stable'
cache: true
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: "1.6.0"
terraform_wrapper: false
- name: Run E2E Tests
uses: nick-fields/retry@v3
with:
timeout_minutes: 60
max_attempts: 3
command: |
chmod +x ./E2E/Terraform/e2e-tests/scripts/*.sh
./E2E/Terraform/e2e-tests/scripts/index.sh

View File

@@ -1,101 +0,0 @@
name: Terraform Provider Generation
on:
pull_request:
push:
branches:
- main
- master
- develop
workflow_dispatch: # Allow manual trigger
jobs:
generate-terraform-provider:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{ github.run_number }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: latest
cache: 'npm'
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: 'stable'
cache: true
- name: Install Common dependencies
run: cd Common && npm install
- name: Install root dependencies
run: npm install
- name: Install Script dependencies
run: cd Scripts && npm install
- name: Generate Terraform provider
run: npm run generate-terraform-provider
- name: Verify provider generation
run: |
PROVIDER_DIR="./Terraform"
# Check if provider directory was created
if [ ! -d "$PROVIDER_DIR" ]; then
echo "❌ Terraform provider directory not created"
exit 1
fi
echo "✅ Provider directory created: $PROVIDER_DIR"
# Count generated files
GO_FILES=$(find "$PROVIDER_DIR" -name "*.go" | wc -l)
echo "📊 Generated Go files: $GO_FILES"
if [ "$GO_FILES" -eq 0 ]; then
echo "❌ No Go files were generated"
exit 1
fi
# Check for essential files
if [ -f "$PROVIDER_DIR/go.mod" ]; then
echo "✅ Go module file created"
fi
if [ -f "$PROVIDER_DIR/README.md" ]; then
echo "✅ Documentation created"
fi
# Show directory structure for debugging
echo "📁 Provider directory structure:"
ls -la "$PROVIDER_DIR" || true
- name: Test Go build
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: |
PROVIDER_DIR="./Terraform"
if [ -d "$PROVIDER_DIR" ] && [ -f "$PROVIDER_DIR/go.mod" ]; then
cd "$PROVIDER_DIR"
echo "🔨 Testing Go build..."
go mod tidy
go build -v ./...
echo "✅ Go build successful"
else
echo "⚠️ Cannot test build - missing go.mod or provider directory"
fi
- name: Upload Terraform provider as artifact
uses: actions/upload-artifact@v4
with:
name: Terraform
path: ./Terraform/
retention-days: 30

File diff suppressed because it is too large Load Diff

View File

@@ -1,23 +0,0 @@
name: AIAgent Test
on:
pull_request:
push:
branches-ignore:
- 'hotfix-*'
- 'release'
jobs:
test:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd AIAgent && npm install && npm run test

View File

@@ -1,21 +0,0 @@
name: CLI Test
on:
pull_request:
push:
branches-ignore:
- 'hotfix-*' # excludes hotfix branches
- 'release'
jobs:
test:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Common && npm install
- run: cd CLI && npm install && npm run test

View File

@@ -0,0 +1,20 @@
name: Fluent Ingest Test
on:
pull_request:
push:
branches-ignore:
- 'hotfix-*' # excludes hotfix branches
- 'release'
jobs:
test:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd FluentIngest && npm install && npm run test

View File

@@ -0,0 +1,21 @@
name: Incoming Request Ingest Test
on:
pull_request:
push:
branches-ignore:
- 'hotfix-*' # excludes hotfix branches
- 'release'
jobs:
test:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd IncomingRequestIngest && npm install && npm run test

View File

@@ -1,39 +0,0 @@
name: MobileApp Test
on:
pull_request:
push:
branches-ignore:
- 'hotfix-*' # excludes hotfix branches
- 'release'
jobs:
expo-doctor:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd MobileApp && npm install
- name: Run Expo Doctor
run: cd MobileApp && npx expo-doctor@latest
expo-web-export:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd MobileApp && npm install
- name: Export Web Bundle
uses: nick-fields/retry@v3
with:
timeout_minutes: 30
max_attempts: 3
command: cd MobileApp && npx expo export --platform web

View File

@@ -0,0 +1,21 @@
name: OpenTelemetryIngest Test
on:
pull_request:
push:
branches-ignore:
- 'hotfix-*' # excludes hotfix branches
- 'release'
jobs:
test:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd OpenTelemetryIngest && npm install && npm run test

View File

@@ -0,0 +1,21 @@
name: ProbeIngest Test
on:
pull_request:
push:
branches-ignore:
- 'hotfix-*' # excludes hotfix branches
- 'release'
jobs:
test:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd ProbeIngest && npm install && npm run test

View File

@@ -29,3 +29,14 @@ jobs:
with:
node-version: latest
- run: cd Home && npm install && npm run test
test-worker:
runs-on: ubuntu-latest
env:
CI_PIPELINE_ID: ${{github.run_number}}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: latest
- run: cd Worker && npm install && npm run test

39
.gitignore vendored
View File

@@ -65,7 +65,7 @@ secret.env
# This file is autogenerated from the template
*/.env
**/Dockerfile
*/Dockerfile
config.env
config.env.tmp
config.env.temp
@@ -86,6 +86,9 @@ Backups/*.tar
.env
Haraka/dkim/keys/private_base64.txt
Haraka/dkim/keys/public_base64.txt
.eslintcache
HelmChart/Values/*.values.yaml
@@ -109,37 +112,3 @@ App/greenlock/greenlock.d/config.json
App/greenlock/greenlock.d/config.json.bak
Examples/otel-dotnet/bin/Debug/net6.0/Grpc.Core.Api.dll.txt
InfrastructureAgent/oneuptime-infrastructure-agent
# ESLint cache
.eslintcache*
# Terraform generated files
openapi.json
Terraform/terraform-provider-oneuptime/**
Terraform/openapi.json
TerraformTest/**
terraform-provider-example/**
# MCP Server
MCP/build/
MCP/.env
MCP/node_modules
Dashboard/public/sw.js
App/Dashboard/public/sw.js
.claude/settings.local.json
Common/.claude/settings.local.json
E2E/Terraform/e2e-tests/test-env.sh
# Terraform state and plan files
*.tfplan
tfplan
terraform.tfstate
terraform.tfstate.backup
.terraform/
.terraform.lock.hcl
.claude/worktrees/**
App/FeatureSet/Dashboard/public/sw.js
RFP/

15
.oneuptime/README.md Normal file
View File

@@ -0,0 +1,15 @@
## OneUptime Copilot
This folder contains the configuration files for the OneUptime Copilot. The Copilot is a tool that automatically improves your code. It can fix issues, improve code quality, and help you ship faster.
This folder has the following structure:
- `config.js`: The configuration file for the Copilot. You can customize the Copilot's behavior by changing this file.
- `scripts`: A folder containing scripts that the Copilot runs. These are hooks that run at different stages of the Copilot's process.
- `on-after-clone.sh`: A script that runs after the Copilot clones your repository.
- `on-before-code-change.sh`: A script that runs before the Copilot makes changes to your code.
- `on-after-code-change.sh`: A script that runs after the Copilot makes changes to your code.
- `on-before-commit.sh`: A script that runs before the Copilot commits changes to your repository.
- `on-after-commit.sh`: A script that runs after the Copilot commits changes to your repository.

10
.oneuptime/config.js Normal file
View File

@@ -0,0 +1,10 @@
// This is the configuration file for the oneuptime copilot.
const getCopilotConfig = () => {
return {
// The version of the schema for this configuration file.
schemaVersion: '1.0',
}
}
export default getCopilotConfig;

View File

@@ -0,0 +1,16 @@
# Description: Copilot clones your repository and to improve your code.
# This scirpt runs after the clone process is completed.
# Some of the common tasks you can do here are:
# 1. Install dependencies
# 2. Run linting
# 3. Run tests
# 4. Run build
# 5. Run any other command that you want to run after the clone process is completed.
# If this script fails, copilot will not proceed with the next steps to improve your code.
# This step is to ensure that the code is in a good state before we start improving it.
# If you want to skip this script, you can keep this file empty.
# It's highly recommended to run linting and tests in this script to ensure the code is in a good state.
# This scirpt will run on ubuntu machine. So, make sure the commands you run are compatible with ubuntu.
npm install
npm run lint

View File

@@ -0,0 +1,13 @@
# Description: Copilot will run this script after we make improvements to your code and write it to disk.
# Some of the common tasks you can do here are:
# 1. Run linting
# 2. Run tests
# 3. Run build
# 4. Run any other command that you want to run after the code is changed.
# If this script fails, copilot will not commit the changes to your repository.
# This step is to ensure that the code is in a good state before we commit the changes.
# If you want to skip this script, you can keep this file empty.
# It's highly recommended to run linting and tests in this script to ensure the code is in a good state.
# This scirpt will run on ubuntu machine. So, make sure the commands you run are compatible with ubuntu.
npm run fix

View File

@@ -0,0 +1 @@
# Description: Copilot will run this script after the commit process is completed.

View File

@@ -0,0 +1,9 @@
# Description: Copilot will run this script before we make changes to your code.
# Some of the common tasks you can do here are:
# 1. Install dependencies
# 2. Run any other command that you want to run before the code is changed.
# If this script fails, copilot will not make any changes to the code.
# This step is to ensure that the code is in a good state before we start making changes.
# If you want to skip this script, you can keep this file empty.
# It's highly recommended to run things like installing dependencies in this script.
# This scirpt will run on ubuntu machine. So, make sure the commands you run are compatible with ubuntu.

View File

@@ -0,0 +1 @@
# Description: Copilot will run this script before we commit the changes to your repository.

View File

@@ -49,4 +49,5 @@ LICENSE
marketing/*/*
licenses/*
certifications/*
ApiReference/public/assets/*
JavaScriptSDK/src/cli/server-monitor/out/scripts/prettify/*

102
.vscode/launch.json vendored
View File

@@ -19,6 +19,20 @@
}
],
"configurations": [
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/TestServer",
"name": "Copilot: Debug with Docker",
"port": 9985,
"remoteRoot": "/usr/src/app",
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"restart": true,
"autoAttachChildProcesses": true
},
{
"name": "Debug Infrastructure Agent",
"type": "go",
@@ -105,6 +119,20 @@
"restart": true,
"autoAttachChildProcesses": true
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/APIReference",
"name": "API Reference: Debug with Docker",
"port": 8737,
"remoteRoot": "/usr/src/app",
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"restart": true,
"autoAttachChildProcesses": true
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/TestServer",
@@ -135,8 +163,50 @@
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/Telemetry",
"name": "Telemetry: Debug with Docker",
"localRoot": "${workspaceFolder}/ProbeIngest",
"name": "ProbeIngest: Debug with Docker",
"port": 9932,
"remoteRoot": "/usr/src/app",
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"restart": true,
"autoAttachChildProcesses": true
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/ServerMonitorIngest",
"name": "ServerMonitorIngest: Debug with Docker",
"port": 9941,
"remoteRoot": "/usr/src/app",
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"restart": true,
"autoAttachChildProcesses": true
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/IncomingRequestIngest",
"name": "IncomingRequestIngest: Debug with Docker",
"port": 9933,
"remoteRoot": "/usr/src/app",
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"restart": true,
"autoAttachChildProcesses": true
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/OpenTelemetryIngest",
"name": "OpenTelemetryIngest: Debug with Docker",
"port": 9938,
"remoteRoot": "/usr/src/app",
"request": "attach",
@@ -147,6 +217,34 @@
"restart": true,
"autoAttachChildProcesses": true
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/FluentIngest",
"name": "Fluent Ingest: Debug with Docker",
"port": 9937,
"remoteRoot": "/usr/src/app",
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"restart": true,
"autoAttachChildProcesses": true
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/IsolatedVM",
"name": "Isolated VM: Debug with Docker",
"port": 9974,
"remoteRoot": "/usr/src/app",
"request": "attach",
"skipFiles": [
"<node_internals>/**"
],
"type": "node",
"restart": true,
"autoAttachChildProcesses": true
},
{
"address": "127.0.0.1",
"localRoot": "${workspaceFolder}/Workflow",

View File

@@ -1 +0,0 @@
This is a local development server hosted at HOST env variable (please read config.env file). This project is hosted on docker compose for local development. When you make any changes to the codebase the container hot-reloads. Please make sure you wait for it to restart to test. If you need access to the database during development, credentials are in config.env file.

View File

@@ -1,95 +0,0 @@
import Express, {
ExpressRequest,
ExpressResponse,
ExpressRouter,
NextFunction,
} from "Common/Server/Utils/Express";
import Response from "Common/Server/Utils/Response";
import { ONEUPTIME_URL } from "../Config";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import HTTPMethod from "Common/Types/API/HTTPMethod";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import URL from "Common/Types/API/URL";
import { JSONObject } from "Common/Types/JSON";
import API from "Common/Utils/API";
import logger from "Common/Server/Utils/Logger";
import AIAgentAPIRequest from "../Utils/AIAgentAPIRequest";
const router: ExpressRouter = Express.getRouter();
/*
* Metrics endpoint for Keda autoscaling
* Returns the number of pending AI agent tasks
*/
router.get(
"/queue-size",
async (
req: ExpressRequest,
res: ExpressResponse,
next: NextFunction,
): Promise<void> => {
try {
/*
* Get the pending task count from OneUptime API
* This is the correct metric - the number of tasks waiting to be processed
*/
const pendingTaskCountUrl: URL = URL.fromString(
ONEUPTIME_URL.toString(),
).addRoute("/api/ai-agent-task/get-pending-task-count");
logger.debug(
"Fetching pending task count from OneUptime API for KEDA scaling",
);
// Use AI Agent authentication (AI Agent key and AI Agent ID)
const requestBody: JSONObject = AIAgentAPIRequest.getDefaultRequestBody();
const result: HTTPResponse<JSONObject> | HTTPErrorResponse =
await API.fetch<JSONObject>({
method: HTTPMethod.POST,
url: pendingTaskCountUrl,
data: requestBody,
headers: {},
});
if (result instanceof HTTPErrorResponse) {
logger.error("Error fetching pending task count from OneUptime API");
logger.error(result);
throw result;
}
logger.debug(
"Pending task count fetched successfully from OneUptime API",
);
logger.debug(result.data);
// Extract count from the response - this is the number of tasks pending to be processed
let queueSize: number = (result.data["count"] as number) || 0;
// if string then convert to number
if (typeof queueSize === "string") {
const parsedQueueSize: number = parseInt(queueSize, 10);
if (!isNaN(parsedQueueSize)) {
queueSize = parsedQueueSize;
} else {
logger.warn(
"Pending task count is not a valid number, defaulting to 0",
);
queueSize = 0;
}
}
logger.debug(`Pending task count for KEDA: ${queueSize}`);
return Response.sendJsonObjectResponse(req, res, {
queueSize: queueSize,
});
} catch (err) {
logger.error("Error in metrics queue-size endpoint");
logger.error(err);
return next(err);
}
},
);
export default router;

View File

@@ -1,103 +0,0 @@
import {
CodeAgent,
CodeAgentType,
getCodeAgentDisplayName,
} from "./CodeAgentInterface";
import OpenCodeAgent from "./OpenCodeAgent";
import logger from "Common/Server/Utils/Logger";
// Factory class to create code agents
export default class CodeAgentFactory {
// Default agent type to use
private static defaultAgentType: CodeAgentType = CodeAgentType.OpenCode;
// Create an agent of the specified type
public static createAgent(type: CodeAgentType): CodeAgent {
logger.debug(`Creating code agent: ${getCodeAgentDisplayName(type)}`);
switch (type) {
case CodeAgentType.OpenCode:
return new OpenCodeAgent();
/*
* Future agents can be added here:
* case CodeAgentType.Goose:
* return new GooseAgent();
* case CodeAgentType.ClaudeCode:
* return new ClaudeCodeAgent();
*/
default:
throw new Error(`Unknown code agent type: ${type}`);
}
}
// Create the default agent
public static createDefaultAgent(): CodeAgent {
return this.createAgent(this.defaultAgentType);
}
// Set the default agent type
public static setDefaultAgentType(type: CodeAgentType): void {
this.defaultAgentType = type;
}
// Get the default agent type
public static getDefaultAgentType(): CodeAgentType {
return this.defaultAgentType;
}
// Get all available agent types
public static getAvailableAgentTypes(): Array<CodeAgentType> {
return Object.values(CodeAgentType);
}
// Check if an agent type is available on the system
public static async isAgentAvailable(type: CodeAgentType): Promise<boolean> {
try {
const agent: CodeAgent = this.createAgent(type);
return await agent.isAvailable();
} catch (error) {
logger.error(`Error checking agent availability for ${type}:`);
logger.error(error);
return false;
}
}
// Get the first available agent
public static async getFirstAvailableAgent(): Promise<CodeAgent | null> {
for (const type of this.getAvailableAgentTypes()) {
if (await this.isAgentAvailable(type)) {
return this.createAgent(type);
}
}
return null;
}
/*
* Create agent with fallback
* Tries to create the specified type, falls back to first available
*/
public static async createAgentWithFallback(
preferredType?: CodeAgentType,
): Promise<CodeAgent> {
// If preferred type is specified and available, use it
if (preferredType && (await this.isAgentAvailable(preferredType))) {
return this.createAgent(preferredType);
}
// Try the default type
if (await this.isAgentAvailable(this.defaultAgentType)) {
return this.createAgent(this.defaultAgentType);
}
// Fall back to first available
const agent: CodeAgent | null = await this.getFirstAvailableAgent();
if (!agent) {
throw new Error("No code agents are available on this system");
}
return agent;
}
}

View File

@@ -1,94 +0,0 @@
import LlmType from "Common/Types/LLM/LlmType";
import TaskLogger from "../Utils/TaskLogger";
// Configuration for the LLM provider
export interface CodeAgentLLMConfig {
llmType: LlmType;
apiKey?: string;
baseUrl?: string;
modelName?: string;
}
// The task to be executed by the code agent
export interface CodeAgentTask {
workingDirectory: string;
prompt: string;
context?: string;
timeoutMs?: number;
servicePath?: string; // Path within the repo where the service code lives
}
// Result from the code agent execution
export interface CodeAgentResult {
success: boolean;
filesModified: Array<string>;
summary: string;
logs: Array<string>;
error?: string;
exitCode?: number;
}
// Progress event from the code agent
export interface CodeAgentProgressEvent {
type: "stdout" | "stderr" | "status";
message: string;
timestamp: Date;
}
// Callback type for progress events
export type CodeAgentProgressCallback = (
event: CodeAgentProgressEvent,
) => void | Promise<void>;
/*
* Abstract interface for code agents
* This allows us to support multiple agents (OpenCode, Goose, Claude Code, etc.)
*/
export interface CodeAgent {
// Name of the agent (e.g., "OpenCode", "Goose", "ClaudeCode")
readonly name: string;
// Initialize the agent with LLM configuration
initialize(config: CodeAgentLLMConfig, logger?: TaskLogger): Promise<void>;
// Execute a task and return the result
executeTask(task: CodeAgentTask): Promise<CodeAgentResult>;
// Set a callback for progress events (streaming output)
onProgress(callback: CodeAgentProgressCallback): void;
// Check if the agent is available on the system
isAvailable(): Promise<boolean>;
// Abort the current task execution
abort(): Promise<void>;
// Clean up any resources used by the agent
cleanup(): Promise<void>;
}
// Enum for supported code agent types
export enum CodeAgentType {
OpenCode = "OpenCode",
/*
* Future agents:
* Goose = "Goose",
* ClaudeCode = "ClaudeCode",
* Aider = "Aider",
*/
}
// Helper function to get display name for agent type
export function getCodeAgentDisplayName(type: CodeAgentType): string {
switch (type) {
case CodeAgentType.OpenCode:
return "OpenCode AI";
default:
return type;
}
}
// Helper function to check if an agent type is valid
export function isValidCodeAgentType(type: string): type is CodeAgentType {
return Object.values(CodeAgentType).includes(type as CodeAgentType);
}

View File

@@ -1,15 +0,0 @@
// Export all code agent related types and classes
export {
CodeAgent,
CodeAgentLLMConfig,
CodeAgentTask,
CodeAgentResult,
CodeAgentProgressEvent,
CodeAgentProgressCallback,
CodeAgentType,
getCodeAgentDisplayName,
isValidCodeAgentType,
} from "./CodeAgentInterface";
export { default as CodeAgentFactory } from "./CodeAgentFactory";
export { default as OpenCodeAgent } from "./OpenCodeAgent";

View File

@@ -1,562 +0,0 @@
import {
CodeAgent,
CodeAgentLLMConfig,
CodeAgentTask,
CodeAgentResult,
CodeAgentProgressCallback,
CodeAgentProgressEvent,
} from "./CodeAgentInterface";
import TaskLogger from "../Utils/TaskLogger";
import Execute from "Common/Server/Utils/Execute";
import LocalFile from "Common/Server/Utils/LocalFile";
import LlmType from "Common/Types/LLM/LlmType";
import logger from "Common/Server/Utils/Logger";
import path from "path";
import { ChildProcess, spawn } from "child_process";
import BadDataException from "Common/Types/Exception/BadDataException";
// OpenCode configuration file structure
interface OpenCodeConfig {
provider?: Record<string, unknown>;
model?: string;
small_model?: string;
disabled_providers?: Array<string>;
enabled_providers?: Array<string>;
}
export default class OpenCodeAgent implements CodeAgent {
public readonly name: string = "OpenCode";
private config: CodeAgentLLMConfig | null = null;
private taskLogger: TaskLogger | null = null;
private progressCallback: CodeAgentProgressCallback | null = null;
private currentProcess: ChildProcess | null = null;
private aborted: boolean = false;
// Track original opencode.json content for restoration
private originalOpenCodeConfig: string | null = null;
private openCodeConfigPath: string | null = null;
// Default timeout: 30 minutes
private static readonly DEFAULT_TIMEOUT_MS: number = 30 * 60 * 1000;
public async initialize(
config: CodeAgentLLMConfig,
taskLogger?: TaskLogger,
): Promise<void> {
this.config = config;
if (taskLogger) {
this.taskLogger = taskLogger;
}
await this.log(`Initializing ${this.name} with ${config.llmType} provider`);
}
public async executeTask(task: CodeAgentTask): Promise<CodeAgentResult> {
if (!this.config) {
return this.createErrorResult(
"Agent not initialized. Call initialize() first.",
);
}
this.aborted = false;
const logs: Array<string> = [];
const timeoutMs: number =
task.timeoutMs || OpenCodeAgent.DEFAULT_TIMEOUT_MS;
try {
await this.log(`Executing task in directory: ${task.workingDirectory}`);
// Create OpenCode config file in the working directory
await this.createOpenCodeConfig(task.workingDirectory);
// Build the prompt
const fullPrompt: string = this.buildFullPrompt(task);
await this.log("Starting OpenCode execution...");
logs.push(`Prompt: ${fullPrompt.substring(0, 500)}...`);
// Execute OpenCode
const output: string = await this.runOpenCode(
task.workingDirectory,
fullPrompt,
timeoutMs,
(event: CodeAgentProgressEvent) => {
logs.push(`[${event.type}] ${event.message}`);
if (this.progressCallback) {
this.progressCallback(event);
}
},
);
logs.push(
`Output: ${output.substring(0, 1000)}${output.length > 1000 ? "..." : ""}`,
);
if (this.aborted) {
return this.createErrorResult("Task was aborted", logs);
}
// Check for modified files
const modifiedFiles: Array<string> = await this.getModifiedFiles(
task.workingDirectory,
);
// Restore or delete opencode.json before returning
await this.restoreOpenCodeConfig();
await this.log(
`OpenCode completed. ${modifiedFiles.length} files modified.`,
);
return {
success: true,
filesModified: modifiedFiles,
summary: this.extractSummary(output),
logs,
exitCode: 0,
};
} catch (error) {
const errorMessage: string =
error instanceof Error ? error.message : String(error);
// Restore or delete opencode.json on error
await this.restoreOpenCodeConfig();
await this.log(`OpenCode execution failed: ${errorMessage}`);
logs.push(`Error: ${errorMessage}`);
return this.createErrorResult(errorMessage, logs);
}
}
public onProgress(callback: CodeAgentProgressCallback): void {
this.progressCallback = callback;
}
public async isAvailable(): Promise<boolean> {
try {
const result: string = await Execute.executeCommandFile({
command: "opencode",
args: ["--version"],
cwd: process.cwd(),
});
logger.debug(`OpenCode version check: ${result}`);
return true;
} catch (error) {
logger.debug("OpenCode is not available:");
logger.debug(error);
return false;
}
}
public async abort(): Promise<void> {
this.aborted = true;
if (this.currentProcess) {
this.currentProcess.kill("SIGTERM");
this.currentProcess = null;
}
await this.log("OpenCode execution aborted");
}
public async cleanup(): Promise<void> {
if (this.currentProcess) {
this.currentProcess.kill("SIGTERM");
this.currentProcess = null;
}
this.config = null;
this.progressCallback = null;
}
// Create OpenCode configuration file in the workspace
private async createOpenCodeConfig(workingDirectory: string): Promise<void> {
if (!this.config) {
throw new Error("Config not initialized");
}
const configPath: string = path.join(workingDirectory, "opencode.json");
this.openCodeConfigPath = configPath;
// Check if opencode.json already exists and backup its content
try {
const existingContent: string = await LocalFile.read(configPath);
this.originalOpenCodeConfig = existingContent;
await this.log("Backed up existing opencode.json from repository");
} catch {
// File doesn't exist, which is the normal case
this.originalOpenCodeConfig = null;
}
const openCodeConfig: OpenCodeConfig = {
model: this.getModelString(),
small_model: this.getSmallModelString(),
};
// Set enabled providers based on LLM type
if (this.config.llmType === LlmType.Anthropic) {
openCodeConfig.enabled_providers = ["anthropic"];
} else if (this.config.llmType === LlmType.OpenAI) {
openCodeConfig.enabled_providers = ["openai"];
}
await LocalFile.write(configPath, JSON.stringify(openCodeConfig, null, 2));
await this.log(`Created OpenCode config at ${configPath}`);
}
// Restore or delete opencode.json after execution
private async restoreOpenCodeConfig(): Promise<void> {
if (!this.openCodeConfigPath) {
return;
}
try {
if (this.originalOpenCodeConfig !== null) {
// Restore the original file content
await LocalFile.write(
this.openCodeConfigPath,
this.originalOpenCodeConfig,
);
await this.log("Restored original opencode.json from repository");
} else {
// Delete the file we created
await LocalFile.deleteFile(this.openCodeConfigPath);
await this.log("Deleted generated opencode.json config file");
}
} catch (error) {
// Log but don't throw - cleanup failure shouldn't fail the task
logger.warn(`Failed to restore/delete opencode.json: ${error}`);
}
// Reset the tracking variables
this.openCodeConfigPath = null;
this.originalOpenCodeConfig = null;
}
// Get the model string in OpenCode format (provider/model)
private getModelString(): string {
if (!this.config) {
throw new Error("Config not initialized");
}
const provider: string = this.getProviderName();
const model: string = this.config.modelName || this.getDefaultModel();
return `${provider}/${model}`;
}
// Get the small model string for quick operations
private getSmallModelString(): string {
if (!this.config) {
throw new Error("Config not initialized");
}
const provider: string = this.getProviderName();
const smallModel: string = this.getDefaultSmallModel();
return `${provider}/${smallModel}`;
}
// Get provider name for OpenCode config
private getProviderName(): string {
if (!this.config) {
return "anthropic";
}
switch (this.config.llmType) {
case LlmType.Anthropic:
return "anthropic";
case LlmType.OpenAI:
return "openai";
case LlmType.Ollama:
return "ollama";
default:
throw new BadDataException("Unsupported LLM type for OpenCode agent");
}
}
// Get default model based on provider
private getDefaultModel(): string {
if (!this.config) {
return "claude-sonnet-4-20250514";
}
switch (this.config.llmType) {
case LlmType.Anthropic:
return "claude-sonnet-4-20250514";
case LlmType.OpenAI:
return "gpt-4o";
case LlmType.Ollama:
return "llama2";
default:
throw new BadDataException("Unsupported LLM type for OpenCode agent");
}
}
// Get default small model for quick operations
private getDefaultSmallModel(): string {
if (!this.config) {
return "claude-haiku-4-20250514";
}
switch (this.config.llmType) {
case LlmType.Anthropic:
return "claude-haiku-4-20250514";
case LlmType.OpenAI:
return "gpt-4o-mini";
case LlmType.Ollama:
return "llama2";
default:
throw new BadDataException("Unsupported LLM type for OpenCode agent");
}
}
// Build the full prompt including context
private buildFullPrompt(task: CodeAgentTask): string {
let prompt: string = task.prompt;
if (task.context) {
prompt = `${task.context}\n\n${prompt}`;
}
if (task.servicePath) {
prompt = `The service code is located at: ${task.servicePath}\n\n${prompt}`;
}
return prompt;
}
// Run OpenCode in non-interactive mode
private async runOpenCode(
workingDirectory: string,
prompt: string,
timeoutMs: number,
onOutput: (event: CodeAgentProgressEvent) => void,
): Promise<string> {
return new Promise(
(resolve: (value: string) => void, reject: (reason: Error) => void) => {
if (!this.config) {
reject(new Error("Config not initialized"));
return;
}
// Set environment variables for API key
const env: NodeJS.ProcessEnv = { ...process.env };
if (this.config.apiKey) {
switch (this.config.llmType) {
case LlmType.Anthropic:
env["ANTHROPIC_API_KEY"] = this.config.apiKey;
break;
case LlmType.OpenAI:
env["OPENAI_API_KEY"] = this.config.apiKey;
break;
case LlmType.Ollama:
if (this.config.baseUrl) {
env["OLLAMA_HOST"] = this.config.baseUrl;
}
break;
}
}
/*
* Use CLI mode flags to ensure output goes to stdout/stderr instead of TUI
* Pass prompt via stdin using "-" to avoid command line argument issues with long prompts
*/
const args: Array<string> = [
"run",
"--print-logs",
"--log-level",
"DEBUG",
"--format",
"default",
"-", // Read prompt from stdin
];
logger.debug(
`Running: opencode ${args.join(" ")} (prompt via stdin, ${prompt.length} chars)`,
);
const child: ChildProcess = spawn("opencode", args, {
cwd: workingDirectory,
env,
stdio: ["pipe", "pipe", "pipe"],
});
this.currentProcess = child;
// Write prompt to stdin and close it
if (child.stdin) {
child.stdin.write(prompt);
child.stdin.end();
}
let stdout: string = "";
let stderr: string = "";
// Set timeout
const timeout: ReturnType<typeof setTimeout> = setTimeout(() => {
if (child.pid) {
child.kill("SIGTERM");
reject(
new Error(
`OpenCode execution timed out after ${timeoutMs / 1000} seconds`,
),
);
}
}, timeoutMs);
child.stdout?.on("data", (data: Buffer) => {
const text: string = data.toString();
stdout += text;
// Stream to console immediately
const trimmedText: string = text.trim();
if (trimmedText) {
logger.info(`[OpenCode stdout] ${trimmedText}`);
// Stream to task logger for server-side logging
if (this.taskLogger) {
this.taskLogger
.info(`[OpenCode] ${trimmedText}`)
.catch((err: Error) => {
logger.error(`Failed to log OpenCode output: ${err.message}`);
});
}
}
onOutput({
type: "stdout",
message: trimmedText,
timestamp: new Date(),
});
});
child.stderr?.on("data", (data: Buffer) => {
const text: string = data.toString();
stderr += text;
// Stream to console immediately
const trimmedText: string = text.trim();
if (trimmedText) {
logger.warn(`[OpenCode stderr] ${trimmedText}`);
// Stream to task logger for server-side logging
if (this.taskLogger) {
this.taskLogger
.warning(`[OpenCode stderr] ${trimmedText}`)
.catch((err: Error) => {
logger.error(`Failed to log OpenCode stderr: ${err.message}`);
});
}
}
onOutput({
type: "stderr",
message: trimmedText,
timestamp: new Date(),
});
});
child.on("close", (code: number | null) => {
clearTimeout(timeout);
this.currentProcess = null;
if (this.aborted) {
reject(new Error("Execution aborted"));
return;
}
if (code === 0 || code === null) {
resolve(stdout);
} else {
reject(
new Error(
`OpenCode exited with code ${code}. stderr: ${stderr.substring(0, 500)}`,
),
);
}
});
child.on("error", (error: Error) => {
clearTimeout(timeout);
this.currentProcess = null;
reject(error);
});
},
);
}
// Get list of modified files using git
private async getModifiedFiles(
workingDirectory: string,
): Promise<Array<string>> {
try {
const result: string = await Execute.executeCommandFile({
command: "git",
args: ["status", "--porcelain"],
cwd: workingDirectory,
});
if (!result.trim()) {
return [];
}
return result
.split("\n")
.filter((line: string) => {
return line.trim().length > 0;
})
.map((line: string) => {
// Git status format: "XY filename"
return line.substring(3).trim();
});
} catch (error) {
logger.error("Error getting modified files:");
logger.error(error);
return [];
}
}
// Extract summary from OpenCode output
private extractSummary(output: string): string {
// Try to extract a meaningful summary from the output
const lines: Array<string> = output.split("\n").filter((line: string) => {
return line.trim().length > 0;
});
// Return last few meaningful lines as summary
const summaryLines: Array<string> = lines.slice(-5);
return summaryLines.join("\n") || "No summary available";
}
// Create error result helper
private createErrorResult(
errorMessage: string,
logs: Array<string> = [],
): CodeAgentResult {
return {
success: false,
filesModified: [],
summary: "",
logs,
error: errorMessage,
exitCode: 1,
};
}
// Logging helper
private async log(message: string): Promise<void> {
if (this.taskLogger) {
await this.taskLogger.info(`[${this.name}] ${message}`);
} else {
logger.debug(`[${this.name}] ${message}`);
}
}
}

View File

@@ -1,36 +0,0 @@
import URL from "Common/Types/API/URL";
import ObjectID from "Common/Types/ObjectID";
import logger from "Common/Server/Utils/Logger";
import Port from "Common/Types/Port";
if (!process.env["ONEUPTIME_URL"]) {
logger.error("ONEUPTIME_URL is not set");
process.exit();
}
export const ONEUPTIME_URL: URL = URL.fromString(
process.env["ONEUPTIME_URL"] || "https://oneuptime.com",
);
export const AI_AGENT_ID: ObjectID | null = process.env["AI_AGENT_ID"]
? new ObjectID(process.env["AI_AGENT_ID"])
: null;
if (!process.env["AI_AGENT_KEY"]) {
logger.error("AI_AGENT_KEY is not set");
process.exit();
}
export const AI_AGENT_KEY: string = process.env["AI_AGENT_KEY"];
export const AI_AGENT_NAME: string | null =
process.env["AI_AGENT_NAME"] || null;
export const AI_AGENT_DESCRIPTION: string | null =
process.env["AI_AGENT_DESCRIPTION"] || null;
export const HOSTNAME: string = process.env["HOSTNAME"] || "localhost";
export const PORT: Port = new Port(
process.env["PORT"] ? parseInt(process.env["PORT"]) : 3875,
);

View File

@@ -1,82 +0,0 @@
#
# OneUptime-AIAgent Dockerfile
#
# Pull base image nodejs image.
FROM public.ecr.aws/docker/library/node:24.9
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
RUN npm config set fetch-retries 5
RUN npm config set fetch-retry-mintimeout 20000
RUN npm config set fetch-retry-maxtimeout 60000
ARG GIT_SHA
ARG APP_VERSION
ARG IS_ENTERPRISE_EDITION=false
ENV GIT_SHA=${GIT_SHA}
ENV APP_VERSION=${APP_VERSION}
ENV IS_ENTERPRISE_EDITION=${IS_ENTERPRISE_EDITION}
ENV NODE_OPTIONS="--use-openssl-ca"
## Add Intermediate Certs
COPY ./SslCertificates /usr/local/share/ca-certificates
RUN update-ca-certificates
# IF APP_VERSION is not set, set it to 1.0.0
RUN if [ -z "$APP_VERSION" ]; then export APP_VERSION=1.0.0; fi
RUN apt-get update
# Install bash.
RUN apt-get install bash -y && apt-get install curl -y
# Install OpenCode AI coding assistant
RUN curl -fsSL https://opencode.ai/install | bash
# Add OpenCode to PATH (installed to $HOME/.opencode/bin by default)
ENV PATH="/root/.opencode/bin:${PATH}"
#Use bash shell by default
SHELL ["/bin/bash", "-c"]
RUN mkdir -p /usr/src
WORKDIR /usr/src/Common
COPY ./Common/package*.json /usr/src/Common/
# Set version in ./Common/package.json to the APP_VERSION
RUN sed -i "s/\"version\": \".*\"/\"version\": \"$APP_VERSION\"/g" /usr/src/Common/package.json
RUN npm install
COPY ./Common /usr/src/Common
ENV PRODUCTION=true
WORKDIR /usr/src/app
# Install app dependencies
COPY ./AIAgent/package*.json /usr/src/app/
# Set version in ./AIAgent/package.json to the APP_VERSION
RUN sed -i "s/\"version\": \".*\"/\"version\": \"$APP_VERSION\"/g" /usr/src/app/package.json
RUN npm install
# Expose ports.
# - 3875: OneUptime-AIAgent
EXPOSE 3875
{{ if eq .Env.ENVIRONMENT "development" }}
#Run the app
CMD [ "npm", "run", "dev" ]
{{ else }}
# Copy app source
COPY ./AIAgent /usr/src/app
# Bundle app source
RUN npm run compile
# Set permission to write logs and cache in case container run as non root
RUN chown -R 1000:1000 "/tmp/npm" && chmod -R 2777 "/tmp/npm"
#Run the app
CMD [ "npm", "start" ]
{{ end }}

View File

@@ -1,90 +0,0 @@
import { PORT } from "./Config";
import AliveJob from "./Jobs/Alive";
import startTaskProcessingLoop from "./Jobs/ProcessScheduledTasks";
import Register from "./Services/Register";
import MetricsAPI from "./API/Metrics";
import {
getTaskHandlerRegistry,
FixExceptionTaskHandler,
} from "./TaskHandlers/Index";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import logger from "Common/Server/Utils/Logger";
import App from "Common/Server/Utils/StartServer";
import Telemetry from "Common/Server/Utils/Telemetry";
import Profiling from "Common/Server/Utils/Profiling";
import Express, { ExpressApplication } from "Common/Server/Utils/Express";
import "ejs";
const APP_NAME: string = "ai-agent";
const init: PromiseVoidFunction = async (): Promise<void> => {
try {
// Initialize telemetry
Telemetry.init({
serviceName: APP_NAME,
});
// Initialize profiling (opt-in via ENABLE_PROFILING env var)
Profiling.init({
serviceName: APP_NAME,
});
logger.info("AI Agent Service - Starting...");
// init the app
await App.init({
appName: APP_NAME,
port: PORT,
isFrontendApp: false,
statusOptions: {
liveCheck: async () => {},
readyCheck: async () => {},
},
});
// Add metrics API routes for KEDA autoscaling
const app: ExpressApplication = Express.getExpressApp();
app.use("/metrics", MetricsAPI);
// add default routes
await App.addDefaultRoutes();
try {
// Register this AI Agent.
await Register.registerAIAgent();
logger.debug("AI Agent registered");
AliveJob();
// Register task handlers
logger.debug("Registering task handlers...");
const taskHandlerRegistry: ReturnType<typeof getTaskHandlerRegistry> =
getTaskHandlerRegistry();
taskHandlerRegistry.register(new FixExceptionTaskHandler());
logger.debug(
`Registered ${taskHandlerRegistry.getHandlerCount()} task handler(s): ${taskHandlerRegistry.getRegisteredTaskTypes().join(", ")}`,
);
// Start task processing loop (runs in background)
startTaskProcessingLoop().catch((err: Error) => {
logger.error("Task processing loop failed:");
logger.error(err);
});
} catch (err) {
logger.error("Register AI Agent failed");
logger.error(err);
throw err;
}
} catch (err) {
logger.error("App Init Failed:");
logger.error(err);
throw err;
}
};
init().catch((err: Error) => {
logger.error(err);
logger.error("Exiting node process");
process.exit(1);
});

View File

@@ -1,56 +0,0 @@
import { ONEUPTIME_URL } from "../Config";
import Register from "../Services/Register";
import AIAgentAPIRequest from "../Utils/AIAgentAPIRequest";
import URL from "Common/Types/API/URL";
import API from "Common/Utils/API";
import { EVERY_MINUTE } from "Common/Utils/CronTime";
import LocalCache from "Common/Server/Infrastructure/LocalCache";
import BasicCron from "Common/Server/Utils/BasicCron";
import logger from "Common/Server/Utils/Logger";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import { JSONObject } from "Common/Types/JSON";
const InitJob: VoidFunction = (): void => {
BasicCron({
jobName: "AIAgent:Alive",
options: {
schedule: EVERY_MINUTE,
runOnStartup: false,
},
runFunction: async () => {
logger.debug("Checking if AI Agent is alive...");
const aiAgentId: string | undefined = LocalCache.getString(
"AI_AGENT",
"AI_AGENT_ID",
);
if (!aiAgentId) {
logger.warn(
"AI Agent is not registered yet. Skipping alive check. Trying to register AI Agent again...",
);
await Register.registerAIAgent();
return;
}
logger.debug("AI Agent ID: " + aiAgentId.toString());
const aliveUrl: URL = URL.fromString(ONEUPTIME_URL.toString()).addRoute(
"/api/ai-agent/alive",
);
const result: HTTPResponse<JSONObject> = await API.post({
url: aliveUrl,
data: AIAgentAPIRequest.getDefaultRequestBody(),
});
if (result.isSuccess()) {
logger.debug("AI Agent update sent to server successfully.");
} else {
logger.error("Failed to send AI Agent update to server.");
}
},
});
};
export default InitJob;

View File

@@ -1,257 +0,0 @@
import { ONEUPTIME_URL } from "../Config";
import AIAgentAPIRequest from "../Utils/AIAgentAPIRequest";
import AIAgentTaskLog from "../Utils/AIAgentTaskLog";
import TaskLogger from "../Utils/TaskLogger";
import BackendAPI from "../Utils/BackendAPI";
import {
getTaskHandlerRegistry,
TaskContext,
TaskMetadata,
TaskHandler,
TaskResult,
} from "../TaskHandlers/Index";
import TaskHandlerRegistry from "../TaskHandlers/TaskHandlerRegistry";
import URL from "Common/Types/API/URL";
import API from "Common/Utils/API";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import { JSONObject } from "Common/Types/JSON";
import logger from "Common/Server/Utils/Logger";
import AIAgentTaskStatus from "Common/Types/AI/AIAgentTaskStatus";
import AIAgentTaskType from "Common/Types/AI/AIAgentTaskType";
import ObjectID from "Common/Types/ObjectID";
import Sleep from "Common/Types/Sleep";
// Type for task data from the API
interface AIAgentTaskData {
_id: string;
projectId: string;
taskType: AIAgentTaskType;
metadata: TaskMetadata;
createdAt: string;
status?: AIAgentTaskStatus;
}
// Type for API response containing task
interface GetPendingTaskResponse {
task: AIAgentTaskData | null;
}
const SLEEP_WHEN_NO_TASKS_MS: number = 60 * 1000; // 1 minute
type ExecuteTaskFunction = (task: AIAgentTaskData) => Promise<void>;
/**
* Execute an AI Agent task using the registered task handler
*/
const executeTask: ExecuteTaskFunction = async (
task: AIAgentTaskData,
): Promise<void> => {
const taskIdString: string = task._id;
const projectIdString: string = task.projectId;
const taskId: ObjectID = new ObjectID(taskIdString);
const projectId: ObjectID = new ObjectID(projectIdString);
const taskType: AIAgentTaskType = task.taskType;
const metadata: TaskMetadata = task.metadata || {};
const createdAt: Date = new Date(task.createdAt);
// Get the task handler from the registry
const registry: TaskHandlerRegistry = getTaskHandlerRegistry();
const handler: TaskHandler | undefined = registry.getHandler(taskType);
if (!handler) {
throw new Error(`No handler registered for task type: ${taskType}`);
}
// Create task logger
const taskLogger: TaskLogger = new TaskLogger({
taskId: taskIdString,
context: `${handler.name}`,
});
// Create backend API client
const backendAPI: BackendAPI = new BackendAPI();
// Build task context
const context: TaskContext = {
taskId,
projectId,
taskType,
metadata,
logger: taskLogger,
backendAPI,
createdAt,
startedAt: new Date(),
};
try {
// Log handler starting
await taskLogger.info(
`Starting ${handler.name} for task type: ${taskType}`,
);
// Validate metadata if the handler supports it
if (handler.validateMetadata && !handler.validateMetadata(metadata)) {
throw new Error(`Invalid metadata for task type: ${taskType}`);
}
// Execute the task handler
const result: TaskResult = await handler.execute(context);
// Log result
if (result.success) {
await taskLogger.info(`Task completed: ${result.message}`);
if (result.pullRequestsCreated && result.pullRequestsCreated > 0) {
await taskLogger.info(
`Created ${result.pullRequestsCreated} pull request(s): ${result.pullRequestUrls?.join(", ") || ""}`,
);
}
} else {
await taskLogger.warning(`Task did not succeed: ${result.message}`);
}
// Flush all pending logs
await taskLogger.flush();
/*
* If the task was not successful and we want to report it as an error
* Note: Based on user requirements, "no fix found" should be Completed, not Error
* Only throw if there was an actual error (not just "no action taken")
*/
if (!result.success && result.data?.["isError"]) {
throw new Error(result.message);
}
} catch (error) {
// Ensure logs are flushed even on error
await taskLogger.flush();
throw error;
}
};
const startTaskProcessingLoop: () => Promise<void> =
async (): Promise<void> => {
logger.info("Starting AI Agent task processing loop...");
const getPendingTaskUrl: URL = URL.fromString(
ONEUPTIME_URL.toString(),
).addRoute("/api/ai-agent-task/get-pending-task");
const updateTaskStatusUrl: URL = URL.fromString(
ONEUPTIME_URL.toString(),
).addRoute("/api/ai-agent-task/update-task-status");
/* Continuous loop to process tasks */
while (true) {
try {
/* Fetch one scheduled task */
const getPendingTaskResult: HTTPResponse<JSONObject> = await API.post({
url: getPendingTaskUrl,
data: AIAgentAPIRequest.getDefaultRequestBody(),
});
if (!getPendingTaskResult.isSuccess()) {
logger.error("Failed to fetch pending task from server");
logger.debug(
`Sleeping for ${SLEEP_WHEN_NO_TASKS_MS / 1000} seconds before retrying...`,
);
await Sleep.sleep(SLEEP_WHEN_NO_TASKS_MS);
continue;
}
const responseData: GetPendingTaskResponse =
getPendingTaskResult.data as unknown as GetPendingTaskResponse;
const task: AIAgentTaskData | null = responseData.task;
if (!task || !task._id) {
logger.debug("No pending tasks available");
logger.debug(
`Sleeping for ${SLEEP_WHEN_NO_TASKS_MS / 1000} seconds before checking again...`,
);
await Sleep.sleep(SLEEP_WHEN_NO_TASKS_MS);
continue;
}
const taskId: string = task._id;
const taskType: string = task.taskType || "Unknown";
logger.info(`Processing task: ${taskId} (type: ${taskType})`);
try {
/* Mark task as InProgress */
const inProgressResult: HTTPResponse<JSONObject> = await API.post({
url: updateTaskStatusUrl,
data: {
...AIAgentAPIRequest.getDefaultRequestBody(),
taskId: taskId,
status: AIAgentTaskStatus.InProgress,
},
});
if (!inProgressResult.isSuccess()) {
logger.error(
`Failed to mark task ${taskId} as InProgress. Skipping.`,
);
continue;
}
/* Send task started log */
await AIAgentTaskLog.sendTaskStartedLog(taskId);
/* Execute the task using the handler system */
await executeTask(task);
/* Mark task as Completed */
const completedResult: HTTPResponse<JSONObject> = await API.post({
url: updateTaskStatusUrl,
data: {
...AIAgentAPIRequest.getDefaultRequestBody(),
taskId: taskId,
status: AIAgentTaskStatus.Completed,
},
});
if (!completedResult.isSuccess()) {
logger.error(`Failed to mark task ${taskId} as Completed`);
} else {
/* Send task completed log */
await AIAgentTaskLog.sendTaskCompletedLog(taskId);
logger.info(`Task completed successfully: ${taskId}`);
}
} catch (error) {
/* Mark task as Error with error message */
const errorMessage: string =
error instanceof Error ? error.message : "Unknown error occurred";
const errorResult: HTTPResponse<JSONObject> = await API.post({
url: updateTaskStatusUrl,
data: {
...AIAgentAPIRequest.getDefaultRequestBody(),
taskId: taskId,
status: AIAgentTaskStatus.Error,
statusMessage: errorMessage,
},
});
if (!errorResult.isSuccess()) {
logger.error(
`Failed to mark task ${taskId} as Error: ${errorMessage}`,
);
}
/* Send task error log */
await AIAgentTaskLog.sendTaskErrorLog(taskId, errorMessage);
logger.error(`Task failed: ${taskId} - ${errorMessage}`);
logger.error(error);
}
} catch (error) {
logger.error("Error in task processing loop:");
logger.error(error);
logger.debug(
`Sleeping for ${SLEEP_WHEN_NO_TASKS_MS / 1000} seconds before retrying...`,
);
await Sleep.sleep(SLEEP_WHEN_NO_TASKS_MS);
}
}
};
export default startTaskProcessingLoop;

View File

@@ -1,127 +0,0 @@
import {
ONEUPTIME_URL,
AI_AGENT_ID,
AI_AGENT_KEY,
AI_AGENT_NAME,
AI_AGENT_DESCRIPTION,
} from "../Config";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import URL from "Common/Types/API/URL";
import { JSONObject } from "Common/Types/JSON";
import Sleep from "Common/Types/Sleep";
import API from "Common/Utils/API";
import { HasClusterKey } from "Common/Server/EnvironmentConfig";
import LocalCache from "Common/Server/Infrastructure/LocalCache";
import logger from "Common/Server/Utils/Logger";
import ClusterKeyAuthorization from "Common/Server/Middleware/ClusterKeyAuthorization";
export default class Register {
public static async registerAIAgent(): Promise<void> {
// register AI agent with 10 retries and 30 second interval between each retry.
let currentRetry: number = 0;
const maxRetry: number = 10;
const retryIntervalInSeconds: number = 30;
while (currentRetry < maxRetry) {
try {
logger.debug(`Registering AI Agent. Attempt: ${currentRetry + 1}`);
await Register._registerAIAgent();
logger.debug(`AI Agent registered successfully.`);
break;
} catch (error) {
logger.error(
`Failed to register AI Agent. Retrying after ${retryIntervalInSeconds} seconds...`,
);
logger.error(error);
currentRetry++;
await Sleep.sleep(retryIntervalInSeconds * 1000);
}
}
}
private static async _registerAIAgent(): Promise<void> {
if (HasClusterKey) {
// Clustered mode: Auto-register and get ID from server
const aiAgentRegistrationUrl: URL = URL.fromString(
ONEUPTIME_URL.toString(),
).addRoute("/api/ai-agent/register");
logger.debug("Registering AI Agent...");
logger.debug("Sending request to: " + aiAgentRegistrationUrl.toString());
const result: HTTPResponse<JSONObject> = await API.post({
url: aiAgentRegistrationUrl,
data: {
aiAgentKey: AI_AGENT_KEY,
aiAgentName: AI_AGENT_NAME,
aiAgentDescription: AI_AGENT_DESCRIPTION,
clusterKey: ClusterKeyAuthorization.getClusterKey(),
},
});
if (!result.isSuccess()) {
logger.error(
`Failed to register AI Agent. Status: ${result.statusCode}`,
);
logger.error(result.data);
throw new Error(
"Failed to register AI Agent: HTTP " + result.statusCode,
);
}
logger.debug("AI Agent Registered");
logger.debug(result.data);
const aiAgentId: string | undefined = result.data["_id"] as
| string
| undefined;
if (!aiAgentId) {
logger.error("AI Agent ID not found in response");
logger.error(result.data);
throw new Error("AI Agent ID not found in registration response");
}
LocalCache.setString("AI_AGENT", "AI_AGENT_ID", aiAgentId);
} else {
// Non-clustered mode: Validate AI agent by sending alive request
if (!AI_AGENT_ID) {
logger.error("AI_AGENT_ID or ONEUPTIME_SECRET should be set");
return process.exit();
}
const aliveUrl: URL = URL.fromString(ONEUPTIME_URL.toString()).addRoute(
"/api/ai-agent/alive",
);
logger.debug("Registering AI Agent...");
logger.debug("Sending request to: " + aliveUrl.toString());
const result: HTTPResponse<JSONObject> = await API.post({
url: aliveUrl,
data: {
aiAgentKey: AI_AGENT_KEY.toString(),
aiAgentId: AI_AGENT_ID.toString(),
},
});
if (result.isSuccess()) {
LocalCache.setString(
"AI_AGENT",
"AI_AGENT_ID",
AI_AGENT_ID.toString() as string,
);
logger.debug("AI Agent registered successfully");
} else {
throw new Error("Failed to register AI Agent: " + result.statusCode);
}
}
logger.debug(
`AI Agent ID: ${LocalCache.getString("AI_AGENT", "AI_AGENT_ID") || "Unknown"}`,
);
}
}

View File

@@ -1,454 +0,0 @@
import {
BaseTaskHandler,
TaskContext,
TaskResult,
TaskMetadata,
TaskResultData,
} from "./TaskHandlerInterface";
import AIAgentTaskType from "Common/Types/AI/AIAgentTaskType";
import {
LLMConfig,
ExceptionDetails,
CodeRepositoryInfo,
RepositoryToken,
} from "../Utils/BackendAPI";
import RepositoryManager, {
RepositoryConfig,
CloneResult,
} from "../Utils/RepositoryManager";
import PullRequestCreator, {
PullRequestResult,
} from "../Utils/PullRequestCreator";
import WorkspaceManager, { WorkspaceInfo } from "../Utils/WorkspaceManager";
import {
CodeAgentFactory,
CodeAgent,
CodeAgentType,
CodeAgentTask,
CodeAgentResult,
CodeAgentProgressEvent,
CodeAgentLLMConfig,
} from "../CodeAgents/Index";
// Metadata structure for Fix Exception tasks
export interface FixExceptionMetadata extends TaskMetadata {
exceptionId: string;
serviceId?: string;
stackTrace?: string;
errorMessage?: string;
}
export default class FixExceptionTaskHandler extends BaseTaskHandler<FixExceptionMetadata> {
public readonly taskType: AIAgentTaskType = AIAgentTaskType.FixException;
public readonly name: string = "Fix Exception Handler";
// Default timeout for code agent execution (30 minutes)
private static readonly CODE_AGENT_TIMEOUT_MS: number = 30 * 60 * 1000;
public async execute(
context: TaskContext<FixExceptionMetadata>,
): Promise<TaskResult> {
const metadata: FixExceptionMetadata = context.metadata;
await this.log(
context,
`Starting Fix Exception task for exception: ${metadata.exceptionId} (taskId: ${context.taskId.toString()})`,
);
let workspace: WorkspaceInfo | null = null;
try {
// Step 1: Get LLM configuration for the project
await this.log(context, "Fetching LLM provider configuration...");
const llmConfig: LLMConfig = await context.backendAPI.getLLMConfig(
context.projectId.toString(),
);
await this.log(
context,
`Using LLM provider: ${llmConfig.llmType}${llmConfig.modelName ? ` (${llmConfig.modelName})` : ""}`,
);
// Step 2: Get exception details
await this.log(context, "Fetching exception details...");
const exceptionDetails: ExceptionDetails =
await context.backendAPI.getExceptionDetails(metadata.exceptionId);
if (!exceptionDetails.service) {
await this.log(context, "No service linked to this exception", "error");
return this.createFailureResult("No service linked to this exception", {
isError: true,
});
}
await this.log(
context,
`Exception: ${exceptionDetails.exception.message.substring(0, 100)}...`,
);
await this.log(context, `Service: ${exceptionDetails.service.name}`);
// Step 3: Get linked code repositories
await this.log(context, "Finding linked code repositories...");
const repositories: Array<CodeRepositoryInfo> =
await context.backendAPI.getCodeRepositories(
exceptionDetails.service.id,
);
if (repositories.length === 0) {
await this.log(
context,
"No code repositories linked to this service",
"error",
);
return this.createFailureResult(
"No code repositories linked to this service via Service Catalog",
{ isError: true },
);
}
await this.log(
context,
`Found ${repositories.length} linked code repository(ies)`,
);
// Step 4: Create workspace for the task
workspace = await WorkspaceManager.createWorkspace(
context.taskId.toString(),
);
await this.log(context, `Created workspace: ${workspace.workspacePath}`);
// Step 5: Process each repository
const pullRequestUrls: Array<string> = [];
const errors: Array<string> = [];
for (const repo of repositories) {
try {
await this.log(
context,
`Processing repository: ${repo.organizationName}/${repo.repositoryName}`,
);
const prUrl: string | null = await this.processRepository(
context,
repo,
exceptionDetails,
llmConfig,
workspace,
);
if (prUrl) {
pullRequestUrls.push(prUrl);
}
} catch (error) {
const errorMessage: string =
error instanceof Error ? error.message : String(error);
errors.push(
`${repo.organizationName}/${repo.repositoryName}: ${errorMessage}`,
);
await this.log(
context,
`Failed to process repository ${repo.organizationName}/${repo.repositoryName}: ${errorMessage}`,
"error",
);
// Continue with next repository
}
}
// Step 6: Return result
if (pullRequestUrls.length > 0) {
await this.log(
context,
`Successfully created ${pullRequestUrls.length} pull request(s)`,
);
const resultData: TaskResultData = {
pullRequests: pullRequestUrls,
};
if (errors.length > 0) {
resultData.errors = errors;
}
return {
success: true,
message: `Created ${pullRequestUrls.length} pull request(s)`,
pullRequestsCreated: pullRequestUrls.length,
pullRequestUrls,
data: resultData,
};
}
// No PRs created - mark as error
await this.log(
context,
"No fixes could be applied to any repository",
"error",
);
return this.createFailureResult(
errors.length > 0
? `No fixes could be applied. Errors: ${errors.join("; ")}`
: "No fixes could be applied to any repository",
{ isError: true },
);
} catch (error) {
const errorMessage: string =
error instanceof Error ? error.message : String(error);
await this.log(context, `Task failed: ${errorMessage}`, "error");
// Mark as an actual error (not just "no action taken") so task gets Error status
return this.createFailureResult(errorMessage, { isError: true });
} finally {
// Cleanup workspace
if (workspace) {
await this.log(context, "Cleaning up workspace...");
await WorkspaceManager.deleteWorkspace(workspace.workspacePath);
}
// Flush logs
await context.logger.flush();
}
}
// Process a single repository
private async processRepository(
context: TaskContext<FixExceptionMetadata>,
repo: CodeRepositoryInfo,
exceptionDetails: ExceptionDetails,
llmConfig: LLMConfig,
workspace: WorkspaceInfo,
): Promise<string | null> {
// Get access token for the repository
await this.log(
context,
`Getting access token for ${repo.organizationName}/${repo.repositoryName}...`,
);
const tokenData: RepositoryToken =
await context.backendAPI.getRepositoryToken(repo.id);
// Clone the repository
await this.log(
context,
`Cloning repository ${repo.organizationName}/${repo.repositoryName}...`,
);
const repoConfig: RepositoryConfig = {
organizationName: tokenData.organizationName,
repositoryName: tokenData.repositoryName,
token: tokenData.token,
repositoryUrl: tokenData.repositoryUrl,
};
const repoManager: RepositoryManager = new RepositoryManager(
context.logger,
);
const cloneResult: CloneResult = await repoManager.cloneRepository(
repoConfig,
workspace.workspacePath,
);
// Create a fix branch
const branchName: string = `oneuptime-fix-exception-${context.taskId.toString().substring(0, 8)}`;
await this.log(context, `Creating branch: ${branchName}`);
await repoManager.createBranch(cloneResult.repositoryPath, branchName);
// Build the prompt for the code agent
const prompt: string = this.buildFixPrompt(
exceptionDetails,
repo.servicePathInRepository,
);
// Initialize code agent
await this.log(context, "Initializing code agent...");
const agent: CodeAgent = CodeAgentFactory.createAgent(
CodeAgentType.OpenCode,
);
const agentConfig: CodeAgentLLMConfig = {
llmType: llmConfig.llmType,
};
if (llmConfig.apiKey) {
agentConfig.apiKey = llmConfig.apiKey;
}
if (llmConfig.baseUrl) {
agentConfig.baseUrl = llmConfig.baseUrl;
}
if (llmConfig.modelName) {
agentConfig.modelName = llmConfig.modelName;
}
await agent.initialize(agentConfig, context.logger);
// Set up progress callback to log agent output
agent.onProgress((event: CodeAgentProgressEvent) => {
context.logger.logProcessOutput("CodeAgent", event.message);
});
// Execute the code agent
await this.log(context, "Running code agent to fix exception...");
const codeAgentTask: CodeAgentTask = {
workingDirectory: cloneResult.repositoryPath,
prompt,
timeoutMs: FixExceptionTaskHandler.CODE_AGENT_TIMEOUT_MS,
};
if (repo.servicePathInRepository) {
codeAgentTask.servicePath = repo.servicePathInRepository;
}
const agentResult: CodeAgentResult = await agent.executeTask(codeAgentTask);
// Check if any changes were made
if (!agentResult.success || agentResult.filesModified.length === 0) {
await this.log(
context,
`Code agent did not make any changes: ${agentResult.error || agentResult.summary}`,
"warning",
);
await agent.cleanup();
return null;
}
await this.log(
context,
`Code agent modified ${agentResult.filesModified.length} file(s)`,
);
// Add all changes and commit
await this.log(context, "Committing changes...");
await repoManager.addAllChanges(cloneResult.repositoryPath);
const commitMessage: string = this.buildCommitMessage(exceptionDetails);
await repoManager.commitChanges(cloneResult.repositoryPath, commitMessage);
// Push the branch
await this.log(context, `Pushing branch ${branchName}...`);
await repoManager.pushBranch(
cloneResult.repositoryPath,
branchName,
repoConfig,
);
// Create pull request
await this.log(context, "Creating pull request...");
const prCreator: PullRequestCreator = new PullRequestCreator(
context.logger,
);
const prTitle: string = PullRequestCreator.generatePRTitle(
exceptionDetails.exception.message,
);
const prBody: string = PullRequestCreator.generatePRBody({
exceptionMessage: exceptionDetails.exception.message,
exceptionType: exceptionDetails.exception.exceptionType,
stackTrace: exceptionDetails.exception.stackTrace,
serviceName: exceptionDetails.service?.name || "Unknown Service",
summary: agentResult.summary,
});
const prResult: PullRequestResult = await prCreator.createPullRequest({
token: tokenData.token,
organizationName: tokenData.organizationName,
repositoryName: tokenData.repositoryName,
baseBranch: repo.mainBranchName || "main",
headBranch: branchName,
title: prTitle,
body: prBody,
});
await this.log(context, `Pull request created: ${prResult.htmlUrl}`);
// Record the PR in the backend
await context.backendAPI.recordPullRequest({
taskId: context.taskId.toString(),
codeRepositoryId: repo.id,
pullRequestUrl: prResult.htmlUrl,
pullRequestNumber: prResult.number,
pullRequestId: prResult.id,
title: prResult.title,
description: prBody.substring(0, 1000),
headRefName: branchName,
baseRefName: repo.mainBranchName || "main",
});
// Cleanup agent
await agent.cleanup();
return prResult.htmlUrl;
}
// Build the prompt for the code agent
private buildFixPrompt(
exceptionDetails: ExceptionDetails,
servicePathInRepository: string | null,
): string {
let prompt: string = `You are a software engineer fixing a bug in a codebase.
## Exception Information
**Exception Type:** ${exceptionDetails.exception.exceptionType}
**Error Message:**
${exceptionDetails.exception.message}
**Stack Trace:**
\`\`\`
${exceptionDetails.exception.stackTrace}
\`\`\`
## Task
Please analyze the stack trace and fix the exception. Here are the steps to follow:
1. Identify the root cause of the exception from the stack trace
2. Find the relevant source files in the codebase
3. Implement a fix for the issue
4. Make sure your fix handles edge cases appropriately
5. The fix should be minimal and focused - only change what's necessary
## Guidelines
- Do NOT add excessive error handling or logging unless necessary
- Do NOT refactor unrelated code
- Keep the fix simple and targeted
- Preserve existing code style and patterns
- If you cannot determine how to fix the issue, explain why
Please proceed with analyzing and fixing this exception.`;
if (servicePathInRepository) {
prompt = `The service code is located in the \`${servicePathInRepository}\` directory.\n\n${prompt}`;
}
return prompt;
}
// Build commit message for the fix
private buildCommitMessage(exceptionDetails: ExceptionDetails): string {
const shortMessage: string = exceptionDetails.exception.message
.replace(/\n/g, " ")
.replace(/\s+/g, " ")
.trim()
.substring(0, 50);
return `fix: ${shortMessage}
This commit fixes an exception detected by OneUptime.
Exception Type: ${exceptionDetails.exception.exceptionType}
Exception ID: ${exceptionDetails.exception.id}
Automatically generated by OneUptime AI Agent.`;
}
// Validate metadata
public validateMetadata(metadata: FixExceptionMetadata): boolean {
return Boolean(metadata.exceptionId);
}
// Get handler description
public getDescription(): string {
return "Analyzes exceptions detected by OneUptime and attempts to fix them by modifying the source code and creating a pull request.";
}
}

View File

@@ -1,16 +0,0 @@
// Export all task handler related types and classes
export {
TaskHandler,
TaskContext,
TaskResult,
TaskMetadata,
TaskResultData,
BaseTaskHandler,
} from "./TaskHandlerInterface";
export {
default as TaskHandlerRegistry,
getTaskHandlerRegistry,
} from "./TaskHandlerRegistry";
export { default as FixExceptionTaskHandler } from "./FixExceptionTaskHandler";

View File

@@ -1,161 +0,0 @@
import AIAgentTaskType from "Common/Types/AI/AIAgentTaskType";
import ObjectID from "Common/Types/ObjectID";
import TaskLogger from "../Utils/TaskLogger";
import BackendAPI from "../Utils/BackendAPI";
// Base interface for task metadata - handlers should define their own specific metadata types
export interface TaskMetadata {
// All metadata must have at least these optional fields for extensibility
[key: string]: unknown;
}
// Base interface for task result data
export interface TaskResultData {
// Pull requests created (for Fix Exception tasks)
pullRequests?: Array<string>;
// Errors encountered during processing
errors?: Array<string>;
// Flag to indicate if this is an error result (not just "no action taken")
isError?: boolean;
// Additional data fields
[key: string]: unknown;
}
// Context provided to task handlers
export interface TaskContext<TMetadata extends TaskMetadata = TaskMetadata> {
// Task identification
taskId: ObjectID;
projectId: ObjectID;
taskType: AIAgentTaskType;
// Task metadata (varies by task type)
metadata: TMetadata;
// Utilities
logger: TaskLogger;
backendAPI: BackendAPI;
// Task timestamps
createdAt: Date;
startedAt: Date;
}
// Result returned by task handlers
export interface TaskResult {
// Whether the task completed successfully
success: boolean;
// Human-readable message describing the result
message: string;
// Additional data about the result (optional)
data?: TaskResultData;
// Number of PRs created (for Fix Exception tasks)
pullRequestsCreated?: number;
// List of PR URLs created
pullRequestUrls?: Array<string>;
}
// Interface that all task handlers must implement
export interface TaskHandler<TMetadata extends TaskMetadata = TaskMetadata> {
// The type of task this handler processes
readonly taskType: AIAgentTaskType;
// Human-readable name for the handler
readonly name: string;
// Execute the task and return a result
execute(context: TaskContext<TMetadata>): Promise<TaskResult>;
// Check if this handler can process a given task
canHandle(taskType: AIAgentTaskType): boolean;
// Optional: Validate task metadata before execution
validateMetadata?(metadata: TMetadata): boolean;
// Optional: Get a description of what this handler does
getDescription?(): string;
}
// Abstract base class that provides common functionality for task handlers
export abstract class BaseTaskHandler<
TMetadata extends TaskMetadata = TaskMetadata,
> implements TaskHandler<TMetadata>
{
public abstract readonly taskType: AIAgentTaskType;
public abstract readonly name: string;
public abstract execute(context: TaskContext<TMetadata>): Promise<TaskResult>;
public canHandle(taskType: AIAgentTaskType): boolean {
return taskType === this.taskType;
}
// Create a success result
protected createSuccessResult(
message: string,
data?: TaskResultData,
): TaskResult {
const result: TaskResult = {
success: true,
message,
};
if (data) {
result.data = data;
}
return result;
}
// Create a failure result
protected createFailureResult(
message: string,
data?: TaskResultData,
): TaskResult {
const result: TaskResult = {
success: false,
message,
};
if (data) {
result.data = data;
}
return result;
}
// Create a result for when no action was taken
protected createNoActionResult(message: string): TaskResult {
return {
success: true,
message,
pullRequestsCreated: 0,
};
}
// Log to the task logger
protected async log(
context: TaskContext<TMetadata>,
message: string,
level: "info" | "debug" | "warning" | "error" = "info",
): Promise<void> {
switch (level) {
case "debug":
await context.logger.debug(message);
break;
case "warning":
await context.logger.warning(message);
break;
case "error":
await context.logger.error(message);
break;
case "info":
default:
await context.logger.info(message);
break;
}
}
}

View File

@@ -1,93 +0,0 @@
import { TaskHandler } from "./TaskHandlerInterface";
import AIAgentTaskType from "Common/Types/AI/AIAgentTaskType";
import logger from "Common/Server/Utils/Logger";
/*
* Registry for task handlers
* Allows dynamic registration and lookup of handlers by task type
*/
export default class TaskHandlerRegistry {
private static instance: TaskHandlerRegistry | null = null;
private handlers: Map<AIAgentTaskType, TaskHandler> = new Map();
// Private constructor for singleton pattern
private constructor() {}
// Get the singleton instance
public static getInstance(): TaskHandlerRegistry {
if (!TaskHandlerRegistry.instance) {
TaskHandlerRegistry.instance = new TaskHandlerRegistry();
}
return TaskHandlerRegistry.instance;
}
// Reset the singleton (useful for testing)
public static resetInstance(): void {
TaskHandlerRegistry.instance = null;
}
// Register a task handler
public register(handler: TaskHandler): void {
if (this.handlers.has(handler.taskType)) {
logger.warn(
`Overwriting existing handler for task type: ${handler.taskType}`,
);
}
this.handlers.set(handler.taskType, handler);
logger.debug(
`Registered handler "${handler.name}" for task type: ${handler.taskType}`,
);
}
// Register multiple handlers at once
public registerAll(handlers: Array<TaskHandler>): void {
for (const handler of handlers) {
this.register(handler);
}
}
// Unregister a handler
public unregister(taskType: AIAgentTaskType): void {
if (this.handlers.has(taskType)) {
this.handlers.delete(taskType);
logger.debug(`Unregistered handler for task type: ${taskType}`);
}
}
// Get a handler for a specific task type
public getHandler(taskType: AIAgentTaskType): TaskHandler | undefined {
return this.handlers.get(taskType);
}
// Check if a handler exists for a task type
public hasHandler(taskType: AIAgentTaskType): boolean {
return this.handlers.has(taskType);
}
// Get all registered task types
public getRegisteredTaskTypes(): Array<AIAgentTaskType> {
return Array.from(this.handlers.keys());
}
// Get all registered handlers
public getAllHandlers(): Array<TaskHandler> {
return Array.from(this.handlers.values());
}
// Get the number of registered handlers
public getHandlerCount(): number {
return this.handlers.size;
}
// Clear all handlers
public clear(): void {
this.handlers.clear();
logger.debug("Cleared all task handlers");
}
}
// Export a convenience function to get the registry instance
export function getTaskHandlerRegistry(): TaskHandlerRegistry {
return TaskHandlerRegistry.getInstance();
}

View File

@@ -1,17 +0,0 @@
import BadDataException from "Common/Types/Exception/BadDataException";
import ObjectID from "Common/Types/ObjectID";
import LocalCache from "Common/Server/Infrastructure/LocalCache";
export default class AIAgentUtil {
public static getAIAgentId(): ObjectID {
const id: string | undefined =
LocalCache.getString("AI_AGENT", "AI_AGENT_ID") ||
process.env["AI_AGENT_ID"];
if (!id) {
throw new BadDataException("AI Agent ID not found");
}
return new ObjectID(id);
}
}

View File

@@ -1,12 +0,0 @@
import { AI_AGENT_KEY } from "../Config";
import AIAgentUtil from "./AIAgent";
import { JSONObject } from "Common/Types/JSON";
export default class AIAgentAPIRequest {
public static getDefaultRequestBody(): JSONObject {
return {
aiAgentKey: AI_AGENT_KEY,
aiAgentId: AIAgentUtil.getAIAgentId().toString(),
};
}
}

View File

@@ -1,79 +0,0 @@
import { ONEUPTIME_URL } from "../Config";
import AIAgentAPIRequest from "./AIAgentAPIRequest";
import URL from "Common/Types/API/URL";
import API from "Common/Utils/API";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import { JSONObject } from "Common/Types/JSON";
import LogSeverity from "Common/Types/Log/LogSeverity";
import logger from "Common/Server/Utils/Logger";
export interface SendLogOptions {
taskId: string;
severity: LogSeverity;
message: string;
}
export default class AIAgentTaskLog {
private static createLogUrl: URL | null = null;
private static getCreateLogUrl(): URL {
if (!this.createLogUrl) {
this.createLogUrl = URL.fromString(ONEUPTIME_URL.toString()).addRoute(
"/api/ai-agent-task-log/create-log",
);
}
return this.createLogUrl;
}
public static async sendLog(options: SendLogOptions): Promise<boolean> {
try {
const result: HTTPResponse<JSONObject> = await API.post({
url: this.getCreateLogUrl(),
data: {
...AIAgentAPIRequest.getDefaultRequestBody(),
taskId: options.taskId,
severity: options.severity,
message: options.message,
},
});
if (!result.isSuccess()) {
logger.error(`Failed to send log for task ${options.taskId}`);
return false;
}
return true;
} catch (error) {
logger.error(`Error sending log for task ${options.taskId}:`);
logger.error(error);
return false;
}
}
public static async sendTaskStartedLog(taskId: string): Promise<boolean> {
return this.sendLog({
taskId,
severity: LogSeverity.Information,
message: "Task execution started",
});
}
public static async sendTaskCompletedLog(taskId: string): Promise<boolean> {
return this.sendLog({
taskId,
severity: LogSeverity.Information,
message: "Task execution completed successfully",
});
}
public static async sendTaskErrorLog(
taskId: string,
errorMessage: string,
): Promise<boolean> {
return this.sendLog({
taskId,
severity: LogSeverity.Error,
message: `Task execution failed: ${errorMessage}`,
});
}
}

View File

@@ -1,394 +0,0 @@
import { ONEUPTIME_URL } from "../Config";
import AIAgentAPIRequest from "./AIAgentAPIRequest";
import URL from "Common/Types/API/URL";
import API from "Common/Utils/API";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import { JSONObject } from "Common/Types/JSON";
import LlmType from "Common/Types/LLM/LlmType";
import AIAgentTaskStatus from "Common/Types/AI/AIAgentTaskStatus";
import logger from "Common/Server/Utils/Logger";
// API Response types
interface LLMConfigResponse {
llmType: LlmType;
apiKey?: string;
baseUrl?: string;
modelName?: string;
message?: string;
}
interface ExceptionResponse {
id: string;
message: string;
stackTrace: string;
exceptionType: string;
fingerprint: string;
}
interface ServiceResponse {
id: string;
name: string;
description: string;
}
interface ExceptionDetailsResponse {
exception: ExceptionResponse;
service: ServiceResponse | null;
message?: string;
}
interface CodeRepositoryResponse {
id: string;
name: string;
repositoryHostedAt: string;
organizationName: string;
repositoryName: string;
mainBranchName: string;
servicePathInRepository: string | null;
gitHubAppInstallationId: string | null;
}
interface CodeRepositoriesResponse {
repositories: Array<CodeRepositoryResponse>;
message?: string;
}
interface RepositoryTokenResponse {
token: string;
expiresAt: string;
repositoryUrl: string;
organizationName: string;
repositoryName: string;
message?: string;
}
interface RecordPullRequestResponse {
success: boolean;
pullRequestId: string;
message?: string;
}
interface UpdateTaskStatusResponse {
success?: boolean;
message?: string;
}
// Exported types
export interface LLMConfig {
llmType: LlmType;
apiKey?: string;
baseUrl?: string;
modelName?: string;
}
export interface ExceptionDetails {
exception: {
id: string;
message: string;
stackTrace: string;
exceptionType: string;
fingerprint: string;
};
service: {
id: string;
name: string;
description: string;
} | null;
}
export interface CodeRepositoryInfo {
id: string;
name: string;
repositoryHostedAt: string;
organizationName: string;
repositoryName: string;
mainBranchName: string;
servicePathInRepository: string | null;
gitHubAppInstallationId: string | null;
}
export interface RepositoryToken {
token: string;
expiresAt: Date;
repositoryUrl: string;
organizationName: string;
repositoryName: string;
}
export interface RecordPullRequestOptions {
taskId: string;
codeRepositoryId: string;
pullRequestUrl: string;
pullRequestNumber?: number;
pullRequestId?: number;
title: string;
description?: string;
headRefName?: string;
baseRefName?: string;
}
export interface RecordPullRequestResult {
success: boolean;
pullRequestId: string;
}
export default class BackendAPI {
private baseUrl: URL;
public constructor() {
this.baseUrl = URL.fromString(ONEUPTIME_URL.toString());
}
// Get LLM configuration for a project
public async getLLMConfig(projectId: string): Promise<LLMConfig> {
const url: URL = URL.fromURL(this.baseUrl).addRoute(
"/api/ai-agent-data/get-llm-config",
);
const response: HTTPResponse<JSONObject> = await API.post({
url,
data: {
...AIAgentAPIRequest.getDefaultRequestBody(),
projectId: projectId,
},
});
if (!response.isSuccess()) {
const data: LLMConfigResponse =
response.data as unknown as LLMConfigResponse;
const errorMessage: string = data?.message || "Failed to get LLM config";
throw new Error(errorMessage);
}
const data: LLMConfigResponse =
response.data as unknown as LLMConfigResponse;
logger.debug(`Got LLM config for project ${projectId}: ${data.llmType}`);
const llmConfig: LLMConfig = {
llmType: data.llmType,
};
if (data.apiKey) {
llmConfig.apiKey = data.apiKey;
}
if (data.baseUrl) {
llmConfig.baseUrl = data.baseUrl;
}
if (data.modelName) {
llmConfig.modelName = data.modelName;
}
return llmConfig;
}
// Get exception details with telemetry service info
public async getExceptionDetails(
exceptionId: string,
): Promise<ExceptionDetails> {
const url: URL = URL.fromURL(this.baseUrl).addRoute(
"/api/ai-agent-data/get-exception-details",
);
const response: HTTPResponse<JSONObject> = await API.post({
url,
data: {
...AIAgentAPIRequest.getDefaultRequestBody(),
exceptionId: exceptionId,
},
});
if (!response.isSuccess()) {
const data: ExceptionDetailsResponse =
response.data as unknown as ExceptionDetailsResponse;
const errorMessage: string =
data?.message || "Failed to get exception details";
throw new Error(errorMessage);
}
const data: ExceptionDetailsResponse =
response.data as unknown as ExceptionDetailsResponse;
logger.debug(
`Got exception details for ${exceptionId}: ${data.exception.message.substring(0, 100)}`,
);
return {
exception: {
id: data.exception.id,
message: data.exception.message,
stackTrace: data.exception.stackTrace,
exceptionType: data.exception.exceptionType,
fingerprint: data.exception.fingerprint,
},
service: data.service
? {
id: data.service.id,
name: data.service.name,
description: data.service.description,
}
: null,
};
}
// Get code repositories linked to a service
public async getCodeRepositories(
serviceId: string,
): Promise<Array<CodeRepositoryInfo>> {
const url: URL = URL.fromURL(this.baseUrl).addRoute(
"/api/ai-agent-data/get-code-repositories",
);
const response: HTTPResponse<JSONObject> = await API.post({
url,
data: {
...AIAgentAPIRequest.getDefaultRequestBody(),
serviceId: serviceId,
},
});
if (!response.isSuccess()) {
const data: CodeRepositoriesResponse =
response.data as unknown as CodeRepositoriesResponse;
const errorMessage: string =
data?.message || "Failed to get code repositories";
throw new Error(errorMessage);
}
const data: CodeRepositoriesResponse =
response.data as unknown as CodeRepositoriesResponse;
logger.debug(
`Got ${data.repositories.length} code repositories for service ${serviceId}`,
);
return data.repositories.map((repo: CodeRepositoryResponse) => {
return {
id: repo.id,
name: repo.name,
repositoryHostedAt: repo.repositoryHostedAt,
organizationName: repo.organizationName,
repositoryName: repo.repositoryName,
mainBranchName: repo.mainBranchName,
servicePathInRepository: repo.servicePathInRepository,
gitHubAppInstallationId: repo.gitHubAppInstallationId,
};
});
}
// Get access token for a code repository
public async getRepositoryToken(
codeRepositoryId: string,
): Promise<RepositoryToken> {
const url: URL = URL.fromURL(this.baseUrl).addRoute(
"/api/ai-agent-data/get-repository-token",
);
const response: HTTPResponse<JSONObject> = await API.post({
url,
data: {
...AIAgentAPIRequest.getDefaultRequestBody(),
codeRepositoryId: codeRepositoryId,
},
});
if (!response.isSuccess()) {
const data: RepositoryTokenResponse =
response.data as unknown as RepositoryTokenResponse;
const errorMessage: string =
data?.message || "Failed to get repository token";
throw new Error(errorMessage);
}
const data: RepositoryTokenResponse =
response.data as unknown as RepositoryTokenResponse;
logger.debug(
`Got access token for repository ${data.organizationName}/${data.repositoryName}`,
);
return {
token: data.token,
expiresAt: new Date(data.expiresAt),
repositoryUrl: data.repositoryUrl,
organizationName: data.organizationName,
repositoryName: data.repositoryName,
};
}
// Record a pull request created by the AI Agent
public async recordPullRequest(
options: RecordPullRequestOptions,
): Promise<RecordPullRequestResult> {
const url: URL = URL.fromURL(this.baseUrl).addRoute(
"/api/ai-agent-data/record-pull-request",
);
const response: HTTPResponse<JSONObject> = await API.post({
url,
data: {
...AIAgentAPIRequest.getDefaultRequestBody(),
taskId: options.taskId,
codeRepositoryId: options.codeRepositoryId,
pullRequestUrl: options.pullRequestUrl,
pullRequestNumber: options.pullRequestNumber,
pullRequestId: options.pullRequestId,
title: options.title,
description: options.description,
headRefName: options.headRefName,
baseRefName: options.baseRefName,
},
});
if (!response.isSuccess()) {
const data: RecordPullRequestResponse =
response.data as unknown as RecordPullRequestResponse;
const errorMessage: string =
data?.message || "Failed to record pull request";
throw new Error(errorMessage);
}
const data: RecordPullRequestResponse =
response.data as unknown as RecordPullRequestResponse;
logger.debug(`Recorded pull request: ${options.pullRequestUrl}`);
return {
success: data.success,
pullRequestId: data.pullRequestId,
};
}
// Update task status (wrapper around existing endpoint)
public async updateTaskStatus(
taskId: string,
status: AIAgentTaskStatus,
statusMessage?: string,
): Promise<void> {
const url: URL = URL.fromURL(this.baseUrl).addRoute(
"/api/ai-agent-task/update-task-status",
);
const response: HTTPResponse<JSONObject> = await API.post({
url,
data: {
...AIAgentAPIRequest.getDefaultRequestBody(),
taskId: taskId,
status: status,
statusMessage: statusMessage,
},
});
if (!response.isSuccess()) {
const data: UpdateTaskStatusResponse =
response.data as unknown as UpdateTaskStatusResponse;
const errorMessage: string =
data?.message || "Failed to update task status";
throw new Error(errorMessage);
}
logger.debug(`Updated task ${taskId} status to ${status}`);
}
}

View File

@@ -1,369 +0,0 @@
import API from "Common/Utils/API";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import HTTPErrorResponse from "Common/Types/API/HTTPErrorResponse";
import URL from "Common/Types/API/URL";
import { JSONObject, JSONArray } from "Common/Types/JSON";
import logger from "Common/Server/Utils/Logger";
import Headers from "Common/Types/API/Headers";
import TaskLogger from "./TaskLogger";
export interface PullRequestOptions {
token: string;
organizationName: string;
repositoryName: string;
baseBranch: string;
headBranch: string;
title: string;
body: string;
draft?: boolean;
}
export interface PullRequestResult {
id: number;
number: number;
url: string;
htmlUrl: string;
state: string;
title: string;
}
export default class PullRequestCreator {
private static readonly GITHUB_API_BASE: string = "https://api.github.com";
private static readonly GITHUB_API_VERSION: string = "2022-11-28";
private logger: TaskLogger | null = null;
public constructor(taskLogger?: TaskLogger) {
if (taskLogger) {
this.logger = taskLogger;
}
}
// Create a pull request on GitHub
public async createPullRequest(
options: PullRequestOptions,
): Promise<PullRequestResult> {
await this.log(
`Creating pull request: ${options.title} (${options.headBranch} -> ${options.baseBranch})`,
);
const url: URL = URL.fromString(
`${PullRequestCreator.GITHUB_API_BASE}/repos/${options.organizationName}/${options.repositoryName}/pulls`,
);
const headers: Headers = this.getHeaders(options.token);
const response: HTTPResponse<JSONObject> = await API.post({
url,
data: {
title: options.title,
body: options.body,
head: options.headBranch,
base: options.baseBranch,
draft: options.draft || false,
},
headers,
});
if (!response.isSuccess()) {
const errorData: JSONObject = response.data as JSONObject;
const errorMessage: string =
(errorData["message"] as string) || "Failed to create pull request";
logger.error(`GitHub API error: ${errorMessage}`);
throw new Error(`Failed to create pull request: ${errorMessage}`);
}
const data: JSONObject = response.data as JSONObject;
const result: PullRequestResult = {
id: data["id"] as number,
number: data["number"] as number,
url: data["url"] as string,
htmlUrl: data["html_url"] as string,
state: data["state"] as string,
title: data["title"] as string,
};
await this.log(`Pull request created: ${result.htmlUrl}`);
return result;
}
// Get an existing pull request by number
public async getPullRequest(
token: string,
organizationName: string,
repositoryName: string,
pullNumber: number,
): Promise<PullRequestResult | null> {
const url: URL = URL.fromString(
`${PullRequestCreator.GITHUB_API_BASE}/repos/${organizationName}/${repositoryName}/pulls/${pullNumber}`,
);
const headers: Headers = this.getHeaders(token);
const response: HTTPResponse<JSONObject> = await API.get({
url,
headers,
});
if (!response.isSuccess()) {
return null;
}
const data: JSONObject = response.data as JSONObject;
return {
id: data["id"] as number,
number: data["number"] as number,
url: data["url"] as string,
htmlUrl: data["html_url"] as string,
state: data["state"] as string,
title: data["title"] as string,
};
}
// Check if a pull request already exists for a branch
public async findExistingPullRequest(
token: string,
organizationName: string,
repositoryName: string,
headBranch: string,
baseBranch: string,
): Promise<PullRequestResult | null> {
const url: URL = URL.fromString(
`${PullRequestCreator.GITHUB_API_BASE}/repos/${organizationName}/${repositoryName}/pulls`,
);
const headers: Headers = this.getHeaders(token);
const response: HTTPResponse<JSONArray> | HTTPErrorResponse = await API.get(
{
url,
headers,
params: {
head: `${organizationName}:${headBranch}`,
base: baseBranch,
state: "open",
},
},
);
if (!response.isSuccess()) {
return null;
}
const pulls: JSONArray = response.data as JSONArray;
if (pulls.length > 0) {
const data: JSONObject = pulls[0] as JSONObject;
return {
id: data["id"] as number,
number: data["number"] as number,
url: data["url"] as string,
htmlUrl: data["html_url"] as string,
state: data["state"] as string,
title: data["title"] as string,
};
}
return null;
}
// Update an existing pull request
public async updatePullRequest(
token: string,
organizationName: string,
repositoryName: string,
pullNumber: number,
updates: { title?: string; body?: string; state?: "open" | "closed" },
): Promise<PullRequestResult> {
const url: URL = URL.fromString(
`${PullRequestCreator.GITHUB_API_BASE}/repos/${organizationName}/${repositoryName}/pulls/${pullNumber}`,
);
const headers: Headers = this.getHeaders(token);
const response: HTTPResponse<JSONObject> = await API.patch({
url,
data: updates,
headers,
});
if (!response.isSuccess()) {
const errorData: JSONObject = response.data as JSONObject;
const errorMessage: string =
(errorData["message"] as string) || "Failed to update pull request";
throw new Error(`Failed to update pull request: ${errorMessage}`);
}
const data: JSONObject = response.data as JSONObject;
return {
id: data["id"] as number,
number: data["number"] as number,
url: data["url"] as string,
htmlUrl: data["html_url"] as string,
state: data["state"] as string,
title: data["title"] as string,
};
}
// Add labels to a pull request
public async addLabels(
token: string,
organizationName: string,
repositoryName: string,
issueNumber: number,
labels: Array<string>,
): Promise<void> {
await this.log(`Adding labels to PR #${issueNumber}: ${labels.join(", ")}`);
const url: URL = URL.fromString(
`${PullRequestCreator.GITHUB_API_BASE}/repos/${organizationName}/${repositoryName}/issues/${issueNumber}/labels`,
);
const headers: Headers = this.getHeaders(token);
const response: HTTPResponse<JSONObject> = await API.post({
url,
data: { labels },
headers,
});
if (!response.isSuccess()) {
logger.warn(`Failed to add labels to PR #${issueNumber}`);
}
}
// Add reviewers to a pull request
public async requestReviewers(
token: string,
organizationName: string,
repositoryName: string,
pullNumber: number,
reviewers: Array<string>,
teamReviewers?: Array<string>,
): Promise<void> {
await this.log(`Requesting reviewers for PR #${pullNumber}`);
const url: URL = URL.fromString(
`${PullRequestCreator.GITHUB_API_BASE}/repos/${organizationName}/${repositoryName}/pulls/${pullNumber}/requested_reviewers`,
);
const headers: Headers = this.getHeaders(token);
const data: JSONObject = {
reviewers,
};
if (teamReviewers && teamReviewers.length > 0) {
data["team_reviewers"] = teamReviewers;
}
const response: HTTPResponse<JSONObject> = await API.post({
url,
data,
headers,
});
if (!response.isSuccess()) {
logger.warn(`Failed to request reviewers for PR #${pullNumber}`);
}
}
// Add a comment to a pull request
public async addComment(
token: string,
organizationName: string,
repositoryName: string,
issueNumber: number,
comment: string,
): Promise<void> {
await this.log(`Adding comment to PR #${issueNumber}`);
const url: URL = URL.fromString(
`${PullRequestCreator.GITHUB_API_BASE}/repos/${organizationName}/${repositoryName}/issues/${issueNumber}/comments`,
);
const headers: Headers = this.getHeaders(token);
const response: HTTPResponse<JSONObject> = await API.post({
url,
data: { body: comment },
headers,
});
if (!response.isSuccess()) {
logger.warn(`Failed to add comment to PR #${issueNumber}`);
}
}
// Generate PR body from exception details
public static generatePRBody(data: {
exceptionMessage: string;
exceptionType: string;
stackTrace: string;
serviceName: string;
summary: string;
}): string {
return `## Exception Fix
This pull request was automatically generated by OneUptime AI Agent to fix an exception.
### Exception Details
**Service:** ${data.serviceName}
**Type:** ${data.exceptionType}
**Message:** ${data.exceptionMessage}
### Stack Trace
\`\`\`
${data.stackTrace.substring(0, 2000)}${data.stackTrace.length > 2000 ? "\n...(truncated)" : ""}
\`\`\`
### Summary of Changes
${data.summary}
---
*This PR was automatically generated by [OneUptime AI Agent](https://oneuptime.com)*`;
}
// Generate PR title from exception
public static generatePRTitle(exceptionMessage: string): string {
// Truncate and clean the message for use as a title
const cleanMessage: string = exceptionMessage
.replace(/\n/g, " ")
.replace(/\s+/g, " ")
.trim();
const maxLength: number = 70;
if (cleanMessage.length <= maxLength) {
return `fix: ${cleanMessage}`;
}
return `fix: ${cleanMessage.substring(0, maxLength - 3)}...`;
}
// Helper method to get GitHub API headers
private getHeaders(token: string): Headers {
return {
Authorization: `Bearer ${token}`,
Accept: "application/vnd.github+json",
"X-GitHub-Api-Version": PullRequestCreator.GITHUB_API_VERSION,
"Content-Type": "application/json",
};
}
// Helper method for logging
private async log(message: string): Promise<void> {
if (this.logger) {
await this.logger.info(message);
} else {
logger.debug(message);
}
}
}

View File

@@ -1,313 +0,0 @@
import Execute from "Common/Server/Utils/Execute";
import LocalFile from "Common/Server/Utils/LocalFile";
import logger from "Common/Server/Utils/Logger";
import path from "path";
import TaskLogger from "./TaskLogger";
export interface CloneResult {
workingDirectory: string;
repositoryPath: string;
}
export interface RepositoryConfig {
organizationName: string;
repositoryName: string;
token: string;
repositoryUrl?: string;
}
export default class RepositoryManager {
private logger: TaskLogger | null = null;
public constructor(taskLogger?: TaskLogger) {
if (taskLogger) {
this.logger = taskLogger;
}
}
// Clone a repository with token-based authentication
public async cloneRepository(
config: RepositoryConfig,
workDir: string,
): Promise<CloneResult> {
await this.log(
`Cloning repository ${config.organizationName}/${config.repositoryName}...`,
);
// Build the authenticated URL
const authUrl: string = this.buildAuthenticatedUrl(config);
// Ensure the working directory exists
await LocalFile.makeDirectory(workDir);
// Clone the repository
await this.runGitCommand(workDir, ["clone", authUrl]);
const repositoryPath: string = path.join(workDir, config.repositoryName);
await this.log(`Repository cloned to ${repositoryPath}`);
// Set git config for the repository
await this.setGitConfig(repositoryPath);
return {
workingDirectory: workDir,
repositoryPath: repositoryPath,
};
}
// Build URL with embedded token for authentication
private buildAuthenticatedUrl(config: RepositoryConfig): string {
if (config.repositoryUrl) {
// If URL is provided, inject token
const url: URL = new URL(config.repositoryUrl);
url.username = "x-access-token";
url.password = config.token;
return url.toString();
}
// Default to GitHub
return `https://x-access-token:${config.token}@github.com/${config.organizationName}/${config.repositoryName}.git`;
}
// Set git user config for commits
private async setGitConfig(repoPath: string): Promise<void> {
await this.runGitCommand(repoPath, [
"config",
"user.name",
"OneUptime AI Agent",
]);
await this.runGitCommand(repoPath, [
"config",
"user.email",
"ai-agent@oneuptime.com",
]);
}
// Create a new branch
public async createBranch(
repoPath: string,
branchName: string,
): Promise<void> {
await this.log(`Creating branch: ${branchName}`);
await this.runGitCommand(repoPath, ["checkout", "-b", branchName]);
await this.log(`Branch ${branchName} created and checked out`);
}
// Checkout existing branch
public async checkoutBranch(
repoPath: string,
branchName: string,
): Promise<void> {
await this.log(`Checking out branch: ${branchName}`);
await this.runGitCommand(repoPath, ["checkout", branchName]);
}
// Create branch if doesn't exist, or checkout if it does
public async createOrCheckoutBranch(
repoPath: string,
branchName: string,
): Promise<void> {
try {
// Check if branch exists locally
await this.runGitCommand(repoPath, ["rev-parse", "--verify", branchName]);
await this.checkoutBranch(repoPath, branchName);
} catch {
// Branch doesn't exist, create it
await this.createBranch(repoPath, branchName);
}
}
// Add all changes to staging
public async addAllChanges(repoPath: string): Promise<void> {
await this.log("Adding all changes to git staging...");
await this.runGitCommand(repoPath, ["add", "-A"]);
}
// Check if there are any changes to commit
public async hasChanges(repoPath: string): Promise<boolean> {
try {
const status: string = await this.runGitCommand(repoPath, [
"status",
"--porcelain",
]);
return status.trim().length > 0;
} catch (error) {
logger.error("Error checking for changes:");
logger.error(error);
return false;
}
}
// Get list of changed files
public async getChangedFiles(repoPath: string): Promise<Array<string>> {
const status: string = await this.runGitCommand(repoPath, [
"status",
"--porcelain",
]);
if (!status.trim()) {
return [];
}
return status
.split("\n")
.filter((line: string) => {
return line.trim().length > 0;
})
.map((line: string) => {
// Status output format is "XY filename" where XY is the status
return line.substring(3).trim();
});
}
// Commit changes
public async commitChanges(repoPath: string, message: string): Promise<void> {
await this.log(`Committing changes: ${message.substring(0, 50)}...`);
await Execute.executeCommandFile({
command: "git",
args: ["commit", "-m", message],
cwd: repoPath,
});
await this.log("Changes committed successfully");
}
// Push branch to remote
public async pushBranch(
repoPath: string,
branchName: string,
config: RepositoryConfig,
): Promise<void> {
await this.log(`Pushing branch ${branchName} to remote...`);
// Set the remote URL with authentication
const authUrl: string = this.buildAuthenticatedUrl(config);
// Update the remote URL
await this.runGitCommand(repoPath, [
"remote",
"set-url",
"origin",
authUrl,
]);
// Push with tracking
await this.runGitCommand(repoPath, ["push", "-u", "origin", branchName]);
await this.log(`Branch ${branchName} pushed to remote`);
}
// Get the current branch name
public async getCurrentBranch(repoPath: string): Promise<string> {
const branch: string = await this.runGitCommand(repoPath, [
"rev-parse",
"--abbrev-ref",
"HEAD",
]);
return branch.trim();
}
// Get the current commit hash
public async getCurrentCommitHash(repoPath: string): Promise<string> {
const hash: string = await this.runGitCommand(repoPath, [
"rev-parse",
"HEAD",
]);
return hash.trim();
}
// Pull latest changes from remote
public async pullChanges(repoPath: string): Promise<void> {
await this.log("Pulling latest changes from remote...");
await this.runGitCommand(repoPath, ["pull"]);
}
// Discard all local changes
public async discardChanges(repoPath: string): Promise<void> {
await this.log("Discarding all local changes...");
await this.runGitCommand(repoPath, ["checkout", "."]);
await this.runGitCommand(repoPath, ["clean", "-fd"]);
}
// Clean up the repository directory
public async cleanup(workDir: string): Promise<void> {
await this.log(`Cleaning up workspace: ${workDir}`);
try {
await LocalFile.deleteDirectory(workDir);
await this.log("Workspace cleaned up successfully");
} catch (error) {
logger.error(`Error cleaning up workspace ${workDir}:`);
logger.error(error);
}
}
// Get diff of current changes
public async getDiff(repoPath: string): Promise<string> {
try {
const diff: string = await this.runGitCommand(repoPath, ["diff"]);
return diff;
} catch (error) {
logger.error("Error getting diff:");
logger.error(error);
return "";
}
}
// Get staged diff
public async getStagedDiff(repoPath: string): Promise<string> {
try {
const diff: string = await this.runGitCommand(repoPath, [
"diff",
"--staged",
]);
return diff;
} catch (error) {
logger.error("Error getting staged diff:");
logger.error(error);
return "";
}
}
// Helper method to run git commands
private async runGitCommand(
repoPath: string,
args: Array<string>,
): Promise<string> {
const cwd: string = path.resolve(repoPath);
const logArgs: Array<string> = args.map((arg: string) => {
// Mask tokens in URLs
if (arg.includes("x-access-token:")) {
return arg.replace(/x-access-token:[^@]+@/, "x-access-token:***@");
}
return arg.includes(" ") ? `"${arg}"` : arg;
});
logger.debug(`Executing git command in ${cwd}: git ${logArgs.join(" ")}`);
return Execute.executeCommandFile({
command: "git",
args,
cwd,
});
}
// Helper method for logging
private async log(message: string): Promise<void> {
if (this.logger) {
await this.logger.info(message);
} else {
logger.debug(message);
}
}
}

View File

@@ -1,229 +0,0 @@
import { ONEUPTIME_URL } from "../Config";
import AIAgentAPIRequest from "./AIAgentAPIRequest";
import URL from "Common/Types/API/URL";
import API from "Common/Utils/API";
import HTTPResponse from "Common/Types/API/HTTPResponse";
import { JSONObject } from "Common/Types/JSON";
import LogSeverity from "Common/Types/Log/LogSeverity";
import logger from "Common/Server/Utils/Logger";
import OneUptimeDate from "Common/Types/Date";
export interface TaskLoggerOptions {
taskId: string;
context?: string;
batchSize?: number;
flushIntervalMs?: number;
}
interface LogEntry {
severity: LogSeverity;
message: string;
timestamp: Date;
}
export default class TaskLogger {
private taskId: string;
private context: string | undefined;
private logBuffer: Array<LogEntry> = [];
private batchSize: number;
private flushIntervalMs: number;
private flushTimer: ReturnType<typeof setInterval> | null = null;
private createLogUrl: URL | null = null;
public constructor(options: TaskLoggerOptions) {
this.taskId = options.taskId;
this.context = options.context;
this.batchSize = options.batchSize || 10;
this.flushIntervalMs = options.flushIntervalMs || 5000; // 5 seconds default
// Start periodic flush timer
this.startFlushTimer();
}
private getCreateLogUrl(): URL {
if (!this.createLogUrl) {
this.createLogUrl = URL.fromString(ONEUPTIME_URL.toString()).addRoute(
"/api/ai-agent-task-log/create-log",
);
}
return this.createLogUrl;
}
private startFlushTimer(): void {
this.flushTimer = setInterval(() => {
this.flush().catch((err: Error) => {
logger.error(`Error flushing logs: ${err.message}`);
});
}, this.flushIntervalMs);
}
private stopFlushTimer(): void {
if (this.flushTimer) {
clearInterval(this.flushTimer);
this.flushTimer = null;
}
}
private formatMessage(
severity: LogSeverity,
message: string,
timestamp: Date,
): string {
const timestampStr: string = OneUptimeDate.toDateTimeLocalString(timestamp);
const severityStr: string = severity.toUpperCase().padEnd(7);
const contextStr: string = this.context ? `[${this.context}] ` : "";
return `[${timestampStr}] [${severityStr}] ${contextStr}${message}`;
}
private addToBuffer(severity: LogSeverity, message: string): void {
const entry: LogEntry = {
severity,
message,
timestamp: OneUptimeDate.getCurrentDate(),
};
this.logBuffer.push(entry);
// Also log locally for debugging
logger.debug(
`[Task ${this.taskId}] ${this.formatMessage(entry.severity, entry.message, entry.timestamp)}`,
);
// Auto-flush if buffer is full
if (this.logBuffer.length >= this.batchSize) {
this.flush().catch((err: Error) => {
logger.error(`Error auto-flushing logs: ${err.message}`);
});
}
}
private async sendLogToServer(
severity: LogSeverity,
message: string,
): Promise<boolean> {
try {
const result: HTTPResponse<JSONObject> = await API.post({
url: this.getCreateLogUrl(),
data: {
...AIAgentAPIRequest.getDefaultRequestBody(),
taskId: this.taskId,
severity: severity,
message: message,
},
});
if (!result.isSuccess()) {
logger.error(`Failed to send log for task ${this.taskId}`);
return false;
}
return true;
} catch (error) {
logger.error(`Error sending log for task ${this.taskId}:`);
logger.error(error);
return false;
}
}
// Public logging methods
public async debug(message: string): Promise<void> {
this.addToBuffer(LogSeverity.Debug, message);
}
public async info(message: string): Promise<void> {
this.addToBuffer(LogSeverity.Information, message);
}
public async warning(message: string): Promise<void> {
this.addToBuffer(LogSeverity.Warning, message);
}
public async error(message: string): Promise<void> {
this.addToBuffer(LogSeverity.Error, message);
// Immediately flush on errors
await this.flush();
}
public async trace(message: string): Promise<void> {
this.addToBuffer(LogSeverity.Trace, message);
}
// Log output from external processes like OpenCode
public async logProcessOutput(
processName: string,
output: string,
): Promise<void> {
const lines: Array<string> = output.split("\n").filter((line: string) => {
return line.trim().length > 0;
});
for (const line of lines) {
this.addToBuffer(LogSeverity.Information, `[${processName}] ${line}`);
}
}
// Log a code block (useful for stack traces, code snippets, etc.)
public async logCodeBlock(
title: string,
code: string,
severity: LogSeverity = LogSeverity.Information,
): Promise<void> {
const formattedCode: string = `${title}:\n\`\`\`\n${code}\n\`\`\``;
this.addToBuffer(severity, formattedCode);
}
// Flush all buffered logs to the server
public async flush(): Promise<void> {
if (this.logBuffer.length === 0) {
return;
}
// Get all entries and clear buffer
const entries: Array<LogEntry> = [...this.logBuffer];
this.logBuffer = [];
// Send each log entry separately to preserve individual log lines
for (const entry of entries) {
const formattedMessage: string = this.formatMessage(
entry.severity,
entry.message,
entry.timestamp,
);
await this.sendLogToServer(entry.severity, formattedMessage);
}
}
// Cleanup method - call when task is done
public async dispose(): Promise<void> {
this.stopFlushTimer();
await this.flush();
}
// Helper methods for common log patterns
public async logStepStart(stepName: string): Promise<void> {
await this.info(`Starting: ${stepName}`);
}
public async logStepComplete(stepName: string): Promise<void> {
await this.info(`Completed: ${stepName}`);
}
public async logStepFailed(stepName: string, error: string): Promise<void> {
await this.error(`Failed: ${stepName} - ${error}`);
}
// Create a child logger with additional context
public createChildLogger(childContext: string): TaskLogger {
const fullContext: string = this.context
? `${this.context}:${childContext}`
: childContext;
return new TaskLogger({
taskId: this.taskId,
context: fullContext,
batchSize: this.batchSize,
flushIntervalMs: this.flushIntervalMs,
});
}
}

View File

@@ -1,221 +0,0 @@
import LocalFile from "Common/Server/Utils/LocalFile";
import logger from "Common/Server/Utils/Logger";
import ObjectID from "Common/Types/ObjectID";
import path from "path";
import os from "os";
export interface WorkspaceInfo {
workspacePath: string;
taskId: string;
createdAt: Date;
}
export default class WorkspaceManager {
private static readonly BASE_TEMP_DIR: string = path.join(
os.tmpdir(),
"oneuptime-ai-agent",
);
// Create a new workspace for a task
public static async createWorkspace(taskId: string): Promise<WorkspaceInfo> {
const timestamp: number = Date.now();
const uniqueId: string = ObjectID.generate().toString().substring(0, 8);
const workspaceName: string = `task-${taskId}-${timestamp}-${uniqueId}`;
const workspacePath: string = path.join(this.BASE_TEMP_DIR, workspaceName);
logger.debug(`Creating workspace: ${workspacePath}`);
// Create the workspace directory
await LocalFile.makeDirectory(workspacePath);
return {
workspacePath,
taskId,
createdAt: new Date(),
};
}
// Create a subdirectory within a workspace
public static async createSubdirectory(
workspacePath: string,
subdirectoryName: string,
): Promise<string> {
const subdirectoryPath: string = path.join(workspacePath, subdirectoryName);
await LocalFile.makeDirectory(subdirectoryPath);
return subdirectoryPath;
}
// Check if workspace exists
public static async workspaceExists(workspacePath: string): Promise<boolean> {
try {
await LocalFile.readDirectory(workspacePath);
return true;
} catch {
return false;
}
}
// Delete a workspace and all its contents
public static async deleteWorkspace(workspacePath: string): Promise<void> {
logger.debug(`Deleting workspace: ${workspacePath}`);
try {
// Verify the path is within our temp directory to prevent accidental deletion
const normalizedPath: string = path.normalize(workspacePath);
const normalizedBase: string = path.normalize(this.BASE_TEMP_DIR);
if (!normalizedPath.startsWith(normalizedBase)) {
throw new Error(
`Security error: Cannot delete path outside workspace base: ${workspacePath}`,
);
}
await LocalFile.deleteDirectory(workspacePath);
logger.debug(`Workspace deleted: ${workspacePath}`);
} catch (error) {
logger.error(`Error deleting workspace ${workspacePath}:`);
logger.error(error);
}
}
// Write a file to workspace
public static async writeFile(
workspacePath: string,
relativePath: string,
content: string,
): Promise<string> {
const filePath: string = path.join(workspacePath, relativePath);
// Ensure parent directory exists
const parentDir: string = path.dirname(filePath);
await LocalFile.makeDirectory(parentDir);
await LocalFile.write(filePath, content);
return filePath;
}
// Read a file from workspace
public static async readFile(
workspacePath: string,
relativePath: string,
): Promise<string> {
const filePath: string = path.join(workspacePath, relativePath);
return LocalFile.read(filePath);
}
// Check if a file exists in workspace
public static async fileExists(
workspacePath: string,
relativePath: string,
): Promise<boolean> {
try {
const filePath: string = path.join(workspacePath, relativePath);
await LocalFile.read(filePath);
return true;
} catch {
return false;
}
}
// Delete a file from workspace
public static async deleteFile(
workspacePath: string,
relativePath: string,
): Promise<void> {
const filePath: string = path.join(workspacePath, relativePath);
await LocalFile.deleteFile(filePath);
}
// List files in workspace directory
public static async listFiles(workspacePath: string): Promise<Array<string>> {
const entries: Array<{ name: string; isDirectory(): boolean }> =
await LocalFile.readDirectory(workspacePath);
return entries.map((entry: { name: string }) => {
return entry.name;
});
}
// Get the full path for a relative path in workspace
public static getFullPath(
workspacePath: string,
relativePath: string,
): string {
return path.join(workspacePath, relativePath);
}
// Clean up old workspaces (older than specified hours)
public static async cleanupOldWorkspaces(
maxAgeHours: number = 24,
): Promise<number> {
logger.debug(`Cleaning up workspaces older than ${maxAgeHours} hours`);
let cleanedCount: number = 0;
try {
// Ensure base directory exists
try {
await LocalFile.readDirectory(this.BASE_TEMP_DIR);
} catch {
// Base directory doesn't exist, nothing to clean
return 0;
}
const entries: Array<{ name: string; isDirectory(): boolean }> =
await LocalFile.readDirectory(this.BASE_TEMP_DIR);
const maxAge: number = maxAgeHours * 60 * 60 * 1000; // Convert to milliseconds
const now: number = Date.now();
for (const entry of entries) {
if (!entry.isDirectory()) {
continue;
}
const workspacePath: string = path.join(this.BASE_TEMP_DIR, entry.name);
/*
* Try to extract timestamp from directory name
* Format: task-{taskId}-{timestamp}-{uniqueId}
*/
const match: RegExpMatchArray | null = entry.name.match(
/task-[^-]+-(\d+)-[^-]+/,
);
if (match) {
const timestamp: number = parseInt(match[1] || "0", 10);
if (now - timestamp > maxAge) {
await this.deleteWorkspace(workspacePath);
cleanedCount++;
}
}
}
} catch (error) {
logger.error("Error during workspace cleanup:");
logger.error(error);
}
logger.debug(`Cleaned up ${cleanedCount} old workspaces`);
return cleanedCount;
}
// Initialize workspace manager (create base directory if needed)
public static async initialize(): Promise<void> {
try {
await LocalFile.makeDirectory(this.BASE_TEMP_DIR);
logger.debug(
`Workspace base directory initialized: ${this.BASE_TEMP_DIR}`,
);
} catch (error) {
logger.error("Error initializing workspace manager:");
logger.error(error);
}
}
// Get the base temp directory path
public static getBaseTempDir(): string {
return this.BASE_TEMP_DIR;
}
}

View File

@@ -1,11 +0,0 @@
{
"watch": [
"./",
"../Common"
],
"ext": "ts,tsx",
"ignore": ["./node_modules/**", "./public/**", "./bin/**", "./build/**"],
"watchOptions": {"useFsEvents": false, "interval": 500},
"env": {"TS_NODE_TRANSPILE_ONLY": "1", "TS_NODE_FILES": "false"},
"exec": "node -r ts-node/register/transpile-only Index.ts"
}

4777
AIAgent/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,34 +0,0 @@
{
"name": "@oneuptime/ai-agent",
"version": "1.0.0",
"description": "OneUptime AI Agent",
"repository": {
"type": "git",
"url": "https://github.com/OneUptime/oneuptime"
},
"main": "index.js",
"scripts": {
"start": "export NODE_OPTIONS='--max-old-space-size=8096' && node --require ts-node/register Index.ts",
"compile": "tsc",
"clear-modules": "rm -rf node_modules && rm package-lock.json && npm install",
"dev": "npx nodemon",
"audit": "npm audit --audit-level=low",
"dep-check": "npm install -g depcheck && depcheck ./ --skip-missing=true",
"test": "jest --detectOpenHandles --passWithNoTests",
"coverage": "jest --detectOpenHandles --coverage",
"debug:test": "node --inspect node_modules/.bin/jest --runInBand ./Tests --detectOpenHandles"
},
"author": "OneUptime <hello@oneuptime.com> (https://oneuptime.com/)",
"license": "Apache-2.0",
"dependencies": {
"Common": "file:../Common",
"ejs": "^3.1.10",
"ts-node": "^10.9.1"
},
"devDependencies": {
"@types/jest": "^27.5.2",
"@types/node": "^17.0.31",
"jest": "^28.1.0",
"nodemon": "^2.0.20"
}
}

View File

@@ -1,45 +0,0 @@
{
"ts-node": {
"compilerOptions": {
"module": "commonjs",
"resolveJsonModule": true
}
},
"compilerOptions": {
"target": "es2017",
"jsx": "react",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"rootDir": "",
"moduleResolution": "node",
"typeRoots": [
"./node_modules/@types"
],
"types": ["node", "jest"],
"sourceMap": true,
"outDir": "build/dist",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"useUnknownInCatchVariables": true,
"alwaysStrict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"skipLibCheck": true,
"resolveJsonModule": true
},
"include": ["/**/*.ts"],
"exclude": ["node_modules"]
}

30
APIReference/.gitignore vendored Executable file
View File

@@ -0,0 +1,30 @@
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
#/backend/node_modules
/kubernetes
/node_modules
.idea
# misc
.DS_Store
npm-debug.log*
yarn-debug.log*
yarn-error.log*
yarn.lock
**/*/paymentService.test.js
apiTest.rest
application_security_dir
container_security_dir
# coverage
/coverage
/.nyc_output
/greenlock.d/config.json
/greenlock.d/config.json.bak
/.greenlockrc

View File

@@ -0,0 +1,6 @@
{
"query": {
"name": "Hello",
// other filters
}
}

View File

@@ -0,0 +1,70 @@
#
# OneUptime-App Dockerfile
#
# Pull base image nodejs image.
FROM public.ecr.aws/docker/library/node:23.8-alpine3.21
RUN mkdir /tmp/npm && chmod 2777 /tmp/npm && chown 1000:1000 /tmp/npm && npm config set cache /tmp/npm --global
RUN npm config set fetch-retries 5
RUN npm config set fetch-retry-mintimeout 100000
RUN npm config set fetch-retry-maxtimeout 600000
ARG GIT_SHA
ARG APP_VERSION
ENV GIT_SHA=${GIT_SHA}
ENV APP_VERSION=${APP_VERSION}
# IF APP_VERSION is not set, set it to 1.0.0
RUN if [ -z "$APP_VERSION" ]; then export APP_VERSION=1.0.0; fi
# Install bash.
RUN apk add bash && apk add curl
# Install python
RUN apk update && apk add --no-cache --virtual .gyp python3 make g++
#Use bash shell by default
SHELL ["/bin/bash", "-c"]
RUN mkdir /usr/src
WORKDIR /usr/src/Common
COPY ./Common/package*.json /usr/src/Common/
# Set version in ./Common/package.json to the APP_VERSION
RUN sed -i "s/\"version\": \".*\"/\"version\": \"$APP_VERSION\"/g" /usr/src/Common/package.json
RUN npm install
COPY ./Common /usr/src/Common
ENV PRODUCTION=true
WORKDIR /usr/src/app
# Install app dependencies
COPY ./APIReference/package*.json /usr/src/app/
# Set version in ./App/package.json to the APP_VERSION
RUN sed -i "s/\"version\": \".*\"/\"version\": \"$APP_VERSION\"/g" /usr/src/app/package.json
RUN npm install
# Expose ports.
# - 1446: OneUptime-api-reference
EXPOSE 1446
{{ if eq .Env.ENVIRONMENT "development" }}
#Run the app
CMD [ "npm", "run", "dev" ]
{{ else }}
# Copy app source
COPY ./APIReference /usr/src/app
# Bundle app source
RUN npm run compile
#Run the app
CMD [ "npm", "start" ]
{{ end }}

52
APIReference/Index.ts Executable file
View File

@@ -0,0 +1,52 @@
import APIReferenceRoutes from "./Routes";
import { PromiseVoidFunction } from "Common/Types/FunctionTypes";
import InfrastructureStatus from "Common/Server/Infrastructure/Status";
import logger from "Common/Server/Utils/Logger";
import App from "Common/Server/Utils/StartServer";
import Telemetry from "Common/Server/Utils/Telemetry";
import "ejs";
const APP_NAME: string = "reference";
const init: PromiseVoidFunction = async (): Promise<void> => {
try {
// Initialize telemetry
Telemetry.init({
serviceName: APP_NAME,
});
const statusCheck: PromiseVoidFunction = async (): Promise<void> => {
// Check the status of infrastructure components
return await InfrastructureStatus.checkStatusWithRetry({
checkClickhouseStatus: false,
checkPostgresStatus: false,
checkRedisStatus: false,
retryCount: 3,
});
};
// Initialize the app with service name and status checks
await App.init({
appName: APP_NAME,
statusOptions: {
liveCheck: statusCheck,
readyCheck: statusCheck,
},
});
await APIReferenceRoutes.init();
// Add default routes to the app
await App.addDefaultRoutes();
} catch (err) {
logger.error("App Init Failed:");
logger.error(err);
throw err;
}
};
init().catch((err: Error) => {
logger.error(err);
logger.error("Exiting node process");
process.exit(1);
});

29
APIReference/README.md Executable file
View File

@@ -0,0 +1,29 @@
# README
This README would normally document whatever steps are necessary to get your application up and running.
### What is this repository for?
- Quick summary
- Version
- [Learn Markdown](https://bitbucket.org/tutorials/markdowndemo)
### How do I get set up?
- Summary of set up
- Configuration
- Dependencies
- Database configuration
- How to run tests
- Deployment instructions
### Contribution guidelines
- Writing tests
- Code review
- Other guidelines
### Who do I talk to?
- Repo owner or admin
- Other community or team contact

86
APIReference/Routes.ts Executable file
View File

@@ -0,0 +1,86 @@
import AuthenticationServiceHandler from "./Service/Authentication";
import DataTypeServiceHandler from "./Service/DataType";
import ErrorServiceHandler from "./Service/Errors";
import IntroductionServiceHandler from "./Service/Introduction";
import ModelServiceHandler from "./Service/Model";
import PageNotFoundServiceHandler from "./Service/PageNotFound";
import PaginationServiceHandler from "./Service/Pagination";
import PermissionServiceHandler from "./Service/Permissions";
import StatusServiceHandler from "./Service/Status";
import { StaticPath } from "./Utils/Config";
import ResourceUtil, { ModelDocumentation } from "./Utils/Resources";
import Dictionary from "Common/Types/Dictionary";
import FeatureSet from "Common/Server/Types/FeatureSet";
import Express, {
ExpressApplication,
ExpressRequest,
ExpressResponse,
ExpressStatic,
} from "Common/Server/Utils/Express";
const APIReferenceFeatureSet: FeatureSet = {
init: async (): Promise<void> => {
const ResourceDictionary: Dictionary<ModelDocumentation> =
ResourceUtil.getResourceDictionaryByPath();
const app: ExpressApplication = Express.getExpressApp();
// Serve static files for the API reference with a cache max age of 30 days
app.use("/reference", ExpressStatic(StaticPath, { maxAge: 2592000 }));
// Redirect index page to the introduction page
app.get(["/reference"], (_req: ExpressRequest, res: ExpressResponse) => {
return res.redirect("/reference/introduction");
});
// Handle "Page Not Found" page
app.get(
["/reference/page-not-found"],
(req: ExpressRequest, res: ExpressResponse) => {
return PageNotFoundServiceHandler.executeResponse(req, res);
},
);
// Handle all other pages based on the "page" parameter
app.get(
["/reference/:page"],
(req: ExpressRequest, res: ExpressResponse) => {
const page: string | undefined = req.params["page"];
if (!page) {
return PageNotFoundServiceHandler.executeResponse(req, res);
}
const currentResource: ModelDocumentation | undefined =
ResourceDictionary[page];
// Execute the appropriate service handler based on the "page" parameter
if (req.params["page"] === "permissions") {
return PermissionServiceHandler.executeResponse(req, res);
} else if (req.params["page"] === "authentication") {
return AuthenticationServiceHandler.executeResponse(req, res);
} else if (req.params["page"] === "pagination") {
return PaginationServiceHandler.executeResponse(req, res);
} else if (req.params["page"] === "errors") {
return ErrorServiceHandler.executeResponse(req, res);
} else if (req.params["page"] === "introduction") {
return IntroductionServiceHandler.executeResponse(req, res);
} else if (req.params["page"] === "status") {
return StatusServiceHandler.executeResponse(req, res);
} else if (req.params["page"] === "data-types") {
return DataTypeServiceHandler.executeResponse(req, res);
} else if (currentResource) {
return ModelServiceHandler.executeResponse(req, res);
}
// page not found
return PageNotFoundServiceHandler.executeResponse(req, res);
},
);
app.get("/reference/*", (req: ExpressRequest, res: ExpressResponse) => {
return PageNotFoundServiceHandler.executeResponse(req, res);
});
},
};
export default APIReferenceFeatureSet;

View File

@@ -1,13 +1,10 @@
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import Dictionary from "Common/Types/Dictionary";
// Retrieve resources documentation
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
const DataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
export default class ServiceHandler {
public static async executeResponse(
@@ -19,7 +16,7 @@ export default class ServiceHandler {
// Extract page parameter from request
const page: string | undefined = req.params["page"];
const pageData: Dictionary<unknown> = {};
const pageData: any = {};
// Set default page title and description for the authentication page
pageTitle = "Authentication";
@@ -29,7 +26,6 @@ export default class ServiceHandler {
return res.render(`${ViewsPath}/pages/index`, {
page: page,
resources: Resources,
dataTypes: DataTypes,
pageTitle: pageTitle,
enableGoogleTagManager: IsBillingEnabled,
pageDescription: pageDescription,

View File

@@ -0,0 +1,148 @@
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { CodeExamplesPath, ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import LocalCache from "Common/Server/Infrastructure/LocalCache";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import LocalFile from "Common/Server/Utils/LocalFile";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
export default class ServiceHandler {
public static async executeResponse(
_req: ExpressRequest,
res: ExpressResponse,
): Promise<void> {
const pageData: any = {};
pageData.selectCode = await LocalCache.getOrSetString(
"data-type",
"select",
async () => {
return await LocalFile.read(`${CodeExamplesPath}/DataTypes/Select.md`);
},
);
pageData.sortCode = await LocalCache.getOrSetString(
"data-type",
"sort",
async () => {
return await LocalFile.read(`${CodeExamplesPath}/DataTypes/Sort.md`);
},
);
pageData.equalToCode = await LocalCache.getOrSetString(
"data-type",
"equal-to",
async () => {
return await LocalFile.read(`${CodeExamplesPath}/DataTypes/EqualTo.md`);
},
);
pageData.equalToOrNullCode = await LocalCache.getOrSetString(
"data-type",
"equal-to-or-null",
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/DataTypes/EqualToOrNull.md`,
);
},
);
pageData.greaterThanCode = await LocalCache.getOrSetString(
"data-type",
"greater-than",
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/DataTypes/GreaterThan.md`,
);
},
);
pageData.greaterThanOrEqualCode = await LocalCache.getOrSetString(
"data-type",
"greater-than-or-equal",
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/DataTypes/GreaterThanOrEqual.md`,
);
},
);
pageData.lessThanCode = await LocalCache.getOrSetString(
"data-type",
"less-than",
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/DataTypes/LessThan.md`,
);
},
);
pageData.lessThanOrEqualCode = await LocalCache.getOrSetString(
"data-type",
"less-than-or-equal",
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/DataTypes/LessThanOrEqual.md`,
);
},
);
pageData.lessThanOrNullCode = await LocalCache.getOrSetString(
"data-type",
"less-than-or-equal",
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/DataTypes/LessThanOrNull.md`,
);
},
);
pageData.greaterThanOrNullCode = await LocalCache.getOrSetString(
"data-type",
"less-than-or-equal",
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/DataTypes/LessThanOrNull.md`,
);
},
);
pageData.isNullCode = await LocalCache.getOrSetString(
"data-type",
"is-null",
async () => {
return await LocalFile.read(`${CodeExamplesPath}/DataTypes/IsNull.md`);
},
);
pageData.notNullCode = await LocalCache.getOrSetString(
"data-type",
"not-null",
async () => {
return await LocalFile.read(`${CodeExamplesPath}/DataTypes/NotNull.md`);
},
);
pageData.notEqualToCode = await LocalCache.getOrSetString(
"data-type",
"not-equals",
async () => {
return await LocalFile.read(
`${CodeExamplesPath}/DataTypes/NotEqualTo.md`,
);
},
);
res.status(200);
return res.render(`${ViewsPath}/pages/index`, {
page: "data-types",
pageTitle: "Data Types",
enableGoogleTagManager: IsBillingEnabled,
pageDescription:
"Data Types that can be used to interact with OneUptime API",
resources: Resources,
pageData: pageData,
});
}
}

View File

@@ -1,13 +1,10 @@
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import Dictionary from "Common/Types/Dictionary";
// Fetch a list of resources used in the application
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
const DataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
export default class ServiceHandler {
// Handles the HTTP response for a given request
@@ -20,7 +17,7 @@ export default class ServiceHandler {
// Get the 'page' parameter from the request
const page: string | undefined = req.params["page"];
const pageData: Dictionary<unknown> = {};
const pageData: any = {};
// Set the default page title and description
pageTitle = "Errors";
@@ -30,7 +27,6 @@ export default class ServiceHandler {
return res.render(`${ViewsPath}/pages/index`, {
page: page,
resources: Resources,
dataTypes: DataTypes,
pageTitle: pageTitle,
enableGoogleTagManager: IsBillingEnabled,
pageDescription: pageDescription,

View File

@@ -1,13 +1,10 @@
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import Dictionary from "Common/Types/Dictionary";
// Get all resources and featured resources from ResourceUtil
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
const DataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
const FeaturedResources: Array<ModelDocumentation> =
ResourceUtil.getFeaturedResources();
@@ -23,10 +20,10 @@ export default class ServiceHandler {
// Get the requested page from the URL parameters
const page: string | undefined = req.params["page"];
const pageData: Dictionary<unknown> = {};
const pageData: any = {};
// Set featured resources for the page
pageData["featuredResources"] = FeaturedResources;
pageData.featuredResources = FeaturedResources;
// Set page title and description
pageTitle = "Introduction";
@@ -36,7 +33,6 @@ export default class ServiceHandler {
return res.render(`${ViewsPath}/pages/index`, {
page: page,
resources: Resources,
dataTypes: DataTypes,
pageTitle: pageTitle,
enableGoogleTagManager: IsBillingEnabled,
pageDescription: pageDescription,

View File

@@ -0,0 +1,271 @@
import { CodeExamplesPath, ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import PageNotFoundServiceHandler from "./PageNotFound";
import { AppApiRoute } from "Common/ServiceRoute";
import { ColumnAccessControl } from "Common/Types/BaseDatabase/AccessControl";
import { getTableColumns } from "Common/Types/Database/TableColumn";
import Dictionary from "Common/Types/Dictionary";
import ObjectID from "Common/Types/ObjectID";
import Permission, {
PermissionHelper,
PermissionProps,
} from "Common/Types/Permission";
import LocalCache from "Common/Server/Infrastructure/LocalCache";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import LocalFile from "Common/Server/Utils/LocalFile";
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
// Get all resources and resource dictionary
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources();
const ResourceDictionary: Dictionary<ModelDocumentation> =
ResourceUtil.getResourceDictionaryByPath();
// Get all permission props
const PermissionDictionary: Dictionary<PermissionProps> =
PermissionHelper.getAllPermissionPropsAsDictionary();
export default class ServiceHandler {
// Execute response for a given page
public static async executeResponse(
req: ExpressRequest,
res: ExpressResponse,
): Promise<void> {
let pageTitle: string = "";
let pageDescription: string = "";
let page: string | undefined = req.params["page"];
const pageData: any = {};
// Check if page is provided
if (!page) {
return PageNotFoundServiceHandler.executeResponse(req, res);
}
// Get current resource
const currentResource: ModelDocumentation | undefined =
ResourceDictionary[page];
// Check if current resource exists
if (!currentResource) {
return PageNotFoundServiceHandler.executeResponse(req, res);
}
// Set page title and description
pageTitle = currentResource.name;
pageDescription = currentResource.description;
page = "model";
// Get table columns for current resource
const tableColumns: any = getTableColumns(currentResource.model);
// Filter out columns with no access
for (const key in tableColumns) {
const accessControl: ColumnAccessControl | null =
currentResource.model.getColumnAccessControlFor(key);
if (!accessControl) {
delete tableColumns[key];
continue;
}
if (
accessControl?.create.length === 0 &&
accessControl?.read.length === 0 &&
accessControl?.update.length === 0
) {
delete tableColumns[key];
continue;
}
tableColumns[key].permissions = accessControl;
}
// Remove unnecessary columns
delete tableColumns["deletedAt"];
delete tableColumns["deletedByUserId"];
delete tableColumns["deletedByUser"];
delete tableColumns["version"];
// Set page data
pageData.title = currentResource.model.singularName;
pageData.description = currentResource.model.tableDescription;
pageData.columns = tableColumns;
pageData.tablePermissions = {
read: currentResource.model.readRecordPermissions.map(
(permission: Permission) => {
return PermissionDictionary[permission];
},
),
update: currentResource.model.updateRecordPermissions.map(
(permission: Permission) => {
return PermissionDictionary[permission];
},
),
delete: currentResource.model.deleteRecordPermissions.map(
(permission: Permission) => {
return PermissionDictionary[permission];
},
),
create: currentResource.model.createRecordPermissions.map(
(permission: Permission) => {
return PermissionDictionary[permission];
},
),
};
// Cache the list request data
pageData.listRequest = await LocalCache.getOrSetString(
"model",
"list-request",
async () => {
// Read the list request data from a file
return await LocalFile.read(`${CodeExamplesPath}/Model/ListRequest.md`);
},
);
// Cache the item request data
pageData.itemRequest = await LocalCache.getOrSetString(
"model",
"item-request",
async () => {
// Read the item request data from a file
return await LocalFile.read(`${CodeExamplesPath}/Model/ItemRequest.md`);
},
);
// Cache the item response data
pageData.itemResponse = await LocalCache.getOrSetString(
"model",
"item-response",
async () => {
// Read the item response data from a file
return await LocalFile.read(
`${CodeExamplesPath}/Model/ItemResponse.md`,
);
},
);
// Cache the count request data
pageData.countRequest = await LocalCache.getOrSetString(
"model",
"count-request",
async () => {
// Read the count request data from a file
return await LocalFile.read(
`${CodeExamplesPath}/Model/CountRequest.md`,
);
},
);
// Cache the count response data
pageData.countResponse = await LocalCache.getOrSetString(
"model",
"count-response",
async () => {
// Read the CountResponse.md file
return await LocalFile.read(
`${CodeExamplesPath}/Model/CountResponse.md`,
);
},
);
pageData.updateRequest = await LocalCache.getOrSetString(
"model",
"update-request",
async () => {
// Read the UpdateRequest.md file
return await LocalFile.read(
`${CodeExamplesPath}/Model/UpdateRequest.md`,
);
},
);
pageData.updateResponse = await LocalCache.getOrSetString(
"model",
"update-response",
async () => {
// Read the UpdateResponse.md file
return await LocalFile.read(
`${CodeExamplesPath}/Model/UpdateResponse.md`,
);
},
);
pageData.createRequest = await LocalCache.getOrSetString(
"model",
"create-request",
async () => {
// Read the CreateRequest.md file
return await LocalFile.read(
`${CodeExamplesPath}/Model/CreateRequest.md`,
);
},
);
pageData.createResponse = await LocalCache.getOrSetString(
"model",
"create-response",
async () => {
// Read the CreateResponse.md file
return await LocalFile.read(
`${CodeExamplesPath}/Model/CreateResponse.md`,
);
},
);
pageData.deleteRequest = await LocalCache.getOrSetString(
"model",
"delete-request",
async () => {
// Read the DeleteRequest.md file
return await LocalFile.read(
`${CodeExamplesPath}/Model/DeleteRequest.md`,
);
},
);
pageData.deleteResponse = await LocalCache.getOrSetString(
"model",
"delete-response",
async () => {
// Read the DeleteResponse.md file
return await LocalFile.read(
`${CodeExamplesPath}/Model/DeleteResponse.md`,
);
},
);
// Get list response from cache or set it if it's not available
pageData.listResponse = await LocalCache.getOrSetString(
"model",
"list-response",
async () => {
// Read the list response from a file
return await LocalFile.read(
`${CodeExamplesPath}/Model/ListResponse.md`,
);
},
);
// Generate a unique ID for the example object
pageData.exampleObjectID = ObjectID.generate();
// Construct the API path for the current resource
pageData.apiPath =
AppApiRoute.toString() + currentResource.model.crudApiPath?.toString();
// Check if the current resource is a master admin API
pageData.isMasterAdminApiDocs = currentResource.model.isMasterAdminApiDocs;
// Render the index page with the required data
return res.render(`${ViewsPath}/pages/index`, {
page: page,
resources: Resources,
pageTitle: pageTitle,
enableGoogleTagManager: IsBillingEnabled,
pageDescription: pageDescription,
pageData: pageData,
});
}
}

View File

@@ -1,11 +1,9 @@
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources(); // Get an array of model documentation resources
const DataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
export default class ServiceHandler {
// This is a static method that handles the response
@@ -23,7 +21,6 @@ export default class ServiceHandler {
enableGoogleTagManager: IsBillingEnabled,
pageDescription: "Page you're looking for is not found.", // The page description
resources: Resources, // The array of model documentation resources
dataTypes: DataTypes,
pageData: {}, // An empty object to hold any additional page data
});
}

View File

@@ -1,14 +1,11 @@
import { IsBillingEnabled } from "Common/Server/EnvironmentConfig";
import { CodeExamplesPath, ViewsPath } from "../Utils/Config";
import ResourceUtil, { ModelDocumentation } from "../Utils/Resources";
import DataTypeUtil, { DataTypeDocumentation } from "../Utils/DataTypes";
import LocalCache from "Common/Server/Infrastructure/LocalCache";
import { ExpressRequest, ExpressResponse } from "Common/Server/Utils/Express";
import LocalFile from "Common/Server/Utils/LocalFile";
import Dictionary from "Common/Types/Dictionary";
const Resources: Array<ModelDocumentation> = ResourceUtil.getResources(); // Get all resources from ResourceUtil
const DataTypes: Array<DataTypeDocumentation> = DataTypeUtil.getDataTypes();
export default class ServiceHandler {
public static async executeResponse(
@@ -18,14 +15,14 @@ export default class ServiceHandler {
let pageTitle: string = ""; // Initialize page title
let pageDescription: string = ""; // Initialize page description
const page: string | undefined = req.params["page"]; // Get the page parameter from the request
const pageData: Dictionary<unknown> = {}; // Initialize page data object
const pageData: any = {}; // Initialize page data object
// Set page title and description
pageTitle = "Pagination";
pageDescription = "Learn how to paginate requests with OneUptime API";
// Get response and request code from LocalCache or LocalFile
pageData["responseCode"] = await LocalCache.getOrSetString(
pageData.responseCode = await LocalCache.getOrSetString(
"pagination",
"response",
async () => {
@@ -36,7 +33,7 @@ export default class ServiceHandler {
},
);
pageData["requestCode"] = await LocalCache.getOrSetString(
pageData.requestCode = await LocalCache.getOrSetString(
"pagination",
"request",
async () => {
@@ -51,7 +48,6 @@ export default class ServiceHandler {
return res.render(`${ViewsPath}/pages/index`, {
page: page, // Pass the page parameter
resources: Resources, // Pass all resources
dataTypes: DataTypes,
pageTitle: pageTitle,
enableGoogleTagManager: IsBillingEnabled, // Pass the page title
pageDescription: pageDescription, // Pass the page description

Some files were not shown because too many files have changed in this diff Show More