Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .air.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
root = "."
tmp_dir = "tmp"

[build]
cmd = "go build -o ./tmp/runright ./cmd/runright"
bin = "./tmp/runright serve"
include_ext = ["go", "json", "yaml", "yml", "toml"]
exclude_dir = ["tmp", "bin", "web/node_modules", ".git"]
delay = 200
stop_on_error = true

[log]
time = true
7 changes: 7 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:

permissions:
contents: write
id-token: write # required for cosign keyless signing

jobs:
goreleaser:
Expand All @@ -22,6 +23,12 @@ jobs:
go-version-file: go.mod
cache: true

- name: Install cosign
uses: sigstore/cosign-installer@v3

- name: Install syft
uses: anchore/sbom-action/download-syft@v0

- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ on:
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: runright
POSTGRES_PASSWORD: runright
POSTGRES_DB: runright
ports:
- 5435:5432
options: >-
--health-cmd "pg_isready -U runright -d runright"
--health-interval 10s
--health-timeout 5s
--health-retries 10
steps:
- uses: actions/checkout@v4

Expand All @@ -28,4 +42,6 @@ jobs:
run: go vet ./cmd/... ./internal/...

- name: go test
env:
DATABASE_URL: postgres://runright:runright@127.0.0.1:5435/runright?sslmode=disable
run: go test -race -count=1 ./cmd/... ./internal/...
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,7 @@ logs/
# Test artifacts
.runright/
metrics-summary.json

# Local benchmark and temp artifacts
tmp/
scripts/bench-agent.sh
21 changes: 21 additions & 0 deletions .goreleaser.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,27 @@ archives:
checksum:
name_template: "checksums.txt"

# Generate a Software Bill of Materials (SBOM) for each archive using syft.
sboms:
- artifacts: archive
args: ["$artifact", "--output", "spdx-json=$document"]
documents:
- "{{ .ArtifactName }}.spdx.json"

# Sign the checksums file with cosign (keyless OIDC — no long-lived key needed).
signs:
- cmd: cosign
env:
- COSIGN_EXPERIMENTAL=1
artifacts: checksum
output: true
args:
- sign-blob
- --yes
- "--output-certificate=${certificate}"
- "--output-signature=${signature}"
- "${artifact}"

release:
github:
owner: gbudjeakp
Expand Down
14 changes: 14 additions & 0 deletions Dockerfile.backend.dev
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
FROM golang:1.26-alpine

RUN apk add --no-cache git

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

RUN go install github.com/air-verse/air@v1.61.7

EXPOSE 8080

CMD ["air", "-c", ".air.toml"]
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ PLATFORMS := \
windows/amd64

.PHONY: build build-all build-linux build-linux-amd64 build-linux-arm64 \
test lint clean run-server docker-up docker-down catalog-update seed deploy
test lint clean run-server docker-up docker-down catalog-update seed deploy bench-agent

build:
go build $(BUILD_FLAGS) -o bin/$(BINARY) $(CMD_DIR)
Expand Down Expand Up @@ -91,3 +91,6 @@ web-build:

tidy:
go mod tidy

