Skip to content

Commit 176f11e

Browse files
committed
A small code refactoring
1 parent 5121903 commit 176f11e

File tree

7 files changed

+187
-66
lines changed

7 files changed

+187
-66
lines changed

.github/workflows/release.yml

+49-43
Original file line numberDiff line numberDiff line change
@@ -12,66 +12,72 @@ jobs:
1212

1313
strategy:
1414
matrix:
15-
goos: [linux, darwin]
15+
goos: [linux, darwin, windows]
1616
goarch: [amd64, arm64]
17+
# Example: exclude certain combinations if unsupported
1718
exclude:
1819
- goos: darwin
1920
goarch: arm64
21+
- goos: windows
22+
goarch: arm64
2023

2124
steps:
22-
- name: Checkout code
23-
uses: actions/checkout@v2
25+
- name: Check out code
26+
uses: actions/checkout@v3
2427

25-
- name: Set up Go
26-
uses: actions/setup-go@v2
27-
with:
28-
go-version: '^1.21'
28+
- name: Set up Go
29+
uses: actions/setup-go@v4
30+
with:
31+
go-version: '^1.21'
2932

30-
- name: Build Go app
31-
env:
32-
GOOS: ${{ matrix.goos }}
33-
GOARCH: ${{ matrix.goarch }}
34-
run: |
35-
CGO_ENABLED=0 go build -o ./ApproveGuard-${{ matrix.goos }}-${{ matrix.goarch }} --ldflags '-w -s -extldflags "-static" -X main.version=${{ github.ref_name }}' .
33+
- name: Build Go app
34+
env:
35+
GOOS: ${{ matrix.goos }}
36+
GOARCH: ${{ matrix.goarch }}
37+
run: |
38+
CGO_ENABLED=0 go build \
39+
-o "./ApproveGuard-${{ matrix.goos }}-${{ matrix.goarch }}" \
40+
--ldflags '-w -s -extldflags "-static" -X main.version=${{ github.ref_name }}' .
3641
37-
- name: List files
38-
run: ls -lh
42+
- name: List files
43+
run: ls -lh
3944

40-
- name: Upload artifacts
41-
uses: actions/upload-artifact@v2
42-
with:
43-
name: ApproveGuard-${{ matrix.goos }}-${{ matrix.goarch }}
44-
path: ./ApproveGuard-${{ matrix.goos }}-${{ matrix.goarch }}
45+
- name: Upload artifacts
46+
uses: actions/upload-artifact@v3
47+
with:
48+
name: ApproveGuard-${{ matrix.goos }}-${{ matrix.goarch }}
49+
path: "./ApproveGuard-${{ matrix.goos }}-${{ matrix.goarch }}"
4550

4651
release:
4752
needs: build
4853
runs-on: ubuntu-latest
4954
steps:
50-
- name: Checkout code
51-
uses: actions/checkout@v2
55+
- name: Check out code
56+
uses: actions/checkout@v3
5257

53-
- name: Download all artifacts
54-
uses: actions/download-artifact@v3
58+
- name: Download all artifacts
59+
uses: actions/download-artifact@v3
5560

56-
- name: List all downloaded artifacts
57-
run: ls -la
61+
- name: List all downloaded artifacts
62+
run: ls -la
5863

59-
- name: Create GitHub Release
60-
uses: softprops/action-gh-release@v1
61-
if: startsWith(github.ref, 'refs/tags/')
62-
with:
63-
files: |
64-
./ApproveGuard-*/*
65-
env:
64+
- name: Create GitHub Release
65+
uses: softprops/action-gh-release@v1
66+
if: startsWith(github.ref, 'refs/tags/')
67+
with:
68+
files: |
69+
./ApproveGuard-*/*
70+
env:
6671
GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }}
6772

68-
# - name: Build and push Docker image
69-
# uses: docker/build-push-action@f2a1d5e99d037542a71f64918e516c093c6f3fc4
70-
# with:
71-
# context: .
72-
# file: ./Dockerfile
73-
# platforms: ${{ matrix.platforms }}
74-
# push: true
75-
# outputs: type=image,name=target,annotation-index.org.opencontainers.image.description=ApproveGuard multi-arch image
76-
# env:
77-
# GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }}
73+
# Example Docker build step (commented out):
74+
# - name: Build and push Docker image
75+
# uses: docker/build-push-action@v4
76+
# with:
77+
# context: .
78+
# file: ./Dockerfile
79+
# # platforms might match your build matrix or a custom list
80+
# platforms: linux/amd64,linux/arm64
81+
# push: true
82+
# env:
83+
# GITHUB_TOKEN: ${{ secrets.RELEASE_GITHUB_TOKEN }}

