Compare commits

...

18 Commits

Author SHA1 Message Date
Bo-Yi Wu cd7b8b4656 chore(docker): bump alpine base image to 3.23
- Upgrade FROM alpine:3.17 to alpine:3.23
- Resolve Trivy CRITICAL/HIGH CVE findings from EOL alpine 3.17
2026-05-08 23:55:41 +08:00
Bo-Yi Wu 530ced4f2a chore: bump go directive to 1.25.10
- Update go.mod go directive from 1.25.9 to 1.25.10
2026-05-08 21:19:20 +08:00
Bo-Yi Wu 072876987d ci: bump golangci-lint to v2.12
- Upgrade golangci-lint version from v2.11 to v2.12
2026-05-08 20:13:40 +08:00
Bo-Yi Wu caaf39fc08 ci(actions): bump trivy-action to v0.36.0 and codecov-action to v6 2026-04-25 16:51:03 +08:00
Bo-Yi Wu d746084872 ci(docker): fail push when trivy finds CRITICAL/HIGH issues 2026-04-16 23:01:15 +08:00
Bo-Yi Wu 6379123ca3 ci: enable check-latest in docker and goreleaser workflows 2026-04-16 22:42:55 +08:00
Bo-Yi Wu cff7d4e183 fix: skip integration tests without telegram secrets; apply modernize fix 2026-04-16 22:39:48 +08:00
Bo-Yi Wu 7007692d2e ci: enable check-latest for setup-go to fetch newest patch 2026-04-16 21:15:46 +08:00
Bo-Yi Wu 23f0958c87 ci: pin golangci-lint to v2.11 2026-04-16 21:11:25 +08:00
Bo-Yi Wu c8c37942c1 ci: bump GitHub Actions and add Go 1.25/1.26 to test matrix 2026-04-16 21:03:32 +08:00
Bo-Yi Wu a7daa0df80 chore: bump go directive to 1.25.9 2026-04-16 20:58:04 +08:00
Bo-Yi Wu f3ace6f519 ci: add Trivy security scanning for source code and Docker image
- Add independent trivy.yml workflow with repo scan and image scan jobs
- Add Trivy image scan step in docker.yml before pushing Docker image
- Add security-events permission for SARIF upload
- Add Trivy Security Scan badge to README
2026-04-16 18:10:12 +08:00
Bo-Yi Wu 4de983b4ef fix(deps): upgrade golang.org/x/crypto to fix CVE vulnerabilities
- bump golang.org/x/crypto to v0.45.0 (fixes CVE-2024-45337 CRITICAL,
  CVE-2025-22869 HIGH, CVE-2025-47914 MEDIUM, CVE-2025-58181 MEDIUM)