bench-agent:
./scripts/bench-agent.sh
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,24 @@ go test ./internal/...
cd web && pnpm dev
```

### Backend Hot Reload (Docker)

Use the dev profile to run the backend with live reload via Air:

```bash
docker compose up -d postgres dashboard
docker compose --profile dev up -d backend-dev
```

Now backend code changes reload automatically. You do not need `docker compose down` each time.

If `backend` is already running, stop only that service first:

```bash
docker compose stop backend
docker compose rm -f backend
```

---

Elastic License 2.0 (ELv2). See [LICENSE](LICENSE)
63 changes: 57 additions & 6 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,26 @@ inputs:
description: "Filter recommendations by cloud provider: aws, gcp, or github (default: all)"
required: false
default: ""
max-cost-per-hour:
description: "Budget guard: fail the step if the detected machine's on-demand price exceeds this value (e.g. 0.50). Empty = disabled."
required: false
default: ""
notify-slack:
description: "Slack incoming webhook URL. When set, posts a per-job saving alert on completion."
required: false
default: ""
notify-teams:
description: "Microsoft Teams incoming webhook URL. When set, posts a per-job saving alert on completion."
required: false
default: ""
dry-run:
description: "Observation-only mode: collect metrics and log recommendations but skip PR comments and artifact uploads."
required: false
default: "false"
rollup:
description: "Combine all RunRight PR comments for this workflow into a single comment table (one row per job)."
required: false
default: "false"

outputs:
recommendation-json:
Expand Down Expand Up @@ -96,6 +116,8 @@ runs:
RUNRIGHT_OUTPUT_DIR: ${{ inputs.output-dir }}
RUNRIGHT_JOB_ID: ${{ inputs.job-id }}
RUNRIGHT_HTTP_URL: ${{ inputs.http-url }}
RUNRIGHT_SLACK_WEBHOOK: ${{ inputs.notify-slack }}
RUNRIGHT_TEAMS_WEBHOOK: ${{ inputs.notify-teams }}
run: bash "$GITHUB_ACTION_PATH/scripts/monitor-start.sh"

- name: Run wrapped command
Expand All @@ -118,6 +140,8 @@ runs:
RUNRIGHT_OUTPUT_DIR: ${{ inputs.output-dir }}
RUNRIGHT_JOB_ID: ${{ inputs.job-id }}
RUNRIGHT_HTTP_URL: ${{ inputs.http-url }}
RUNRIGHT_SLACK_WEBHOOK: ${{ inputs.notify-slack }}
RUNRIGHT_TEAMS_WEBHOOK: ${{ inputs.notify-teams }}
run: bash "$GITHUB_ACTION_PATH/scripts/monitor-start.sh"

# ── Standalone: stop step ─────────────────────────────────────────────────
Expand All @@ -135,6 +159,9 @@ runs:
RUNRIGHT_OUTPUT_DIR: ${{ inputs.output-dir }}
RUNRIGHT_JOB_ID: ${{ inputs.job-id }}
RUNRIGHT_PROVIDER: ${{ inputs.provider }}
RUNRIGHT_HTTP_URL: ${{ inputs.http-url }}
RUNRIGHT_MAX_COST_PER_HOUR: ${{ inputs.max-cost-per-hour }}
RUNRIGHT_DRY_RUN: ${{ inputs.dry-run }}
run: bash "$GITHUB_ACTION_PATH/scripts/recommend.sh"

# ── PR comment ────────────────────────────────────────────────────────────
Expand All @@ -143,11 +170,13 @@ runs:
if: |
(inputs.step == 'stop' || inputs.run != '') &&
inputs.pr-comment == 'true' &&
inputs.dry-run != 'true' &&
github.event_name == 'pull_request'
uses: actions/github-script@v7
env:
COMMENT_FILE: /tmp/runright-pr-comment.md
JOB_LABEL: ${{ inputs.job-id }}
ROLLUP_MODE: ${{ inputs.rollup }}
with:
github-token: ${{ inputs.github-token }}
script: |
Expand All @@ -156,37 +185,59 @@ runs:
if (!fs.existsSync(commentPath)) { return }

const body = fs.readFileSync(commentPath, 'utf8')
const marker = `<!-- runright:${process.env.JOB_LABEL} -->`
const rollup = process.env.ROLLUP_MODE === 'true'
const jobLabel = process.env.JOB_LABEL

// In rollup mode all jobs in the same workflow share a single comment.
// In per-job mode each job has its own comment keyed by job-id.
const marker = rollup ? `<!-- runright:rollup -->` : `<!-- runright:${jobLabel} -->`

// Find an existing RunRight comment for this job so we update instead
// of creating a new comment on every push to the PR branch.
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
})
const existing = comments.find(c => c.body.includes(marker))

let newBody = body
if (rollup && existing) {
// Each job upserts its own fenced block inside the shared comment.
const jobMarker = `<!-- runright-job:${jobLabel} -->`
const endMarker = `<!-- /runright-job:${jobLabel} -->`
const jobBlock = `${jobMarker}\n${body}\n${endMarker}`
if (existing.body.includes(jobMarker)) {
newBody = existing.body.replace(
new RegExp(`${jobMarker}[\\s\\S]*?${endMarker}`),
jobBlock
)
} else {
newBody = existing.body + '\n\n---\n\n' + jobBlock
}
}

if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
body: newBody,
})
} else {
const postBody = rollup
? `${marker}\n\n<!-- runright-job:${jobLabel} -->\n${body}\n<!-- /runright-job:${jobLabel} -->`
: body
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
body,
body: postBody,
})
}

# ── Upload artifact ───────────────────────────────────────────────────────
- name: Upload metrics artifact
id: upload
if: (inputs.step == 'stop' || inputs.run != '') && inputs.upload-artifact == 'true'
if: (inputs.step == 'stop' || inputs.run != '') && inputs.upload-artifact == 'true' && inputs.dry-run != 'true'
uses: actions/upload-artifact@v4
with:
name: runright-metrics-${{ inputs.job-id }}
Expand Down
5 changes: 4 additions & 1 deletion catalog/updater/aws/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,10 @@ func convertAWS(it ec2types.InstanceTypeInfo) *types.MachineType {
if it.VCpuInfo.DefaultVCpus != nil {
vcpus = int(*it.VCpuInfo.DefaultVCpus)
}
memGiB := float64(it.MemoryInfo.SizeInMiB) / 1024.0
if it.MemoryInfo.SizeInMiB == nil {
return nil
}
memGiB := float64(*it.MemoryInfo.SizeInMiB) / 1024.0

arch := "x86_64"
if len(it.ProcessorInfo.SupportedArchitectures) > 0 {
Expand Down
92 changes: 92 additions & 0 deletions ci-examples/azure-pipelines.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# RunRight for Azure Pipelines
#
# Add this template to your azure-pipelines.yml or include it as a template.
# RunRight detects the Azure Pipelines platform automatically via the
# TF_BUILD and AGENT_* env vars exposed by every Azure DevOps agent.
#
# Requirements:
# - An ubuntu-* or self-hosted Linux agent
# - curl available (standard on Ubuntu-hosted agents)
#
# Optional: set RUNRIGHT_HTTP_URL as a pipeline or library variable to push
# metrics to your self-hosted RunRight backend.

trigger:
- main

pool:
vmImage: ubuntu-latest

variables:
RUNRIGHT_OUTPUT_DIR: $(Build.ArtifactStagingDirectory)/.runright
RUNRIGHT_INTERVAL: 5s
RUNRIGHT_EXPORT: file
# RUNRIGHT_HTTP_URL: https://your-runright-backend.example.com
# RUNRIGHT_SLACK_WEBHOOK: $(SLACK_WEBHOOK) # set as secret variable
# RUNRIGHT_TEAMS_WEBHOOK: $(TEAMS_WEBHOOK) # set as secret variable

steps:
# ── Install RunRight ───────────────────────────────────────────────────────
- bash: |
ARCH=$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')
curl -fsSL \
"https://github.com/gbudjeakp/run-right/releases/latest/download/runright_linux_${ARCH}.tar.gz" \
| tar -xz
sudo mv runright /usr/local/bin/runright
echo "RunRight installed: $(runright --version 2>&1 || true)"
displayName: Install RunRight

# ── Start monitor ──────────────────────────────────────────────────────────
- bash: |
mkdir -p "$(RUNRIGHT_OUTPUT_DIR)"
runright monitor \
--interval "$(RUNRIGHT_INTERVAL)" \
--export "$(RUNRIGHT_EXPORT)" \
--output-dir "$(RUNRIGHT_OUTPUT_DIR)" \
--job-id "$(System.JobDisplayName)" \
--http-url "$(RUNRIGHT_HTTP_URL)" \
--slack-webhook "$(RUNRIGHT_SLACK_WEBHOOK)" \
--teams-webhook "$(RUNRIGHT_TEAMS_WEBHOOK)" &
echo $! > /tmp/runright-monitor.pid
echo "RunRight monitor started (PID $(cat /tmp/runright-monitor.pid))"
displayName: Start RunRight monitor
env:
RUNRIGHT_SLACK_WEBHOOK: $(RUNRIGHT_SLACK_WEBHOOK)
RUNRIGHT_TEAMS_WEBHOOK: $(RUNRIGHT_TEAMS_WEBHOOK)

# ── Your build / test steps ────────────────────────────────────────────────
- bash: |
echo "Your build commands here"
# make build
# dotnet build
# mvn package
displayName: Build

# ── Stop monitor and recommend ─────────────────────────────────────────────
- bash: |
if [ -f /tmp/runright-monitor.pid ]; then
kill -TERM "$(cat /tmp/runright-monitor.pid)" 2>/dev/null || true
for i in $(seq 1 40); do
kill -0 "$(cat /tmp/runright-monitor.pid)" 2>/dev/null || break
sleep 0.25
done
rm -f /tmp/runright-monitor.pid
fi

SUMMARY="$(RUNRIGHT_OUTPUT_DIR)/metrics-summary.json"
if [ -f "$SUMMARY" ]; then
echo "=== RunRight Recommendations ==="
runright recommend --metrics "$SUMMARY" --format table || true
else
echo "RunRight: no metrics file found, skipping recommendations."
fi
displayName: Stop monitor & generate recommendations
condition: always()

# ── Publish metrics artifact ───────────────────────────────────────────────
- task: PublishBuildArtifacts@1
inputs:
pathToPublish: $(RUNRIGHT_OUTPUT_DIR)
artifactName: runright-metrics
displayName: Upload RunRight metrics
condition: always()
Loading
Loading