Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: CI

on:
push:
branches:
- master
pull_request:
branches:
- master

env:
JIRA_API_TOKEN: SUPER_SECRET_TOKEN_FOR_TESTING_ONLY

jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.22.x'
- name: Run tests
run: go test ./...

build-and-push-docker-image:
runs-on: ubuntu-latest
needs: test
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build Docker image
run: |
docker build -t ghcr.io/${{ github.repository_owner }}/sigrab:latest -t ghcr.io/${{ github.repository_owner }}/sigrab:${{ github.sha }} .
- name: Push Docker image
if: github.event_name == 'push' && github.ref == 'refs/heads/master'
run: |
docker push ghcr.io/${{ github.repository_owner }}/sigrab:latest
docker push ghcr.io/${{ github.repository_owner }}/sigrab:${{ github.sha }}
20 changes: 20 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Builder stage
FROM golang:1.22-alpine AS builder

WORKDIR /app

# Copy all project files
COPY . .

# Build the Go project
RUN go build -o /sigrab cmd/sigrab/main.go

# Runtime stage
FROM alpine:latest
RUN apk add --no-cache ca-certificates

# Copy the built executable from the builder stage
COPY --from=builder /sigrab /sigrab

# Set the entrypoint
ENTRYPOINT ["/sigrab"]
43 changes: 43 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,46 @@ sigrab \

# View help and available flags
sigrab --help

---

## 🐳 Docker Usage

This project can also be run using Docker. This provides a convenient way to run `sigrab` without needing to install Go or manage dependencies locally.

### Build the Image Locally

You can build the Docker image from the Dockerfile in the project root:

```bash
docker build -t sigrab .
```

### Run the Docker Image

To run the built image, you need to pass the `JIRA_API_TOKEN` environment variable and any command-line arguments `sigrab` requires.

```bash
# Set your JIRA API token
export JIRA_API_TOKEN="your-jira-api-token"

# Run the Docker container
docker run \
-e JIRA_API_TOKEN="$JIRA_API_TOKEN" \
sigrab \
--url "https://yourcompany.atlassian.net" \
--from DEV-123 \
--to DEV-140
```

### Use Pre-built Images from GitHub Container Registry

Pre-built Docker images are available on GitHub Container Registry. You can pull the latest image using:

```bash
docker pull ghcr.io/YOUR_GITHUB_USERNAME_OR_ORG/sigrab:latest
```

Replace `YOUR_GITHUB_USERNAME_OR_ORG` with the actual GitHub username or organization where the repository is hosted.

You can then run the pulled image as described above, just replace `sigrab` with `ghcr.io/YOUR_GITHUB_USERNAME_OR_ORG/sigrab:latest` in the `docker run` command.
22 changes: 22 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
module github.com/tutunak/sigrab

go 1.22.2

