From 90f612f211a3b6d8853506972c002fcae81088e1 Mon Sep 17 00:00:00 2001 From: Jonathan Miller Date: Mon, 25 May 2026 19:05:13 -0500 Subject: [PATCH] feat: auto-generate SHA256 checksums for release attachments When a release is created or updated with attachments, automatically compute SHA256 checksums for every file and attach a checksums.sha256 manifest file. The manifest follows the standard sha256sum format: Existing checksums.sha256 files are replaced when attachments change. Checksums are generated for both CreateRelease and UpdateRelease flows. Co-Authored-By: Claude Opus 4.6 (1M context) --- services/release/checksum.go | 82 ++++++++++++++++++++++++++++++++++++ services/release/release.go | 14 ++++++ 2 files changed, 96 insertions(+) create mode 100644 services/release/checksum.go diff --git a/services/release/checksum.go b/services/release/checksum.go new file mode 100644 index 0000000000..fd4f7ae0d5 --- /dev/null +++ b/services/release/checksum.go @@ -0,0 +1,82 @@ +// Copyright 2026 Moko Consulting +// SPDX-License-Identifier: GPL-3.0-or-later + +package release + +import ( + "bytes" + "context" + "crypto/sha256" + "fmt" + "io" + + repo_model "git.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo" + "git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log" + "git.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/storage" + attachment_service "git.mokoconsulting.tech/MokoConsulting/MokoGitea/services/attachment" +) + +// GenerateReleaseChecksums computes SHA256 checksums for all attachments +// on a release and adds a checksums.sha256 manifest file as an attachment. +func GenerateReleaseChecksums(ctx context.Context, rel *repo_model.Release) error { + // Load attachments into rel.Attachments + if err := repo_model.GetReleaseAttachments(ctx, rel); err != nil { + return fmt.Errorf("GetReleaseAttachments: %w", err) + } + + if len(rel.Attachments) == 0 { + return nil + } + + // Remove existing checksums file if present + for _, a := range rel.Attachments { + if a.Name == "checksums.sha256" { + if err := repo_model.DeleteAttachment(ctx, a, true); err != nil { + log.Warn("Failed to delete old checksums.sha256: %v", err) + } + break + } + } + + // Compute SHA256 for each attachment + var manifest bytes.Buffer + for _, a := range rel.Attachments { + if a.Name == "checksums.sha256" { + continue + } + + fr, err := storage.Attachments.Open(a.RelativePath()) + if err != nil { + log.Warn("Cannot open attachment %s for checksumming: %v", a.Name, err) + continue + } + + h := sha256.New() + if _, err := io.Copy(h, fr); err != nil { + fr.Close() + log.Warn("Cannot read attachment %s for checksumming: %v", a.Name, err) + continue + } + fr.Close() + + fmt.Fprintf(&manifest, "%x %s\n", h.Sum(nil), a.Name) + } + + if manifest.Len() == 0 { + return nil + } + + // Create the checksums.sha256 attachment + checksumAttach := &repo_model.Attachment{ + RepoID: rel.RepoID, + ReleaseID: rel.ID, + Name: "checksums.sha256", + } + + if _, err := attachment_service.NewAttachment(ctx, checksumAttach, &manifest, int64(manifest.Len())); err != nil { + return fmt.Errorf("create checksums.sha256 attachment: %w", err) + } + + log.Info("Generated checksums.sha256 for release %s (repo %d)", rel.TagName, rel.RepoID) + return nil +} diff --git a/services/release/release.go b/services/release/release.go index 5afc6cbe22..c54a06f7e6 100644 --- a/services/release/release.go +++ b/services/release/release.go @@ -190,6 +190,13 @@ func CreateRelease(gitRepo *git.Repository, rel *repo_model.Release, attachmentU return err } + // Generate SHA256 checksums for all release attachments + if len(attachmentUUIDs) > 0 { + if err := GenerateReleaseChecksums(gitRepo.Ctx, rel); err != nil { + log.Error("GenerateReleaseChecksums for %s: %v", rel.TagName, err) + } + } + if !rel.IsDraft { notify_service.NewRelease(gitRepo.Ctx, rel) } @@ -344,6 +351,13 @@ func UpdateRelease(ctx context.Context, doer *user_model.User, gitRepo *git.Repo } } + // Regenerate checksums when attachments change + if len(addAttachmentUUIDs) > 0 || len(delAttachmentUUIDs) > 0 { + if err := GenerateReleaseChecksums(ctx, rel); err != nil { + log.Error("GenerateReleaseChecksums for %s: %v", rel.TagName, err) + } + } + if !rel.IsDraft { if !isTagCreated && !isConvertedFromTag { notify_service.UpdateRelease(gitRepo.Ctx, doer, rel) -- 2.52.0