- bump golang.org/x/sys to v0.38.0
2026-04-16 12:13:16 +08:00
Bo-Yi Wu b4a51bd6b6 ci(actions): upgrade GitHub Actions to latest versions
- bump actions/checkout to v6
- bump actions/setup-go to v6
- bump actions/cache to v5
- bump goreleaser/goreleaser-action to v7
- bump golangci/golangci-lint-action to v9
- bump github/codeql-action/* to v4
- bump codecov/codecov-action to v5
- bump docker/build-push-action to v7
- bump docker/login-action to v4
- bump docker/metadata-action to v6
- bump docker/setup-buildx-action to v4
- bump docker/setup-qemu-action to v4
- bump hadolint/hadolint-action to v3.3.0
- bump aquasecurity/trivy-action to v0.35.0
2026-04-16 12:06:53 +08:00
Bo-Yi Wu 30b9c501ff perf(plugin): hoist repeated work out of per-user loop
- Pre-render message templates before iterating users
- Pre-parse locations and venues before iterating users
- Inline trivial templateMessage wrapper into call site
- Extract escapeMarkdownFields variadic helper for bulk field escaping
- Use strings.TrimSpace instead of strings.Trim(" ") for robustness
- Pre-allocate slices in trimElement, escapeMarkdown, and globList
- Fix comment typo and inconsistent audio caption trailing period
2026-04-08 23:10:59 +08:00
Bo-Yi Wu cbae1a3737 chore: migrate golangci-lint config to v2
- Add version field and migrate to v2 config format
- Replace deprecated linters with modern equivalents
- Add new linters: bidichk, depguard, forbidigo, revive, testifylint, etc.
- Move gofmt, gofumpt, golines to formatters section
- Configure revive rules, depguard deny list, and exclusion presets
2026-04-08 23:00:38 +08:00
Bo-Yi Wu 0bc6220388 fix: resolve all golangci-lint issues
- Replace fmt.Printf with log.Printf to satisfy forbidigo
- Fix ineffectual assignment to err in proxy block by avoiding variable shadowing
- Format long lines with golines
- Use assert.Error/NoError instead of NotNil/Nil for error assertions
- Use assert.True/False instead of Equal for boolean assertions
- Use assert.Empty instead of Equal with len check
- Use require for mid-test error assertions that guard subsequent operations
- Add testify/require import
2026-04-08 22:47:32 +08:00
Bo-Yi Wu 8587a97ab1 refactor: simplify plugin code and fix minor issues
- Replace os.Open/bufio/io.ReadAll with os.ReadFile in loadTextFromFile
- Guard proxy URL parsing inside socks5 check to avoid false errors
- Remove redundant trimElement wrapping around globList calls
- Remove dead title assignment in convertLocation
- Switch Plugin methods to pointer receivers to avoid struct copying
- Use %w for error wrapping in fmt.Errorf calls
- Fix "unmarshall" typos to "unmarshal" in error messages
- Fix duplicate DIST variable and GXZ_PAGAGE typo in Makefile
2026-04-08 22:40:06 +08:00
13 changed files with 422 additions and 180 deletions
+3 -3
View File
@@ -38,11 +38,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -51,4 +51,4 @@ jobs:
# queries: ./path/to/local/query, your-org/your-repo/queries@main
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4
+39 -8
View File
@@ -10,16 +10,22 @@ on:
branches:
- "master"
permissions:
contents: read
packages: write
security-events: write
jobs:
build-docker:
runs-on: ubuntu-latest
steps:
- name: Setup go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: "^1"
check-latest: true
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -29,19 +35,19 @@ jobs:
make build_linux_arm64
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
uses: docker/setup-buildx-action@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -49,7 +55,7 @@ jobs:
- name: Docker meta
id: docker-meta
uses: docker/metadata-action@v5
uses: docker/metadata-action@v6
with:
images: |
${{ github.repository }}
@@ -60,8 +66,33 @@ jobs:
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
- name: Build image for scanning
uses: docker/build-push-action@v7
with:
context: .
file: docker/Dockerfile
platforms: linux/amd64
push: false
load: true
tags: drone-telegram:scan
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@v0.36.0
with:
image-ref: "drone-telegram:scan"
format: "sarif"
output: "trivy-image-results.sarif"
severity: "CRITICAL,HIGH"
exit-code: '1'
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: "trivy-image-results.sarif"
category: "trivy-docker-image"
- name: Build and push
uses: docker/build-push-action@v6
uses: docker/build-push-action@v7
with:
context: .
platforms: linux/amd64,linux/arm64
+3 -3
View File
@@ -13,17 +13,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
uses: goreleaser/goreleaser-action@v7
with:
# either 'goreleaser' (default) or 'goreleaser-pro'
distribution: goreleaser
+11 -10
View File
@@ -9,21 +9,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
- name: Setup go
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- name: Setup golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v9
with:
version: latest
version: v2.12
args: --verbose
- uses: hadolint/hadolint-action@v3.1.0
- uses: hadolint/hadolint-action@v3.3.0
name: hadolint for Dockerfile
with:
dockerfile: docker/Dockerfile
@@ -32,7 +32,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest]
go: [1.22, 1.23]
go: [1.25, 1.26]
include:
- os: ubuntu-latest
go-build: ~/.cache/go-build
@@ -43,16 +43,17 @@ jobs:
GOPROXY: https://proxy.golang.org
steps:
- name: Set up Go ${{ matrix.go }}
uses: actions/setup-go@v5
uses: actions/setup-go@v6
with:
go-version: ${{ matrix.go }}
check-latest: true
- name: Checkout Code
uses: actions/checkout@v4
uses: actions/checkout@v6
with:
ref: ${{ github.ref }}
- uses: actions/cache@v4
- uses: actions/cache@v5
with:
path: |
${{ matrix.go-build }}
@@ -64,6 +65,6 @@ jobs:
run: |
make test
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
uses: codecov/codecov-action@v6
with:
flags: ${{ matrix.os }},go-${{ matrix.go }}
+85
View File
@@ -0,0 +1,85 @@
name: Trivy Security Scan
on:
push:
branches:
- master
pull_request:
branches:
- master
schedule:
# Run daily at 00:00 UTC
- cron: "0 0 * * *"
workflow_dispatch:
permissions:
contents: read
security-events: write
jobs:
trivy-repo-scan:
name: Trivy Repository Scan
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Run Trivy vulnerability scanner (repo)
uses: aquasecurity/trivy-action@v0.36.0
with:
scan-type: "fs"
scan-ref: "."
format: "sarif"
output: "trivy-repo-results.sarif"
severity: "CRITICAL,HIGH"
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: "trivy-repo-results.sarif"
trivy-image-scan:
name: Trivy Image Scan
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup go
uses: actions/setup-go@v6
with:
go-version-file: go.mod
check-latest: true
- name: Build binary
run: |
make build_linux_amd64
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Build Docker image for scanning
uses: docker/build-push-action@v7
with:
context: .
file: docker/Dockerfile
platforms: linux/amd64
push: false
load: true
tags: drone-telegram:scan
- name: Run Trivy vulnerability scanner (image)
uses: aquasecurity/trivy-action@v0.36.0
with:
image-ref: "drone-telegram:scan"
format: "sarif"
output: "trivy-image-results.sarif"
severity: "CRITICAL,HIGH"
- name: Upload Trivy image scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v4
if: always()
with:
sarif_file: "trivy-image-results.sarif"
category: "trivy-image"
+100 -23
View File
@@ -1,37 +1,114 @@
version: "2"
output:
sort-order:
- file
linters:
disable-all: true
default: none
enable:
- bidichk
- bodyclose
- dogsled
- depguard
- errcheck
- exportloopref
- exhaustive
- gochecknoinits
- goconst
- forbidigo
- gocheckcompilerdirectives
- gocritic
- gofmt
- goimports
- goprintffuncname
- gosec
- gosimple
- govet
- ineffassign
- misspell
- mirror
- modernize
- nakedret
- noctx
- nilnil
- nolintlint
- perfsprint
- revive
- staticcheck
- stylecheck
- typecheck
- testifylint
- unconvert
- unparam
- unused
- whitespace
- gofumpt
- usestdlibvars
- usetesting
- wastedassign
settings:
depguard:
rules:
main:
deny:
- pkg: io/ioutil
desc: use os or io instead
- pkg: golang.org/x/exp
desc: it's experimental and unreliable
- pkg: github.com/pkg/errors
desc: use builtin errors package instead
nolintlint:
allow-unused: false
require-explanation: true
require-specific: true
gocritic:
enabled-checks:
- equalFold
disabled-checks: []
revive:
severity: error
rules:
- name: blank-imports
- name: constant-logical-expr
- name: context-as-argument
- name: context-keys-type
- name: dot-imports
- name: empty-lines
- name: error-return
- name: error-strings
- name: exported
- name: identical-branches
- name: if-return
- name: increment-decrement
- name: modifies-value-receiver
- name: package-comments
- name: redefines-builtin-id
- name: superfluous-else
- name: time-naming
- name: unexported-return
- name: var-declaration
- name: var-naming
disabled: true
staticcheck:
checks:
- all
testifylint: {}
usetesting:
os-temp-dir: true
perfsprint:
concat-loop: false
govet:
enable:
- nilness
- unusedwrite
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- errcheck
- staticcheck
- unparam
path: _test\.go
issues:
exclude-rules:
# Exclude `lll` issues for long lines with `go:generate`.
- linters:
- lll
source: "^//go:generate "
max-issues-per-linter: 0
max-same-issues: 0
formatters:
enable:
- gofmt
- gofumpt
- golines
settings:
gofumpt:
extra-rules: true
exclusions:
generated: lax
run:
timeout: 10m
+3 -4
View File
@@ -1,4 +1,3 @@
DIST := dist
EXECUTABLE := drone-telegram
GOFMT ?= gofumpt -l
DIST := dist
@@ -9,7 +8,7 @@ GOFILES := $(shell find . -name "*.go" -type f)
HAS_GO = $(shell hash $(GO) > /dev/null 2>&1 && echo "GO" || echo "NOGO" )
XGO_PACKAGE ?= src.techknowlogick.com/xgo@latest
XGO_VERSION := go-1.19.x
GXZ_PAGAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11
GXZ_PACKAGE ?= github.com/ulikunitz/xz/cmd/gxz@v0.5.11
LINUX_ARCHS ?= linux/amd64,linux/arm64
DARWIN_ARCHS ?= darwin-10.12/amd64,darwin-10.12/arm64
@@ -116,7 +115,7 @@ coverage:
.PHONY: deps-backend
deps-backend:
$(GO) mod download
$(GO) install $(GXZ_PAGAGE)
$(GO) install $(GXZ_PACKAGE)
$(GO) install $(XGO_PACKAGE)
.PHONY: release
@@ -156,7 +155,7 @@ release-check: | $(DIST_DIRS)
.PHONY: release-compress
release-compress: | $(DIST_DIRS)
cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "compressing $${file}" && $(GO) run $(GXZ_PAGAGE) -k -9 $${file}; done;
cd $(DIST)/release/; for file in `find . -type f -name "*"`; do echo "compressing $${file}" && $(GO) run $(GXZ_PACKAGE) -k -9 $${file}; done;
clean:
$(GO) clean -x -i ./...
+1
View File
@@ -3,6 +3,7 @@
![logo](./images/logo.png)
[![GoDoc](https://godoc.org/github.com/appleboy/drone-telegram?status.svg)](https://godoc.org/github.com/appleboy/drone-telegram)
[![Trivy Security Scan](https://github.com/appleboy/drone-telegram/actions/workflows/trivy.yml/badge.svg?branch=master)](https://github.com/appleboy/drone-telegram/actions/workflows/trivy.yml)
[![codecov](https://codecov.io/gh/appleboy/drone-telegram/branch/master/graph/badge.svg)](https://codecov.io/gh/appleboy/drone-telegram)
[![Go Report Card](https://goreportcard.com/badge/github.com/appleboy/drone-telegram)](https://goreportcard.com/report/github.com/appleboy/drone-telegram)
+1 -1
View File
@@ -1,4 +1,4 @@
FROM alpine:3.17
FROM alpine:3.23
ARG TARGETOS
ARG TARGETARCH
+3 -3
View File
@@ -1,6 +1,6 @@
module github.com/appleboy/drone-telegram
go 1.22
go 1.25.10
require (
github.com/appleboy/drone-template-lib v1.3.0
@@ -28,7 +28,7 @@ require (
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/spf13/cast v1.7.0 // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/sys v0.38.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
+4 -4
View File
@@ -64,12 +64,12 @@ github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQ
github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog=
github.com/urfave/cli v1.22.15 h1:nuqt+pdC/KqswQKhETJjo7pvn/k4xMUxgW6liI7XpnM=
github.com/urfave/cli v1.22.15/go.mod h1:wSan1hmo5zeyLGBjRJbzRTNk8gwoYa2B9n4q9dmRIc0=
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+103 -90
View File
@@ -1,13 +1,12 @@
package main
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"html"
"io"
"log"
"maps"
"net/http"
"net/url"
"os"
@@ -118,10 +117,10 @@ var icons = map[string]string{
}
func trimElement(keys []string) []string {
var newKeys []string
newKeys := make([]string, 0, len(keys))
for _, value := range keys {
value = strings.Trim(value, " ")
value = strings.TrimSpace(value)
if len(value) == 0 {
continue
}
@@ -132,7 +131,7 @@ func trimElement(keys []string) []string {
}
func escapeMarkdown(keys []string) []string {
var newKeys []string
newKeys := make([]string, 0, len(keys))
for _, value := range keys {
value = escapeMarkdownOne(value)
@@ -152,14 +151,20 @@ func escapeMarkdownOne(str string) string {
return str
}
func escapeMarkdownFields(fields ...*string) {
for _, f := range fields {
*f = escapeMarkdownOne(*f)
}
}
func globList(keys []string) []string {
var newKeys []string
newKeys := make([]string, 0, len(keys))
for _, pattern := range keys {
pattern = strings.Trim(pattern, " ")
pattern = strings.TrimSpace(pattern)
matches, err := filepath.Glob(pattern)
if err != nil {
fmt.Printf("Glob error for %q: %s\n", pattern, err)
log.Printf("Glob error for %q: %s", pattern, err)
continue
}
newKeys = append(newKeys, matches...)
@@ -183,7 +188,6 @@ func convertLocation(value string) (Location, bool) {
}
if len(values) > 3 {
title = values[2]
address = values[3]
}
@@ -208,13 +212,7 @@ func convertLocation(value string) (Location, bool) {
}
func loadTextFromFile(filename string) ([]string, error) {
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
r := bufio.NewReader(f)
content, err := io.ReadAll(r)
content, err := os.ReadFile(filename)
if err != nil {
return nil, err
}
@@ -258,12 +256,8 @@ func parseTo(to []string, authorEmail string, matchEmail bool) []int64 {
return ids
}
func templateMessage(t string, plugin Plugin) (string, error) {
return template.RenderTrim(t, plugin)
}
// Exec executes the plugin.
func (p Plugin) Exec() (err error) {
func (p *Plugin) Exec() (err error) {
if len(p.Config.Token) == 0 || len(p.Config.To) == 0 {
return errors.New("missing telegram token or user list")
}
@@ -273,7 +267,7 @@ func (p Plugin) Exec() (err error) {
case len(p.Config.MessageFile) > 0:
message, err = loadTextFromFile(p.Config.MessageFile)
if err != nil {
return fmt.Errorf("error loading message file '%s': %v", p.Config.MessageFile, err)
return fmt.Errorf("error loading message file '%s': %w", p.Config.MessageFile, err)
}
case len(p.Config.Message) > 0:
message = []string{p.Config.Message}
@@ -285,36 +279,46 @@ func (p Plugin) Exec() (err error) {
if p.Config.TemplateVars != "" {
p.Tpl = make(map[string]string)
if err = json.Unmarshal([]byte(p.Config.TemplateVars), &p.Tpl); err != nil {
return fmt.Errorf("unable to unmarshall template vars from JSON string '%s': %v", p.Config.TemplateVars, err)
return fmt.Errorf(
"unable to unmarshal template vars from JSON string '%s': %w",
p.Config.TemplateVars,
err,
)
}
}
if p.Config.TemplateVarsFile != "" {
content, err := os.ReadFile(p.Config.TemplateVarsFile)
if err != nil {
return fmt.Errorf("unable to read file with template vars '%s': %v", p.Config.TemplateVarsFile, err)
return fmt.Errorf(
"unable to read file with template vars '%s': %w",
p.Config.TemplateVarsFile,
err,
)
}
vars := make(map[string]string)
if err = json.Unmarshal(content, &vars); err != nil {
return fmt.Errorf("unable to unmarshall template vars from JSON file '%s': %v", p.Config.TemplateVarsFile, err)
return fmt.Errorf(
"unable to unmarshal template vars from JSON file '%s': %w",
p.Config.TemplateVarsFile,
err,
)
}
// Merging templates variables from file to the variables form plugin settings (variables from file takes precedence)
// File variables take precedence over inline variables
if p.Tpl == nil {
p.Tpl = vars
} else {
for k, v := range vars {
p.Tpl[k] = v
}
maps.Copy(p.Tpl, vars)
}
}
var proxyURL *url.URL
if proxyURL, err = url.Parse(p.Config.Socks5); err != nil {
return fmt.Errorf("unable to unmarshall socks5 proxy url from string '%s': %v", p.Config.Socks5, err)
}
var bot *tgbotapi.BotAPI
if len(p.Config.Socks5) > 0 {
var proxyURL *url.URL
proxyURL, err = url.Parse(p.Config.Socks5)
if err != nil {
return fmt.Errorf("unable to parse socks5 proxy URL '%s': %w", p.Config.Socks5, err)
}
proxyClient := &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}}
bot, err = tgbotapi.NewBotAPIWithClient(p.Config.Token, proxyClient)
} else {
@@ -328,12 +332,12 @@ func (p Plugin) Exec() (err error) {
bot.Debug = p.Config.Debug
ids := parseTo(p.Config.To, p.Commit.Email, p.Config.MatchEmail)
photos := globList(trimElement(p.Config.Photo))
documents := globList(trimElement(p.Config.Document))
stickers := globList(trimElement(p.Config.Sticker))
audios := globList(trimElement(p.Config.Audio))
voices := globList(trimElement(p.Config.Voice))
videos := globList(trimElement(p.Config.Video))
photos := globList(p.Config.Photo)
documents := globList(p.Config.Document)
stickers := globList(p.Config.Sticker)
audios := globList(p.Config.Audio)
voices := globList(p.Config.Voice)
videos := globList(p.Config.Video)
locations := trimElement(p.Config.Location)
venues := trimElement(p.Config.Venue)
@@ -342,30 +346,43 @@ func (p Plugin) Exec() (err error) {
if p.Config.Format == formatMarkdown {
message = escapeMarkdown(message)
p.Commit.Message = escapeMarkdownOne(p.Commit.Message)
p.Commit.Branch = escapeMarkdownOne(p.Commit.Branch)
p.Commit.Link = escapeMarkdownOne(p.Commit.Link)
p.Commit.Author = escapeMarkdownOne(p.Commit.Author)
p.Commit.Email = escapeMarkdownOne(p.Commit.Email)
p.Build.Tag = escapeMarkdownOne(p.Build.Tag)
p.Build.Link = escapeMarkdownOne(p.Build.Link)
p.Build.PR = escapeMarkdownOne(p.Build.PR)
p.Repo.Namespace = escapeMarkdownOne(p.Repo.Namespace)
p.Repo.Name = escapeMarkdownOne(p.Repo.Name)
escapeMarkdownFields(
&p.Commit.Message, &p.Commit.Branch, &p.Commit.Link,
&p.Commit.Author, &p.Commit.Email,
&p.Build.Tag, &p.Build.Link, &p.Build.PR,
&p.Repo.Namespace, &p.Repo.Name,
)
}
// pre-render message templates (identical for all users)
var renderedMessages []string
for _, value := range message {
txt, err := template.RenderTrim(value, p)
if err != nil {
return err
}
renderedMessages = append(renderedMessages, html.UnescapeString(txt))
}
// pre-parse locations and venues (identical for all users)
var parsedLocations []Location
for _, value := range locations {
loc, empty := convertLocation(value)
if !empty {
parsedLocations = append(parsedLocations, loc)
}
}
var parsedVenues []Location
for _, value := range venues {
loc, empty := convertLocation(value)
if !empty {
parsedVenues = append(parsedVenues, loc)
}
}
// send message.
for _, user := range ids {
for _, value := range message {
txt, err := templateMessage(value, p)
if err != nil {
return err
}
txt = html.UnescapeString(txt)
for _, txt := range renderedMessages {
msg := tgbotapi.NewMessage(user, txt)
msg.ParseMode = p.Config.Format
msg.DisableWebPagePreview = p.Config.DisableWebPagePreview
@@ -398,7 +415,7 @@ func (p Plugin) Exec() (err error) {
for _, value := range audios {
msg := tgbotapi.NewAudioUpload(user, value)
msg.Title = "Audio Message."
msg.Title = "Audio Message"
if err := p.Send(bot, msg); err != nil {
return err
}
@@ -419,27 +436,21 @@ func (p Plugin) Exec() (err error) {
}
}
for _, value := range locations {
location, empty := convertLocation(value)
if empty {
continue
}
msg := tgbotapi.NewLocation(user, location.Latitude, location.Longitude)
for _, loc := range parsedLocations {
msg := tgbotapi.NewLocation(user, loc.Latitude, loc.Longitude)
if err := p.Send(bot, msg); err != nil {
return err
}
}
for _, value := range venues {
location, empty := convertLocation(value)
if empty {
continue
}
msg := tgbotapi.NewVenue(user, location.Title, location.Address, location.Latitude, location.Longitude)
for _, loc := range parsedVenues {
msg := tgbotapi.NewVenue(
user,
loc.Title,
loc.Address,
loc.Latitude,
loc.Longitude,
)
if err := p.Send(bot, msg); err != nil {
return err
}
@@ -450,7 +461,7 @@ func (p Plugin) Exec() (err error) {
}
// Send bot message.
func (p Plugin) Send(bot *tgbotapi.BotAPI, msg tgbotapi.Chattable) error {
func (p *Plugin) Send(bot *tgbotapi.BotAPI, msg tgbotapi.Chattable) error {
message, err := bot.Send(msg)
if p.Config.Debug {
@@ -467,7 +478,7 @@ func (p Plugin) Send(bot *tgbotapi.BotAPI, msg tgbotapi.Chattable) error {
}
// Message is plugin default message.
func (p Plugin) Message() []string {
func (p *Plugin) Message() []string {
icon := icons[strings.ToLower(p.Build.Status)]
if p.Config.GitHub {
@@ -485,14 +496,16 @@ func (p Plugin) Message() []string {
// chore: update default template
//
// 🌐 https://cloud.drone.io/appleboy/drone-telegram/106
return []string{fmt.Sprintf("%s Build #%d of `%s` %s.\n\n📝 Commit by %s on `%s`:\n``` %s ```\n\n🌐 %s",
icon,
p.Build.Number,
p.Repo.FullName,
p.Build.Status,
p.Commit.Author,
p.Commit.Branch,
p.Commit.Message,
p.Build.Link,
)}
return []string{
fmt.Sprintf("%s Build #%d of `%s` %s.\n\n📝 Commit by %s on `%s`:\n``` %s ```\n\n🌐 %s",
icon,
p.Build.Number,
p.Repo.FullName,
p.Build.Status,
p.Commit.Author,
p.Commit.Branch,
p.Commit.Message,
p.Build.Link,
),
}
}
+66 -31
View File
@@ -7,14 +7,22 @@ import (
"github.com/appleboy/drone-template-lib/template"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func skipIfNoTelegramSecrets(t *testing.T) {
t.Helper()
if os.Getenv("TELEGRAM_TOKEN") == "" || os.Getenv("TELEGRAM_TO") == "" {
t.Skip("TELEGRAM_TOKEN/TELEGRAM_TO not set; skipping integration test")
}
}
func TestMissingDefaultConfig(t *testing.T) {
var plugin Plugin
err := plugin.Exec()
assert.NotNil(t, err)
assert.Error(t, err)
}
func TestMissingUserConfig(t *testing.T) {
@@ -26,7 +34,7 @@ func TestMissingUserConfig(t *testing.T) {
err := plugin.Exec()
assert.NotNil(t, err)
assert.Error(t, err)
}
func TestDefaultMessageFormat(t *testing.T) {
@@ -51,7 +59,13 @@ func TestDefaultMessageFormat(t *testing.T) {
message := plugin.Message()
assert.Equal(t, []string{"✅ Build #101 of `appleboy/go-hello` success.\n\n📝 Commit by Bo-Yi Wu on `master`:\n``` update travis ```\n\n🌐 https://github.com/appleboy/go-hello"}, message)
assert.Equal(
t,
[]string{
"✅ Build #101 of `appleboy/go-hello` success.\n\n📝 Commit by Bo-Yi Wu on `master`:\n``` update travis ```\n\n🌐 https://github.com/appleboy/go-hello",
},
message,
)
}
func TestDefaultMessageFormatFromGitHub(t *testing.T) {
@@ -73,7 +87,11 @@ func TestDefaultMessageFormatFromGitHub(t *testing.T) {
message := plugin.Message()
assert.Equal(t, []string{"appleboy/go-hello/test-workflow triggered by appleboy (push)"}, message)
assert.Equal(
t,
[]string{"appleboy/go-hello/test-workflow triggered by appleboy (push)"},
message,
)
}
func TestSendMessage(t *testing.T) {
@@ -97,8 +115,13 @@ func TestSendMessage(t *testing.T) {
},
Config: Config{
Token: os.Getenv("TELEGRAM_TOKEN"),
To: []string{os.Getenv("TELEGRAM_TO"), os.Getenv("TELEGRAM_TO") + ":appleboy@gmail.com", "中文ID", "1234567890"},
Token: os.Getenv("TELEGRAM_TOKEN"),
To: []string{
os.Getenv("TELEGRAM_TO"),
os.Getenv("TELEGRAM_TO") + ":appleboy@gmail.com",
"中文ID",
"1234567890",
},
Message: "Test Telegram Chat Bot From Travis or Local, commit message: 『{{ build.message }}』",
Photo: []string{"tests/github.png", "1234", " "},
Document: []string{"tests/gophercolor.png", "1234", " "},
@@ -106,27 +129,33 @@ func TestSendMessage(t *testing.T) {
Audio: []string{"tests/audio.mp3", "1234", " "},
Voice: []string{"tests/voice.ogg", "1234", " "},
Location: []string{"24.9163213 121.1424972", "1", " "},
Venue: []string{"35.661777 139.704051 竹北體育館 新竹縣竹北市", "24.9163213 121.1424972", "1", " "},
Video: []string{"tests/video.mp4", "1234", " "},
Debug: false,
Venue: []string{
"35.661777 139.704051 竹北體育館 新竹縣竹北市",
"24.9163213 121.1424972",
"1",
" ",
},
Video: []string{"tests/video.mp4", "1234", " "},
Debug: false,
},
}
err := plugin.Exec()
assert.NotNil(t, err)
require.Error(t, err)
plugin.Config.Format = formatMarkdown
plugin.Config.Message = "Test escape under_score"
err = plugin.Exec()
assert.NotNil(t, err)
require.Error(t, err)
// disable message
plugin.Config.Message = ""
err = plugin.Exec()
assert.NotNil(t, err)
assert.Error(t, err)
}
func TestDisableWebPagePreviewMessage(t *testing.T) {
skipIfNoTelegramSecrets(t)
plugin := Plugin{
Config: Config{
Token: os.Getenv("TELEGRAM_TOKEN"),
@@ -138,16 +167,17 @@ func TestDisableWebPagePreviewMessage(t *testing.T) {
plugin.Config.Message = "DisableWebPagePreview https://www.google.com.tw"
err := plugin.Exec()
assert.Nil(t, err)
require.NoError(t, err)
// disable message
plugin.Config.DisableWebPagePreview = false
plugin.Config.Message = "EnableWebPagePreview https://www.google.com.tw"
err = plugin.Exec()
assert.Nil(t, err)
assert.NoError(t, err)
}
func TestDisableNotificationMessage(t *testing.T) {
skipIfNoTelegramSecrets(t)
plugin := Plugin{
Config: Config{
Token: os.Getenv("TELEGRAM_TOKEN"),
@@ -159,13 +189,13 @@ func TestDisableNotificationMessage(t *testing.T) {
plugin.Config.Message = "DisableNotification https://www.google.com.tw"
err := plugin.Exec()
assert.Nil(t, err)
require.NoError(t, err)
// disable message
plugin.Config.DisableNotification = false
plugin.Config.Message = "EnableNotification https://www.google.com.tw"
err = plugin.Exec()
assert.Nil(t, err)
assert.NoError(t, err)
}
func TestBotError(t *testing.T) {
@@ -194,7 +224,7 @@ func TestBotError(t *testing.T) {
}
err := plugin.Exec()
assert.NotNil(t, err)
assert.Error(t, err)
}
func TestTrimElement(t *testing.T) {
@@ -270,7 +300,7 @@ func TestParseTo(t *testing.T) {
// test empty ids
ids = parseTo([]string{"", " ", " "}, "a@gmail.com", true)
assert.Equal(t, 0, len(ids))
assert.Empty(t, ids)
}
func TestGlobList(t *testing.T) {
@@ -294,27 +324,27 @@ func TestConvertLocation(t *testing.T) {
input = "1"
result, empty = convertLocation(input)
assert.Equal(t, true, empty)
assert.True(t, empty)
assert.Equal(t, Location{}, result)
// strconv.ParseInt: parsing "測試": invalid syntax
input = "測試 139.704051"
result, empty = convertLocation(input)
assert.Equal(t, true, empty)
assert.True(t, empty)
assert.Equal(t, Location{}, result)
// strconv.ParseInt: parsing "測試": invalid syntax
input = "35.661777 測試"
result, empty = convertLocation(input)
assert.Equal(t, true, empty)
assert.True(t, empty)
assert.Equal(t, Location{}, result)
input = "35.661777 139.704051"
result, empty = convertLocation(input)
assert.Equal(t, false, empty)
assert.False(t, empty)
assert.Equal(t, Location{
Latitude: float64(35.661777),
Longitude: float64(139.704051),
@@ -323,7 +353,7 @@ func TestConvertLocation(t *testing.T) {
input = "35.661777 139.704051 title"
result, empty = convertLocation(input)
assert.Equal(t, false, empty)
assert.False(t, empty)
assert.Equal(t, Location{
Title: "title",
Address: "",
@@ -334,7 +364,7 @@ func TestConvertLocation(t *testing.T) {
input = "35.661777 139.704051 title address"
result, empty = convertLocation(input)
assert.Equal(t, false, empty)
assert.False(t, empty)
assert.Equal(t, Location{
Title: "title",
Address: "address",
@@ -344,6 +374,7 @@ func TestConvertLocation(t *testing.T) {
}
func TestHTMLMessage(t *testing.T) {
skipIfNoTelegramSecrets(t)
plugin := Plugin{
Repo: Repo{
Name: "go-hello",
@@ -374,13 +405,14 @@ Test HTML Format
},
}
assert.Nil(t, plugin.Exec())
assert.NoError(t, plugin.Exec())
plugin.Config.MessageFile = "tests/message_html.txt"
assert.Nil(t, plugin.Exec())
assert.NoError(t, plugin.Exec())
}
func TestMessageFile(t *testing.T) {
skipIfNoTelegramSecrets(t)
plugin := Plugin{
Repo: Repo{
Name: "go-hello",
@@ -408,10 +440,11 @@ func TestMessageFile(t *testing.T) {
}
err := plugin.Exec()
assert.Nil(t, err)
assert.NoError(t, err)
}
func TestTemplateVars(t *testing.T) {
skipIfNoTelegramSecrets(t)
plugin := Plugin{
Repo: Repo{
Name: "go-hello",
@@ -441,10 +474,11 @@ func TestTemplateVars(t *testing.T) {
}
err := plugin.Exec()
assert.Nil(t, err)
assert.NoError(t, err)
}
func TestTemplateVarsFile(t *testing.T) {
skipIfNoTelegramSecrets(t)
plugin := Plugin{
Repo: Repo{
Name: "go-hello",
@@ -472,10 +506,11 @@ func TestTemplateVarsFile(t *testing.T) {
}
err := plugin.Exec()
assert.Nil(t, err)
assert.NoError(t, err)
}
func TestProxySendMessage(t *testing.T) {
skipIfNoTelegramSecrets(t)
plugin := Plugin{
Repo: Repo{
Name: "go-hello",
@@ -505,7 +540,7 @@ func TestProxySendMessage(t *testing.T) {
}
err := plugin.Exec()
assert.Nil(t, err)
assert.NoError(t, err)
}
func TestBuildTemplate(t *testing.T) {
@@ -533,5 +568,5 @@ Commit msg: {{uppercasefirst commit.message}}
duration: {{duration build.started build.finished}}
`, plugin)
assert.Nil(t, err)
assert.NoError(t, err)
}