Merge branch 'main' of https://git.kocoder.xyz/Diplomarbeit-Absolventenverein/pm
Some checks failed
Word Count / count-words (push) Failing after 32s

This commit is contained in:
2026-03-05 13:47:54 +01:00
7 changed files with 584 additions and 15 deletions

View File

@@ -0,0 +1,32 @@
name: Gemini Writing Review
on:
pull_request:
types: [opened, synchronized, reopened]
jobs:
gemini-review:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Run Gemini Review
env:
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITEA_URL: ${{ github.server_url }}
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
cd scripts/gemini_review
go build -o gemini_review_bin main.go
cd ../..
./scripts/gemini_review/gemini_review_bin

View File

@@ -0,0 +1,31 @@
name: Word Count
on:
push:
branches: [ "main", "master" ]
pull_request:
branches: [ "main", "master" ]
jobs:
count-words:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22'
- name: Run Gemini Review
env:
GITEA_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITEA_URL: ${{ github.server_url }}
GITHUB_REPOSITORY: ${{ github.repository }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
cd scripts/wordcount
go build -o wordcount_bin main.go
cd ../..
./scripts/wordcount/wordcount_bin

View File

@@ -3,9 +3,76 @@ include_toc: true
gitea: none
---
## 1. Einleitung des individuellen Teils
In diesem Abschnitt wird meine persönliche Aufgabenstellung im Rahmen des Projektes (`Alumnihub`) beschrieben.
### Auftrag / persönliche Aufgabenstellungen
Meine Zuständigkeiten und Verantwortlichkeiten:
- Product Owner
- Infrastruktur
- Entwicklung
- Auswertungen
- Schwarzes Brett
### Motivation
Lernen von ASP.NET und der Entwicklung mit Blazor und Oqtane. Ich habe Interesse an dem Thema Webentwicklung. Privat entwickel ich schon seit Jahren viel mit React.JS.
## 2. Anforderungen an das entwickelte Modul bzw. die Funktionalität
- funktionale / nichtfunktionale Anforderungen
- Use Cases
## 3. Technische Grundlagen
Mein Aufgabenbereich umfasst einerseits die Entwicklung eigener Module, sowie das Bereitstellen des Services. Als Betriebssystem habe ich mich für Linux entschieden, einfach, da ich mit Linux im Serverumfeld die meisten und besten Erfahrungen gemacht habe.
(Diese Entscheidung wurde gemeinsam getroffen:)
Auch steht die Wahl der Programmiersprache und des CMS an. Nachdem wir im Unterricht fast ausschließlich mit C# entwickelt haben und nicht in eine komplett unbekannte Entwicklungsumgebung abdriften wollten, haben wir uns für Webentwicklung mit ASP.NET Core 9 und (Upgrade im Lauf der Diplomarbeit auf .NET Core 10) dem CMS Oqtane entschieden. Auch hier gab es einige Kandidaten:
- Piranha CMS
>Piranha erscheint auf den ersten Blick nicht so flexibel wie Oqtane, basiert auf .NET 8.0 und wird nicht so aktiv gewartet.
- Umbraco
>Viel Arbeit mit Partials, welche in der Admin Oberfläche geschieht, aber sehr gut dokumentiert. Im großen und ganzen wirkt Umbraco nicht so flexibel.
- DNN / Dot Net Nuke
>Platzhirsch. Kennt man, wird von der DNN Foundation gewartet. Arbeitet mit dem Dotnet Framework, welches nicht unter Linux läuft. Und ein Windows Server würde ich ich nicht einfach so ins Internet, abgesehen von den Lizenzkosten, die das kosten würde.
- Oqtane
>Schlecht dokumentiert, auf den ersten Blick sehr modular und flexibel.
Am Ende haben wir uns für das Oqtane Framework trotz seiner schlechten Dokumentation entschieden.
Im Bereich der Datenbanken musste ich mir ein paar Fragen stellen.
1. Auf welche Art Datenbank setzen wir? SQL, NoSQL, Graph, ...
2. Mit welcher speziellen implementiereung bekommen wir Support und haben Wissen im Team?
3. Ist das auserkorene System kompatibel mit dem CMS auf dem wir aufbauen?
Es war von Anfang an klar, dass es ein SQL basiertes System wird, da wir im Team nur mit SQL-basierten Systemen erfahrungen haben. Außerdem unterstützt unser CMS (Oqtane) nur SQL basierte Systeme. In der Linuxwelt kommen jetzt nur noch ein paar Datenbanken in die Auswahl: PostgreSQL, MySQL / MariaDB, SQLite. Da ist die Wahl auf PostgreSQL gefallen. Grund dafür war meine Vorerfahrung mit diesem DBMS, welche ich im Nebenjob errungen habe.
# Technologie
## Entwicklung mit Asp.Net (Was ist Blazor? / Was ist Razor? / Kestrel)
## Entwicklung mit ASP.NET (Was ist Blazor? / Was ist Razor? / Kestrel)
## Was ist Oqtane? Architektur von Oqtane?
Oqtane ist ein Framework und CMS zur Entwicklung von Webseiten mithilfe von ASP.NET und Blazor. [^5] Ein Oqtane-System besteht aus mehreren Komponenten.
In dieser Diplomarbeit fokussieren wir uns hauptsächlich auf `Themes` und `Modules`, aber es gibt auch `Language Packs` und `Pure Extensions`. [^6]
Ein `Module` (Modul) soll neue Funktionalitäten in das CMS hinzufügen und ein `Theme` soll die ganze Gestaltung der Website (die Shell) festlegen. [^6]
[^5]: https://www.oqtane.org/#about
[^6]: https://docs.oqtane.org/dev/extensions/index.html
### Architektur eines Moduls
Ein Modul in Oqtane besteht aus 4 Projekten. Server, Client, Shared und Package.
Im Server-Projekt liegt Sourcecode, welcher serverseitig ausgeführt werden soll. In der Praxis bedeutet das: alle Repositories, Controller, Manager, Migrationen und Server-Services (entwickelt nach einem Interface definiert im Client) und Server-Startuplogik.
Im Client-Projekt liegen Code und Razor-Komponenten für den Client. Also Client-Staruplogik, Client-Services (+ Inferfaces dafür, die Services hier sollen lediglich die Server-Services über HTTP aufrufen), Ressourcendateien (.resx), die Komponenten / das User Interface und die Moduldefinitionen für jedes Modul.
Im Shared-Projekt wird geteilter Sourcecode abgelegt, der server- und clientseitig verwendet wird. In der Praxis bleibt es hierbei bei den EntityFramework-Modellen zum Speichern der Daten im Arbeitsspeicher.
Im Package Projekt findet man Skripte zum Debuggen und Releasen eines Moduls. Und die NuGet-Spezifikation.
- Beim Debug werden die DLLs, PDBs und statischen Assets wie Skripte und Stylesheets der 3 anderen Projekte in den bereits gebauten Oqtane.Server `oqtane.framework/oqtane.server/bin/debug/net10.0/...` kopiert.
- Beim Release wird ein NuGet-Paket erstellt und unter oqtane.framework/oqtane.server/Packages abgelegt. Dort abgelegte NuGet-Pakete werden beim nächsten Start des Oqtane Servers installiert (DB Migrationen werden gemacht und die Pakete entpackt).
## Systemarchitektur (Postgres / Oqtane / Nginx )
```mermaid
architecture-beta
@@ -27,13 +94,13 @@ architecture-beta
## Dependency injection
### Dependency Inversion Principle [^1]
Das Dependency-Inversion-Principle (DIP / auf Deutsch: Abhängigkeits-Umkehr-Prinzip) ist eines von den 5 `SOLID` Prinzipien in der Softwareentwicklung.
Das Dependency-Inversion-Principle (DIP / auf Deutsch: Abhängigkeits-Umkehr-Prinzip) ist eines von den fünf `SOLID` Prinzipien in der Softwareentwicklung.
Das DIP unterscheidet zwischen high-level und low-level Modulen.
- Die high-level Module beschreiben die Applikations- / Buisnesslogik, ohne direkt mit den low-level Modulen zu interagieren, sondern lediglich auf abstraktionen. [^3]
- Die Abstraktionen sollen nicht von Implementierungsdetails abhängig sein, sondern die low-level Implementierung sollen gemäß der Abstraktionsschickt implemetiert werden. [^3]
- Die High-Level-Module beschreiben die Applikations- / Businesslogik, ohne direkt mit den Low-Level-Modulen zu interagieren, sondern lediglich auf Abstraktionen. [^3]
- Die Abstraktionen sollen nicht von Implementierungsdetails abhängig sein, sondern die Low-Level-Implementierung sollen gemäß der Abstraktionsschicht implementiert werden. [^3]
Ausgangslage ist eine Softwarearchitektur im Direct-Dependency-Graph Model.
Ausgangslage ist eine Softwarearchitektur im Direct-Dependency-Graph-Modell.
```mermaid
architecture-beta
@@ -45,7 +112,7 @@ architecture-beta
b:R --> L:c
```
Bei diesem Beispiel ist die Klasse A ein high-level Modul, welches direkt auf die Klasse B referenziert, was das DI-Prinzip verbietet.
Das Problem dabei: Die einzelnen Klassen sind eng gekoppelt, was das austauschen von B mit einer anderen Klasse unmöglich macht. Genau dieses Problem wird vom DIP gelöst.
Das Problem dabei: Die einzelnen Klassen sind eng gekoppelt, was das Austauschen von B mit einer anderen Klasse unmöglich macht. Genau dieses Problem wird vom DIP gelöst.
```mermaid
architecture-beta
@@ -60,14 +127,14 @@ architecture-beta
b:B --> T:ic
ic:R <-- L:c
```
Das high-level Modul ruft lediglich eine Abstraktion eines low-level Moduls auf, welche von einem, oder mehreren low-level Modulen implementiert worden ist. Für das high-level Modul ist es hier egal, welches low-level Modul die Implementierung bereitstellt. Dadurch erhält man einen viel modulareren Aufbau in der Software. Die einzelnen Module sind auch leichter austauschbar, testbar. Genau diese Modularität macht dependency injection möglich.
Das High-Level-Modul ruft lediglich eine Abstraktion eines Low-Level-Moduls auf, welche von einem, oder mehreren Low-Level-Modulen implementiert wurde. Für das High-Level-Modul ist es hier egal, welches Low-Level-Modul die Implementierung bereitstellt. Dadurch erhält man einen viel modulareren Aufbau in der Software. Die einzelnen Module sind auch leichter austauschbar und testbar. Genau diese Modularität macht Dependency Injection möglich.
### Microsoft Dependency Injection Framework
Dependency Injektion ist in .Net genau so wie Konfiguration, Protokollierung und das Optionsmuster ins Framework integriert. [^4]
Dependency Injektion ist in .NET genau so wie Konfiguration, Protokollierung und das Optionsmuster ins Framework integriert. [^4]
Alle Dependencies werden in einem `service container` zur verwaltung registriert. .Net hat einen eingebauten `service container` (eine Implementierung des `IServiceProvider`). [^4]
Alle Dependencies werden in einem `Service-Container` zur Verwaltung registriert. .NET hat einen eingebauten `Service-Container` (eine Implementierung des `IServiceProvider`). [^4]
Das Dependency Injection Framework verwaltet alle Instanzen. Nach Bedarf werden instanzen erstellt, oder wieder entsorgt (sofern das Service nicht mehr gebraucht wird). Beim instanzieren einer Klasse werden alle im Konstruktor erwarteten Dependencies bereitgestellt, bzw. selbst instanziert und dannach bereitgestellt. [^4]
Das Dependency Injection Framework verwaltet alle Instanzen. Nach Bedarf werden Instanzen erstellt, oder wieder entsorgt (sofern das Service nicht mehr gebraucht wird). Beim Instanziieren einer Klasse werden alle im Konstruktor erwarteten Dependencies bereitgestellt, bzw. selbst instanziiert und dannach bereitgestellt. [^4]
Hier ein Beispiel aus der Dokumentation von Microsoft: [^4]
```c#
@@ -105,13 +172,13 @@ public sealed class Worker(IMessageWriter messageWriter) : BackgroundService
}
}
```
Das ist ein simples Beispiel, welches Teile des DI Frameworks zeigt. Wir haben einen Service (Klasse Worker), ein Dependency (Klasse MessageWriter) und eine Abstraktionsebene, von dem Dependency (Interface IMessage Writer).
Das ist ein simples Beispiel, welches Teile des DI Frameworks zeigt. Wir haben einen Service (Klasse Worker), ein Dependency (Klasse MessageWriter) und eine Abstraktionsebene, von dem Dependency (Interface IMessageWriter).
Bei Programstart wird zuerst manuell der `Service Container` erstellt, dannach alle Module registriert (entweder als HostedService, oder als Modul mit einer spezifischen Lifetime (Scoped, Transient, Singleton)).
Bei Programmstart wird zuerst manuell der `Service-Container` erstellt, dannach alle Module registriert (entweder als HostedService, oder als Modul mit einer spezifischen Lifetime (Scoped, Transient, Singleton)).
Mit dem Aufruf von `builder.Build()` wird intern ein Dependency Graph erstellt und mit `host.Run()` wird versucht die Klasse Worker zu instanzieren und zu starten. Nachdem Worker ein Dependency auf IMessageWriter hat wird über den zuvor erstelltem Dependency Graph die implementierung von IMessageWriter gesucht. Jetzt wird MessageWriter instanziert und dem Konstruktor von Worker übergeben, damit seine Dependencies befriedigt werden.
Mit dem Aufruf von `builder.Build()` wird intern ein Dependency Graph erstellt und mit `host.Run()` wird versucht die Klasse Worker zu instanziieren und zu starten. Nachdem Worker ein Dependency auf IMessageWriter hat, wird über den zuvor erstellten Dependency-Graph die Implementierung von IMessageWriter gesucht. Jetzt wird MessageWriter instanziiert und dem Konstruktor von Worker übergeben, damit seine Dependencies befriedigt werden.
So sehen der Abhängigkeitsgraph bei diesem Beispiel aus.
So sieht der Abhängigkeitsgraph bei diesem Beispiel aus.
```mermaid
architecture-beta
@@ -130,6 +197,29 @@ architecture-beta
[^3]: https://www.oodesign.com/dependency-inversion-principle
[^4]: https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection/overview
# Continuous Integration
## Automatisierter Build und Release Prozess mithilfe von Gitea Actions.
Gitea, das Versionskontrollsystem dieser Diplomarbeit, hat einen Continuous-Integration-System eingebaut. Im Kern ist es baugleich zu den GitHub-Pipelines. Man kann im `.gitea/workflow` Ordner `.yml` Dateien ablegen, welche dann das Verhalten der Workflows definieren.
Man kann definieren auf welcher Änderung im Git Repository die Pipeline losgetreten wird (Keyword: `on`) und entweder eigene Kommandos aufreihen, oder auf bestehende `actions` zurückgreifen, welche dann der Reihe nach ausgeführt werden (Keyword: `jobs`).
Die meisten Pipelines sind folgendermaßen Aufgebaut:
Clone -> Checkout -> Submodule Checkout (optional) -> Dependencies einrichten (zum Beispiel das dotnet SDK) -> Build ausführen. -> Release erstellen und Artefakte veröffentlichen (z.B. in Registries). Aber man kann auch andere Dinge tun, z.B. mithilfe von Künstlicher Intelligenz Code und Dokumentation überprüfen.
Anwendungen von Gitea Actions bei dieser Diplomarbeit:
- APT-Package Repository:
> Zum Bauen von Oqtane und allen Modulen, verpacken in ein .deb Paket und in die Registry pushen.
- Interfaces Projekt
> Zum Bauen vom Interfaces-Projekt, verpacken in ein NuGet Paket und in die Registry pushen.
- ursprünglich: oqtane.framework
> Zum bauen und Verpacken in einen Docker Container und in die Registry pushen.
- PM Repository:
> Zum automatischen Überprüfen der Dokumente, unter anderem, mithilfe von KI, wie zum Beispiel Gemini.
# Projektmanagement
## Scrum
## YouTrack
@@ -142,7 +232,7 @@ architecture-beta
## Arbeitszeiteinschätzung (Zeitverzug)
## Teamleitung (Motivation / Downsizing)
## Produktion != Staging
## Sprints und Meetings (in Zukunft ja Asyncron
## Sprints und Meetings (in Zukunft ja asynchron)
# Modules
## Mass Mailer

