Skip to content

Commit 81422b8

Browse files
Merge branch 'main' into bill
2 parents f781837 + 0f170aa commit 81422b8

File tree

8 files changed

+194
-26
lines changed

8 files changed

+194
-26
lines changed

.github/workflows/ci.yml

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
name: tests-and-release
2+
3+
on:
4+
pull_request:
5+
workflow_dispatch:
6+
push:
7+
branches:
8+
- main
9+
tags:
10+
- "*"
11+
12+
permissions:
13+
contents: write
14+
15+
env:
16+
GO_VERSION: "1.22"
17+
18+
jobs:
19+
unit-test:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- name: checkout
23+
uses: actions/checkout@v4
24+
25+
- name: set up go
26+
uses: actions/setup-go@v5
27+
with:
28+
go-version: ${{ env.GO_VERSION}}
29+
30+
- name: unit tests
31+
run: go test ./...
32+
33+
release:
34+
runs-on: ubuntu-latest
35+
needs: unit-test
36+
if: startsWith(github.ref, 'refs/tags/')
37+
steps:
38+
- name: checkout
39+
uses: actions/checkout@v4
40+
with:
41+
fetch-depth: 0
42+
43+
- name: set up go
44+
uses: actions/setup-go@v5
45+
with:
46+
go-version: ${{ env.GO_VERSION}}
47+
48+
- name: create release
49+
uses: goreleaser/goreleaser-action@master
50+
with:
51+
version: latest
52+
args: release --clean
53+
env:
54+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

cmd/copy.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ func checkTmetricEntries(tmetricUser tmetric.User, config *config.Config) ([]tme
4444
)
4545
}
4646

47-
if len(tmetric.GetEntriesWithoutLinkToOpenProject(timeEntries)) > 0 {
47+
if len(tmetric.GetEntriesWithoutLinkToOpenProject(config, timeEntries)) > 0 {
4848
return nil, fmt.Errorf(
4949
"some time-entries are not linked to an OpenProject work-package, run the 'check tmetric' command to fix it",
5050
)

cmd/diff.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,19 @@ package cmd
1919

2020
import (
2121
"fmt"
22+
"os"
23+
"path"
24+
"strconv"
25+
"strings"
26+
"time"
27+
2228
"github.com/JankariTech/OpenProjectTmetricIntegration/config"
2329
"github.com/JankariTech/OpenProjectTmetricIntegration/openproject"
2430
"github.com/JankariTech/OpenProjectTmetricIntegration/tmetric"
2531
"github.com/jedib0t/go-pretty/v6/table"
2632
"github.com/jedib0t/go-pretty/v6/text"
2733
"github.com/spf13/cobra"
2834
"golang.org/x/term"
29-
"os"
30-
"path"
31-
"strconv"
32-
"strings"
33-
"time"
3435
)
3536

3637
type tableRow struct {
@@ -126,6 +127,7 @@ var diffCmd = &cobra.Command{
126127
{Number: 4, WidthMax: widthContentColumns},
127128
})
128129

130+
totalTimeDiff := 0
129131
for currentDay := start; !currentDay.After(end); currentDay = currentDay.AddDate(0, 0, 1) {
130132
row := tableRow{}
131133
row.Date = currentDay.Format("2006-01-02")
@@ -178,9 +180,13 @@ var diffCmd = &cobra.Command{
178180
}
179181
}
180182
if sumDurationTmetric > sumDurationOpenProject {
181-
row.DiffInTime = strconv.Itoa(sumDurationTmetric - sumDurationOpenProject)
183+
diff := sumDurationTmetric - sumDurationOpenProject
184+
row.DiffInTime = strconv.Itoa(diff)
185+
totalTimeDiff += diff
182186
} else {
183-
row.DiffInTime = strconv.Itoa(sumDurationOpenProject - sumDurationTmetric)
187+
diff := sumDurationOpenProject - sumDurationTmetric
188+
row.DiffInTime = strconv.Itoa(diff)
189+
totalTimeDiff += diff
184190
}
185191

186192
outputTable.AppendRow(table.Row{
@@ -193,6 +199,14 @@ var diffCmd = &cobra.Command{
193199
})
194200
outputTable.AppendSeparator()
195201
}
202+
outputTable.AppendRow(table.Row{
203+
"",
204+
"",
205+
"",
206+
"Total Diff",
207+
"",
208+
strconv.Itoa(totalTimeDiff),
209+
})
196210
outputTable.Render()
197211
},
198212
}

