Skip to content

Commit 2f4fe0a

Browse files
authored
Merge pull request #387 from vchrombie/feat/output-filename-env-18
feat: add OUTPUT_FILENAME to customize markdown output
2 parents 6fc8c2c + cc09d38 commit 2f4fe0a

5 files changed

Lines changed: 130 additions & 7 deletions

File tree

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ This action can be configured to authenticate with GitHub App Installation or Pe
9494
| `END_DATE` | False | Current Date | The date at which you want to stop gathering contributor information. Must be later than the `START_DATE`. ie. Aug 2nd, 2023 would be `2023-08-02` |
9595
| `SPONSOR_INFO` | False | False | If you want to include sponsor information in the output. This will include the sponsor count and the sponsor URL. This will impact action performance. ie. SPONSOR_INFO = "False" or SPONSOR_INFO = "True" |
9696
| `LINK_TO_PROFILE` | False | True | If you want to link usernames to their GitHub profiles in the output. ie. LINK_TO_PROFILE = "True" or LINK_TO_PROFILE = "False" |
97+
| `OUTPUT_FILENAME` | False | contributors.md | The output filename for the markdown report. ie. OUTPUT_FILENAME = "my-report.md" |
9798

9899
**Note**: If `start_date` and `end_date` are specified then the action will determine if the contributor is new. A new contributor is one that has contributed in the date range specified but not before the start date.
99100

@@ -119,6 +120,8 @@ jobs:
119120
runs-on: ubuntu-latest
120121
permissions:
121122
issues: write
123+
env:
124+
OUTPUT_FILENAME: contributors.md
122125

123126
steps:
124127
- name: Get dates for last month
@@ -148,7 +151,7 @@ jobs:
148151
with:
149152
title: Monthly contributor report
150153
token: ${{ secrets.GITHUB_TOKEN }}
151-
content-filepath: ./contributors.md
154+
content-filepath: ./${{ env.OUTPUT_FILENAME }}
152155
assignees: <YOUR_GITHUB_HANDLE_HERE>
153156
```
154157
@@ -170,6 +173,8 @@ jobs:
170173
runs-on: ubuntu-latest
171174
permissions:
172175
issues: write
176+
env:
177+
OUTPUT_FILENAME: contributors.md
173178

174179
steps:
175180
- name: Get dates for last month
@@ -204,7 +209,7 @@ jobs:
204209
with:
205210
title: Monthly contributor report
206211
token: ${{ secrets.GITHUB_TOKEN }}
207-
content-filepath: ./contributors.md
212+
content-filepath: ./${{ env.OUTPUT_FILENAME }}
208213
assignees: <YOUR_GITHUB_HANDLE_HERE>
209214
```
210215
@@ -245,7 +250,7 @@ jobs:
245250

246251
When running as a GitHub Action, the contributors report is automatically displayed in the [GitHub Actions Job Summary](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#adding-a-job-summary). This provides immediate visibility of the results directly in the workflow run interface without needing to check separate files or issues.
247252

248-
The job summary contains the same markdown content that is written to the `contributors.md` file, making it easy to view contributor information right in the GitHub Actions UI.
253+
The job summary contains the same markdown content that is written to the configured output file (`contributors.md` by default), making it easy to view contributor information right in the GitHub Actions UI.
249254

250255
## Local usage without Docker
251256

contributors.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def main():
2727
end_date,
2828
sponsor_info,
2929
link_to_profile,
30+
output_filename,
3031
) = env.get_env_vars()
3132

