diff --git a/.circleci/config.yml b/.circleci/config.yml index a8829f9..30725af 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,7 +14,7 @@ jobs: docker: # Specify the version you desire here # See: https://circleci.com/developer/images/image/cimg/go - - image: cimg/go:1.24.2@sha256:cd027ede83e11c7b1002dfff3f4975fbf0124c5028df4c63da571c30db88fb3c + - image: cimg/go:1.24.3@sha256:5f7cdf218958c02c0da1356a3a2a8d1394c80206322d0790b968443f6875a59e # Add steps to the job # See: https://circleci.com/docs/jobs-steps/#steps-overview & https://circleci.com/docs/configuration-reference/#steps diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 02309a9..d1f9351 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -33,9 +33,9 @@ jobs: platforms: linux/amd64,linux/arm64,linux/386,linux/arm/v6 - name: Set up Go - uses: actions/setup-go@29694d72cd5e7ef3b09496b39f28a942af47737e + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 with: - go-version: 1.24.3 + go-version: 1.24.x - name: Login to Docker Hub uses: docker/login-action@6d4b68b490aef8836e8fb5e50ee7b3bdfa5894f0 diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 8210616..20bbdc4 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -7,6 +7,9 @@ permissions: contents: write actions: read +env: + GO_VERSION: 1.24.x + jobs: build: runs-on: ubuntu-latest @@ -20,9 +23,9 @@ jobs: git config user.email 41898282+github-actions[bot]@users.noreply.github.com - name: Setup Go - uses: actions/setup-go@29694d72cd5e7ef3b09496b39f28a942af47737e + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 with: - go-version: "1.24" + go-version: ${{ env.GO_VERSION }} - name: Generate Service Config Docs run: | @@ -31,7 +34,7 @@ jobs: ./generate-service-config-docs.sh - name: Setup Python - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 + uses: actions/setup-python@5db1cf9a59fb97c40a68accab29236f0da7e94db with: python-version: "3.13.3" cache: "pip" diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 4225409..e5cfa63 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 - name: Set up Go - uses: actions/setup-go@29694d72cd5e7ef3b09496b39f28a942af47737e + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 with: go-version: "1.24.3" @@ -23,7 +23,7 @@ jobs: run: go mod download - name: Run golangci-lint - uses: golangci/golangci-lint-action@4d56fa9e3c67fb4afa92b38c99fc7f20f5eeff4e + uses: golangci/golangci-lint-action@481777f62fe06de6923fd3a69efd3ba597fe628a with: args: --timeout=5m --config= # Use default linter settings diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index 693b4df..90d2e5a 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -3,8 +3,9 @@ name: Pull Request on: workflow_dispatch: {} pull_request: - branches: - - main + paths-ignore: + - "docs/*" + - ".github/*" permissions: contents: read diff --git a/.github/workflows/release-dev.yaml b/.github/workflows/release-dev.yaml index 07b4d04..03ead36 100644 --- a/.github/workflows/release-dev.yaml +++ b/.github/workflows/release-dev.yaml @@ -8,6 +8,7 @@ on: - "v*" paths-ignore: - "docs/*" + - ".github/*" permissions: contents: write @@ -27,6 +28,7 @@ jobs: uses: ./.github/workflows/build.yaml secrets: inherit needs: + - lint - test with: snapshot: true diff --git a/.github/workflows/release-production.yaml b/.github/workflows/release-production.yaml index 6f1b156..c44cdd8 100644 --- a/.github/workflows/release-production.yaml +++ b/.github/workflows/release-production.yaml @@ -23,15 +23,10 @@ jobs: uses: ./.github/workflows/build.yaml secrets: inherit needs: + - lint - test - renew-docs: - name: Refresh pkg.go.dev - needs: build - runs-on: ubuntu-latest - steps: - - name: Pull new module version - uses: nicholas-fedor/go-proxy-pull-action@ad5d0f8b44e5478055cf78227eb300d2b02786f2 - with: - goproxy: https://proxy.golang.org - import_path: github.com/nicholas-fedor/shoutrrr + update-go-docs: + uses: ./.github/workflows/update-go-docs.yaml + needs: + - build diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 56ada1d..9dcba9f 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@85e6279cec87321a52edac9c87bce653a07cf6c2 - name: Set up Go - uses: actions/setup-go@29694d72cd5e7ef3b09496b39f28a942af47737e + uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 with: go-version: "1.24.3" @@ -27,6 +27,6 @@ jobs: go test -v -coverprofile coverage.out -covermode atomic ./... - name: Upload coverage reports to Codecov - uses: codecov/codecov-action@ad3126e916f78f00edff4ed0317cf185271ccc2d + uses: codecov/codecov-action@15559ed290fa727036809b67ab0f646ffa6c5158 with: token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/update-go-docs.yaml b/.github/workflows/update-go-docs.yaml new file mode 100644 index 0000000..3d56d17 --- /dev/null +++ b/.github/workflows/update-go-docs.yaml @@ -0,0 +1,19 @@ +name: Update pkg.go.dev + +on: + - workflow_dispatch + - workflow_call + +permissions: + contents: read + +jobs: + update-go-docs: + name: Update pkg.go.dev + runs-on: ubuntu-latest + steps: + - name: Pull new module version + uses: nicholas-fedor/go-proxy-pull-action@ad5d0f8b44e5478055cf78227eb300d2b02786f2 + with: + goproxy: https://proxy.golang.org + import_path: github.com/nicholas-fedor/shoutrrr diff --git a/build.sh b/build.sh index c529784..b03eb31 100755 --- a/build.sh +++ b/build.sh @@ -1,3 +1,9 @@ #!/bin/sh -go build -o shoutrrr/ ./shoutrrr +# Get Git information +VERSION=$(git describe --tags --always --dirty 2>/dev/null || echo "unknown") +COMMIT=$(git rev-parse --short HEAD 2>/dev/null || echo "unknown") +DATE=$(git log -1 --format=%cd --date=iso | date -u +%Y-%m-%dT%H:%M:%SZ 2>/dev/null || echo "unknown") + +# Build with ldflags +go build -ldflags "-s -w -X github.com/nicholas-fedor/shoutrrr/internal/meta.Version=$VERSION -X github.com/nicholas-fedor/shoutrrr/internal/meta.Commit=$COMMIT -X github.com/nicholas-fedor/shoutrrr/internal/meta.Date=$DATE" -o shoutrrr ./shoutrrr diff --git a/docs/generators/basic.md b/docs/generators/basic.md index e1c35b5..92206c7 100644 --- a/docs/generators/basic.md +++ b/docs/generators/basic.md @@ -3,9 +3,11 @@ The basic generator looks at the `key:""`, `desc:""` and `default:""` tags on service configuration structs and uses them to ask the user to fill in their corresponding values. Example: -```shell -$ shoutrrr generate telegram + +```bash +shoutrrr generate telegram ``` + ```yaml Generating URL for telegram using basic generator Enter the configuration values as prompted diff --git a/docs/generators/overview.md b/docs/generators/overview.md index a02b10b..7c4542b 100644 --- a/docs/generators/overview.md +++ b/docs/generators/overview.md @@ -1,10 +1,10 @@ # Generators -Generators are used to create service configurations via the command line. +Generators are used to create service configurations via the command line. The main generator is the reflection based [Basic generator](./basic) that aims to be able to generator configurations for all the core services via a set of simple questions. ## Usage ```bash -$ shoutrrr generate [OPTIONS] -g -``` \ No newline at end of file +shoutrrr generate [OPTIONS] -g +``` diff --git a/go.mod b/go.mod index aef11bc..e28dd72 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/nicholas-fedor/shoutrrr -go 1.24.2 +go 1.24.3 require ( github.com/fatih/color v1.18.0 @@ -14,7 +14,7 @@ require ( ) require ( - cloud.google.com/go/compute/metadata v0.6.0 // indirect + cloud.google.com/go/compute/metadata v0.7.0 // indirect golang.org/x/net v0.40.0 ) @@ -24,21 +24,21 @@ require ( github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/google/go-cmp v0.7.0 // indirect - github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect + github.com/google/pprof v0.0.0-20250501235452-c0086092b71a // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/sagikazarmark/locafero v0.9.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.14.0 // indirect - github.com/spf13/cast v1.7.1 // indirect + github.com/spf13/cast v1.8.0 // indirect github.com/spf13/pflag v1.0.6 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/sys v0.33.0 // indirect golang.org/x/text v0.25.0 // indirect - golang.org/x/tools v0.32.0 // indirect + golang.org/x/tools v0.33.0 // indirect google.golang.org/protobuf v1.36.6 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 1723270..2c72255 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I= -cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg= +cloud.google.com/go/compute/metadata v0.7.0 h1:PBWF+iiAerVNe8UCHxdOt6eHLVc3ydFeOCw78U8ytSU= +cloud.google.com/go/compute/metadata v0.7.0/go.mod h1:j5MvL9PprKL39t166CoB1uVHfQMs4tFQZZcKwksXUjo= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -17,8 +17,8 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= -github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= +github.com/google/pprof v0.0.0-20250501235452-c0086092b71a h1:rDA3FfmxwXR+BVKKdz55WwMJ1pD2hJQNW31d+l3mPk4= +github.com/google/pprof v0.0.0-20250501235452-c0086092b71a/go.mod h1:5hDyRhoBCxViHszMt12TnOpEI4VVi+U8Gm9iphldiMA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jarcoal/httpmock v1.4.0 h1:BvhqnH0JAYbNudL2GMJKgOHe2CtKlzJ/5rWKyp+hc2k= @@ -52,8 +52,8 @@ github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9yS github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= github.com/spf13/afero v1.14.0/go.mod h1:acJQ8t0ohCGuMN3O+Pv0V0hgMxNYDlvdk+VTfyZmbYo= -github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= -github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.8.0 h1:gEN9K4b8Xws4EX0+a0reLmhq8moKn7ntRlQYgjPeCDk= +github.com/spf13/cast v1.8.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= @@ -68,25 +68,17 @@ go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= -golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/net v0.40.0 h1:79Xs7wF06Gbdcg4kdCCIQArK11Z1hr5POQ6+fIYHNuY= golang.org/x/net v0.40.0/go.mod h1:y0hY0exeL2Pku80/zKK7tpntoX23cqL3Oa6njdgRtds= -golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98= -golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= -golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= -golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4= golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA= -golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= -golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= +golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= +golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/goreleaser.yml b/goreleaser.yml index 93331ba..0c1cd9b 100644 --- a/goreleaser.yml +++ b/goreleaser.yml @@ -12,7 +12,10 @@ builds: - arm - arm64 ldflags: - - -s -w -X github.com/nicholas-fedor/shoutrrr/internal/meta.Version={{ .Version }} + - -s -w + - -X github.com/nicholas-fedor/shoutrrr/internal/meta.Version={{ .Version }} + - -X github.com/nicholas-fedor/shoutrrr/internal/meta.Commit={{.Commit}} + - -X github.com/nicholas-fedor/shoutrrr/internal/meta.Date={{.Date}} archives: - id: default # Unique ID for this archive configuration diff --git a/internal/meta/doc.go b/internal/meta/doc.go new file mode 100644 index 0000000..c32968c --- /dev/null +++ b/internal/meta/doc.go @@ -0,0 +1,3 @@ +// Package meta provides functionality to parse and manage metadata information +// for Shoutrrr using Go's debug.ReadBuildInfo and GoReleaser build flags. +package meta diff --git a/internal/meta/meta.go b/internal/meta/meta.go new file mode 100644 index 0000000..85572fe --- /dev/null +++ b/internal/meta/meta.go @@ -0,0 +1,161 @@ +package meta + +import ( + "fmt" + "runtime/debug" + "time" +) + +// Constants for repeated string values. +const ( + devVersion = "dev" + unknownValue = "unknown" + trueValue = "true" + commitSHALength = 7 // Length to shorten Git commit SHA +) + +// These values are populated by GoReleaser during release builds. +var ( + // Version is the Shoutrrr version (e.g., "v0.0.1"). + Version = devVersion + // Commit is the Git commit SHA (e.g., "abc123"). + Commit = unknownValue + // Date is the build or commit timestamp in RFC3339 format (e.g., "2025-05-07T00:00:00Z"). + Date = unknownValue +) + +// Info holds version information for Shoutrrr. +type Info struct { + Version string + Commit string + Date string +} + +// GetMetaStr returns the formatted version string, including commit info only if available. +func GetMetaStr() string { + version := GetVersion() + date := GetDate() + commit := GetCommit() + + if commit == unknownValue { + return fmt.Sprintf("%s (Built on %s)", version, date) + } + + return fmt.Sprintf("%s (Built on %s from Git SHA %s)", version, date, commit) +} + +// GetVersion returns the version string, using debug.ReadBuildInfo for source builds +// or GoReleaser variables for release builds. +func GetVersion() string { + version := Version + + // If building from source (not GoReleaser), try to get version from debug.ReadBuildInfo + if version == devVersion || version == "" { + if info, ok := debug.ReadBuildInfo(); ok { + // Get the module version (e.g., v1.1.4 or v1.1.4+dirty) + version = info.Main.Version + if version == "(devel)" || version == "" { + version = devVersion + } + // Check for dirty state + for _, setting := range info.Settings { + if setting.Key == "vcs.modified" && setting.Value == trueValue && + version != unknownValue && !contains(version, "+dirty") { + version += "+dirty" + } + } + } + } else { + // GoReleaser provides a valid version without 'v' prefix, so add it + if version != "" && version != "v" { + version = "v" + version + } + } + + // Fallback default if still unset or invalid + if version == "" || version == devVersion || version == "v" { + return unknownValue + } + + return version +} + +// GetCommit returns the commit SHA, using debug.ReadBuildInfo for source builds +// or GoReleaser variables for release builds. +func GetCommit() string { + // Return Commit if set by GoReleaser (non-empty and not "unknown") + if Commit != unknownValue && Commit != "" { + if len(Commit) >= commitSHALength { + return Commit[:commitSHALength] + } + + return Commit + } + + // Try to get commit from debug.ReadBuildInfo for source builds + if info, ok := debug.ReadBuildInfo(); ok { + for _, setting := range info.Settings { + if setting.Key == "vcs.revision" && setting.Value != "" { + if len(setting.Value) >= commitSHALength { + return setting.Value[:commitSHALength] + } + + return setting.Value + } + } + } + + // Fallback to unknown if no commit is found + return unknownValue +} + +// GetDate returns the build or commit date, using debug.ReadBuildInfo for source builds +// or GoReleaser variables for release builds. +func GetDate() string { + date := Date + + // If building from source (not GoReleaser), try to get date from debug.ReadBuildInfo + if date == unknownValue || date == "" { + if info, ok := debug.ReadBuildInfo(); ok { + for _, setting := range info.Settings { + if setting.Key == "vcs.time" { + if t, err := time.Parse(time.RFC3339, setting.Value); err == nil { + return t.Format("2006-01-02") // Shorten to YYYY-MM-DD + } + } + } + } + // Fallback to current date if no VCS time is available + return time.Now().UTC().Format("2006-01-02") + } + + // Shorten date if provided by GoReleaser + if date != "" && date != unknownValue { + if t, err := time.Parse(time.RFC3339, date); err == nil { + return t.Format("2006-01-02") // Shorten to YYYY-MM-DD + } + } + + // Fallback to current date if date is invalid + return time.Now().UTC().Format("2006-01-02") +} + +// GetMetaInfo returns version information by combining GetVersion, GetCommit, and GetDate. +func GetMetaInfo() Info { + return Info{ + Version: GetVersion(), + Commit: GetCommit(), + Date: GetDate(), + } +} + +// contains checks if a string contains a substring. +func contains(s, substr string) bool { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return true + } + } + + return false +} diff --git a/internal/meta/meta_test.go b/internal/meta/meta_test.go new file mode 100644 index 0000000..aea028c --- /dev/null +++ b/internal/meta/meta_test.go @@ -0,0 +1,282 @@ +package meta + +import ( + "regexp" + "runtime/debug" + "strings" + "testing" + "time" +) + +func TestGetVersionInfo(t *testing.T) { + tests := []struct { + name string + setVars func() + expect Info + partialMatch bool + }{ + { + name: "GoReleaser build", + setVars: func() { + Version = "0.0.1" + Commit = "abc123456789" + Date = "2025-05-07T00:00:00Z" + }, + expect: Info{ + Version: "v0.0.1", + Commit: "abc1234", + Date: "2025-05-07", + }, + }, + { + name: "Source build with default values", + setVars: func() { + Version = devVersion + Commit = unknownValue + Date = unknownValue + }, + expect: Info{ + Version: unknownValue, + Commit: unknownValue, + Date: time.Now().UTC().Format("2006-01-02"), + }, + partialMatch: true, + }, + { + name: "Source build with empty values", + setVars: func() { + Version = "" + Commit = "" + Date = "" + }, + expect: Info{ + Version: unknownValue, + Commit: unknownValue, + Date: time.Now().UTC().Format("2006-01-02"), + }, + }, + { + name: "Invalid GoReleaser version", + setVars: func() { + Version = "v" + Commit = "" + Date = "" + }, + expect: Info{ + Version: unknownValue, + Commit: unknownValue, + Date: time.Now().UTC().Format("2006-01-02"), + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setVars() + + info := GetMetaInfo() + + if !tt.partialMatch { + if info.Version != tt.expect.Version { + t.Errorf("Version = %q, want %q", info.Version, tt.expect.Version) + } + + if info.Commit != tt.expect.Commit { + t.Errorf("Commit = %q, want %q", info.Commit, tt.expect.Commit) + } + + // Validate Date format (YYYY-MM-DD) instead of exact match + if !regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`).MatchString(info.Date) { + t.Errorf("Date = %q, want valid YYYY-MM-DD format", info.Date) + } + } else if info.Version != tt.expect.Version && !strings.Contains(info.Version, "+dirty") { + t.Errorf("Version = %q, want %q or dirty variant", info.Version, tt.expect.Version) + } + }) + } +} + +func TestGetVersionInfo_VCSData(t *testing.T) { + Version = devVersion + Commit = unknownValue + Date = unknownValue + + info := GetMetaInfo() + + if buildInfo, ok := debug.ReadBuildInfo(); ok { + var vcsRevision, vcsTime, vcsModified string + + for _, setting := range buildInfo.Settings { + switch setting.Key { + case "vcs.revision": + vcsRevision = setting.Value + case "vcs.time": + vcsTime = setting.Value + case "vcs.modified": + vcsModified = setting.Value + } + } + + if vcsRevision != "" { + expectedCommit := vcsRevision + if len(vcsRevision) >= 7 { + expectedCommit = vcsRevision[:7] + } + + if info.Commit == unknownValue { + t.Errorf( + "Expected commit %q, got %q; ensure repository has commit history", + expectedCommit, + info.Commit, + ) + } else if info.Commit != expectedCommit { + t.Errorf("Commit = %q, want %q", info.Commit, expectedCommit) + } + } else { + t.Logf("No vcs.revision found; ensure repository has Git metadata to cover commit assignment") + } + + if vcsTime != "" { + if parsedTime, err := time.Parse(time.RFC3339, vcsTime); err == nil { + expectedDate := parsedTime.Format("2006-01-02") + if info.Date == unknownValue { + t.Errorf( + "Expected date %q, got %q; ensure vcs.time is a valid RFC3339 timestamp", + expectedDate, + info.Date, + ) + } else if info.Date != expectedDate { + t.Errorf("Date = %q, want %q", info.Date, expectedDate) + } + } else { + t.Logf("vcs.time %q is invalid; date should be in YYYY-MM-DD format", vcsTime) + + if !regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`).MatchString(info.Date) { + t.Errorf("Date = %q, want valid YYYY-MM-DD format", info.Date) + } + } + } else { + t.Logf("No vcs.time found; date should be in YYYY-MM-DD format") + + if !regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`).MatchString(info.Date) { + t.Errorf("Date = %q, want valid YYYY-MM-DD format", info.Date) + } + } + + if vcsModified == trueValue && info.Version != unknownValue { + if !strings.Contains(info.Version, "+dirty") { + t.Errorf( + "Expected version to contain '+dirty', got %q; ensure repository has uncommitted changes", + info.Version, + ) + } + } else if vcsModified != trueValue { + t.Logf("Repository is clean (vcs.modified=%q); make uncommitted changes to cover '+dirty' case", vcsModified) + } + } else { + t.Logf("debug.ReadBuildInfo() failed; ensure tests run in a Git repository to cover VCS parsing") + } +} + +func TestGetVersionInfo_InvalidVCSTime(t *testing.T) { + Version = devVersion + Commit = unknownValue + Date = unknownValue + + info := GetMetaInfo() + + if !regexp.MustCompile(`^\d{4}-\d{2}-\d{2}$`).MatchString(info.Date) { + t.Errorf("Date = %q, want valid YYYY-MM-DD format", info.Date) + } +} + +func TestGetMetaStr(t *testing.T) { + tests := []struct { + name string + setVars func() + expect string + }{ + { + name: "With commit (GoReleaser build)", + setVars: func() { + Version = "0.8.10" + Commit = "a6fcf77abcdef" + Date = "2025-05-27T00:00:00Z" + }, + expect: "v0.8.10 (Built on 2025-05-27 from Git SHA a6fcf77)", + }, + { + name: "Without commit (go install build)", + setVars: func() { + Version = "0.8.10" + Commit = unknownValue + Date = unknownValue + }, + expect: "v0.8.10 (Built on " + time.Now().UTC().Format("2006-01-02") + ")", + }, + { + name: "Invalid version", + setVars: func() { + Version = "v" + Commit = unknownValue + Date = unknownValue + }, + expect: "unknown (Built on " + time.Now().UTC().Format("2006-01-02") + ")", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.setVars() + + result := GetMetaStr() + if !strings.HasPrefix(result, strings.Split(tt.expect, " (")[0]) || + !regexp.MustCompile(`\d{4}-\d{2}-\d{2}`).MatchString(result) { + t.Errorf("GetMetaStr() = %q, want format like %q", result, tt.expect) + } + }) + } +} + +func TestContains(t *testing.T) { + tests := []struct { + name string + s string + substr string + expected bool + }{ + { + name: "Substring found", + s: "v1.0.0+dirty", + substr: "+dirty", + expected: true, + }, + { + name: "Substring not found", + s: "v1.0.0", + substr: "+dirty", + expected: false, + }, + { + name: "Empty string", + s: "", + substr: "+dirty", + expected: false, + }, + { + name: "Empty substring", + s: "v1.0.0", + substr: "", + expected: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := contains(tt.s, tt.substr) + if result != tt.expected { + t.Errorf("contains(%q, %q) = %v, want %v", tt.s, tt.substr, result, tt.expected) + } + }) + } +} diff --git a/internal/meta/version.go b/internal/meta/version.go deleted file mode 100644 index 20e5ee6..0000000 --- a/internal/meta/version.go +++ /dev/null @@ -1,7 +0,0 @@ -package meta - -// Version of Shoutrrr. -const Version = `0.6-dev` - -// DocsVersion is prepended to documentation URLs and usually equals MAJOR.MINOR of Version. -const DocsVersion = `dev` diff --git a/pkg/services/lark/lark_config.go b/pkg/services/lark/lark_config.go index c550ef0..4149bb6 100644 --- a/pkg/services/lark/lark_config.go +++ b/pkg/services/lark/lark_config.go @@ -55,12 +55,18 @@ func (config *Config) SetURL(url *url.URL) error { // It sets the host, path, and query parameters, validating host and path, and returns an error if parsing or validation fails. func (config *Config) setURL(resolver types.ConfigQueryResolver, url *url.URL) error { config.Host = url.Host - if config.Host != larkHost && config.Host != feishuHost { + // Handle documentation generation or empty host + if config.Host == "" || (url.User != nil && url.User.Username() == "dummy") { + config.Host = "open.larksuite.com" + } else if config.Host != larkHost && config.Host != feishuHost { return ErrInvalidHost } config.Path = strings.Trim(url.Path, "/") - if config.Path == "" { + // Handle documentation generation with empty path + if config.Path == "" && (url.User != nil && url.User.Username() == "dummy") { + config.Path = "token" + } else if config.Path == "" { return ErrNoPath } diff --git a/pkg/services/lark/lark_service.go b/pkg/services/lark/lark_service.go index a5f7dc7..7a5d7e6 100644 --- a/pkg/services/lark/lark_service.go +++ b/pkg/services/lark/lark_service.go @@ -50,7 +50,7 @@ var httpClient = &http.Client{Timeout: defaultTime} // Service sends notifications to Lark. type Service struct { standard.Standard - config *Config + Config *Config pkr format.PropKeyResolver } @@ -60,7 +60,7 @@ func (service *Service) Send(message string, params *types.Params) error { return ErrLargeMessage } - config := *service.config + config := *service.Config if err := service.pkr.UpdateConfigFromParams(&config, params); err != nil { return fmt.Errorf("updating params: %w", err) } @@ -79,10 +79,10 @@ func (service *Service) Send(message string, params *types.Params) error { // Initialize configures the service with a URL and logger. func (service *Service) Initialize(configURL *url.URL, logger types.StdLogger) error { service.SetLogger(logger) - service.config = &Config{} - service.pkr = format.NewPropKeyResolver(service.config) + service.Config = &Config{} + service.pkr = format.NewPropKeyResolver(service.Config) - return service.config.SetURL(configURL) + return service.Config.SetURL(configURL) } // GetID returns the service identifier. @@ -174,8 +174,8 @@ func (service *Service) handleResponse(resp *http.Response) error { service.Logf( "Notification sent successfully to %s/%s", - service.config.Host, - service.config.Path, + service.Config.Host, + service.Config.Path, ) return nil diff --git a/pkg/services/lark/lark_test.go b/pkg/services/lark/lark_test.go index 4faeb02..125ac81 100644 --- a/pkg/services/lark/lark_test.go +++ b/pkg/services/lark/lark_test.go @@ -88,14 +88,14 @@ var _ = ginkgo.Describe("Lark Test", func() { data[i] = "0123456789" } message := strings.Join(data, "") - service := Service{config: &Config{Host: larkHost, Path: "token"}} + service := Service{Config: &Config{Host: larkHost, Path: "token"}} gomega.Expect(service.Send(message, nil)).To(gomega.MatchError(ErrLargeMessage)) }) }) ginkgo.When("an invalid param is passed", func() { ginkgo.It("should fail to send messages", func() { - service := Service{config: &Config{Host: larkHost, Path: "token"}} + service := Service{Config: &Config{Host: larkHost, Path: "token"}} gomega.Expect( service.Send("test message", &types.Params{"invalid": "value"}), ).To(gomega.MatchError(gomega.ContainSubstring("not a valid config key: invalid"))) diff --git a/pkg/services/teams/teams_config.go b/pkg/services/teams/teams_config.go index 5cb7c81..122afc9 100644 --- a/pkg/services/teams/teams_config.go +++ b/pkg/services/teams/teams_config.go @@ -119,7 +119,10 @@ func (config *Config) setURL(resolver types.ConfigQueryResolver, url *url.URL) e return err } - if config.Host == "" { + // Allow dummy URL during documentation generation + if config.Host == "" && (url.User != nil && url.User.Username() == "dummy") { + config.Host = "dummy.webhook.office.com" + } else if config.Host == "" { return ErrMissingHostParameter } diff --git a/pkg/util/docs.go b/pkg/util/docs.go index 74341e0..d57072f 100644 --- a/pkg/util/docs.go +++ b/pkg/util/docs.go @@ -14,5 +14,11 @@ func DocsURL(path string) string { path = path[1:] } - return fmt.Sprintf("https://nicholas-fedor.github.io/shoutrrr/%s/%s", meta.DocsVersion, path) + // Use commit for dev builds, version for releases + version := meta.GetVersion() + if version == "unknown" || version == "dev" { + version = meta.GetCommit() + } + + return fmt.Sprintf("https://nicholas-fedor.github.io/shoutrrr/%s/%s", version, path) } diff --git a/pkg/util/util_test.go b/pkg/util/util_test.go index 8ba10ba..2c8aaa4 100644 --- a/pkg/util/util_test.go +++ b/pkg/util/util_test.go @@ -92,7 +92,7 @@ var _ = ginkgo.Describe("the util package", func() { ginkgo.It("should return the expected URL", func() { expectedBase := fmt.Sprintf( `https://nicholas-fedor.github.io/shoutrrr/%s/`, - meta.DocsVersion, + meta.GetVersion(), ) gomega.Expect(util.DocsURL(``)).To(gomega.Equal(expectedBase)) gomega.Expect(util.DocsURL(`services/logger`)). diff --git a/shoutrrr.go b/shoutrrr.go index 77e39cf..12a46b6 100644 --- a/shoutrrr.go +++ b/shoutrrr.go @@ -50,7 +50,7 @@ func NewSender(logger types.StdLogger, serviceURLs ...string) (*router.ServiceRo return sr, nil } -// Version returns the current shoutrrr version. +// Version returns the current Shoutrrr version. func Version() string { return meta.Version } diff --git a/shoutrrr/cmd/docs/docs.go b/shoutrrr/cmd/docs/docs.go index 5b576c7..97c74bf 100644 --- a/shoutrrr/cmd/docs/docs.go +++ b/shoutrrr/cmd/docs/docs.go @@ -75,7 +75,9 @@ func printDocs(docFormat string, services []string) cmd.Result { // Initialize the service to populate Config dummyURL, _ := url.Parse(scheme + "://dummy@dummy.com") if err := service.Initialize(dummyURL, logger); err != nil { - return cmd.InvalidUsage(fmt.Sprintf("failed to initialize service %q: %v", scheme, err)) + return cmd.InvalidUsage( + fmt.Sprintf("failed to initialize service %q: %v\n", scheme, err), + ) } config := format.GetServiceConfig(service) diff --git a/shoutrrr/main.go b/shoutrrr/main.go index d79f3bb..2e6bfa1 100644 --- a/shoutrrr/main.go +++ b/shoutrrr/main.go @@ -15,9 +15,8 @@ import ( ) var cobraCmd = &cobra.Command{ - Use: "shoutrrr", - Version: meta.Version, - Short: "Shoutrrr CLI", + Use: "shoutrrr", + Short: "Shoutrrr CLI", } func init() { @@ -26,6 +25,8 @@ func init() { cobraCmd.AddCommand(generate.Cmd) cobraCmd.AddCommand(send.Cmd) cobraCmd.AddCommand(docs.Cmd) + + cobraCmd.Version = meta.GetMetaStr() } func main() {