Skip to content

Commit d954449

Browse files
committed
Add clickable GitHub profile links to contributors
1 parent c28afa7 commit d954449

15 files changed

Lines changed: 735 additions & 26 deletions

File tree

api/api.gen.go

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cmd/worker/main.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424

2525
"github.com/spongepowered/systemofadownload/internal/activity"
2626
"github.com/spongepowered/systemofadownload/internal/gitcache"
27+
"github.com/spongepowered/systemofadownload/internal/githubapi"
2728
"github.com/spongepowered/systemofadownload/internal/otelsetup"
2829
"github.com/spongepowered/systemofadownload/internal/repository"
2930
"github.com/spongepowered/systemofadownload/internal/sonatype"
@@ -38,6 +39,7 @@ type Config struct {
3839
SonatypeRepoDenyList []string
3940
DatabaseURL string
4041
GitCacheDir string
42+
GitHubToken string
4143
MetricsPort string
4244
BuildID string
4345
PodName string
@@ -98,6 +100,7 @@ func NewConfig() *Config {
98100
SonatypeRepoDenyList: repoDeny,
99101
DatabaseURL: databaseURL,
100102
GitCacheDir: gitCacheDir,
103+
GitHubToken: os.Getenv("GITHUB_TOKEN"),
101104
MetricsPort: metricsPort,
102105
BuildID: buildID,
103106
PodName: podName,
@@ -274,8 +277,11 @@ func main() {
274277
},
275278
activity.NewVersionIndexActivities,
276279
activity.NewVersionOrderingActivities,
277-
func(repo repository.Repository) *activity.ChangelogActivities {
278-
return &activity.ChangelogActivities{Repo: repo}
280+
func(cfg *Config, repo repository.Repository) *activity.ChangelogActivities {
281+
return &activity.ChangelogActivities{
282+
Repo: repo,
283+
GitHub: githubapi.NewClient(http.DefaultClient, cfg.GitHubToken),
284+
}
279285
},
280286
func(cfg *Config) *gitcache.Manager {
281287
return gitcache.NewManager(cfg.GitCacheDir)

docs/WORKFLOWS.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,15 @@ VersionSyncWorkflow ────────────────────
3737
│ ├── [per submodule, parallel] |
3838
│ │ ├── EnsureRepoCloned (local: submodule repo) |
3939
│ │ └── GetCommitDetails (local: submodule SHA) |
40-
│ └── StoreEnrichedCommit (DB) |
40+
│ └── StoreEnrichedCommit (GitHub author lookup + DB)|
4141
└── ChangelogBatchWorkflow (sequential, sort_order ASC) |
4242
└── ChangelogVersionWorkflow (per version) |
4343
├── GetPreviousVersionCommit (DB) |
4444
├── [wait for N-1 enrichment if needed] |
4545
├── ComputeChangelog (local: git log) |
4646
├── [per submodule with changed pointer] |
4747
│ └── ComputeChangelog (local: submodule log) |
48-
└── StoreChangelog (DB) |
48+
└── StoreChangelog (GitHub author lookup + DB)|
4949
──────────────────────────────────────────────────────────────────────┘
5050
```
5151

@@ -56,6 +56,7 @@ The worker uses Temporal's [Worker Deployment Versioning](https://docs.temporal.
5656
**Configuration (env vars):**
5757
- `BUILD_ID` (required) — image tag or version identifier, set by the deployment pipeline. The worker will refuse to start if this is empty.
5858
- `POD_NAME` (optional) — Kubernetes pod name, used as the Temporal client `Identity` for traceability in the Temporal UI.
59+
- `GITHUB_TOKEN` (optional) — authenticates commit-author lookups for higher API limits and private repositories. Public GitHub repositories are queried without authentication when unset.
5960

6061
**Worker registration:**
6162
```go
@@ -132,6 +133,15 @@ Processes a single version: fetches assets from Sonatype, stores them, identifie
132133

133134
Downloads jar files and parses `META-INF/git.properties` or `META-INF/MANIFEST.MF` to extract git commit SHAs and repository URLs. Uses the same sliding window pattern as batch indexing (window size 3, page size 5).
134135

136+
During commit and changelog persistence, GitHub-hosted commits are resolved to
137+
their associated GitHub username. GitHub noreply addresses are handled locally;
138+
other addresses use the GitHub commit API. The username is stored alongside the
139+
Git author name and email, while lookup failures retain the Git name fallback.
140+
`StoreChangelog` has a two-minute activity timeout to accommodate bounded,
141+
parallel lookups across large changelogs. GitHub lookups stop five seconds
142+
before the activity deadline so optional enrichment cannot consume the time
143+
needed for the database write.
144+
135145
### VersionOrderingWorkflow
136146

137147
Computes version sort ordering using schema-driven parsing and optionally the Mojang version manifest for correct Minecraft version placement.

internal/activity/changelog_activities.go

Lines changed: 144 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,21 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"log/slog"
8+
"sync"
9+
"time"
710

811
"github.com/spongepowered/systemofadownload/internal/db"
912
"github.com/spongepowered/systemofadownload/internal/domain"
13+
"github.com/spongepowered/systemofadownload/internal/githubapi"
1014
"github.com/spongepowered/systemofadownload/internal/repository"
1115
)
1216

1317
// ChangelogActivities provides normal (non-local) activities for
1418
// commit enrichment DB reads and writes.
1519
type ChangelogActivities struct {
16-
Repo repository.Repository
20+
Repo repository.Repository
21+
GitHub *githubapi.Client
1722
}
1823

1924
// FetchVersionsForEnrichmentInput is the input for FetchVersionsForEnrichment.
@@ -203,6 +208,8 @@ type StoreEnrichedCommitInput struct {
203208

204209
// StoreEnrichedCommit writes the enriched commit info back to the DB.
205210
func (a *ChangelogActivities) StoreEnrichedCommit(ctx context.Context, input StoreEnrichedCommitInput) error { //nolint:gocritic // Temporal activity signature requires value type
211+
a.resolveCommitInfoAuthors(ctx, &input.CommitInfo)
212+
206213
data, err := json.Marshal(input.CommitInfo)
207214
if err != nil {
208215
return fmt.Errorf("marshaling enriched commit: %w", err)
@@ -225,6 +232,8 @@ type StoreChangelogInput struct {
225232
// StoreChangelog reads the current commit_body, merges the changelog into it,
226233
// and writes it back. This preserves the enrichment data already stored.
227234
func (a *ChangelogActivities) StoreChangelog(ctx context.Context, input StoreChangelogInput) error {
235+
a.resolveChangelogAuthors(ctx, &input.Changelog)
236+
228237
return a.Repo.WithTx(ctx, func(tx repository.Tx) error {
229238
av, err := tx.GetArtifactVersionByID(ctx, input.VersionID)
230239
if err != nil {
@@ -256,3 +265,137 @@ func (a *ChangelogActivities) StoreChangelog(ctx context.Context, input StoreCha
256265
})
257266
})
258267
}
268+
269+
type authorLookup struct {
270+
author *domain.CommitAuthor
271+
repository string
272+
sha string
273+
}
274+
275+
func (a *ChangelogActivities) resolveCommitInfoAuthors(ctx context.Context, info *domain.CommitInfo) {
276+
lookups := []authorLookup{{
277+
author: info.Author,
278+
repository: info.Repository,
279+
sha: info.Sha,
280+
}}
281+
for i := range info.Submodules {
282+
lookups = append(lookups, authorLookup{
283+
author: info.Submodules[i].Author,
284+
repository: info.Submodules[i].Repository,
285+
sha: info.Submodules[i].Sha,
286+
})
287+
}
288+
a.resolveAuthors(ctx, lookups)
289+
}
290+
291+
func (a *ChangelogActivities) resolveChangelogAuthors(ctx context.Context, changelog *domain.Changelog) {
292+
var lookups []authorLookup
293+
collectChangelogAuthorLookups(changelog, "", &lookups)
294+
a.resolveAuthors(ctx, lookups)
295+
}
296+
297+
func collectChangelogAuthorLookups(changelog *domain.Changelog, repoURL string, lookups *[]authorLookup) {
298+
if changelog == nil {
299+
return
300+
}
301+
for i := range changelog.Commits {
302+
commit := &changelog.Commits[i]
303+
commitRepo := repoURL
304+
if owner, repo, ok := githubapi.ParseRepository(commit.URL); ok {
305+
commitRepo = "https://github.com/" + owner + "/" + repo
306+
}
307+
*lookups = append(*lookups, authorLookup{
308+
author: commit.Author,
309+
repository: commitRepo,
310+
sha: commit.Sha,
311+
})
312+
}
313+
for subRepo, subChangelog := range changelog.SubmoduleChangelogs {
314+
collectChangelogAuthorLookups(subChangelog, subRepo, lookups)
315+
}
316+
}
317+
318+
func (a *ChangelogActivities) resolveAuthors(ctx context.Context, lookups []authorLookup) {
319+
if a.GitHub == nil || len(lookups) == 0 {
320+
return
321+
}
322+
323+
lookupCtx, cancel, ok := githubLookupContext(ctx)
324+
if !ok {
325+
slog.WarnContext(ctx, "skipping GitHub author lookup to preserve database persistence time")
326+
return
327+
}
328+
defer cancel()
329+
330+
const concurrency = 4
331+
sem := make(chan struct{}, concurrency)
332+
var wg sync.WaitGroup
333+
var errorCount int
334+
var firstErr error
335+
var errorMu sync.Mutex
336+
337+
for i := range lookups {
338+
lookup := lookups[i]
339+
if lookup.author == nil || lookup.author.GitHubUsername != "" {
340+
continue
341+
}
342+
343+
wg.Add(1)
344+
go func() {
345+
defer wg.Done()
346+
select {
347+
case sem <- struct{}{}:
348+
case <-lookupCtx.Done():
349+
errorMu.Lock()
350+
errorCount++
351+
if firstErr == nil {
352+
firstErr = lookupCtx.Err()
353+
}
354+
errorMu.Unlock()
355+
return
356+
}
357+
defer func() { <-sem }()
358+
359+
username, err := a.GitHub.ResolveUsername(
360+
lookupCtx,
361+
lookup.repository,
362+
lookup.sha,
363+
lookup.author.Email,
364+
)
365+
if err != nil {
366+
errorMu.Lock()
367+
errorCount++
368+
if firstErr == nil {
369+
firstErr = err
370+
}
371+
errorMu.Unlock()
372+
return
373+
}
374+
lookup.author.GitHubUsername = username
375+
}()
376+
}
377+
wg.Wait()
378+
379+
if errorCount > 0 {
380+
slog.WarnContext(ctx, "GitHub author lookup failed; using Git author names",
381+
"failures", errorCount,
382+
"error", firstErr,
383+
)
384+
}
385+
}
386+
387+
const githubLookupPersistenceReserve = 5 * time.Second
388+
389+
func githubLookupContext(ctx context.Context) (context.Context, context.CancelFunc, bool) {
390+
if deadline, ok := ctx.Deadline(); ok {
391+
lookupDeadline := deadline.Add(-githubLookupPersistenceReserve)
392+
if !time.Now().Before(lookupDeadline) {
393+
return nil, nil, false
394+
}
395+
lookupCtx, cancel := context.WithDeadline(ctx, lookupDeadline)
396+
return lookupCtx, cancel, true
397+
}
398+
399+
lookupCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
400+
return lookupCtx, cancel, true
401+
}

0 commit comments

Comments
 (0)