3233
# Auth to GitHub.com
@@ -75,7 +76,7 @@ def main():
7576
# print(contributors)
7677
markdown.write_to_markdown(
7778
contributors,
78-
"contributors.md",
79+
output_filename,
7980
start_date,
8081
end_date,
8182
organization,

env.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import datetime
77
import os
8+
import re
89
from os.path import dirname, join
910

1011
from dotenv import load_dotenv
@@ -115,6 +116,7 @@ def get_env_vars(
115116
str,
116117
bool,
117118
bool,
119+
str,
118120
]:
119121
"""
120122
Get the environment variables for use in the action.
@@ -132,9 +134,10 @@ def get_env_vars(
132134
token (str): The GitHub token to use for authentication
133135
ghe (str): The GitHub Enterprise URL to use for authentication
134136
start_date (str): The start date to get contributor information from
135-
end_date (str): The end date to get contributor information to.
137+
end_date (str): The end date to get contributor information to
136138
sponsor_info (str): Whether to get sponsor information on the contributor
137139
link_to_profile (str): Whether to link username to Github profile in markdown output
140+
output_filename (str): The output filename for the markdown report
138141
"""
139142

140143
if not test:
@@ -176,6 +179,16 @@ def get_env_vars(
176179

177180
sponsor_info = get_bool_env_var("SPONSOR_INFO", False)
178181
link_to_profile = get_bool_env_var("LINK_TO_PROFILE", False)
182+
output_filename = os.getenv("OUTPUT_FILENAME", "").strip() or "contributors.md"
183+
if not re.match(r"^[a-zA-Z0-9_\-\.]+$", output_filename):
184+
raise ValueError(
185+
"OUTPUT_FILENAME must contain only alphanumeric characters, "
186+
"hyphens, underscores, and dots"
187+
)
188+
if output_filename != os.path.basename(output_filename):
189+
raise ValueError(
190+
"OUTPUT_FILENAME must be a simple filename without path separators"
191+
)
179192

180193
# Separate repositories_str into a list based on the comma separator
181194
repositories_list = []
@@ -197,4 +210,5 @@ def get_env_vars(
197210
end_date,
198211
sponsor_info,
199212
link_to_profile,
213+
output_filename,
200214
)

test_contributors.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ def test_main_runs_under_main_guard(self):
272272
"2022-12-31",
273273
False,
274274
False,
275+
"contributors.md",
275276
)
276277

277278
mock_auth = MagicMock()
@@ -344,6 +345,7 @@ def test_main_sets_new_contributor_flag(self):
344345
"2022-12-31",
345346
False,
346347
False,
348+
"contributors.md",
347349
)
348350
mock_auth_to_github.return_value = MagicMock()
349351
mock_get_all_contributors.side_effect = [[contributor], []]
@@ -398,6 +400,7 @@ def test_main_fetches_sponsor_info_when_enabled(self):
398400
"",
399401
"true",
400402
False,
403+
"contributors.md",
401404
)
402405
mock_auth_to_github.return_value = MagicMock()
403406
mock_get_all_contributors.return_value = [contributor]

test_env.py

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ def setUp(self):
2323
"GITHUB_APP_ENTERPRISE_ONLY",
2424
"GH_TOKEN",
2525
"ORGANIZATION",
26+
"OUTPUT_FILENAME",
2627
"REPOSITORY",
2728
"START_DATE",
2829
]
@@ -65,6 +66,7 @@ def test_get_env_vars(self):
6566
end_date,
6667
sponsor_info,
6768
link_to_profile,
69+
output_filename,
6870
) = env.get_env_vars()
6971

7072
self.assertEqual(organization, "org")
@@ -79,6 +81,7 @@ def test_get_env_vars(self):
7981
self.assertEqual(end_date, "2022-12-31")
8082
self.assertFalse(sponsor_info)
8183
self.assertTrue(link_to_profile)
84+
self.assertEqual(output_filename, "contributors.md")
8285

8386
@patch.dict(
8487
os.environ,
@@ -175,6 +178,7 @@ def test_get_env_vars_no_dates(self):
175178
end_date,
176179
sponsor_info,
177180
link_to_profile,
181+
output_filename,
178182
) = env.get_env_vars()
179183

180184
self.assertEqual(organization, "org")
@@ -189,12 +193,107 @@ def test_get_env_vars_no_dates(self):
189193
self.assertEqual(end_date, "")
190194
self.assertFalse(sponsor_info)
191195
self.assertTrue(link_to_profile)
196+
self.assertEqual(output_filename, "contributors.md")
192197