require (
github.com/andygrunwald/go-jira v1.16.0
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.10.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/trivago/tgo v1.0.7 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
37 changes: 37 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
github.com/andygrunwald/go-jira v1.16.0 h1:PU7C7Fkk5L96JvPc6vDVIrd99vdPnYudHu4ju2c2ikQ=
github.com/andygrunwald/go-jira v1.16.0/go.mod h1:UQH4IBVxIYWbgagc0LF/k9FRs9xjIiQ8hIcC6HfLwFU=
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=
github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo=
github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M=
github.com/golang-jwt/jwt/v4 v4.4.2 h1:rcc4lwaZgFMCZ5jxF9ABolDcIHdBytAFgqFPbSJQAYs=
github.com/golang-jwt/jwt/v4 v4.4.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
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=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/trivago/tgo v1.0.7 h1:uaWH/XIy9aWYWpjm2CU3RpcqZXmX2ysQ9/Go+d9gyrM=
github.com/trivago/tgo v1.0.7/go.mod h1:w4dpD+3tzNIIiIfkWWa85w5/B77tlvdZckQ+6PkFnhc=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220330033206-e17cdc41300f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
2 changes: 1 addition & 1 deletion internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ func runSigrab(cmd *cobra.Command, args []string) error {

fetcher := jira.NewFetcher(jira.NewClient(cfg.UserEmail, cfg.APIToken, url))

err = fetcher.FetchBackward(to, fullPath)
_, err = fetcher.FetchBackward(to, fullPath)

if err != nil {
return fmt.Errorf("failed to fetch issues: %w", err)
Expand Down
23 changes: 14 additions & 9 deletions internal/jira/fetcher.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,32 @@ func NewFetcher(client *goJira.Client) *Fetcher {
return &Fetcher{client: client}
}

func (f *Fetcher) FetchBackward(to string, path string) error {
func (f *Fetcher) FetchBackward(to string, path string) ([]goJira.Issue, error) {
prefix, endNum, err := utils.ParseIssueKey(to)
if err != nil {

return fmt.Errorf("failed to parse issue key %s: %w", to, err)
return nil, fmt.Errorf("failed to parse issue key %s: %w", to, err)
}

var fetchedIssues []goJira.Issue
for current := endNum; current >= 1; current-- {
issueKey := fmt.Sprintf("%s-%d", prefix, current)
issue, err := GetIssue(f.client, issueKey)
if err != nil {
panic(err)
// If an issue is not found or there's an error, skip it and continue
// This matches the test expectation for "skip non-existent issues"
// Consider logging this error if necessary
continue
}

// Append to maintain fetched order (e.g., N, N-1, N-2) as expected by tests.
fetchedIssues = append(fetchedIssues, *issue)

writer := output.NewWriter()
// writer.WriteToFile expects the directory as the first argument and constructs the filename itself.
if err := writer.WriteToFile(path, *issue); err != nil {

return fmt.Errorf("failed to write issue %s to file: %w", issueKey, err)
// On write error, the test expects nil for the issues slice.
return nil, fmt.Errorf("failed to write issue %s to directory %s: %w", issueKey, path, err)
}

// Prepend to maintain order
}
return nil
return fetchedIssues, nil
}
Comment on lines +18 to 46
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider refactoring to separate concerns.

The FetchBackward method is currently handling both fetching issues and writing them to files, which violates the single responsibility principle. This tight coupling makes the method harder to test and reuse.

Consider refactoring to separate these concerns:

-func (f *Fetcher) FetchBackward(to string, path string) ([]goJira.Issue, error) {
+func (f *Fetcher) FetchBackward(to string) ([]goJira.Issue, error) {
 	prefix, endNum, err := utils.ParseIssueKey(to)
 	if err != nil {
 		return nil, fmt.Errorf("failed to parse issue key %s: %w", to, err)
 	}

 	var fetchedIssues []goJira.Issue
 	for current := endNum; current >= 1; current-- {
 		issueKey := fmt.Sprintf("%s-%d", prefix, current)
 		issue, err := GetIssue(f.client, issueKey)
 		if err != nil {
 			// If an issue is not found or there's an error, skip it and continue
 			// This matches the test expectation for "skip non-existent issues"
 			// Consider logging this error if necessary
 			continue
 		}

 		// Append to maintain fetched order (e.g., N, N-1, N-2) as expected by tests.
 		fetchedIssues = append(fetchedIssues, *issue)
-
-		writer := output.NewWriter()
-		// writer.WriteToFile expects the directory as the first argument and constructs the filename itself.
-		if err := writer.WriteToFile(path, *issue); err != nil {
-			// On write error, the test expects nil for the issues slice.
-			return nil, fmt.Errorf("failed to write issue %s to directory %s: %w", issueKey, path, err)
-		}
 	}
 	return fetchedIssues, nil
 }

Then handle file writing in the calling code or create a separate method for batch writing.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (f *Fetcher) FetchBackward(to string, path string) ([]goJira.Issue, error) {
prefix, endNum, err := utils.ParseIssueKey(to)
if err != nil {
return fmt.Errorf("failed to parse issue key %s: %w", to, err)
return nil, fmt.Errorf("failed to parse issue key %s: %w", to, err)
}
var fetchedIssues []goJira.Issue
for current := endNum; current >= 1; current-- {
issueKey := fmt.Sprintf("%s-%d", prefix, current)
issue, err := GetIssue(f.client, issueKey)
if err != nil {
panic(err)
// If an issue is not found or there's an error, skip it and continue
// This matches the test expectation for "skip non-existent issues"
// Consider logging this error if necessary
continue
}
// Append to maintain fetched order (e.g., N, N-1, N-2) as expected by tests.
fetchedIssues = append(fetchedIssues, *issue)
writer := output.NewWriter()
// writer.WriteToFile expects the directory as the first argument and constructs the filename itself.
if err := writer.WriteToFile(path, *issue); err != nil {
return fmt.Errorf("failed to write issue %s to file: %w", issueKey, err)
// On write error, the test expects nil for the issues slice.
return nil, fmt.Errorf("failed to write issue %s to directory %s: %w", issueKey, path, err)
}
// Prepend to maintain order
}
return nil
return fetchedIssues, nil
}
func (f *Fetcher) FetchBackward(to string) ([]goJira.Issue, error) {
prefix, endNum, err := utils.ParseIssueKey(to)
if err != nil {
return nil, fmt.Errorf("failed to parse issue key %s: %w", to, err)
}
var fetchedIssues []goJira.Issue
for current := endNum; current >= 1; current-- {
issueKey := fmt.Sprintf("%s-%d", prefix, current)
issue, err := GetIssue(f.client, issueKey)
if err != nil {
// If an issue is not found or there's an error, skip it and continue
continue
}
// Append to maintain fetched order (e.g., N, N-1, N-2) as expected by tests.
fetchedIssues = append(fetchedIssues, *issue)
}
return fetchedIssues, nil
}
🤖 Prompt for AI Agents
In internal/jira/fetcher.go around lines 18 to 46, the FetchBackward method
mixes fetching issues and writing them to files, violating single responsibility
principle. Refactor by removing the file writing logic from FetchBackward so it
only fetches and returns issues. Move the file writing code to the caller or a
new dedicated method that takes the fetched issues and writes them to files,
improving testability and reusability.