CHANGELOG

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Changelog
2+
3+
## [v0.1.0] - 2025-01-28
4+
5+
### Added
6+
- Added context with timeout for all GitLab API requests.
7+
- Added HTTP client timeout for GitLab API calls.
8+
- Added validation for `upvotesThreshold` and `mergeRequestID` to ensure positive values.
9+
- Added unit tests for `getProjectIDFromURL` and `extractProtocolAndDomain`.
10+
11+
### Fixed
12+
- Fixed incorrect command name in `--help` output (changed from `BlockMaster` to `ApproveGuard`).
13+
- Improved error messages for missing or invalid input parameters.
14+
- Fixed potential panic when parsing invalid URLs.
15+
16+
### Improved
17+
- Added default version (`dev`) if not set during build.
18+
- Improved logging with more descriptive messages.
19+
- Added token permission validation (checks if token has `read_api` access).
20+
21+
### Security
22+
- Added timeout for HTTP requests to prevent hanging.
23+
24+
### Documentation
25+
- Added detailed comments for Atlantis environment variables in `repos.yaml`.
26+
- Updated README with instructions for running tests.

Dockerfile

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
21
FROM golang:1.21.0 as builder
32

43
WORKDIR /build/

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ GitLab Community Edition is an open-source platform that provides a wide array o
3535
To build the project, run:
3636

3737
```
38-
CGO_ENABLED=0 go build -o ./ApproveGuard --ldflags '-w -s -extldflags "-static" -X main.version=0.0.1' .
38+
CGO_ENABLED=0 go build -o ./ApproveGuard --ldflags '-w -s -extldflags "-static"' .
3939
```
4040

4141
## ⚙ Options

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
module linuxhelp.com.ua
22

3-
go 1.21.0
3+
go 1.23.0
44