View File

@@ -0,0 +1,186 @@
package main
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"os"
"strconv"
"strings"
"code.gitea.io/sdk/gitea"
"github.com/google/generative-ai-go/genai"
"google.golang.org/api/option"
)
type Suggestion struct {
LineNumber uint64 `json:"line_number"`
Comment string `json:"comment"`
Snippet string `json:"snippet,omitempty"`
}
func main() {
apiKey := os.Getenv("GEMINI_API_KEY")
token := os.Getenv("GITEA_TOKEN")
baseURL := os.Getenv("GITEA_URL")
repoFullName := os.Getenv("GITHUB_REPOSITORY")
prNumberStr := os.Getenv("PR_NUMBER")
if apiKey == "" || token == "" || repoFullName == "" || prNumberStr == "" {
log.Fatal("Missing required environment variables: GEMINI_API_KEY, GITEA_TOKEN, GITHUB_REPOSITORY, PR_NUMBER")
}
if baseURL == "" {
baseURL = "https://gitea.com"
}
prNumber, err := strconv.ParseInt(prNumberStr, 10, 64)
if err != nil {
log.Fatalf("Invalid PR_NUMBER: %v", err)
}
repoParts := strings.Split(repoFullName, "/")
if len(repoParts) != 2 {
log.Fatalf("Invalid GITHUB_REPOSITORY format: %s", repoFullName)
}
owner, repo := repoParts[0], repoParts[1]
ctx := context.Background()
// Initialize Gitea Client
client, err := gitea.NewClient(baseURL, gitea.SetToken(token))
if err != nil {
log.Fatalf("Failed to create Gitea client: %v", err)
}
// Initialize Gemini Client
geminiClient, err := genai.NewClient(ctx, option.WithAPIKey(apiKey))
if err != nil {
log.Fatalf("Failed to create Gemini client: %v", err)
}
defer geminiClient.Close()
model := geminiClient.GenerativeModel("gemini-3-flash-preview")
// Get PR files
files, _, err := client.ListPullRequestFiles(owner, repo, prNumber, gitea.ListPullRequestFilesOptions{})
if err != nil {
log.Fatalf("Failed to get PR files: %v", err)
}
var giteaComments []gitea.CreatePullReviewComment
for _, file := range files {
if !strings.HasSuffix(file.Filename, ".md") {
continue
}
fmt.Printf("Reviewing file: %s\n", file.Filename)
content, err := readFile(file.Filename)
if err != nil {
fmt.Printf("Error reading file %s: %v\n", file.Filename, err)
continue
}
suggestions, err := getGeminiReview(ctx, model, content)
if err != nil {
fmt.Printf("Error getting review for %s: %v\n", file.Filename, err)
continue
}
for _, s := range suggestions {
body := s.Comment
if s.Snippet != "" {
body += fmt.Sprintf("\n\n```suggestion\n%s\n```", s.Snippet)
}
giteaComments = append(giteaComments, gitea.CreatePullReviewComment{
Path: file.Filename,
Body: body,
NewLineNum: int64(s.LineNumber),
})
}
}
if len(giteaComments) > 0 {
_, _, err = client.CreatePullReview(owner, repo, prNumber, gitea.CreatePullReviewOptions{
State: gitea.ReviewStateRequestChanges,
Body: "### 🤖 Gemini Writing Review\n\nI've found some areas for improvement in the documentation. Please see the inline comments below.",
Comments: giteaComments,
})
if err != nil {
log.Fatalf("Failed to create PR review: %v", err)
}
fmt.Printf("Successfully created PR review with %d inline comments.\n", len(giteaComments))
} else {
fmt.Println("No Markdown files to review or no suggestions found.")
}
}
func readFile(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
content, err := io.ReadAll(f)
if err != nil {
return "", err
}
return string(content), nil
}
func getGeminiReview(ctx context.Context, model *genai.GenerativeModel, content string) ([]Suggestion, error) {
prompt := fmt.Sprintf(`
Review the following Markdown content for spelling errors, grammar mistakes, and style improvements.
Analyze the text line by line.
For each issue found, provide a suggestion in JSON format with:
- "line_number": The 1-indexed line number where the issue occurs.
- "comment": A brief explanation of the problem and the suggested fix.
- "snippet": The corrected text for that line (optional, but highly recommended for spell/grammar fixes).
Return ONLY a JSON array of suggestions. If no issues are found, return "[]".
Content with line numbers for reference:
%s
`, addLineNumbers(content))
resp, err := model.GenerateContent(ctx, genai.Text(prompt))
if err != nil {
return nil, err
}
if len(resp.Candidates) == 0 || len(resp.Candidates[0].Content.Parts) == 0 {
return nil, nil
}
var rawJS strings.Builder
for _, part := range resp.Candidates[0].Content.Parts {
if text, ok := part.(genai.Text); ok {
rawJS.WriteString(string(text))
}
}
// Clean up markdown code blocks if present
js := strings.TrimSpace(rawJS.String())
js = strings.TrimPrefix(js, "```json")
js = strings.TrimSuffix(js, "```")
js = strings.TrimSpace(js)
var suggestions []Suggestion
if err := json.Unmarshal([]byte(js), &suggestions); err != nil {
return nil, fmt.Errorf("failed to unmarshal suggestions: %v (raw response: %s)", err, js)
}
return suggestions, nil
}
func addLineNumbers(text string) string {
lines := strings.Split(text, "\n")
for i, line := range lines {
lines[i] = fmt.Sprintf("%d: %s", i+1, line)
}
return strings.Join(lines, "\n")
}

44
scripts/go.mod Normal file
View File

@@ -0,0 +1,44 @@
module gemini_review
go 1.25.0
require (
cloud.google.com/go v0.115.0 // indirect
cloud.google.com/go/ai v0.8.0 // indirect
cloud.google.com/go/auth v0.18.2 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/longrunning v0.5.7 // indirect
code.gitea.io/sdk/gitea v0.23.2 // indirect
github.com/42wim/httpsig v1.2.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davidmz/go-pageant v1.0.2 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-fed/httpsig v1.1.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/google/generative-ai-go v0.20.1 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect
github.com/googleapis/gax-go/v2 v2.17.0 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.39.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.50.0 // indirect
golang.org/x/oauth2 v0.35.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
golang.org/x/time v0.14.0 // indirect
google.golang.org/api v0.269.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
google.golang.org/grpc v1.79.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
)

89
scripts/go.sum Normal file
View File

@@ -0,0 +1,89 @@
cloud.google.com/go v0.115.0 h1:CnFSK6Xo3lDYRoBKEcAtia6VSC837/ZkJuRduSFnr14=
cloud.google.com/go v0.115.0/go.mod h1:8jIM5vVgoAEoiVxQ/O4BFTfHqulPZgs/ufEzMcFMdWU=
cloud.google.com/go/ai v0.8.0 h1:rXUEz8Wp2OlrM8r1bfmpF2+VKqc1VJpafE3HgzRnD/w=
cloud.google.com/go/ai v0.8.0/go.mod h1:t3Dfk4cM61sytiggo2UyGsDVW3RF1qGZaUKDrZFyqkE=
cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM=
cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M=
cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
cloud.google.com/go/longrunning v0.5.7 h1:WLbHekDbjK1fVFD3ibpFFVoyizlLRl73I7YKuAKilhU=
cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
code.gitea.io/sdk/gitea v0.23.2 h1:iJB1FDmLegwfwjX8gotBDHdPSbk/ZR8V9VmEJaVsJYg=
code.gitea.io/sdk/gitea v0.23.2/go.mod h1:yyF5+GhljqvA30sRDreoyHILruNiy4ASufugzYg0VHM=
github.com/42wim/httpsig v1.2.3 h1:xb0YyWhkYj57SPtfSttIobJUPJZB9as1nsfo7KWVcEs=
github.com/42wim/httpsig v1.2.3/go.mod h1:nZq9OlYKDrUBhptd77IHx4/sZZD+IxTBADvAPI9G/EM=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0=
github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI=
github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/generative-ai-go v0.20.1 h1:6dEIujpgN2V0PgLhr6c/M1ynRdc7ARtiIDPFzj45uNQ=
github.com/google/generative-ai-go v0.20.1/go.mod h1:TjOnZJmZKzarWbjUJgy+r3Ee7HGBRVLhOIgupnwR4Bg=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.12 h1:Fg+zsqzYEs1ZnvmcztTYxhgCBsx3eEhEwQ1W/lHq/sQ=
github.com/googleapis/enterprise-certificate-proxy v0.3.12/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
github.com/googleapis/gax-go/v2 v2.17.0 h1:RksgfBpxqff0EZkDWYuz9q/uWsTVz+kf43LsZ1J6SMc=
github.com/googleapis/gax-go/v2 v2.17.0/go.mod h1:mzaqghpQp4JDh3HvADwrat+6M3MOIDp5YKHhb9PAgDY=
github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY=
github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0 h1:q4XOmH/0opmeuJtPsbFNivyl7bCt7yRBbeEm2sC/XtQ=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.61.0/go.mod h1:snMWehoOh2wsEwnvvwtDyFCxVeDAODenXHtn5vzrKjo=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
golang.org/x/oauth2 v0.35.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
google.golang.org/api v0.269.0 h1:qDrTOxKUQ/P0MveH6a7vZ+DNHxJQjtGm/uvdbdGXCQg=
google.golang.org/api v0.269.0/go.mod h1:N8Wpcu23Tlccl0zSHEkcAZQKDLdquxK+l9r2LkwAauE=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409 h1:merA0rdPeUV3YIIfHHcH4qBkiQAc1nfCKSI7lB4cV2M=
google.golang.org/genproto/googleapis/api v0.0.0-20260128011058-8636f8732409/go.mod h1:fl8J1IvUjCilwZzQowmw2b7HQB2eAuYBabMXzWurF+I=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d h1:t/LOSXPJ9R0B6fnZNyALBRfZBH0Uy0gT+uR+SJ6syqQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=

