From 886da1fce3767413220b96e02806ae1d4d489a11 Mon Sep 17 00:00:00 2001 From: GRMrGecko Date: Mon, 7 Oct 2024 23:49:47 -0500 Subject: [PATCH] First commit --- .github/workflows/release.yaml | 29 +++ .github/workflows/test_golang.yaml | 21 +++ .gitignore | 1 + .goreleaser.yaml | 26 +++ LICENSE.txt | 19 ++ README.md | 63 +++++++ add_release_cmd.go | 187 ++++++++++++++++++++ flags.go | 39 +++++ go.mod | 8 + go.sum | 12 ++ goreleaser.go | 57 ++++++ main.go | 28 +++ main_test.go | 224 ++++++++++++++++++++++++ manifest.go | 71 ++++++++ prune_cmd.go | 125 +++++++++++++ tests/v0.1.1/artifacts.json | 34 ++++ tests/v0.1.1/checksums.txt | 1 + tests/v0.1.1/config.yaml | 113 ++++++++++++ tests/v0.1.1/example_linux_amd64.tar.gz | 2 + tests/v0.1.1/metadata.json | 12 ++ tests/v0.1.2/artifacts.json | 34 ++++ tests/v0.1.2/checksums.txt | 1 + tests/v0.1.2/config.yaml | 113 ++++++++++++ tests/v0.1.2/example_linux_amd64.tar.gz | 2 + tests/v0.1.2/metadata.json | 12 ++ tests/v0.1/artifacts.json | 47 +++++ tests/v0.1/checksums.txt | 1 + tests/v0.1/config.yaml | 113 ++++++++++++ tests/v0.1/example_linux_amd64.tar.gz | 2 + tests/v0.1/example_linux_amd64/example | 2 + tests/v0.1/metadata.json | 12 ++ util.go | 61 +++++++ 32 files changed, 1472 insertions(+) create mode 100644 .github/workflows/release.yaml create mode 100644 .github/workflows/test_golang.yaml create mode 100644 .gitignore create mode 100644 .goreleaser.yaml create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 add_release_cmd.go create mode 100644 flags.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 goreleaser.go create mode 100644 main.go create mode 100644 main_test.go create mode 100644 manifest.go create mode 100644 prune_cmd.go create mode 100644 tests/v0.1.1/artifacts.json create mode 100644 tests/v0.1.1/checksums.txt create mode 100644 tests/v0.1.1/config.yaml create mode 100644 tests/v0.1.1/example_linux_amd64.tar.gz create mode 100644 tests/v0.1.1/metadata.json create mode 100644 tests/v0.1.2/artifacts.json create mode 100644 tests/v0.1.2/checksums.txt create mode 100644 tests/v0.1.2/config.yaml create mode 100644 tests/v0.1.2/example_linux_amd64.tar.gz create mode 100644 tests/v0.1.2/metadata.json create mode 100644 tests/v0.1/artifacts.json create mode 100644 tests/v0.1/checksums.txt create mode 100644 tests/v0.1/config.yaml create mode 100644 tests/v0.1/example_linux_amd64.tar.gz create mode 100644 tests/v0.1/example_linux_amd64/example create mode 100644 tests/v0.1/metadata.json create mode 100644 util.go diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml new file mode 100644 index 0000000..1d1dd80 --- /dev/null +++ b/.github/workflows/release.yaml @@ -0,0 +1,29 @@ +on: + release: + types: [created] + +permissions: + contents: write + packages: write + +jobs: + goreleaser: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - + name: Set up Go + uses: actions/setup-go@v4 + - + name: Run GoReleaser + uses: goreleaser/goreleaser-action@v6 + with: + distribution: goreleaser + version: latest + args: release --clean + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/test_golang.yaml b/.github/workflows/test_golang.yaml new file mode 100644 index 0000000..9cef003 --- /dev/null +++ b/.github/workflows/test_golang.yaml @@ -0,0 +1,21 @@ +name: Go package + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.21' + + - name: Build + run: go build -v ./... + + - name: Test + run: go test -v ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aaa72fa --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +goreleaser-http-repo-builder diff --git a/.goreleaser.yaml b/.goreleaser.yaml new file mode 100644 index 0000000..af0e3d5 --- /dev/null +++ b/.goreleaser.yaml @@ -0,0 +1,26 @@ +# This is an example .goreleaser.yml file with some sensible defaults. +# Make sure to check the documentation at https://goreleaser.com + +# The lines below are called `modelines`. See `:help modeline` +# Feel free to remove those if you don't want/need to use them. +# yaml-language-server: $schema=https://goreleaser.com/static/schema.json +# vim: set ts=2 sw=2 tw=0 fo=cnqoj + +version: 2 + +before: + hooks: + - go mod tidy + +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + - darwin + +archives: + - format: tar.gz + # this name template makes the OS and Arch compatible with the results of `uname`. + name_template: "{{ .ProjectName }}-{{ .Version }}.{{ .Os }}-{{ .Arch }}" + wrap_in_directory: true diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..ade9c16 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2024 Mr. Gecko's Media (James Coleman). http://mrgeckosmedia.com/ + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..052fe23 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# goreleaser-http-repo-builder + +This tool was written out of the need to build a release repository compatible with [go-selfupdate](https://github.com/creativeprojects/go-selfupdate) with releases built by [goreleaser](https://goreleaser.com/). + +## Example Usage + +The command has extensive help available, the following is an example of building a release and adding it to a new repo. + +```bash +goreleaser release --snapshot --skip=publish +mkdir repo +goreleaser-http-repo-builder add-release --repo=repo/ --release=dist/ +``` + +After adding a release, you can copy the repo to your web server for update distrobution. + +## Example Goreleaser Config + +While there is good [documentation available](https://goreleaser.com/customization/) that I'd recommend reading, the following provides some examples that may be helpful in generating a release that is compatible with go-selfupdate. + +- The checksums file name defaults to preappend the project name, which is not compatible if you wish to use the checksums to verify an update. +- If you're signing releases with an ECDSA key, this is what I found works best. +- If you need to specify the version manually, you can edit the version template. By default, goreleaser will use the git tag to determine the version. + +```yaml +version: 2 + +before: + hooks: + - go mod tidy + +builds: + - env: + - CGO_ENABLED=0 + goos: + - linux + goarch: + - amd64 + - arm64 + +archives: + - format: tar.gz + name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}" + wrap_in_directory: true + +checksum: + name_template: "checksums.txt" + +signs: + - artifacts: all + cmd: openssl + args: + - dgst + - -sha256 + - -sign + - "signing.key" + - -out + - ${signature} + - ${artifact} + +snapshot: + version_template: "v0.1.2" +``` diff --git a/add_release_cmd.go b/add_release_cmd.go new file mode 100644 index 0000000..1a9c1bb --- /dev/null +++ b/add_release_cmd.go @@ -0,0 +1,187 @@ +package main + +import ( + "errors" + "fmt" + "log" + "os" + "path/filepath" + "strings" + "time" +) + +type AddReleaseCmd struct { + Release string `help:"Path to goreleaser dist folder." required:"" type:"existingdir"` + Notes string `help:"Notes about this release."` + Draft bool `help:"Is this release a draft?"` + Prerelease bool `help:"Is this a prelease?"` + IncludeBinary bool `help:"Include binary artifacts."` + Force bool `help:"Force add, removing existing if needed."` + PublishedAt time.Time `help:"Specify exact time for release."` + PublishedAtNow bool `help:"Use the current time for published at instead of the metadata date."` +} + +// Adds a release to a repo. +func (a *AddReleaseCmd) Run() error { + // Read existing manifest for repo. + manifestFile := filepath.Join(app.flags.Repo, "manifest.yaml") + manifest, err := readManifestFile(manifestFile) + if os.IsNotExist(err) { + err = os.MkdirAll(app.flags.Repo, 0755) + } + if err != nil { + return err + } + + // Read metadata from goreleaser. + metadata, err := readMetadataFile(filepath.Join(a.Release, "metadata.json")) + if err != nil { + return err + } + versionPath := filepath.Join(app.flags.Repo, metadata.Version) + + // Read the artifcats to ensure we have a valid release. + artifacts, err := readArtifactFile(filepath.Join(a.Release, "artifacts.json")) + if err != nil { + return err + } + if len(artifacts) == 0 { + return errors.New("no artifacts in release") + } + + // Validate the base dir for artifacts. It could be one dir up, or 2 dirs up. + artifcatBase := a.Release + artifactLayers := 0 + if _, serr := os.Stat(filepath.Join(artifcatBase, artifacts[0].Path)); serr != nil { + artifcatBase = filepath.Dir(artifcatBase) + artifactLayers = 1 + if _, serr := os.Stat(filepath.Join(artifcatBase, artifacts[0].Path)); serr != nil { + artifcatBase = filepath.Dir(artifcatBase) + artifactLayers = 2 + if _, serr := os.Stat(filepath.Join(artifcatBase, artifacts[0].Path)); serr != nil { + return errors.New("unable to determine artificate base path") + } + } + } + + // Check if the version already exists. + existingIndex := -1 + for i, release := range manifest.Releases { + if release.TagName == metadata.Version { + existingIndex = i + break + } + } + + // If the version already exists, ask about replacing. + if existingIndex != -1 { + if !a.Force { + ans := askForConfirmation("This release already exists, should we replace?") + + // If we don't want to replace, we should stop here. + if !ans { + return errors.New("version already exists") + } + } + + // We need to replace the release, so remove it. + manifest.Releases = append(manifest.Releases[:existingIndex], manifest.Releases[existingIndex+1:]...) + + // Remove the version directory. + os.RemoveAll(versionPath) + } + + // Make the release. + manifest.LastReleaseID++ + release := &HttpRelease{ + ReleaseID: manifest.LastReleaseID, + Name: metadata.Name, + TagName: metadata.Version, + URL: metadata.Version, + Draft: a.Draft, + Prerelease: a.Prerelease, + PublishedAt: metadata.Date, + ReleaseNotes: a.Notes, + } + + // If the publish date provided is valid, override. + if !a.PublishedAt.IsZero() { + release.PublishedAt = a.PublishedAt + } + + // If published at is requested to be now, override. + if a.PublishedAtNow { + release.PublishedAt = app.now + } + + // Make the directory for the release. + err = os.Mkdir(versionPath, 0755) + if err != nil && !os.IsExist(err) { + return fmt.Errorf("Error making release directory: %s", err) + } + + // Add artifacts. + for _, artifact := range artifacts { + // Skip binaries if not included. + if artifact.Type == "Binary" && !a.IncludeBinary { + continue + } + + // Get the file path and confirm it exists and get its stat for file size. + path := filepath.Join(artifcatBase, artifact.Path) + stat, serr := os.Stat(path) + if serr != nil { + log.Println("Ignoring artifact", artifact.Name, "as its file does not exist.") + continue + } + + // Determine relative path. + s := strings.Split(artifact.Path, "/") + relativePath := filepath.Join(s[artifactLayers:]...) + + // Determine if artifact is in its own sub dir, make sure it exists. + dir := filepath.Dir(relativePath) + if dir != "." { + os.MkdirAll(filepath.Join(versionPath, dir), 0755) + } + + // Copy artifact to repo. + err = copyFile(path, filepath.Join(versionPath, relativePath)) + if err != nil { + log.Printf("Failed to copy artifact, skipping it: %s", err) + continue + } + + // Make asset. + manifest.LastAssetID++ + asset := &HttpAsset{ + ID: manifest.LastAssetID, + Name: artifact.Name, + Size: int(stat.Size()), + URL: filepath.Join(metadata.Version, relativePath), + } + + // Add to the release. + release.Assets = append(release.Assets, asset) + } + + // Add release to manifest. + manifest.Releases = append(manifest.Releases, release) + + // Write the manifest. + err = writeManifestFile(manifestFile, manifest) + if err != nil { + return err + } + + // If not a draft or prerelease, link latest to this release. + if !a.Draft && !a.Prerelease { + latestPath := filepath.Join(app.flags.Repo, "latest") + os.Remove(latestPath) + os.Symlink(metadata.Version, latestPath) + } + + log.Println("Added release", metadata.Version, "for", metadata.Name, "to the repo", app.flags.Repo) + + return nil +} diff --git a/flags.go b/flags.go new file mode 100644 index 0000000..6174c50 --- /dev/null +++ b/flags.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "github.com/alecthomas/kong" +) + +type VersionFlag bool + +func (v VersionFlag) Decode(ctx *kong.DecodeContext) error { return nil } +func (v VersionFlag) IsBool() bool { return true } +func (v VersionFlag) BeforeApply(app *kong.Kong, vars kong.Vars) error { + fmt.Println(appName + ": " + appVersion) + app.Exit(0) + return nil +} + +// Flags supplied to cli. +type Flags struct { + Version VersionFlag `name:"version" help:"Print version information and quit"` + Repo string `help:"The path to a repo" required:"" type:"existingdir"` + AddRelease AddReleaseCmd `cmd:"" help:"Add an release to the repo"` + Prune PruneCmd `cmd:"" help:"Prune releases from repo."` +} + +// Parse the supplied flags. +func (a *App) ParseFlags() *kong.Context { + app.flags = &Flags{} + + ctx := kong.Parse(app.flags, + kong.Name(appName), + kong.Description(appDescription), + kong.UsageOnError(), + kong.ConfigureHelp(kong.HelpOptions{ + Compact: true, + }), + ) + return ctx +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..58eb336 --- /dev/null +++ b/go.mod @@ -0,0 +1,8 @@ +module github.com/grmrgecko/goreleaser-http-repo-builder + +go 1.23 + +require ( + github.com/alecthomas/kong v1.2.1 + gopkg.in/yaml.v3 v3.0.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..2832959 --- /dev/null +++ b/go.sum @@ -0,0 +1,12 @@ +github.com/alecthomas/assert/v2 v2.10.0 h1:jjRCHsj6hBJhkmhznrCzoNpbA3zqy0fYiUcYZP/GkPY= +github.com/alecthomas/assert/v2 v2.10.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k= +github.com/alecthomas/kong v1.2.1 h1:E8jH4Tsgv6wCRX2nGrdPyHDUCSG83WH2qE4XLACD33Q= +github.com/alecthomas/kong v1.2.1/go.mod h1:rKTSFhbdp3Ryefn8x5MOEprnRFQ7nlmMC01GKhehhBM= +github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= +github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/goreleaser.go b/goreleaser.go new file mode 100644 index 0000000..11e7761 --- /dev/null +++ b/goreleaser.go @@ -0,0 +1,57 @@ +package main + +import ( + "encoding/json" + "os" + "time" +) + +// The metadata needed from goreleaser. +type Metadata struct { + Name string `json:"project_name"` + Version string `json:"version"` + Date time.Time `json:"date"` +} + +// Read and parse metadata file +func readMetadataFile(metadataFile string) (*Metadata, error) { + // Read file, if error return the error. + jsonFile, err := os.Open(metadataFile) + if err != nil { + return nil, err + } + + // Attempt to decode the file. + metadata := new(Metadata) + decoder := json.NewDecoder(jsonFile) + err = decoder.Decode(metadata) + jsonFile.Close() + + // Return the metadata and if any error occurred. + return metadata, err +} + +// Artifcat map. +type Artifact struct { + Name string `json:"name"` + Path string `json:"path"` + Type string `json:"type"` +} + +// Read and parse metadata file +func readArtifactFile(artifactFile string) ([]*Artifact, error) { + // Read file, if error return the error. + jsonFile, err := os.Open(artifactFile) + if err != nil { + return nil, err + } + + // Attempt to decode the file. + var artifacts []*Artifact + decoder := json.NewDecoder(jsonFile) + err = decoder.Decode(&artifacts) + jsonFile.Close() + + // Return the metadata and if any error occurred. + return artifacts, err +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..02664ef --- /dev/null +++ b/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "time" +) + +const ( + appName = "goreleaser-http-repo-builder" + appDescription = "Builds a repo for use with go-selfupdate" + appVersion = "0.1.0" +) + +// App is the global application structure for communicating between servers and storing information. +type App struct { + flags *Flags + now time.Time +} + +var app *App + +func main() { + app = new(App) + app.now = time.Now() + ctx := app.ParseFlags() + + err := ctx.Run() + ctx.FatalIfErrorf(err) +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..e49ec63 --- /dev/null +++ b/main_test.go @@ -0,0 +1,224 @@ +package main + +import ( + "crypto/md5" + "encoding/hex" + "os" + "path/filepath" + "testing" + "time" +) + +// Test the add release option. +func TestAppFunctionality(t *testing.T) { + // Make temp directory to build repo. + dname, err := os.MkdirTemp("", "goreleaser-http-repo-builder") + if err != nil { + t.Errorf("error making tempdir: %s", err) + } + + // Get the tests dir with test files. + testsDir, err := filepath.Abs("tests") + if err != nil { + t.Errorf("error finding tests dir: %s", err) + } + + // Now date for app defines. + now, _ := time.Parse(time.DateOnly, "2024-10-08") + + // Test adding a release of v0.1. + os.Args = []string{"test", "--repo", dname, "add-release", "--notes", "This is a test.", "--release", filepath.Join(testsDir, "v0.1")} + app = new(App) + app.now = now + ctx := app.ParseFlags() + + // Run the command. + err = ctx.Run() + if err != nil { + t.Errorf("error running the app: %s", err) + } + + // Test adding a release of v0.1.1. + os.Args = []string{"test", "--repo", dname, "add-release", "--draft", "--release", filepath.Join(testsDir, "v0.1.1")} + app = new(App) + app.now = now + ctx = app.ParseFlags() + + // Run the command. + err = ctx.Run() + if err != nil { + t.Errorf("error running the app: %s", err) + } + + // Test adding a release of v0.1.2. + os.Args = []string{"test", "--repo", dname, "add-release", "--prerelease", "--published-at", "2024-10-05T22:15:21.731224367-05:00", "--release", filepath.Join(testsDir, "v0.1.2")} + app = new(App) + app.now = now + ctx = app.ParseFlags() + + // Run the command. + err = ctx.Run() + if err != nil { + t.Errorf("error running the app: %s", err) + } + + // Confirm the latest release is v0.1.0. + latestPath, err := os.Readlink(filepath.Join(dname, "latest")) + if err != nil { + t.Errorf("error reading link to latest: %s", err) + } + if latestPath != "v0.1.0" { + t.Error("the latest link isn't correctly linked") + } + + // Hash the manifest file without the published_at dates. + hfun := md5.New() + d, err := os.ReadFile(filepath.Join(dname, "manifest.yaml")) + if err != nil { + t.Errorf("error reading manifest file: %s", err) + } + + // Hash the result and confirm. + hfun.Write(d) + sum := hfun.Sum(nil) + hash := hex.EncodeToString(sum) + if hash != "01240af1d189ea540418903e15eb3068" { + t.Errorf("hash isn't valid for manifest file: %s", hash) + } + + // Binaries are not included. + if _, serr := os.Stat(filepath.Join(dname, "v0.1.0/example_linux_amd64/example")); !os.IsNotExist(serr) { + t.Error("v0.1.0 binary exists, when it shouldn't exist.") + } + + // Confirm the asset was copied correctly. + d, err = os.ReadFile(filepath.Join(dname, "v0.1.0/example_linux_amd64.tar.gz")) + if err != nil { + t.Errorf("error reading test file: %s", err) + } + hfun.Reset() + hfun.Write(d) + sum = hfun.Sum(nil) + hash = hex.EncodeToString(sum) + if hash != "9cffcbe826ae684db1c8a08ff9216f34" { + t.Errorf("hash isn't valid for test file: %s", hash) + } + + // Confirm pruning of max releases works. + os.Args = []string{"test", "--repo", dname, "prune", "--max-age=216h"} + app = new(App) + app.now = now + ctx = app.ParseFlags() + + // Run the command. + err = ctx.Run() + if err != nil { + t.Errorf("error running the app: %s", err) + } + + // Confirm pruned state. + if _, serr := os.Stat(filepath.Join(dname, "v0.1.0/example_linux_amd64.tar.gz")); !os.IsNotExist(serr) { + t.Error("v0.1.0 exists, when it shouldn't exist.") + } + if _, serr := os.Stat(filepath.Join(dname, "v0.1.1/example_linux_amd64.tar.gz")); os.IsNotExist(serr) { + t.Error("v0.1.1 does not exists, when it should.") + } + if _, serr := os.Stat(filepath.Join(dname, "v0.1.2/example_linux_amd64.tar.gz")); os.IsNotExist(serr) { + t.Error("v0.1.2 does not exists, when it should.") + } + + // Delete all files, and reset. + os.RemoveAll(dname) + os.Mkdir(dname, 0755) + + // Test adding a release of v0.1. + os.Args = []string{"test", "--repo", dname, "add-release", "--include-binary", "--release", filepath.Join(testsDir, "v0.1")} + app = new(App) + app.now = now + ctx = app.ParseFlags() + + // Run the command. + err = ctx.Run() + if err != nil { + t.Errorf("error running the app: %s", err) + } + + // Test adding a release of v0.1.1. + os.Args = []string{"test", "--repo", dname, "add-release", "--release", filepath.Join(testsDir, "v0.1.1")} + app = new(App) + app.now = now + ctx = app.ParseFlags() + + // Run the command. + err = ctx.Run() + if err != nil { + t.Errorf("error running the app: %s", err) + } + + // Test adding a release of v0.1.2. + os.Args = []string{"test", "--repo", dname, "add-release", "--published-at-now", "--release", filepath.Join(testsDir, "v0.1.2")} + app = new(App) + app.now = now + ctx = app.ParseFlags() + + // Run the command. + err = ctx.Run() + if err != nil { + t.Errorf("error running the app: %s", err) + } + + // Confirm the latest release is v0.1.2. + latestPath, err = os.Readlink(filepath.Join(dname, "latest")) + if err != nil { + t.Errorf("error reading link to latest: %s", err) + } + if latestPath != "v0.1.2" { + t.Error("the latest link isn't correctly linked") + } + + // Hash the manifest file without the published_at dates. + hfun.Reset() + d, err = os.ReadFile(filepath.Join(dname, "manifest.yaml")) + if err != nil { + t.Errorf("error reading manifest file: %s", err) + } + + // Hash the result and confirm. + hfun.Write(d) + sum = hfun.Sum(nil) + hash = hex.EncodeToString(sum) + if hash != "dfac4ec2fc35bb04c8f5f79e057dfbe9" { + t.Errorf("hash isn't valid for manifest file: %s", hash) + } + + // Binaries are not included. + if _, serr := os.Stat(filepath.Join(dname, "v0.1.0/example_linux_amd64/example")); os.IsNotExist(serr) { + t.Error("v0.1.0 binary does not exists, when it shouldn.") + } + + // Confirm pruning of max releases works. + os.Args = []string{"test", "--repo", dname, "prune", "--max-releases=1"} + app = new(App) + app.now = now + ctx = app.ParseFlags() + + // Run the command. + err = ctx.Run() + if err != nil { + t.Errorf("error running the app: %s", err) + } + + // Confirm pruned state. + if _, serr := os.Stat(filepath.Join(dname, "v0.1.0/example_linux_amd64.tar.gz")); !os.IsNotExist(serr) { + t.Error("v0.1.0 exists, when it shouldn't exist.") + } + if _, serr := os.Stat(filepath.Join(dname, "v0.1.1/example_linux_amd64.tar.gz")); !os.IsNotExist(serr) { + t.Error("v0.1.1 exists, when it shouldn't exist.") + } + if _, serr := os.Stat(filepath.Join(dname, "v0.1.2/example_linux_amd64.tar.gz")); os.IsNotExist(serr) { + t.Error("v0.1.2 does not exists, when it should.") + } + + // Cleanup. + os.RemoveAll(dname) +} diff --git a/manifest.go b/manifest.go new file mode 100644 index 0000000..4994e91 --- /dev/null +++ b/manifest.go @@ -0,0 +1,71 @@ +package main + +import ( + "os" + "time" + + "gopkg.in/yaml.v3" +) + +// An individual asset. +type HttpAsset struct { + ID int64 `yaml:"id"` + Name string `yaml:"name"` + Size int `yaml:"size"` + URL string `yaml:"url"` +} + +// An individual release. +type HttpRelease struct { + ReleaseID int64 `yaml:"release_id"` + Name string `yaml:"name"` + TagName string `yaml:"tag_name"` + URL string `yaml:"url"` + Draft bool `yaml:"draft"` + Prerelease bool `yaml:"prerelease"` + PublishedAt time.Time `yaml:"published_at"` + ReleaseNotes string `yaml:"release_notes"` + Assets []*HttpAsset `yaml:"assets"` +} + +// The manifest file structure. +type HttpManifest struct { + LastReleaseID int64 `yaml:"last_release_id"` + LastAssetID int64 `yaml:"last_asset_id"` + Releases []*HttpRelease `yaml:"releases"` +} + +// Read and parse manifest file. +func readManifestFile(manifestFile string) (*HttpManifest, error) { + // We always want a manifest incase repo just needs to start from scratch. + manifest := new(HttpManifest) + + // Read file, if error return the error. + yamlFile, err := os.Open(manifestFile) + if err != nil { + return manifest, err + } + + // Attempt to decode the file. + decoder := yaml.NewDecoder(yamlFile) + err = decoder.Decode(manifest) + yamlFile.Close() + + // Return the manifest and if any error occurred. + return manifest, err +} + +// Write manifest file. +func writeManifestFile(manifestFile string, manifest *HttpManifest) error { + // Open the file for write. + yamlFile, err := os.OpenFile(manifestFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return err + } + defer yamlFile.Close() + + // Encode data. + encoder := yaml.NewEncoder(yamlFile) + err = encoder.Encode(manifest) + return err +} diff --git a/prune_cmd.go b/prune_cmd.go new file mode 100644 index 0000000..395f3b2 --- /dev/null +++ b/prune_cmd.go @@ -0,0 +1,125 @@ +package main + +import ( + "errors" + "fmt" + "log" + "os" + "path/filepath" + "time" +) + +type PruneCmd struct { + MaxAge time.Duration `help:"Delete releases older than."` + MaxReleases int `help:"Maximum number of releases to keep."` + DryRun bool `help:"Just log the result without actually pruning."` +} + +// Extra help to explain you can't set 2 prune stratages. +func (a *PruneCmd) Help() string { + return "You cannot use both max-age and max-releases, only set one." +} + +// Verify the options provided to the command. +func (a *PruneCmd) AfterApply() error { + // If both stratages are defined, we don't allow that. + if a.MaxAge > time.Duration(0) && a.MaxReleases > 0 { + return errors.New("must only provide one prune argument") + } + // If no stratages are defined, we don't allow that. + if a.MaxAge <= time.Duration(0) && a.MaxReleases <= 0 { + return errors.New("must provide one prune argument") + } + return nil +} + +// Adds a release to a repo. +func (a *PruneCmd) Run() error { + // Read existing manifest for repo. + manifestFile := filepath.Join(app.flags.Repo, "manifest.yaml") + manifest, err := readManifestFile(manifestFile) + if err != nil { + return err + } + + // Keep reference of number of pruned releases. + releasesPruned := 0 + n := len(manifest.Releases) + + // If max releases defined and is less than number of releases, look for items to prune. + if a.MaxReleases > 0 && n > a.MaxReleases { + // Loop starting at max releases. + for i := a.MaxReleases; i < n; i++ { + // Get the current release. + // We want pull from the top of the stack downward to keep newer releases. + version := manifest.Releases[n-(i+1)].TagName + log.Println("Removing release:", version) + + // If this isn't a dry run, remove the version directory. + if !a.DryRun { + err = os.RemoveAll(filepath.Join(app.flags.Repo, version)) + if err != nil { + return fmt.Errorf("untable to remove release files: %s", err) + } + } + + // Count the number pruned. + releasesPruned++ + } + + // Remove releases from the slice. + manifest.Releases = manifest.Releases[n-a.MaxReleases:] + } + + // If we are pruning based on duration, do so. + if a.MaxAge > time.Duration(0) { + // Loop through the releases, and find old releases. + for i := 0; i < n; i++ { + // If n is 1, we removed too many entries and need to stop. + if n == 1 { + log.Println("The repo has only 1 release remaining, ending the prune here to keep 1 release.") + break + } + + // Get the current release, and confirm its age. + release := manifest.Releases[i] + if app.now.Sub(release.PublishedAt) >= a.MaxAge { + // This release is too old, so we need to remove it. + version := release.TagName + log.Println("Removing release:", version) + + // If this isn't a dry run, remove the version files. + if !a.DryRun { + err = os.RemoveAll(filepath.Join(app.flags.Repo, version)) + if err != nil { + return fmt.Errorf("untable to remove release files: %s", err) + } + } + + // Remove the release from the slice. + manifest.Releases = append(manifest.Releases[:i], manifest.Releases[i+1:]...) + + // Back up one on the index and number of releases as one release was deleted. + i-- + n-- + + // Count number of releases pruned. + releasesPruned++ + + } + } + } + + // Write the manifest if this isn't a dry run. + if !a.DryRun { + err = writeManifestFile(manifestFile, manifest) + if err != nil { + return err + } + } + + // Provide details on what's been pruned. + log.Println("Pruned", releasesPruned, "release from the repo.") + + return nil +} diff --git a/tests/v0.1.1/artifacts.json b/tests/v0.1.1/artifacts.json new file mode 100644 index 0000000..4d23f5f --- /dev/null +++ b/tests/v0.1.1/artifacts.json @@ -0,0 +1,34 @@ +[ + { + "name": "metadata.json", + "path": "v0.1.1/metadata.json", + "internal_type": 30, + "type": "Metadata" + }, + { + "name": "example_linux_amd64.tar.gz", + "path": "v0.1.1/example_linux_amd64.tar.gz", + "goos": "linux", + "goarch": "amd64", + "goamd64": "v1", + "internal_type": 1, + "type": "Archive", + "extra": { + "Binaries": [ + "example" + ], + "Checksum": "sha256:9208c58af1265438c6894499847355bd5e77f93d04b201393baf41297d4680a3", + "Format": "tar.gz", + "ID": "default", + "Replaces": null, + "WrappedIn": "example_linux_amd64" + } + }, + { + "name": "checksums.txt", + "path": "v0.1.1/checksums.txt", + "internal_type": 12, + "type": "Checksum", + "extra": {} + } +] diff --git a/tests/v0.1.1/checksums.txt b/tests/v0.1.1/checksums.txt new file mode 100644 index 0000000..8de1a01 --- /dev/null +++ b/tests/v0.1.1/checksums.txt @@ -0,0 +1 @@ +9208c58af1265438c6894499847355bd5e77f93d04b201393baf41297d4680a3 example_linux_amd64.tar.gz diff --git a/tests/v0.1.1/config.yaml b/tests/v0.1.1/config.yaml new file mode 100644 index 0000000..57ec811 --- /dev/null +++ b/tests/v0.1.1/config.yaml @@ -0,0 +1,113 @@ +version: 2 +project_name: example +release: + github: + owner: GRMrGecko + name: goreleaser-http-repo-builder + name_template: '{{.Tag}}' +builds: + - id: example + goos: + - linux + goarch: + - amd64 + - arm64 + goarm: + - "6" + gomips: + - hardfloat + goamd64: + - v1 + targets: + - linux_amd64_v1 + - linux_arm64 + dir: . + main: . + binary: example + builder: go + gobinary: go + command: build + ldflags: + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser + env: + - CGO_ENABLED=0 +archives: + - id: default + name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}' + format: tar.gz + wrap_in_directory: "true" + files: + - src: license* + - src: LICENSE* + - src: readme* + - src: README* + - src: changelog* + - src: CHANGELOG* +snapshot: + version_template: v0.1.0 +checksum: + name_template: checksums.txt + algorithm: sha256 +changelog: + format: '{{ .SHA }}: {{ .Message }} ({{ with .AuthorUsername }}@{{ . }}{{ else }}{{ .AuthorName }} <{{ .AuthorEmail }}>{{ end }})' +dist: dist +env_files: + github_token: ~/.config/goreleaser/github_token + gitlab_token: ~/.config/goreleaser/gitlab_token + gitea_token: ~/.config/goreleaser/gitea_token +before: + hooks: + - go mod tidy +source: + name_template: '{{ .ProjectName }}-{{ .Version }}' + format: tar.gz +gomod: + gobinary: go +announce: + twitter: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + mastodon: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + server: "" + reddit: + title_template: '{{ .ProjectName }} {{ .Tag }} is out!' + url_template: '{{ .ReleaseURL }}' + slack: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + username: GoReleaser + discord: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + author: GoReleaser + color: "3888754" + icon_url: https://goreleaser.com/static/avatar.png + teams: + title_template: '{{ .ProjectName }} {{ .Tag }} is out!' + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + color: '#2D313E' + icon_url: https://goreleaser.com/static/avatar.png + smtp: + subject_template: '{{ .ProjectName }} {{ .Tag }} is out!' + body_template: 'You can view details from: {{ .ReleaseURL }}' + mattermost: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + title_template: '{{ .ProjectName }} {{ .Tag }} is out!' + username: GoReleaser + linkedin: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + telegram: + message_template: '{{ mdv2escape .ProjectName }} {{ mdv2escape .Tag }} is out{{ mdv2escape "!" }} Check it out at {{ mdv2escape .ReleaseURL }}' + parse_mode: MarkdownV2 + webhook: + message_template: '{ "message": "{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}"}' + content_type: application/json; charset=utf-8 + opencollective: + title_template: '{{ .Tag }}' + message_template: '{{ .ProjectName }} {{ .Tag }} is out!
Check it out at {{ .ReleaseURL }}' + bluesky: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' +git: + tag_sort: -version:refname +github_urls: + download: https://github.com +gitlab_urls: + download: https://gitlab.com diff --git a/tests/v0.1.1/example_linux_amd64.tar.gz b/tests/v0.1.1/example_linux_amd64.tar.gz new file mode 100644 index 0000000..5f2efc4 --- /dev/null +++ b/tests/v0.1.1/example_linux_amd64.tar.gz @@ -0,0 +1,2 @@ + +}k ΐI`Ɂc|GNFttVhtF \ No newline at end of file diff --git a/tests/v0.1.1/metadata.json b/tests/v0.1.1/metadata.json new file mode 100644 index 0000000..32dcd8f --- /dev/null +++ b/tests/v0.1.1/metadata.json @@ -0,0 +1,12 @@ +{ + "project_name": "example", + "tag": "v0.0.0", + "previous_tag": "", + "version": "v0.1.1", + "commit": "521be63afb85d785ce36b4bd0d7412664593ac1d", + "date": "2024-09-30T09:26:01.612178185-05:00", + "runtime": { + "goos": "linux", + "goarch": "amd64" + } +} diff --git a/tests/v0.1.2/artifacts.json b/tests/v0.1.2/artifacts.json new file mode 100644 index 0000000..648e34c --- /dev/null +++ b/tests/v0.1.2/artifacts.json @@ -0,0 +1,34 @@ +[ + { + "name": "metadata.json", + "path": "v0.1.2/metadata.json", + "internal_type": 30, + "type": "Metadata" + }, + { + "name": "example_linux_amd64.tar.gz", + "path": "v0.1.2/example_linux_amd64.tar.gz", + "goos": "linux", + "goarch": "amd64", + "goamd64": "v1", + "internal_type": 1, + "type": "Archive", + "extra": { + "Binaries": [ + "example" + ], + "Checksum": "sha256:9208c58af1265438c6894499847355bd5e77f93d04b201393baf41297d4680a3", + "Format": "tar.gz", + "ID": "default", + "Replaces": null, + "WrappedIn": "example_linux_amd64" + } + }, + { + "name": "checksums.txt", + "path": "v0.1.2/checksums.txt", + "internal_type": 12, + "type": "Checksum", + "extra": {} + } +] diff --git a/tests/v0.1.2/checksums.txt b/tests/v0.1.2/checksums.txt new file mode 100644 index 0000000..8de1a01 --- /dev/null +++ b/tests/v0.1.2/checksums.txt @@ -0,0 +1 @@ +9208c58af1265438c6894499847355bd5e77f93d04b201393baf41297d4680a3 example_linux_amd64.tar.gz diff --git a/tests/v0.1.2/config.yaml b/tests/v0.1.2/config.yaml new file mode 100644 index 0000000..57ec811 --- /dev/null +++ b/tests/v0.1.2/config.yaml @@ -0,0 +1,113 @@ +version: 2 +project_name: example +release: + github: + owner: GRMrGecko + name: goreleaser-http-repo-builder + name_template: '{{.Tag}}' +builds: + - id: example + goos: + - linux + goarch: + - amd64 + - arm64 + goarm: + - "6" + gomips: + - hardfloat + goamd64: + - v1 + targets: + - linux_amd64_v1 + - linux_arm64 + dir: . + main: . + binary: example + builder: go + gobinary: go + command: build + ldflags: + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser + env: + - CGO_ENABLED=0 +archives: + - id: default + name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}' + format: tar.gz + wrap_in_directory: "true" + files: + - src: license* + - src: LICENSE* + - src: readme* + - src: README* + - src: changelog* + - src: CHANGELOG* +snapshot: + version_template: v0.1.0 +checksum: + name_template: checksums.txt + algorithm: sha256 +changelog: + format: '{{ .SHA }}: {{ .Message }} ({{ with .AuthorUsername }}@{{ . }}{{ else }}{{ .AuthorName }} <{{ .AuthorEmail }}>{{ end }})' +dist: dist +env_files: + github_token: ~/.config/goreleaser/github_token + gitlab_token: ~/.config/goreleaser/gitlab_token + gitea_token: ~/.config/goreleaser/gitea_token +before: + hooks: + - go mod tidy +source: + name_template: '{{ .ProjectName }}-{{ .Version }}' + format: tar.gz +gomod: + gobinary: go +announce: + twitter: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + mastodon: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + server: "" + reddit: + title_template: '{{ .ProjectName }} {{ .Tag }} is out!' + url_template: '{{ .ReleaseURL }}' + slack: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + username: GoReleaser + discord: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + author: GoReleaser + color: "3888754" + icon_url: https://goreleaser.com/static/avatar.png + teams: + title_template: '{{ .ProjectName }} {{ .Tag }} is out!' + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + color: '#2D313E' + icon_url: https://goreleaser.com/static/avatar.png + smtp: + subject_template: '{{ .ProjectName }} {{ .Tag }} is out!' + body_template: 'You can view details from: {{ .ReleaseURL }}' + mattermost: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + title_template: '{{ .ProjectName }} {{ .Tag }} is out!' + username: GoReleaser + linkedin: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + telegram: + message_template: '{{ mdv2escape .ProjectName }} {{ mdv2escape .Tag }} is out{{ mdv2escape "!" }} Check it out at {{ mdv2escape .ReleaseURL }}' + parse_mode: MarkdownV2 + webhook: + message_template: '{ "message": "{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}"}' + content_type: application/json; charset=utf-8 + opencollective: + title_template: '{{ .Tag }}' + message_template: '{{ .ProjectName }} {{ .Tag }} is out!
Check it out at {{ .ReleaseURL }}' + bluesky: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' +git: + tag_sort: -version:refname +github_urls: + download: https://github.com +gitlab_urls: + download: https://gitlab.com diff --git a/tests/v0.1.2/example_linux_amd64.tar.gz b/tests/v0.1.2/example_linux_amd64.tar.gz new file mode 100644 index 0000000..5f2efc4 --- /dev/null +++ b/tests/v0.1.2/example_linux_amd64.tar.gz @@ -0,0 +1,2 @@ + +}k ΐI`Ɂc|GNFttVhtF \ No newline at end of file diff --git a/tests/v0.1.2/metadata.json b/tests/v0.1.2/metadata.json new file mode 100644 index 0000000..67222af --- /dev/null +++ b/tests/v0.1.2/metadata.json @@ -0,0 +1,12 @@ +{ + "project_name": "example", + "tag": "v0.0.0", + "previous_tag": "", + "version": "v0.1.2", + "commit": "94bb85eb32ea07f33d627ce0dee905e29d8d1c96", + "date": "2024-10-07T22:15:21.731224367-05:00", + "runtime": { + "goos": "linux", + "goarch": "amd64" + } +} diff --git a/tests/v0.1/artifacts.json b/tests/v0.1/artifacts.json new file mode 100644 index 0000000..88021d7 --- /dev/null +++ b/tests/v0.1/artifacts.json @@ -0,0 +1,47 @@ +[ + { + "name": "metadata.json", + "path": "v0.1/metadata.json", + "internal_type": 30, + "type": "Metadata" + }, + { + "name": "example", + "path": "v0.1/example_linux_amd64/example", + "goos": "linux", + "goarch": "amd64", + "internal_type": 4, + "type": "Binary", + "extra": { + "Binary": "example", + "Ext": "", + "ID": "example" + } + }, + { + "name": "example_linux_amd64.tar.gz", + "path": "v0.1/example_linux_amd64.tar.gz", + "goos": "linux", + "goarch": "amd64", + "goamd64": "v1", + "internal_type": 1, + "type": "Archive", + "extra": { + "Binaries": [ + "example" + ], + "Checksum": "sha256:9208c58af1265438c6894499847355bd5e77f93d04b201393baf41297d4680a3", + "Format": "tar.gz", + "ID": "default", + "Replaces": null, + "WrappedIn": "example_linux_amd64" + } + }, + { + "name": "checksums.txt", + "path": "v0.1/checksums.txt", + "internal_type": 12, + "type": "Checksum", + "extra": {} + } +] diff --git a/tests/v0.1/checksums.txt b/tests/v0.1/checksums.txt new file mode 100644 index 0000000..8de1a01 --- /dev/null +++ b/tests/v0.1/checksums.txt @@ -0,0 +1 @@ +9208c58af1265438c6894499847355bd5e77f93d04b201393baf41297d4680a3 example_linux_amd64.tar.gz diff --git a/tests/v0.1/config.yaml b/tests/v0.1/config.yaml new file mode 100644 index 0000000..57ec811 --- /dev/null +++ b/tests/v0.1/config.yaml @@ -0,0 +1,113 @@ +version: 2 +project_name: example +release: + github: + owner: GRMrGecko + name: goreleaser-http-repo-builder + name_template: '{{.Tag}}' +builds: + - id: example + goos: + - linux + goarch: + - amd64 + - arm64 + goarm: + - "6" + gomips: + - hardfloat + goamd64: + - v1 + targets: + - linux_amd64_v1 + - linux_arm64 + dir: . + main: . + binary: example + builder: go + gobinary: go + command: build + ldflags: + - -s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser + env: + - CGO_ENABLED=0 +archives: + - id: default + name_template: '{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}' + format: tar.gz + wrap_in_directory: "true" + files: + - src: license* + - src: LICENSE* + - src: readme* + - src: README* + - src: changelog* + - src: CHANGELOG* +snapshot: + version_template: v0.1.0 +checksum: + name_template: checksums.txt + algorithm: sha256 +changelog: + format: '{{ .SHA }}: {{ .Message }} ({{ with .AuthorUsername }}@{{ . }}{{ else }}{{ .AuthorName }} <{{ .AuthorEmail }}>{{ end }})' +dist: dist +env_files: + github_token: ~/.config/goreleaser/github_token + gitlab_token: ~/.config/goreleaser/gitlab_token + gitea_token: ~/.config/goreleaser/gitea_token +before: + hooks: + - go mod tidy +source: + name_template: '{{ .ProjectName }}-{{ .Version }}' + format: tar.gz +gomod: + gobinary: go +announce: + twitter: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + mastodon: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + server: "" + reddit: + title_template: '{{ .ProjectName }} {{ .Tag }} is out!' + url_template: '{{ .ReleaseURL }}' + slack: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + username: GoReleaser + discord: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + author: GoReleaser + color: "3888754" + icon_url: https://goreleaser.com/static/avatar.png + teams: + title_template: '{{ .ProjectName }} {{ .Tag }} is out!' + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + color: '#2D313E' + icon_url: https://goreleaser.com/static/avatar.png + smtp: + subject_template: '{{ .ProjectName }} {{ .Tag }} is out!' + body_template: 'You can view details from: {{ .ReleaseURL }}' + mattermost: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + title_template: '{{ .ProjectName }} {{ .Tag }} is out!' + username: GoReleaser + linkedin: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' + telegram: + message_template: '{{ mdv2escape .ProjectName }} {{ mdv2escape .Tag }} is out{{ mdv2escape "!" }} Check it out at {{ mdv2escape .ReleaseURL }}' + parse_mode: MarkdownV2 + webhook: + message_template: '{ "message": "{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}"}' + content_type: application/json; charset=utf-8 + opencollective: + title_template: '{{ .Tag }}' + message_template: '{{ .ProjectName }} {{ .Tag }} is out!
Check it out at {{ .ReleaseURL }}' + bluesky: + message_template: '{{ .ProjectName }} {{ .Tag }} is out! Check it out at {{ .ReleaseURL }}' +git: + tag_sort: -version:refname +github_urls: + download: https://github.com +gitlab_urls: + download: https://gitlab.com diff --git a/tests/v0.1/example_linux_amd64.tar.gz b/tests/v0.1/example_linux_amd64.tar.gz new file mode 100644 index 0000000..5f2efc4 --- /dev/null +++ b/tests/v0.1/example_linux_amd64.tar.gz @@ -0,0 +1,2 @@ + +}k ΐI`Ɂc|GNFttVhtF \ No newline at end of file diff --git a/tests/v0.1/example_linux_amd64/example b/tests/v0.1/example_linux_amd64/example new file mode 100644 index 0000000..5f2efc4 --- /dev/null +++ b/tests/v0.1/example_linux_amd64/example @@ -0,0 +1,2 @@ + +}k ΐI`Ɂc|GNFttVhtF \ No newline at end of file diff --git a/tests/v0.1/metadata.json b/tests/v0.1/metadata.json new file mode 100644 index 0000000..3cbe6fc --- /dev/null +++ b/tests/v0.1/metadata.json @@ -0,0 +1,12 @@ +{ + "project_name": "example", + "tag": "v0.0.0", + "previous_tag": "", + "version": "v0.1.0", + "commit": "1869b4455d572d3e3c7e00114586b47f184a2e35", + "date": "2024-09-27T12:10:11.314135185-05:00", + "runtime": { + "goos": "linux", + "goarch": "amd64" + } +} diff --git a/util.go b/util.go new file mode 100644 index 0000000..485c75a --- /dev/null +++ b/util.go @@ -0,0 +1,61 @@ +package main + +import ( + "bufio" + "fmt" + "io" + "os" + "strings" +) + +// Helper for CLI to ask for confirmation. +func askForConfirmation(message string) bool { + // Read stdanrd input for each new line. + scanner := bufio.NewScanner(os.Stdin) + + // Loop the question until answered. + for { + fmt.Printf("%s [y/n]: ", message) + + // Get next line. + scanner.Scan() + resp := strings.ToLower(strings.TrimSpace(scanner.Text())) + + // Check if yes or no. + switch resp { + case "y", "yes": + return true + case "n", "no": + return false + default: + fmt.Println("Invalid answer.") + } + } +} + +// Helper for copying files. +func copyFile(srcFile, dstFile string) (err error) { + // Open the source file. + f, err := os.Open(srcFile) + if err != nil { + return + } + defer f.Close() + + // Open the destination file. + d, err := os.Create(dstFile) + if err != nil { + return + } + defer d.Close() + + // Copy the data to the new file. + _, err = io.Copy(d, f) + if err != nil { + return + } + + // Ensure new file is fully written. + err = d.Sync() + return +}