193-
@patch.dict(os.environ, {})
198+
@patch.dict(
199+
os.environ,
200+
{
201+
"ORGANIZATION": "org",
202+
"REPOSITORY": "repo,repo2",
203+
"GH_APP_ID": "",
204+
"GH_APP_INSTALLATION_ID": "",
205+
"GH_APP_PRIVATE_KEY": "",
206+
"GH_TOKEN": "token",
207+
"GH_ENTERPRISE_URL": "",
208+
"START_DATE": "",
209+
"END_DATE": "",
210+
"SPONSOR_INFO": "False",
211+
"LINK_TO_PROFILE": "True",
212+
"OUTPUT_FILENAME": "custom-report.md",
213+
},
214+
clear=True,
215+
)
216+
def test_get_env_vars_custom_output_filename(self):
217+
"""Test that OUTPUT_FILENAME overrides the default output filename."""
218+
(
219+
_organization,
220+
_repository_list,
221+
_gh_app_id,
222+
_gh_app_installation_id,
223+
_gh_app_private_key,
224+
_gh_app_enterprise_only,
225+
_token,
226+
_ghe,
227+
_start_date,
228+
_end_date,
229+
_sponsor_info,
230+
_link_to_profile,
231+
output_filename,
232+
) = env.get_env_vars()
233+
234+
self.assertEqual(output_filename, "custom-report.md")
235+
236+
@patch.dict(
237+
os.environ,
238+
{
239+
"ORGANIZATION": "org",
240+
"GH_TOKEN": "token",
241+
"OUTPUT_FILENAME": "../../../etc/passwd",
242+
},
243+
clear=True,
244+
)
245+
def test_get_env_vars_output_filename_path_traversal_rejected(self):
246+
"""Test that OUTPUT_FILENAME rejects path traversal attempts."""
247+
with self.assertRaises(ValueError):
248+
env.get_env_vars()
249+
250+
@patch.dict(
251+
os.environ,
252+
{
253+
"ORGANIZATION": "org",
254+
"GH_TOKEN": "token",
255+
"OUTPUT_FILENAME": "/tmp/output.md",
256+
},
257+
clear=True,
258+
)
259+
def test_get_env_vars_output_filename_absolute_path_rejected(self):
260+
"""Test that OUTPUT_FILENAME rejects absolute paths."""
261+
with self.assertRaises(ValueError):
262+
env.get_env_vars()
263+
264+
@patch.dict(
265+
os.environ,
266+
{
267+
"ORGANIZATION": "org",
268+
"GH_TOKEN": "token",
269+
"OUTPUT_FILENAME": "reports/output.md",
270+
},
271+
clear=True,
272+
)
273+
def test_get_env_vars_output_filename_directory_separator_rejected(self):
274+
"""Test that OUTPUT_FILENAME rejects filenames with directory separators."""
275+
with self.assertRaises(ValueError):
276+
env.get_env_vars()
277+
278+
@patch.dict(
279+
os.environ,
280+
{
281+
"ORGANIZATION": "org",
282+
"GH_TOKEN": "token",
283+
"OUTPUT_FILENAME": "file;rm -rf /.md",
284+
},
285+
clear=True,
286+
)
287+
def test_get_env_vars_output_filename_special_chars_rejected(self):
288+
"""Test that OUTPUT_FILENAME rejects filenames with special characters."""
289+
with self.assertRaises(ValueError):
290+
env.get_env_vars()
291+
292+
@patch.dict(os.environ, {}, clear=True)
194293
def test_get_env_vars_missing_org_or_repo(self):
195294
"""Test that an error is raised if required environment variables are not set"""
196295
with self.assertRaises(ValueError) as cm:
197-
env.get_env_vars()
296+
env.get_env_vars(test=True)
198297
the_exception = cm.exception
199298
self.assertEqual(
200299
str(the_exception),
@@ -290,6 +389,7 @@ def test_get_env_vars_valid_date_range(self):
290389
end_date,
291390
_sponsor_info,
292391
_link_to_profile,
392+
_output_filename,
293393
) = env.get_env_vars()
294394
self.assertEqual(start_date, "2024-01-01")
295395
self.assertEqual(end_date, "2025-01-01")

0 commit comments

Comments
 (0)