1
0
Fork 0

Merging upstream version 0.8.13.

Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
Daniel Baumann 2025-05-29 07:18:02 +02:00
parent 4ccf9dc8f1
commit bc0f764250
Signed by: daniel
GPG key ID: FBB4F0E80A80222F
28 changed files with 556 additions and 76 deletions

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -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

View file

@ -3,8 +3,9 @@ name: Pull Request
on:
workflow_dispatch: {}
pull_request:
branches:
- main
paths-ignore:
- "docs/*"
- ".github/*"
permissions:
contents: read

View file

@ -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

View file

@ -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

View file

@ -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 }}

19
.github/workflows/update-go-docs.yaml vendored Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 <GENERATOR> <SERVICE>
```
shoutrrr generate [OPTIONS] -g <GENERATOR> <SERVICE>
```

10
go.mod
View file

@ -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
)

24
go.sum
View file

@ -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=

View file

@ -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

3
internal/meta/doc.go Normal file
View file

@ -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

161
internal/meta/meta.go Normal file
View file

@ -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
}

282
internal/meta/meta_test.go Normal file
View file

@ -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)
}
})
}
}

View file

@ -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`

View file

@ -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
}

View file

@ -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

View file

@ -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")))

View file

@ -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
}

View file

@ -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)
}

View file

@ -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`)).

View file

@ -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
}

View file

@ -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)

View file

@ -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() {