├── .github
└── workflows
│ ├── go.yml
│ └── release.yaml
├── .gitignore
├── LICENSE
├── Procfile
├── core.go
├── core_test.go
├── flake.lock
├── flake.nix
├── fly.toml
├── go.mod
├── go.sum
├── index.html
├── main.go
├── main_test.go
├── notes
├── readme
└── types.go
/.github/workflows/go.yml:
--------------------------------------------------------------------------------
1 | name: Go
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 |
11 | build:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v4
15 |
16 | - name: Set up Go
17 | uses: actions/setup-go@v5
18 |
19 | - name: Build
20 | run: go build -v ./...
21 |
22 | - name: Test
23 | run: go test -v ./...
24 |
--------------------------------------------------------------------------------
/.github/workflows/release.yaml:
--------------------------------------------------------------------------------
1 | name: Release Go project
2 |
3 | on:
4 | push:
5 | tags:
6 | - "*" # triggers only if push new tag version, like `0.8.4` or else
7 |
8 | jobs:
9 | build:
10 | name: GoReleaser build
11 | runs-on: ubuntu-latest
12 |
13 | steps:
14 | - name: Check out code into the Go module directory
15 | uses: actions/checkout@v4
16 | with:
17 | fetch-depth: 0 # See: https://goreleaser.com/ci/actions/
18 |
19 | - name: Set up Go
20 | uses: actions/setup-go@v5
21 |
22 | - name: Build
23 | run: go build -v ./...
24 |
25 | - name: Test
26 | run: go test -v ./...
27 |
28 | - name: Run GoReleaser
29 | uses: goreleaser/goreleaser-action@master
30 | with:
31 | version: latest
32 | args: release
33 | env:
34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
35 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /gh-issues-to-rss
2 | /issues.json
3 | result
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/Procfile:
--------------------------------------------------------------------------------
1 | web: gh-issues-to-rss --server
--------------------------------------------------------------------------------
/core.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "errors"
6 | "fmt"
7 | "net/http"
8 | "os"
9 | "strings"
10 | "time"
11 |
12 | "io"
13 |
14 | "io/fs"
15 |
16 | "github.com/gorilla/feeds"
17 | )
18 |
19 | func makeRequest(repo string) ([]byte, error) {
20 | // do an http get request to the github api. Add auth header if token is present
21 | req, err := http.NewRequest("GET", baseUrl+repo+"/issues?state=all", nil)
22 | if err != nil {
23 | return nil, err
24 | }
25 |
26 | token := os.Getenv("GH_ISSUES_TO_RSS_GITHUB_TOKEN")
27 | if token != "" {
28 | req.Header.Add("Authorization", "Bearer"+token)
29 | }
30 |
31 | response, err := http.DefaultClient.Do(req)
32 | if err != nil {
33 | return nil, err
34 | }
35 |
36 | if response.StatusCode != 200 {
37 | return nil, errors.New("unable to fetch data, make sure you have a valid repo")
38 | }
39 |
40 | defer response.Body.Close()
41 |
42 | body, err := io.ReadAll(io.Reader(response.Body))
43 | if err != nil {
44 | return nil, err
45 | }
46 |
47 | return body, nil
48 | }
49 |
50 | func saveBackup(repo string, content []byte) error {
51 | path := cacheLocation + "/" + repo
52 | if _, err := os.Stat(path); os.IsNotExist(err) {
53 | err = os.MkdirAll(path, 0755)
54 | if err != nil {
55 | fmt.Println("Unable to create directory for caching:", err)
56 | }
57 | }
58 |
59 | err := os.WriteFile(path+"/issues.json", content, fs.FileMode(0644))
60 | if err != nil {
61 | return err
62 | }
63 | return nil
64 | }
65 |
66 | func loadBackup(repo string, timeout time.Duration) ([]byte, error) {
67 | path := cacheLocation + "/" + repo + "/issues.json"
68 |
69 | fi, err := os.Stat(path)
70 | if err != nil {
71 | return nil, err
72 | }
73 |
74 | if time.Now().Sub(fi.ModTime()) > timeout {
75 | return nil, nil
76 | }
77 |
78 | b, err := os.ReadFile(path)
79 | if err != nil {
80 | return nil, err
81 | }
82 |
83 | return b, nil
84 | }
85 |
86 | func isIn(item string, items []string) bool {
87 | for _, i := range items {
88 | if i == item {
89 | return true
90 | }
91 | }
92 | return false
93 | }
94 |
95 | func generateRss(data []GithubIssue, rc RunConfig) (string, error) {
96 | now := time.Now()
97 | feed := &feeds.Feed{
98 | Title: rc.Repo,
99 | Link: &feeds.Link{Href: "https://github.com/" + rc.Repo},
100 | Created: now,
101 | }
102 |
103 | var items []*feeds.Item
104 | fdata := filterIssues(data, rc)
105 |
106 | for _, entry := range fdata {
107 | entryType := "issue"
108 | if entry.PullRequest.URL != "" {
109 | entryType = "pr"
110 | }
111 | createTime, _ := time.Parse("2006-01-02T15:04:05Z07:00", entry.CreatedAt)
112 | closeTime, _ := time.Parse("2006-01-02T15:04:05Z07:00", entry.ClosedAt)
113 |
114 | if entry.State == "closed" {
115 | if !((entryType == "pr" && !rc.Modes.PRClosed) || (entryType == "issue" && !rc.Modes.IssuesClosed)) {
116 | items = append(items, &feeds.Item{
117 | Title: "[" + entryType + "-" + "closed" + "]: " + entry.Title,
118 | Link: &feeds.Link{Href: entry.HTMLURL},
119 | Description: strings.ReplaceAll(entry.Body, "\n", " "),
120 | Content: strings.ReplaceAll(entry.Body, "\n", " "),
121 | Author: &feeds.Author{Name: entry.User.Login},
122 | Created: closeTime,
123 | })
124 | }
125 | }
126 | if (entryType == "pr" && !rc.Modes.PROpen) || (entryType == "issue" && !rc.Modes.IssueOpen) {
127 | continue
128 | }
129 | items = append(items, &feeds.Item{
130 | Title: "[" + entryType + "-" + "open" + "]: " + entry.Title,
131 | Link: &feeds.Link{Href: entry.HTMLURL},
132 | Description: strings.ReplaceAll(entry.Body, "\n", " "),
133 | Content: strings.ReplaceAll(entry.Body, "\n", " "),
134 | Author: &feeds.Author{Name: entry.User.Login},
135 | Created: createTime,
136 | })
137 |
138 | }
139 | feed.Items = items
140 |
141 | rss, err := feed.ToRss()
142 | if err != nil {
143 | return "", err
144 | }
145 | return rss, nil
146 | }
147 |
148 | func getData(repo string, cacheTimeout time.Duration) ([]byte, error) {
149 | content, err := loadBackup(repo, cacheTimeout)
150 | if err != nil || content == nil {
151 | fmt.Println("No cache found for " + repo + ", fetching from Github")
152 | resp, err := makeRequest(repo)
153 | if err != nil {
154 | return nil, err
155 | }
156 | err = saveBackup(repo, resp)
157 | if err != nil {
158 | fmt.Println("Unable to save backup:", err)
159 | }
160 | return resp, nil
161 | }
162 | return content, nil
163 | }
164 |
165 | func getIssueFeed(rc RunConfig, cacheTimeout time.Duration) (string, error) {
166 | content, err := getData(rc.Repo, cacheTimeout)
167 | if err != nil {
168 | return "", err
169 | }
170 |
171 | data := []GithubIssue{}
172 | if err := json.Unmarshal(content, &data); err != nil {
173 | return "", err
174 | }
175 |
176 | rss, err := generateRss(data, rc)
177 | if err != nil {
178 | return "", err
179 | }
180 | return rss, nil
181 | }
182 |
183 | func filterIssues(issues []GithubIssue, rc RunConfig) []GithubIssue {
184 | var fi []GithubIssue
185 |
186 | for _, issue := range issues {
187 | var issueLabels []string
188 | for _, i := range issue.Labels {
189 | issueLabels = append(issueLabels, i.Name)
190 | }
191 |
192 | bk := false
193 | for _, label := range rc.NotLabels {
194 | if isIn(label, issueLabels) {
195 | bk = true
196 | break
197 | }
198 | }
199 |
200 | if bk {
201 | continue
202 | }
203 |
204 | for _, user := range rc.NotUsers {
205 | if issue.User.Login == user {
206 | bk = true
207 | break
208 | }
209 | }
210 |
211 | if bk {
212 | continue
213 | }
214 |
215 | for _, label := range rc.Labels {
216 | if !isIn(label, issueLabels) {
217 | bk = true
218 | break
219 | }
220 | }
221 |
222 | if bk {
223 | continue
224 | }
225 |
226 | bk = len(rc.Users) != 0
227 |
228 | for _, user := range rc.Users {
229 | if issue.User.Login == user {
230 | bk = false
231 | break
232 | }
233 | }
234 |
235 | if bk {
236 | continue
237 | }
238 |
239 | fi = append(fi, issue)
240 | }
241 |
242 | return fi
243 | }
244 |
--------------------------------------------------------------------------------
/core_test.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 | "strings"
6 | "testing"
7 | "time"
8 |
9 | "os"
10 |
11 | "gopkg.in/h2non/gock.v1"
12 | )
13 |
14 | // Not sure if testing this has a point
15 | func TestMakeRequest(t *testing.T) {
16 | defer gock.Off()
17 | gock.New("https://api.github.com").
18 | Get("/issues").
19 | Reply(200).BodyString("mango")
20 |
21 | content, err := makeRequest("meain/dotfiles")
22 | if err != nil {
23 | t.Fatalf("Unable to fetch star count")
24 | }
25 | if string(content) != "mango" {
26 | t.Fatalf("makeReqest seems to have some problems. expected %v, got %v", "mango", string(content))
27 | }
28 | }
29 |
30 | func TestBackup(t *testing.T) {
31 | cacheLocationBackup := cacheLocation
32 | defer func() { cacheLocation = cacheLocationBackup }()
33 | dir, err := os.MkdirTemp("", "gh-issues-to-rss")
34 | if err != nil {
35 | log.Fatal("Unable to create temp directory:", err)
36 | }
37 | cacheLocation = dir
38 |
39 | err = saveBackup("meain/dotfiles", []byte("dummy"))
40 | if err != nil {
41 | t.Fatalf("Unable to save backup file")
42 | }
43 | content, err := loadBackup("meain/dotfiles", time.Hour)
44 | if err != nil {
45 | t.Fatalf("Unable to load backup file")
46 | }
47 | if string(content) != "dummy" {
48 | t.Fatalf("Invalid backup content")
49 | }
50 | }
51 |
52 | func TestRssGenerationSimple(t *testing.T) {
53 | rssContent := `
54 | [issue-open]: Sample Entry
55 | https://example.com
56 | Some body
57 |
58 | Wed, 08 Sep 2021 12:44:47 +0000
59 |
60 |
61 | `
62 | data := []GithubIssue{
63 | GithubIssue{
64 | CreatedAt: "2021-09-08T12:44:47Z",
65 | Title: "Sample Entry",
66 | HTMLURL: "https://example.com",
67 | Body: "Some body",
68 | },
69 | }
70 |
71 | content, err := generateRss(data, RunConfig{Repo: "meain/dotfiles", Modes: Modes{true, true, true, true}})
72 | if err != nil {
73 | t.Fatalf("Unable to save backup file")
74 | }
75 |
76 | if !strings.Contains(content, rssContent) {
77 | t.Fatalf("Rss feed content does not match up")
78 | }
79 | }
80 |
81 | func TestRssGenerationWithClosed(t *testing.T) {
82 | rssContent := `
83 | [issue-open]: Sample Entry
84 | https://example.com
85 | Some body
86 |
87 | Wed, 08 Sep 2021 12:44:47 +0000
88 |
89 |
90 | [issue-closed]: Another Entry
91 | https://example.com
92 | Another body
93 |
94 | Fri, 08 Oct 2021 12:44:47 +0000
95 |
96 |
97 | [issue-open]: Another Entry
98 | https://example.com
99 | Another body
100 |
101 | Wed, 08 Sep 2021 12:44:47 +0000
102 |
103 |
104 | `
105 | data := []GithubIssue{
106 | GithubIssue{
107 | CreatedAt: "2021-09-08T12:44:47Z",
108 | Title: "Sample Entry",
109 | HTMLURL: "https://example.com",
110 | Body: "Some body",
111 | },
112 | GithubIssue{
113 | CreatedAt: "2021-09-08T12:44:47Z",
114 | ClosedAt: "2021-10-08T12:44:47Z",
115 | State: "closed",
116 | Title: "Another Entry",
117 | HTMLURL: "https://example.com",
118 | Body: "Another body",
119 | },
120 | }
121 | content, err := generateRss(data, RunConfig{Repo: "meain/dotfiles", Modes: Modes{true, true, true, true}})
122 | if err != nil {
123 | t.Fatalf("Unable to save backup file")
124 | }
125 | if !strings.Contains(content, rssContent) {
126 | t.Fatalf("Rss feed content does not match up")
127 | }
128 | }
129 |
130 | func TestRssGenerationWithClosedButOnlyOpen(t *testing.T) {
131 | rssContent := `
132 | [issue-open]: Sample Entry
133 | https://example.com
134 | Some body
135 |
136 | Wed, 08 Sep 2021 12:44:47 +0000
137 |
138 |
139 | [issue-open]: Another Entry
140 | https://example.com
141 | Another body
142 |
143 | Wed, 08 Sep 2021 12:44:47 +0000
144 |
145 |
146 | `
147 | data := []GithubIssue{
148 | GithubIssue{
149 | CreatedAt: "2021-09-08T12:44:47Z",
150 | Title: "Sample Entry",
151 | HTMLURL: "https://example.com",
152 | Body: "Some body",
153 | },
154 | GithubIssue{
155 | CreatedAt: "2021-09-08T12:44:47Z",
156 | ClosedAt: "2021-10-08T12:44:47Z",
157 | State: "closed",
158 | Title: "Another Entry",
159 | HTMLURL: "https://example.com",
160 | Body: "Another body",
161 | },
162 | }
163 | content, err := generateRss(data, RunConfig{Repo: "meain/dotfiles", Modes: Modes{true, false, true, false}})
164 | if err != nil {
165 | t.Fatalf("Unable to save backup file")
166 | }
167 | if !strings.Contains(content, rssContent) {
168 | t.Fatalf("Rss feed content does not match up")
169 | }
170 | }
171 |
--------------------------------------------------------------------------------
/flake.lock:
--------------------------------------------------------------------------------
1 | {
2 | "nodes": {
3 | "flake-utils": {
4 | "inputs": {
5 | "systems": "systems"
6 | },
7 | "locked": {
8 | "lastModified": 1710146030,
9 | "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=",
10 | "owner": "numtide",
11 | "repo": "flake-utils",
12 | "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a",
13 | "type": "github"
14 | },
15 | "original": {
16 | "owner": "numtide",
17 | "repo": "flake-utils",
18 | "type": "github"
19 | }
20 | },
21 | "nixpkgs": {
22 | "locked": {
23 | "lastModified": 1717893485,
24 | "narHash": "sha256-WMU6ZRZrBgEUDIF0siu2aIyVAXcxfElSwzZtS/mSpN4=",
25 | "owner": "NixOS",
26 | "repo": "nixpkgs",
27 | "rev": "3bcedce9f4de37570242faf16e1e143583407eab",
28 | "type": "github"
29 | },
30 | "original": {
31 | "id": "nixpkgs",
32 | "type": "indirect"
33 | }
34 | },
35 | "root": {
36 | "inputs": {
37 | "flake-utils": "flake-utils",
38 | "nixpkgs": "nixpkgs"
39 | }
40 | },
41 | "systems": {
42 | "locked": {
43 | "lastModified": 1681028828,
44 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
45 | "owner": "nix-systems",
46 | "repo": "default",
47 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
48 | "type": "github"
49 | },
50 | "original": {
51 | "owner": "nix-systems",
52 | "repo": "default",
53 | "type": "github"
54 | }
55 | }
56 | },
57 | "root": "root",
58 | "version": 7
59 | }
60 |
--------------------------------------------------------------------------------
/flake.nix:
--------------------------------------------------------------------------------
1 | {
2 | description = "Generate RSS feed for GH issues and PRs";
3 |
4 | inputs.flake-utils.url = "github:numtide/flake-utils";
5 |
6 | outputs = { self, nixpkgs, flake-utils }:
7 | flake-utils.lib.eachDefaultSystem (system:
8 | let
9 | pkgs = nixpkgs.legacyPackages.${system};
10 | deploy = pkgs.writeShellScriptBin "deploy" ''
11 | set -euo pipefail
12 | ${pkgs.flyctl}/bin/fly deploy
13 | '';
14 | in
15 | {
16 | packages = rec {
17 | noah = pkgs.buildGoModule {
18 | pname = "gh-issues-to-rss";
19 | version = "dev";
20 | src = ./.;
21 | vendorHash = "sha256-30tnt3fYXDDm+YQgY65dUcytDokrUdCChOU5LMgnGYE=";
22 | doCheck = false;
23 | };
24 |
25 | default = noah;
26 | };
27 |
28 | devShells.default = pkgs.mkShell {
29 | packages = with pkgs; [
30 | go
31 |
32 | # for deployment
33 | flyctl
34 | deploy
35 | ];
36 | };
37 | }
38 | );
39 | }
--------------------------------------------------------------------------------
/fly.toml:
--------------------------------------------------------------------------------
1 | # fly.toml file generated for gh-issues-to-rss on 2022-07-29T10:06:43+05:30
2 |
3 | app = "gh-issues-to-rss"
4 | kill_signal = "SIGINT"
5 | kill_timeout = 5
6 | processes = []
7 |
8 | [build]
9 | builder = "paketobuildpacks/builder:base"
10 | buildpacks = ["gcr.io/paketo-buildpacks/go"]
11 |
12 | [env]
13 | PORT = "8080"
14 |
15 | [experimental]
16 | allowed_public_ports = []
17 | auto_rollback = true
18 |
19 | [[services]]
20 | http_checks = []
21 | internal_port = 8080
22 | processes = ["app"]
23 | protocol = "tcp"
24 | script_checks = []
25 | [services.concurrency]
26 | hard_limit = 25
27 | soft_limit = 20
28 | type = "connections"
29 |
30 | [[services.ports]]
31 | force_https = true
32 | handlers = ["http"]
33 | port = 80
34 |
35 | [[services.ports]]
36 | handlers = ["tls", "http"]
37 | port = 443
38 |
39 | [[services.tcp_checks]]
40 | grace_period = "1s"
41 | interval = "15s"
42 | restart_limit = 0
43 | timeout = "2s"
44 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/meain/gh-issues-to-rss
2 |
3 | go 1.16
4 |
5 | require (
6 | github.com/google/go-cmp v0.5.6
7 | github.com/gorilla/feeds v1.1.1
8 | github.com/kr/pretty v0.3.0 // indirect
9 | gopkg.in/h2non/gock.v1 v1.1.2
10 | )
11 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
2 | github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ=
3 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
4 | github.com/gorilla/feeds v1.1.1 h1:HwKXxqzcRNg9to+BbvJog4+f3s/xzvtZXICcQGutYfY=
5 | github.com/gorilla/feeds v1.1.1/go.mod h1:Nk0jZrvPFZX1OBe5NPiddPw7CfwF6Q9eqzaBbaightA=
6 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542 h1:2VTzZjLZBgl62/EtslCrtky5vbi9dd7HrQPQIx6wqiw=
7 | github.com/h2non/parth v0.0.0-20190131123155-b4df798d6542/go.mod h1:Ow0tF8D4Kplbc8s8sSb3V2oUCygFHVp8gC3Dn6U4MNI=
8 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
9 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
10 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
11 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
12 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
13 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
14 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
15 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32 h1:W6apQkHrMkS0Muv8G/TipAy/FJl/rCYT0+EuS8+Z0z4=
16 | github.com/nbio/st v0.0.0-20140626010706-e9e8d9816f32/go.mod h1:9wM+0iRr9ahx58uYLpLIr5fm8diHn0JbqRycJi6w0Ms=
17 | github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
18 | github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
19 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
20 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
21 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
22 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
23 | gopkg.in/h2non/gock.v1 v1.1.2 h1:jBbHXgGBK/AoPVfJh5x4r/WxIrElvbLel8TCZkkZJoY=
24 | gopkg.in/h2non/gock.v1 v1.1.2/go.mod h1:n7UGz/ckNChHiK05rDoiC4MYSunEC/lyaUm2WWaDva0=
25 |
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |