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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ concurrency:
cancel-in-progress: true

env:
GO_VERSION: '1.24.3'
GO_VERSION: '1.24.4'
TEMPL_VERSION: 'v0.3.898'

jobs:
Expand Down Expand Up @@ -169,7 +169,7 @@ jobs:

strategy:
matrix:
go-version: ['1.24.3'] # Removed 1.23 to reduce costs and complexity
go-version: ['1.24.4'] # Removed 1.23 to reduce costs and complexity
fail-fast: false

steps:
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ COPY internal/ ./internal/
RUN npx tailwindcss -i ./public/css/style.css -o ./public/css/site.css --minify

# ===== Go Build Stage =====
FROM golang:1.24-alpine AS go-builder
FROM golang:1.24.4-alpine AS go-builder

# Install templ CLI
RUN go install github.com/a-h/templ/cmd/templ@v0.3.898
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/jgndev/jgn.dev

go 1.24.3
go 1.24.4

require (
github.com/a-h/templ v0.3.898
Expand Down
57 changes: 52 additions & 5 deletions internal/application/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"os"
"strings"

"github.com/jgndev/jgn.dev/internal/site"
"github.com/labstack/echo/v4"
)

Expand Down Expand Up @@ -96,18 +97,64 @@ func (app *Application) WebhookHandler(c echo.Context) error {
})
}

// Refresh content from GitHub
log.Printf("Refreshing content due to webhook from %s", payload.Repository.FullName)
if err := app.ContentManager.RefreshContent(); err != nil {
log.Printf("Failed to refresh content: %v", err)
// Determine which content manager to refresh based on repository
repoName := payload.Repository.Name
log.Printf("Refreshing content due to webhook from %s (repo: %s)", payload.Repository.FullName, repoName)

// Import site package to access repository names
var refreshErr error
refreshed := false

// Check if this is the posts repository
if repoName == site.PostRepoName {
log.Printf("Detected posts repository, refreshing ContentManager")
refreshErr = app.ContentManager.RefreshContent()
refreshed = true
}

// Check if this is the cheatsheets repository
if repoName == site.CheatsheetRepoName {
log.Printf("Detected cheatsheets repository, refreshing CheatsheetManager")
refreshErr = app.CheatsheetManager.RefreshContent()
refreshed = true
}

// If repository wasn't recognized, refresh both managers as fallback
if !refreshed {
log.Printf("WARNING: Unknown repository '%s', refreshing both managers as fallback", repoName)

// Try to refresh posts first
if err := app.ContentManager.RefreshContent(); err != nil {
log.Printf("Failed to refresh posts during fallback: %v", err)
refreshErr = err
} else {
log.Printf("Successfully refreshed posts during fallback")
}

// Try to refresh cheatsheets
if err := app.CheatsheetManager.RefreshContent(); err != nil {
log.Printf("Failed to refresh cheatsheets during fallback: %v", err)
if refreshErr == nil {
refreshErr = err
}
} else {
log.Printf("Successfully refreshed cheatsheets during fallback")
}
}

// Handle refresh errors
if refreshErr != nil {
log.Printf("Failed to refresh content for repository %s: %v", repoName, refreshErr)
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "failed to refresh content",
"repository": repoName,
})
}

log.Printf("Successfully refreshed content from webhook")
log.Printf("Successfully refreshed content from webhook for repository: %s", repoName)
return c.JSON(http.StatusOK, map[string]string{
"message": "content refreshed successfully",
"repository": repoName,
})
}

Expand Down
143 changes: 78 additions & 65 deletions internal/contentmanager/cheatsheetmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"sort"
"strings"
"sync"
"time"
)

// CheatsheetManager manages the retrieval, storage, and filtering of cheatsheets from a remote GitHub repository.
Expand Down Expand Up @@ -41,95 +42,107 @@ func NewCheatsheetManager(repoOwner, repoName string) *CheatsheetManager {
// listRepoContent retrieves the content of a GitHub repository at a specified path using the GitHub API.
// It handles both directory listings and single file retrieval, returning a slice of githubContent or an error.
func (cm *CheatsheetManager) listRepoContent(path string) ([]githubContent, error) {
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s", cm.repoOwner, cm.repoName, path)

log.Printf("fetching cheatsheet content from: %s", url)

req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
var contents []githubContent

err := retryWithBackoff(func() error {
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s", cm.repoOwner, cm.repoName, path)

req.Header.Set("Accept", "application/vnd.github.v3+json")
log.Printf("fetching cheatsheet content from: %s", url)

// Add authentication if a token is available
if cm.githubToken != "" {
req.Header.Set("Authorization", "token "+cm.githubToken)
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}

resp, err := cm.client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
req.Header.Set("Accept", "application/vnd.github.v3+json")

if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("GitHub API returned status %d", resp.StatusCode)
}
// Add authentication if a token is available
if cm.githubToken != "" {
req.Header.Set("Authorization", "token "+cm.githubToken)
}

// Try to decode as an array first (directory listing)
var contents []githubContent
if err := json.NewDecoder(resp.Body).Decode(&contents); err != nil {
// If that fails, it might be a single file
resp.Body.Close()
resp, err = cm.client.Do(req)
resp, err := cm.client.Do(req)
if err != nil {
return nil, err
return err
}
defer resp.Body.Close()

var singleContent githubContent
if err := json.NewDecoder(resp.Body).Decode(&singleContent); err != nil {
return nil, fmt.Errorf("failed to decode response as array or single file: %v", err)
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("GitHub API returned status %d", resp.StatusCode)
}
return []githubContent{singleContent}, nil
}

return contents, nil
// Try to decode as an array first (directory listing)
if err := json.NewDecoder(resp.Body).Decode(&contents); err != nil {
// If that fails, it might be a single file
resp.Body.Close()
resp, err = cm.client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()

var singleContent githubContent
if err := json.NewDecoder(resp.Body).Decode(&singleContent); err != nil {
return fmt.Errorf("failed to decode response as array or single file: %v", err)
}
contents = []githubContent{singleContent}
}

return nil
}, 3, time.Second)

return contents, err
}

// fetchFileContent retrieves the content of a file from a GitHub repository using the GitHub API.
// It decodes the content if it is encoded in base64 and returns it as a string. Returns an error if the operation fails.
func (cm *CheatsheetManager) fetchFileContent(path string) (string, error) {
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s", cm.repoOwner, cm.repoName, path)

req, err := http.NewRequest("GET", url, nil)
if err != nil {
return "", err
}

req.Header.Set("Accept", "application/vnd.github.v3+json")

// Add authentication if a token is available
if cm.githubToken != "" {
req.Header.Set("Authorization", "token "+cm.githubToken)
}
var content string

err := retryWithBackoff(func() error {
url := fmt.Sprintf("https://api.github.com/repos/%s/%s/contents/%s", cm.repoOwner, cm.repoName, path)

resp, err := cm.client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}

var result struct {
Content string `json:"content"`
Encoding string `json:"encoding"`
}
req.Header.Set("Accept", "application/vnd.github.v3+json")

if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", err
}
// Add authentication if a token is available
if cm.githubToken != "" {
req.Header.Set("Authorization", "token "+cm.githubToken)
}

if result.Encoding == "base64" {
content, err := base64.StdEncoding.DecodeString(result.Content)
resp, err := cm.client.Do(req)
if err != nil {
return "", err
return err
}
defer resp.Body.Close()

return string(content), nil
}
var result struct {
Content string `json:"content"`
Encoding string `json:"encoding"`
}

return result.Content, nil
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
return err
}

if result.Encoding == "base64" {
decoded, err := base64.StdEncoding.DecodeString(result.Content)
if err != nil {
return err
}
content = string(decoded)
} else {
content = result.Content
}

return nil
}, 3, time.Second)

return content, err
}

// matchesAllTermsCheatsheet checks if all the provided search terms are present in a Cheatsheet's combined text fields.
Expand Down
Loading