cmd/tmetric.go

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,22 +42,15 @@ func validateOpenProjectWorkPackage(input string) error {
4242
}
4343

4444
func handleEntriesWithoutIssue(timeEntries []tmetric.TimeEntry, tmetricUser tmetric.User, config *config.Config) error {
45-
// get all entries that belong to the client and do not have an external link
46-
var entriesWithoutIssue []tmetric.TimeEntry
47-
for _, entry := range timeEntries {
48-
if entry.Project.Client.Id == config.ClientIdInTmetric && entry.Task.ExternalLink.IssueId == "" {
49-
entriesWithoutIssue = append(entriesWithoutIssue, entry)
50-
}
51-
}
52-
53-
if len(entriesWithoutIssue) > 0 {
45+
entriesWithoutLinkToOpenProject := tmetric.GetEntriesWithoutLinkToOpenProject(config, timeEntries)
46+
if len(entriesWithoutLinkToOpenProject) > 0 {
5447
fmt.Println("Some time-entries do not have any workpackages assigned")
5548
}
5649

5750
spinner := newSpinner()
5851
defer spinner.Stop()
5952

60-
for _, entry := range entriesWithoutIssue {
53+
for _, entry := range entriesWithoutLinkToOpenProject {
6154
prompt := promptui.Prompt{
6255
Label: fmt.Sprintf(
6356
"%v => %v %v-%v. Provide a WP number to be assigned to this time-entry (Enter to skip)",

config/config.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type Config struct {
3232
TmetricAPIV3BaseUrl string
3333
TmetricDummyProjectId int
3434
TmetricTagTransferredToOpenProject string
35+
TmetricExternalTaskLink string
3536
}
3637

3738
func NewConfig() *Config {
@@ -69,5 +70,8 @@ func NewConfig() *Config {
6970
TmetricAPIV3BaseUrl: "https://app.tmetric.com/api/v3/",
7071
TmetricDummyProjectId: tmetricDummyProjectId,
7172
TmetricTagTransferredToOpenProject: "transferred-to-openproject",
73+
// this value has always to be "https://community.openproject.org"
74+
// otherwise tmetric does not recognize the integration and does not allow to create the external task
75+
TmetricExternalTaskLink: "https://community.openproject.org/",
7276
}
7377
}

tmetric/timeentry.go

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,18 @@ func (timeEntry *TimeEntry) GetPossibleWorkTypes(config config.Config, user User
174174
// GetIssueIdAsInt returns the issue id as an integer
175175
// the issue Id in tmetric is a string e.g. #1234, but for OpenProject we need the integer to construct the URLs
176176
func (timeEntry *TimeEntry) GetIssueIdAsInt() (int, error) {
177-
issueIdStr := regexp.MustCompile(`#(\d+)`).
178-
FindStringSubmatch(timeEntry.Task.ExternalLink.IssueId)[1]
179-
return strconv.Atoi(issueIdStr)
177+
issueRegex, err := regexp.Compile(`#(\d+)`)
178+
if err != nil {
179+
return 0, err
180+
}
181+
issueIdStr := issueRegex.FindStringSubmatch(timeEntry.Task.ExternalLink.IssueId)
182+
if len(issueIdStr) != 2 {
183+
return 0, fmt.Errorf(
184+
"could not find valid OpenProject workpackage id in tmetric task '%v'",
185+
timeEntry.Task.ExternalLink.IssueId,
186+
)
187+
}
188+
return strconv.Atoi(issueIdStr[1])
180189
}
181190

182191
/*
@@ -187,9 +196,7 @@ func CreateDummyTimeEntry(
187196
workPackage openproject.WorkPackage, tmetricUser User, config *config.Config,
188197
) (*TimeEntry, error) {
189198
dummyTimeEntry := NewDummyTimeEntry(workPackage, config.OpenProjectUrl, config.TmetricDummyProjectId)
190-
// the serviceUrl for the dummy task has always to be "https://community.openproject.org"
191-
// otherwise tmetric does not recognize the integration and does not allow to create the external task
192-
dummyTimeEntry.ServiceUrl = "https://community.openproject.org"
199+
dummyTimeEntry.ServiceUrl = config.TmetricExternalTaskLink
193200
dummyTimerString, _ := json.Marshal(dummyTimeEntry)
194201
httpClient := resty.New()
195202
resp, err := httpClient.R().

tmetric/tmetric.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net/url"
99
"sort"
1010
"strconv"
11+
"strings"
1112
)
1213

1314
// ClientV2 represents a client that is returned by the Tmetric API V2
@@ -212,10 +213,14 @@ func GetEntriesWithoutWorkType(timeEntries []TimeEntry) []TimeEntry {
212213
return entriesWithoutWorkType
213214
}
214215

215-
func GetEntriesWithoutLinkToOpenProject(timeEntries []TimeEntry) []TimeEntry {
216+
func GetEntriesWithoutLinkToOpenProject(config *config.Config, timeEntries []TimeEntry) []TimeEntry {
216217
var entriesWithoutLink []TimeEntry
217218
for _, entry := range timeEntries {
218-
if entry.Task.Id == 0 {
219+
_, err := entry.GetIssueIdAsInt()
220+
if entry.Task.Id == 0 ||
221+
err != nil ||
222+
entry.Task.ExternalLink.IssueId == "" ||
223+
(!strings.HasPrefix(entry.Task.ExternalLink.Link, config.TmetricExternalTaskLink+"work_packages")) {
219224
entriesWithoutLink = append(entriesWithoutLink, entry)
220225
}
221226
}

tmetric/tmetric_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,97 @@ import (
88
"testing"
99
)
1010

11+
func Test_GetEntriesWithoutLinkToOpenProject(t *testing.T) {
12+
tests := []struct {
13+
name string
14+
task Task
15+
}{
16+
{
17+
name: "empty task",
18+
task: Task{},
19+
},
20+
{
21+
name: "no external link",
22+
task: Task{
23+
Id: 123,
24+
Name: "some WP",
25+
},
26+
},
27+
{
28+
name: "link is not to openproject",
29+
task: Task{
30+
Id: 345,
31+
Name: "some WP",
32+
ExternalLink: ExternalLink{
33+
Link: "https://some_host/work_packages/123",
34+
IssueId: "#123",
35+
},
36+
},
37+
},
38+
{
39+
name: "IssueId is empty",
40+
task: Task{
41+
Id: 345,
42+
Name: "some WP",
43+
ExternalLink: ExternalLink{
44+
Link: "https://community.openproject.org/work_packages/123",
45+
IssueId: "",
46+
},
47+
},
48+
},
49+
{
50+
name: "IssueId has wrong format",
51+
task: Task{
52+
Id: 345,
53+
Name: "some WP",
54+
ExternalLink: ExternalLink{
55+
Link: "https://community.openproject.org/work_packages/123",
56+
IssueId: "!123",
57+
},
58+
},
59+
},
60+
}
61+
for _, tt := range tests {
62+
t.Run(tt.name, func(t *testing.T) {
63+
// adds a correct entry at the beginning and the end and makes sure the invalid
64+
// entry is still in the result
65+
config := &config.Config{
66+
ClientIdInTmetric: 123,
67+
TmetricExternalTaskLink: "https://community.openproject.org/",
68+
}
69+
validTimeEntry := TimeEntry{
70+
Project: Project{
71+
Client: Client{Id: 123},
72+
Name: "Project1",
73+
},
74+
Note: "correct entry",
75+
Task: Task{
76+
Id: 345,
77+
Name: "some WP",
78+
ExternalLink: ExternalLink{
79+
Link: "https://community.openproject.org/work_packages/123",
80+
IssueId: "#123",
81+
},
82+
},
83+
}
84+
invalidTimeEntry := TimeEntry{
85+
Project: Project{
86+
Client: Client{Id: 123},
87+
Name: "Project1",
88+
},
89+
Note: "invalid entry",
90+
Task: tt.task,
91+
}
92+
timeEntriesToCheck := append([]TimeEntry{invalidTimeEntry}, validTimeEntry)
93+
timeEntriesToCheck = append([]TimeEntry{validTimeEntry}, timeEntriesToCheck...)
94+
result := GetEntriesWithoutLinkToOpenProject(config, timeEntriesToCheck)
95+
if !reflect.DeepEqual(result, []TimeEntry{invalidTimeEntry}) {
96+
t.Errorf("got %v, want %v", result, []TimeEntry{invalidTimeEntry})
97+
}
98+
})
99+
}
100+
}
101+
11102
func Test_GetEntriesWithoutWorkType(t *testing.T) {
12103
type args struct {
13104
timeEntries []TimeEntry

0 commit comments

Comments
 (0)