97
scripts/wordcount/main.go Normal file
View File

@@ -0,0 +1,97 @@
package main
import (
"fmt"
"io"
"log"
"os"
"strconv"
"strings"
"code.gitea.io/sdk/gitea"
)
func main() {
token := os.Getenv("GITEA_TOKEN")
baseURL := os.Getenv("GITEA_URL")
repoFullName := os.Getenv("GITHUB_REPOSITORY")
prNumberStr := os.Getenv("PR_NUMBER")
if token == "" || repoFullName == "" || prNumberStr == "" {
log.Fatal("Missing required environment variables: GITEA_TOKEN, GITHUB_REPOSITORY, PR_NUMBER")
}
if baseURL == "" {
baseURL = "https://gitea.com"
}
prNumber, err := strconv.ParseInt(prNumberStr, 10, 64)
if err != nil {
log.Fatalf("Invalid PR_NUMBER: %v", err)
}
repoParts := strings.Split(repoFullName, "/")
if len(repoParts) != 2 {
log.Fatalf("Invalid GITHUB_REPOSITORY format: %s", repoFullName)
}
owner, repo := repoParts[0], repoParts[1]
client, err := gitea.NewClient(baseURL, gitea.SetToken(token))
if err != nil {
log.Fatalf("Failed to create Gitea client: %v", err)
}
// Get PR files
files, _, err := client.ListPullRequestFiles(owner, repo, prNumber, gitea.ListPullRequestFilesOptions{})
if err != nil {
log.Fatalf("Failed to get PR files: %v", err)
}
var counts []string
for _, file := range files {
if !strings.HasSuffix(file.Filename, ".md") {
continue
}
fmt.Printf("Reviewing file: %s\n", file.Filename)
content, err := readFile(file.Filename)
if err != nil {
fmt.Printf("Error reading file %s: %v\n", file.Filename, err)
continue
}
count := countWords(content)
counts = append(counts, fmt.Sprintf("#### Word count for `%s`\n\nWord count: %d", file.Filename, count))
}
if len(counts) > 0 {
commentBody := "### 🤖 Word Count Report\n\n" + strings.Join(counts, "\n\n---\n\n")
_, _, err = client.CreateIssueComment(owner, repo, prNumber, gitea.CreateIssueCommentOption{
Body: commentBody,
})
if err != nil {
log.Fatalf("Failed to post PR comment: %v", err)
}
fmt.Println("Successfully posted review comments.")
} else {
fmt.Println("No Markdown files to review or no suggestions found.")
}
}
func readFile(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", err
}
defer f.Close()
content, err := io.ReadAll(f)
if err != nil {
return "", err
}
return string(content), nil
}
func countWords(text string) int {
return len(strings.Fields(text))
}