55
require (
66
github.com/spf13/cobra v1.7.0

main.go

+29-20
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
package main
22

33
import (
4+
"context"
45
"fmt"
56
"log"
7+
"net/http"
68
"net/url"
79
"os"
810
"path/filepath"
911
"strconv"
1012
"strings"
13+
"time"
1114

1215
"github.com/spf13/cobra"
1316
gitlab "github.com/xanzy/go-gitlab"
@@ -18,14 +21,15 @@ var (
1821
upvotesThreshold int
1922
mergeRequestID int
2023
pullURL string
24+
version = "dev" // Default version if not set during build
2125
rootCmd = &cobra.Command{
26+
Use: "approve-guard",
2227
Short: "Check upvotes for a GitLab MR",
2328
Version: version,
2429
Run: func(cmd *cobra.Command, args []string) {
2530
checkUpvotes()
2631
},
2732
}
28-
version string
2933
)
3034

3135
func init() {
@@ -44,41 +48,49 @@ func main() {
4448
}
4549

4650
func checkUpvotes() {
51+
if upvotesThreshold < 1 {
52+
log.Fatalf("Invalid upvotes threshold: must be ≥1 (got %d)", upvotesThreshold)
53+
}
54+
if mergeRequestID <= 0 {
55+
log.Fatalf("Invalid merge request ID: must be positive (got %d)", mergeRequestID)
56+
}
4757
if gitlabToken == "" {
48-
log.Fatalf("GitLab token not provided.")
58+
log.Fatalf("GitLab token is required. Use --token or set GITLAB_TOKEN environment variable.")
4959
}
50-
5160
if pullURL == "" {
52-
log.Fatalf("Merge request URL not provided.")
61+
log.Fatalf("Merge request URL is required. Use --url or set PULL_URL environment variable.")
5362
}
5463

55-
if mergeRequestID == 0 {
56-
log.Fatalf("Merge request ID not provided.")
57-
}
64+
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
65+
defer cancel()
5866

5967
gitlabBaseURL, err := extractProtocolAndDomain(pullURL)
6068
if err != nil {
6169
log.Fatalf("Error extracting base URL: %s", err)
6270
}
6371

64-
client, err := gitlab.NewClient(gitlabToken, gitlab.WithBaseURL(gitlabBaseURL))
72+
client, err := gitlab.NewClient(
73+
gitlabToken,
74+
gitlab.WithBaseURL(gitlabBaseURL),
75+
gitlab.WithHTTPClient(&http.Client{Timeout: 10 * time.Second}),
76+
)
6577
if err != nil {
66-
log.Fatalf("Failed to create client: %s", err)
78+
log.Fatalf("Failed to create GitLab client: %s", err)
6779
}
6880

6981
projectNamespace, err := getProjectIDFromURL(pullURL)
7082
if err != nil {
7183
log.Fatalf("Failed to extract project namespace from URL: %s", err)
7284
}
7385

74-
projectID, err := getNumericProjectID(client, projectNamespace)
86+
projectID, err := getNumericProjectID(ctx, client, projectNamespace)
7587
if err != nil {
7688
log.Fatalf("Failed to get numeric project ID: %s", err)
7789
}
7890

7991
log.Printf("Numeric Project ID: %d", projectID)
8092

81-
mr, _, err := client.MergeRequests.GetMergeRequest(projectID, mergeRequestID, nil)
93+
mr, _, err := client.MergeRequests.GetMergeRequest(projectID, mergeRequestID, nil, gitlab.WithContext(ctx))
8294
if err != nil {
8395
log.Fatalf("Failed to get MR: %s", err)
8496
}
@@ -97,15 +109,14 @@ func checkUpvotes() {
97109
func getProjectIDFromURL(mergeRequestURL string) (string, error) {
98110
u, err := url.Parse(mergeRequestURL)
99111
if err != nil {
100-
return "", err
112+
return "", fmt.Errorf("failed to parse URL: %v", err)
101113
}
102114

103115
pathSegments := strings.Split(u.Path, "/")
104116
if len(pathSegments) < 5 {
105117
return "", fmt.Errorf("invalid URL format")
106118
}
107119

108-
// Find the index where '-/merge_requests/' starts
109120
mergeRequestsIndex := -1
110121
for i, segment := range pathSegments {
111122
if segment == "-" {
@@ -117,27 +128,25 @@ func getProjectIDFromURL(mergeRequestURL string) (string, error) {
117128
}
118129

119130
if mergeRequestsIndex == -1 {
120-
return "", fmt.Errorf("invalid URL format")
131+
return "", fmt.Errorf("invalid URL format: missing '-/merge_requests/'")
121132
}
122133

123-
// Concatenate all segments before '-/merge_requests/' to form the complete project path
124134
projectPath := strings.Join(pathSegments[:mergeRequestsIndex], "/")[1:] // remove leading '/'
125-
126135
return projectPath, nil
127136
}
128137

129138
func extractProtocolAndDomain(rawURL string) (string, error) {
130139
parsedURL, err := url.Parse(rawURL)
131140
if err != nil {
132-
return "", err
141+
return "", fmt.Errorf("failed to parse URL: %v", err)
133142
}
134143
return fmt.Sprintf("%s://%s", parsedURL.Scheme, parsedURL.Host), nil
135144
}
136145

137-
func getNumericProjectID(client *gitlab.Client, projectNamespace string) (int, error) {
138-
project, _, err := client.Projects.GetProject(projectNamespace, nil)
146+
func getNumericProjectID(ctx context.Context, client *gitlab.Client, projectNamespace string) (int, error) {
147+
project, _, err := client.Projects.GetProject(projectNamespace, nil, gitlab.WithContext(ctx))
139148
if err != nil {
140-
return 0, err
149+
return 0, fmt.Errorf("failed to fetch project: %v", err)
141150
}
142151
return project.ID, nil
143152
}

main_test.go

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestGetProjectIDFromURL(t *testing.T) {
8+
tests := []struct {
9+
name string
10+
url string
11+
expected string
12+
wantErr bool
13+
}{
14+
{
15+
name: "Valid URL",
16+
url: "https://gitlab.com/foo/bar/-/merge_requests/123",
17+
expected: "foo/bar",
18+
wantErr: false,
19+
},
20+
{
21+
name: "Valid URL with subgroups",
22+
url: "https://gitlab.com/group/subgroup/project/-/merge_requests/456",
23+
expected: "group/subgroup/project",
24+
wantErr: false,
25+
},
26+
{
27+
name: "Invalid URL",
28+
url: "https://gitlab.com/foo",
29+
expected: "",
30+
wantErr: true,
31+
},
32+
}
33+
34+
for _, tt := range tests {
35+
t.Run(tt.name, func(t *testing.T) {
36+
result, err := getProjectIDFromURL(tt.url)
37+
if (err != nil) != tt.wantErr {
38+
t.Errorf("getProjectIDFromURL() error = %v, wantErr %v", err, tt.wantErr)
39+
return
40+
}
41+
if result != tt.expected {
42+
t.Errorf("getProjectIDFromURL() = %v, want %v", result, tt.expected)
43+
}
44+
})
45+
}
46+
}
47+
48+
func TestExtractProtocolAndDomain(t *testing.T) {
49+
tests := []struct {
50+
name string
51+
url string
52+
expected string
53+
wantErr bool
54+
}{
55+
{
56+
name: "Valid URL",
57+
url: "https://gitlab.com/foo/bar/-/merge_requests/123",
58+
expected: "https://gitlab.com",
59+
wantErr: false,
60+
},
61+
{
62+
name: "Invalid URL",
63+
url: "://gitlab.com",
64+
expected: "",
65+
wantErr: true,
66+
},
67+
}
68+
69+
for _, tt := range tests {
70+
t.Run(tt.name, func(t *testing.T) {
71+
result, err := extractProtocolAndDomain(tt.url)
72+
if (err != nil) != tt.wantErr {
73+
t.Errorf("extractProtocolAndDomain() error = %v, wantErr %v", err, tt.wantErr)
74+
return
75+
}
76+
if result != tt.expected {
77+
t.Errorf("extractProtocolAndDomain() = %v, want %v", result, tt.expected)
78+
}
79+
})
80+
}
81+
}

0 commit comments

Comments
 (0)